Major changes:

- File selections working
- CSS more responsive, more consistent use of colors and variables
- Keyboard navigation
- Added context menu buttons and handler, the menu is still missing
- Added download and settings buttons (no functions yet)
- Various minor fixes everywhere
This commit is contained in:
Leo Vasanko
2023-11-04 14:10:18 +00:00
parent 997e0b8549
commit 8c6690ea98
79 changed files with 381 additions and 230 deletions

View File

@@ -2,11 +2,11 @@
<html lang=en>
<meta charset=UTF-8>
<title>Cista</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" href="/favicon.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Emoji&family=Roboto:wght@400;700&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<script type="module" src="/src/main.ts"></script>
<div id="app"></div>

View File

@@ -1,7 +1,8 @@
<script setup lang="ts">
import { RouterView } from 'vue-router'
import type { ComputedRef } from 'vue'
import { watchEffect } from 'vue'
import type HeaderMain from '@/components/HeaderMain.vue'
import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
import createWebSocket from '@/repositories/WS'
import {
url_document_watch_ws,
@@ -46,17 +47,61 @@ watchEffect(() => {
documentStore.wsWatch = wsWatch
documentStore.wsUpload = wsUpload
})
const headerMain = ref<typeof HeaderMain | null>(null)
let vert = 0
let vertInterval: any = null
const globalShortcutHandler = (event: KeyboardEvent) => {
if (event.repeat) return
console.log("key pressed", event)
const c = documentStore.fileExplorer.isCursor
const keyup = event.type === "keyup"
// For up/down implement custom fast repeat
if (event.key === "ArrowUp") vert = (keyup ? 0 : event.altKey ? -10 : -1)
else if (event.key === "ArrowDown") vert = (keyup ? 0 : event.altKey ? 10 : 1)
// Find: process on keydown so that we can bypass the built-in search hotkey
else if (!keyup && event.key === 'f' && (event.ctrlKey || event.metaKey)) {
headerMain.value!.toggleSearchInput()
}
// Select all (toggle); keydown to prevent builtin
else if (!keyup && event.key === 'a' && (event.ctrlKey || event.metaKey)) {
documentStore.fileExplorer.toggleSelectAll()
}
// Rename
else if (c && keyup && !event.ctrlKey && (event.key === 'F2' || event.key === "r")) {
documentStore.fileExplorer.cursorRename()
}
// Toggle selections on file explorer; ignore all spaces to prevent scrolling built-in hotkey
else if (c && event.code === 'Space') {
if (keyup && !event.altKey && !event.ctrlKey) documentStore.fileExplorer.cursorSelect()
}
else return
event.preventDefault()
if (vertInterval !== null) clearInterval(vertInterval)
vertInterval = null
if (vert) {
vertInterval = setInterval(() => { console.log("X"), documentStore.fileExplorer.cursorMove(vert) }, 30)
}
}
onMounted(() => {
window.addEventListener("keydown", globalShortcutHandler)
window.addEventListener("keyup", globalShortcutHandler)
})
onUnmounted(() => {
window.removeEventListener("keydown", globalShortcutHandler)
window.removeEventListener("keyup", globalShortcutHandler)
})
export type { Path }
</script>
<template>
<LoginModal />
<header>
<HeaderMain />
<HeaderMain ref="headerMain"/>
<BreadCrumb :path="path.pathList" />
</header>
<RouterView class="page-container" />
<main>
<RouterView />
</main>
</template>
<style scoped>

View File

@@ -1,34 +1,69 @@
@charset "UTF-8";
:root {
--primary-background: #181818;
--secondary-background: #ffffff;
--font-color: #333;
--primary-color: #000;
--primary-background: #eef;
--header-background: #000;
--table-background: #535353;
--primary-color: #ffffff;
--secondary-color: #ccc;
--blue-color: #66ffeb;
--red-color: #ff4d4f;
--header-color: #ccc;
--primary-color: #000;
--secondary-color: #333;
--accent-color: #f80;
--transition-time: 0.2s;
}
@media (prefers-color-scheme: dark) {
:root {
--primary-background: #333;
--secondary-background: #666;
--font-color: #ddd;
--table-background: #535353;
--primary-color: #ffffff;
--primary-color: #ddd;
--primary-background: #003;
--primary-color: #fff;
--secondary-color: #ccc;
--blue-color: #66ffeb;
--red-color: #ff4d4f;
}
}
@media screen and (orientation: portrait) {
html {
font-size: 1.5rem;
}
.size, .modified {
display: none;
}
}
@media screen and (min-width: 800px) and (--webkit-min-device-pixel-ratio: 2) {
html {
font-size: 1.5rem;
}
}
@media screen and (min-width: 1400px) and (--webkit-min-device-pixel-ratio: 3) {
html {
font-size: 2rem;
}
}
body {
background-color: var(--primary-background);
font-size: 1rem;
font-family: 'Roboto', sans-serif;
color: var(--font-color);
color: var(--primary-color);
margin: 0;
}
header {
background-color: var(--header-background);
color: var(--header-color);
}
main {
height: 100%;
}
button {
font: inherit;
color: inherit;
margin: 0;
border: 0;
padding: 0;
background: none;
cursor: pointer;
min-width: 1rem;
min-height: 1rem;
}
:focus {
outline: none;
}
a:link,
a:visited,
a:active,
@@ -46,3 +81,11 @@ table {
display: flex;
flex-direction: column;
}
main {
height: calc(100svh - 6rem);
overflow-y: scroll;
}
thead tr {
position: sticky;
top: 0;
}

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="448" height="512" viewBox="0 0 448 512"><path d="M223.97 175A81 81 0 0 0 143 256c0 44.7 36.27 81.03 80.97 81.03 44.72 0 80.72-36.34 80.72-81.03 0-44.73-36-81-80.8-81zM386.3 302.53l-14.58 35.16 29.47 57.8-36.1 36.1-59.3-28-35.2 14.4-17.87 54.6-2.28 7.24h-51L177.4 418.2l-35.17-14.5-57.9 29.4-36.1-36.1 27.97-59.2-14.47-35.12L0 282.6v-51l61.7-22.1 14.5-35.1-25.96-51.23-3.43-6.72 36.1-36.03 59.3 27.92 35.1-14.5 17.9-54.6 2.3-7.24h51l22.1 61.73 35.07 14.52 58.04-29.4 36.06 36.03-27.96 59.2 14.42 35.17 61.8 20.13v50.97l-61.67 22.18z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="32" viewBox="0 0 448 512"><path d="M223.97 175A81 81 0 0 0 143 256c0 44.7 36.27 81.03 80.97 81.03 44.72 0 80.72-36.34 80.72-81.03 0-44.73-36-81-80.8-81zM386.3 302.53l-14.58 35.16 29.47 57.8-36.1 36.1-59.3-28-35.2 14.4-17.87 54.6-2.28 7.24h-51L177.4 418.2l-35.17-14.5-57.9 29.4-36.1-36.1 27.97-59.2-14.47-35.12L0 282.6v-51l61.7-22.1 14.5-35.1-25.96-51.23-3.43-6.72 36.1-36.03 59.3 27.92 35.1-14.5 17.9-54.6 2.3-7.24h51l22.1 61.73 35.07 14.52 58.04-29.4 36.06 36.03-27.96 59.2 14.42 35.17 61.8 20.13v50.97l-61.67 22.18z"/></svg>

Before

Width:  |  Height:  |  Size: 587 B

After

Width:  |  Height:  |  Size: 586 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="40" viewBox="10 10 372 468"><path d="M128 344V168c0-4.5-3.5-8-8-8h-16c-4.5 0-8 3.5-8 8v176c0 4.5 3.5 8 8 8h16c4.5 0 8-3.5 8-8zm64 0V168c0-4.5-3.5-8-8-8h-16c-4.5 0-8 3.5-8 8v176c0 4.5 3.5 8 8 8h16c4.5 0 8-3.5 8-8zm64 0V168c0-4.5-3.5-8-8-8h-16c-4.5 0-8 3.5-8 8v176c0 4.5 3.5 8 8 8h16c4.5 0 8-3.5 8-8zM120 96h112l-12-29.25c-.75-1-3-2.5-4.25-2.75H136.5c-1.5.25-3.5 1.75-4.25 2.75zm232 8v16c0 4.5-3.5 8-8 8h-24v237c0 27.5-18 51-40 51H72c-22 0-40-22.5-40-50V128H8c-4.5 0-8-3.5-8-8v-16c0-4.5 3.5-8 8-8h77.25l17.5-41.75C107.75 42 122.75 32 136 32h80c13.25 0 28.25 10 33.25 22.25L266.75 96H344c4.5 0 8 3.5 8 8z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="10 10 372 468"><path d="M128 344V168c0-4.5-3.5-8-8-8h-16c-4.5 0-8 3.5-8 8v176c0 4.5 3.5 8 8 8h16c4.5 0 8-3.5 8-8zm64 0V168c0-4.5-3.5-8-8-8h-16c-4.5 0-8 3.5-8 8v176c0 4.5 3.5 8 8 8h16c4.5 0 8-3.5 8-8zm64 0V168c0-4.5-3.5-8-8-8h-16c-4.5 0-8 3.5-8 8v176c0 4.5 3.5 8 8 8h16c4.5 0 8-3.5 8-8zM120 96h112l-12-29.25c-.75-1-3-2.5-4.25-2.75H136.5c-1.5.25-3.5 1.75-4.25 2.75zm232 8v16c0 4.5-3.5 8-8 8h-24v237c0 27.5-18 51-40 51H72c-22 0-40-22.5-40-50V128H8c-4.5 0-8-3.5-8-8v-16c0-4.5 3.5-8 8-8h77.25l17.5-41.75C107.75 42 122.75 32 136 32h80c13.25 0 28.25 10 33.25 22.25L266.75 96H344c4.5 0 8 3.5 8 8z"/></svg>

Before

Width:  |  Height:  |  Size: 670 B

After

Width:  |  Height:  |  Size: 670 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="-1 -3 36 36"><path d="M22.86 15.43q0-.25-.16-.4l-6.3-6.3q-.15-.16-.4-.16t-.4.16L9.3 15q-.18.2-.18.43 0 .25.16.4t.4.17h4v6.3q0 .22.18.4t.4.16h3.44q.23 0 .4-.17t.17-.4V16h4q.22 0 .4-.17t.16-.4zm11.43 5.14q0 2.84-2.06 4.85t-4.85 2H8q-3.3 0-5.65-2.34T0 19.43q0-2.32 1.25-4.3T4.6 12.2l-.03-.77q0-3.8 2.68-6.47t6.47-2.68q2.78 0 5.1 1.56t3.36 4.12q1.27-1.1 2.96-1.1 1.9 0 3.24 1.34t1.34 3.23q0 1.35-.74 2.46 2.32.5 3.82 2.4t1.5 4.23z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="-1 -3 36 36"><path d="M22.86 15.43q0-.25-.16-.4l-6.3-6.3q-.15-.16-.4-.16t-.4.16L9.3 15q-.18.2-.18.43 0 .25.16.4t.4.17h4v6.3q0 .22.18.4t.4.16h3.44q.23 0 .4-.17t.17-.4V16h4q.22 0 .4-.17t.16-.4zm11.43 5.14q0 2.84-2.06 4.85t-4.85 2H8q-3.3 0-5.65-2.34T0 19.43q0-2.32 1.25-4.3T4.6 12.2l-.03-.77q0-3.8 2.68-6.47t6.47-2.68q2.78 0 5.1 1.56t3.36 4.12q1.27-1.1 2.96-1.1 1.9 0 3.24 1.34t1.34 3.23q0 1.35-.74 2.46 2.32.5 3.82 2.4t1.5 4.23z"/></svg>

Before

Width:  |  Height:  |  Size: 507 B

After

Width:  |  Height:  |  Size: 508 B

View File

@@ -92,10 +92,10 @@ const props = withDefaults(
.breadcrumb a:nth-child(even) {
background: var(--breadcrumb-background-even);
}
.breadcrumb a:nth-child(odd):hover {
.breadcrumb a:nth-child(odd):hover, .breadcrumb a:focus:nth-child(odd) {
background: var(--breadcrumb-hover-background-odd);
}
.breadcrumb a:nth-child(even):hover {
.breadcrumb a:nth-child(even):hover, .breadcrumb a:focus:nth-child(even) {
background: var(--breadcrumb-hover-background-even);
}
.breadcrumb a:hover {

View File

@@ -1,96 +1,104 @@
<template>
<main>
<table v-if="props.documents.length || editing">
<thead>
<tr>
<th class="selection">
<input
type="checkbox"
v-model="allSelected"
:indeterminate="selectionIndeterminate"
/>
</th>
<th
class="sortcolumn"
:class="{ sortactive: sort === 'name' }"
@click="toggleSort('name')"
>
Name
</th>
<th
class="sortcolumn modified right"
:class="{ sortactive: sort === 'modified' }"
@click="toggleSort('modified')"
>
Modified
</th>
<th
class="sortcolumn size right"
:class="{ sortactive: sort === 'size' }"
@click="toggleSort('size')"
>
Size
</th>
</tr>
</thead>
<tbody>
<tr v-if="editing?.key === 'new'" class="folder">
<td class="selection"></td>
<td class="name">
<FileRenameInput
:doc="editing"
:rename="mkdir"
<table v-if="props.documents.length || editing" @blur="cursor = null">
<thead>
<tr>
<th class="selection">
<input
type="checkbox"
tabindex="-1"
v-model="allSelected"
:indeterminate="selectionIndeterminate"
/>
</th>
<th
class="sortcolumn"
:class="{ sortactive: sort === 'name' }"
@click="toggleSort('name')"
>
Name
</th>
<th
class="sortcolumn modified right"
:class="{ sortactive: sort === 'modified' }"
@click="toggleSort('modified')"
>
Modified
</th>
<th
class="sortcolumn modified right"
:class="{ sortactive: sort === 'size' }"
@click="toggleSort('size')"
>
Size
</th>
<th class="menu"></th>
</tr>
</thead>
<tbody>
<tr v-if="editing?.key === 'new'" class="folder">
<td class="selection"></td>
<td class="name">
<FileRenameInput
:doc="editing"
:rename="mkdir"
:exit="
() => {
editing = null
}
"
/>
</td>
<td class="modified right">{{ editing.modified }}</td>
<td class="size right">{{ editing.sizedisp }}</td>
<td class="menu"></td>
</tr>
<tr
v-for="doc of sorted(props.documents as FolderDocument[])"
:key="doc.key"
:id="`file-${doc.key}`"
:class="{
file: doc.type === 'file',
folder: doc.type === 'folder',
cursor: cursor === doc
}"
@click="cursor = cursor === doc ? null : doc"
@contextmenu.prevent="contextMenu($event, doc)"
>
<td class="selection" @click.up.stop="cursor = cursor === doc ? doc : null">
<input
type="checkbox"
tabindex="-1"
:checked="documentStore.selected.has(doc.key)"
@change="($event.target as HTMLInputElement).checked ? documentStore.selected.add(doc.key) : documentStore.selected.delete(doc.key)"
/>
</td>
<td class="name">
<template v-if="editing === doc"
><FileRenameInput
:doc="doc"
:rename="rename"
:exit="
() => {
editing = null
}
"
/>
</td>
<td class="right">{{ editing.modified }}</td>
<td class="right">{{ editing.sizedisp }}</td>
</tr>
<tr
v-for="doc of sorted(props.documents as FolderDocument[])"
:key="doc.key"
:class="doc.type === 'folder' ? 'folder' : 'file'"
>
<td class="selection">
<input
type="checkbox"
:checked="doc.key in documentStore.selected"
@change="documentStore.selected.add(doc.key)"
/>
</td>
<td class="name">
<template v-if="editing === doc"
><FileRenameInput
:doc="doc"
:rename="rename"
:exit="
() => {
editing = null
}
"
/></template>
<template v-else>
<a :href="url_for(doc)">{{ doc.name }}</a>
<button @click="() => (editing = doc)">🖊</button>
</template>
</td>
<td class="right">{{ doc.modified }}</td>
<td class="right">{{ doc.sizedisp }}</td>
</tr>
</tbody>
</table>
<div v-else>
<p>No files</p>
</div>
</main>
/></template>
<template v-else>
<a :href="url_for(doc)" tabindex="-1" @contextmenu.stop @click.stop @focus.stop="cursor = doc">{{ doc.name }}</a>
<button @click="() => (editing = doc)">🖊</button>
</template>
</td>
<td class="modified right">{{ doc.modified }}</td>
<td class="size right">{{ doc.sizedisp }}</td>
<td class="menu"><button tabindex="-1" @click.stop="cursor = doc; contextMenu($event, doc)"></button></td>
</tr>
</tbody>
</table>
<div v-else class="empty-container">Nothing to see here</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, watchEffect } from 'vue'
import { useDocumentStore } from '@/stores/documents'
import type { Document, FolderDocument } from '@/repositories/Document'
import FileRenameInput from './FileRenameInput.vue'
@@ -113,8 +121,9 @@ const linkBasePath = computed(() => {
const filesBasePath = computed(() => `/files${linkBasePath.value}`)
const url_for = (doc: FolderDocument) =>
doc.type === 'folder'
? `#${linkBasePath.value}/${doc.name}`
? `#${linkBasePath.value}/${doc.name}/`
: `${filesBasePath.value}/${doc.name}`
const cursor = ref<FolderDocument | null>(null)
// File rename
const editing = ref<FolderDocument | null>(null)
const rename = (doc: FolderDocument, newName: string) => {
@@ -152,6 +161,46 @@ defineExpose({
modified: formatUnixDate(now)
}
},
toggleSelectAll() {
console.log("Select")
allSelected.value = !allSelected.value
},
isCursor() {
return cursor.value !== null && editing.value === null
},
cursorRename() {
editing.value = cursor.value
},
cursorSelect() {
console.log("select", documentStore.selected)
const doc = cursor.value
if (!doc) return
if (documentStore.selected.has(doc.key)) {
documentStore.selected.delete(doc.key)
} else {
documentStore.selected.add(doc.key)
}
},
cursorMove(d: number) {
// Move cursor up or down (keyboard navigation)
const documents = sorted(props.documents as FolderDocument[])
if (documents.length === 0) {
cursor.value = null
return
}
const mod = (a: number, b: number) => ((a % b) + b) % b
const index = cursor.value !== null ? documents.indexOf(cursor.value) : -1
cursor.value = documents[mod(index + d, documents.length + 1)] ?? null
const tr = document.getElementById(`file-${cursor.value.key}`) as HTMLTableRowElement | null
// @ts-ignore
if (tr) tr.scrollIntoView({ block: 'center' })
}
})
watchEffect(() => {
if (cursor.value) {
const a = document.querySelector(`#file-${cursor.value.key} .name a`) as HTMLAnchorElement | null
if (a) a.focus()
}
})
const mkdir = (doc: FolderDocument, name: string) => {
const control = createWebSocket('/api/control', (ev: MessageEvent) => {
@@ -194,7 +243,7 @@ const selectionIndeterminate = computed({
get: () => {
return (
props.documents.length > 0 &&
props.documents.some((doc: Document) => doc.key in documentStore.selected) &&
props.documents.some((doc: Document) => documentStore.selected.has(doc.key)) &&
!allSelected.value
)
},
@@ -205,10 +254,11 @@ const allSelected = computed({
get: () => {
return (
props.documents.length > 0 &&
props.documents.every((doc: Document) => doc.key in documentStore.selected)
props.documents.every((doc: Document) => documentStore.selected.has(doc.key))
)
},
set: (value: boolean) => {
console.log("Setting allSelected", value)
for (const doc of props.documents) {
if (value) {
documentStore.selected.add(doc.key)
@@ -218,26 +268,39 @@ const allSelected = computed({
}
}
})
const contextMenu = (ev: Event, doc: Document) => {
console.log('Context menu', ev, doc)
}
</script>
<style>
<style scoped>
table {
width: 100%;
table-layout: fixed;
}
table input[type='checkbox'] {
width: 1em;
height: 1em;
width: 1rem;
height: 1rem;
}
table .selection {
width: 1rem;
}
table .modified {
width: 10em;
width: 10rem;
}
table .size {
width: 6em;
width: 5rem;
}
table .menu {
width: 1rem;
}
tbody td {
font-size: 1.2rem;
}
table th,
table td {
padding: 0.5em;
padding: 0 0.5rem;
font-weight: normal;
text-align: left;
white-space: nowrap;
@@ -246,28 +309,20 @@ table td {
}
.name {
white-space: nowrap;
text-overflow: initial;
overflow: initial;
}
.name button {
visibility: hidden;
padding-left: 1em;
padding-left: 1rem;
}
.name:hover button {
visibility: visible;
}
.name button {
cursor: pointer;
border: 0;
background: transparent;
}
thead tr {
border: 1px solid #ddd;
background: #ddd;
background: linear-gradient(to bottom, #eee, #fff 30%, #ddd);
color: #000;
}
tbody tr:hover {
background: #00f8;
tbody tr.cursor {
background: var(--accent-color);
}
.right {
text-align: right;
@@ -279,7 +334,7 @@ tbody tr:hover {
cursor: pointer;
}
.sortcolumn:hover::after {
color: #f80;
color: var(--accent-color);
}
.sortcolumn {
padding-right: 1.7em;
@@ -289,16 +344,12 @@ tbody tr:hover {
color: #888;
margin: 0 1em 0 0.5em;
position: absolute;
transition: all 0.2s linear;
transition: all var(--transition-time) linear;
}
.sortactive::after {
transform: rotate(90deg);
color: #000;
}
main {
padding: 5px;
height: 100%;
}
.more-action {
display: flex;
flex-direction: column;
@@ -325,4 +376,10 @@ main {
content: '📁 ';
font-size: 1.5em;
}
.empty-container {
padding-top: 3rem;
text-align: center;
font-size: 3rem;
color: var(--accent-color);
}
</style>

View File

@@ -43,5 +43,6 @@ input#FileRenameInput {
width: 90%;
outline: none;
background: transparent;
font: inherit;
}
</style>

View File

@@ -1,30 +1,27 @@
<script setup lang="ts">
import { useDocumentStore } from '@/stores/documents'
import { ref, nextTick } from 'vue'
import { ref, nextTick, watchEffect } from 'vue'
const documentStore = useDocumentStore()
const searchQuery = ref<string>('')
const showSearchInput = ref<boolean>(false)
const search = ref<HTMLInputElement | null>()
const toggleSearchInput = () => {
showSearchInput.value = !showSearchInput.value
if (!showSearchInput.value) {
searchQuery.value = ''
}
nextTick(() => {
const input = search.value
if (input) input.focus()
executeSearch()
})
}
const executeSearch = (ev: Event) => {
// FIXME: Make reactive instead of this update handler
const query = (ev.target as HTMLInputElement).value
console.log('Searching', query)
documentStore.setFilter(query)
console.log('Filtered')
const executeSearch = () => {
documentStore.setFilter(search.value?.value ?? '')
}
defineExpose({
toggleSearchInput
})
</script>
<template>
@@ -32,26 +29,28 @@ const executeSearch = (ev: Event) => {
<div class="buttons">
<UploadButton />
<SvgButton name="create-folder" @click="() => documentStore.fileExplorer.newFolder()"/>
<template v-if="true">
<template v-if="documentStore.selected.size > 0">
<div class="smallgap"></div>
<p>N selected files:</p>
<p class="select-text">{{ documentStore.selected.size }} selected </p>
<!-- Needs better icons for copy/move/remove -->
<SvgButton name="copy" />
<SvgButton name="download" />
<SvgButton name="copy" />
<SvgButton name="paste" />
<SvgButton name="trash" />
<button @click="documentStore.selected.clear()"></button>
</template>
<div class="spacer"></div>
<SvgButton name="find" @click="toggleSearchInput" />
<template v-if="showSearchInput">
<input
ref="search"
type="search"
v-model="searchQuery"
class="margin-input"
@keyup.esc="toggleSearchInput"
@input="executeSearch"
/>
</template>
<SvgButton name="find" @click="toggleSearchInput" />
<SvgButton name="cog" @click="console.log('TODO open settings')" />
</div>
</nav>
</template>
@@ -68,6 +67,9 @@ const executeSearch = (ev: Event) => {
.smallgap {
margin-left: 2em;
}
.select-text {
color: var(--accent-color);
}
.search-widget {
display: flex;
align-items: center;

View File

@@ -26,7 +26,7 @@
<h3 v-if="loginForm.error.length > 0" class="error-text">
{{ loginForm.error }}
</h3>
<input type="submit" class="button-login" />
<input id="submit" type="submit" class="button-login" />
</form>
</ModalDialog>
</template>

View File

@@ -24,14 +24,17 @@ button {
transition: all 0.2s ease;
padding: 0.5rem;
}
button:hover {
button:hover, button:focus {
color: #fff;
transform: scale(1.1);
}
svg {
fill: #ccc;
transform: fill 0.2s ease;
width: 1rem;
height: 1rem;
}
button:hover svg {
button:hover svg, button:focus svg {
fill: #fff;
}
</style>

View File

@@ -14,5 +14,4 @@ app.config.errorHandler = err => {
app.use(createPinia())
app.use(router)
app.mount('#app')

View File

@@ -78,6 +78,7 @@ export const useDocumentStore = defineStore({
this.document = dataMapped
},
setFilter(filter: string) {
if (filter === '') return this.updateTable({})
function traverseDir(data: DirEntry | FileEntry, path: string) {
if (!('dir' in data)) return
for (const [name, attr] of Object.entries(data.dir)) {

View File

@@ -43,14 +43,14 @@ function enter(el: Element, done: () => void) {
function leave(el: Element, done: () => void) {
const elem = el as HTMLElement
elem.style.transform = 'translateX(-100%)'
setTimeout(done, 300) // Assuming 300ms is your transition duration
setTimeout(done, 200) // Should match --transition-time
}
</script>
<style scoped>
.slide-fade-enter-active,
.slide-fade-leave-active {
transition: transform 300ms ease;
transition: transform var(--transition-time) linear;
}
.slide-fade-enter,
.slide-fade-leave-to /* .slide-fade-leave-active for <2.1.8 */ {