feat: add notes app
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
// "New", bottom right.
|
||||
// The list opens with NotesDrawer (mobile) and NotesSidebarHandle (desktop).
|
||||
const { creating, createAndOpen } = useNewNote()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pointer-events-none fixed inset-x-0 bottom-0 z-40 flex justify-end px-4 pb-[max(1rem,env(safe-area-inset-bottom))] md:px-6 md:pb-6">
|
||||
<UTooltip text="New note" :kbds="['meta', 'alt', 'n']">
|
||||
<UButton
|
||||
icon="i-lucide-square-pen"
|
||||
label="New"
|
||||
size="lg"
|
||||
class="pointer-events-auto rounded-full px-5 shadow-lg"
|
||||
:loading="creating"
|
||||
@click="createAndOpen"
|
||||
/>
|
||||
</UTooltip>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
// Mobile only: the note list, pulled down from the top. Tap or pull the bar at the top edge to open it;
|
||||
// push it back up, tap outside or pick a note to close it.
|
||||
const { drawerOpen } = useNotesSidebar()
|
||||
|
||||
const PULL_DISTANCE_PX = 24
|
||||
let startY: number | null = null
|
||||
|
||||
function onTouchStart(event: TouchEvent) {
|
||||
startY = event.touches[0]?.clientY ?? null
|
||||
}
|
||||
|
||||
function onTouchMove(event: TouchEvent) {
|
||||
const y = event.touches[0]?.clientY
|
||||
if (startY === null || y === undefined) return
|
||||
if (y - startY > PULL_DISTANCE_PX) {
|
||||
drawerOpen.value = true
|
||||
startY = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Show notes"
|
||||
class="fixed inset-x-0 top-0 z-30 flex h-7 touch-none items-start justify-center pt-2"
|
||||
@click="drawerOpen = true"
|
||||
@touchstart.passive="onTouchStart"
|
||||
@touchmove.passive="onTouchMove"
|
||||
>
|
||||
<span class="h-1.5 w-12 rounded-full bg-accented" />
|
||||
</button>
|
||||
|
||||
<UDrawer v-model:open="drawerOpen" direction="top" title="Notes" :ui="{ content: 'max-h-[85dvh]', body: 'overflow-y-auto p-0 sm:p-0' }">
|
||||
<template #body>
|
||||
<NotesList />
|
||||
</template>
|
||||
</UDrawer>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
// The note list, in the desktop sidebar and in the mobile drawer
|
||||
const { docs: allDocs, error } = useNotes()
|
||||
const { query, matches, searching } = useNotesSearch()
|
||||
const docs = computed(() => matches.value ? allDocs.value.filter(doc => matches.value!.includes(doc.id)) : allDocs.value)
|
||||
const { closeOnMobile } = useNotesSidebar()
|
||||
const deleteDoc = useDeleteNote()
|
||||
const route = useRoute()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="px-2 pb-2">
|
||||
<UInput
|
||||
v-model="query"
|
||||
icon="i-lucide-search"
|
||||
placeholder="Search"
|
||||
:loading="searching"
|
||||
class="w-full"
|
||||
@keydown.esc="query = ''"
|
||||
>
|
||||
<template v-if="query" #trailing>
|
||||
<UButton icon="i-lucide-x" color="neutral" variant="link" size="sm" aria-label="Clear search" @click="query = ''" />
|
||||
</template>
|
||||
</UInput>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="px-4 text-sm text-error">Couldn't load the notes: {{ errorMessage(error) }}</p>
|
||||
|
||||
<nav class="px-2 pb-4">
|
||||
<!-- The delete button shows on hover on desktop (instead of the date), always on mobile -->
|
||||
<div v-for="doc in docs" :key="doc.id" class="group relative">
|
||||
<NuxtLink
|
||||
:to="`/notes/${doc.id}`"
|
||||
class="flex items-baseline gap-2 rounded-md py-2.5 ps-2 pe-11 text-base md:py-1.5 md:pe-2 md:text-sm"
|
||||
:class="String(doc.id) === route.params.id ? 'bg-elevated text-highlighted' : 'text-default hover:bg-elevated/50'"
|
||||
@click="closeOnMobile"
|
||||
>
|
||||
<span class="flex-1 truncate" :class="{ 'text-muted': !doc.title }">{{ notesDocName(doc) }}</span>
|
||||
<span class="hidden shrink-0 text-xs text-dimmed tabular-nums md:inline md:group-hover:invisible">{{ notesDocDay(doc.created) }}</span>
|
||||
</NuxtLink>
|
||||
<UButton
|
||||
icon="i-lucide-trash-2"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:aria-label="`Delete ${notesDocName(doc)}`"
|
||||
title="Delete"
|
||||
class="absolute inset-y-0 end-1 my-auto h-fit text-dimmed hover:text-error md:opacity-0 md:group-hover:opacity-100 md:focus-visible:opacity-100"
|
||||
@click="deleteDoc(doc)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="!docs.length && !error && !searching" class="px-2 py-4 text-sm text-muted">
|
||||
{{ query.trim() ? 'No notes match.' : 'No notes yet.' }}
|
||||
</p>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
// Desktop only; on mobile the list is pulled down from the top (NotesDrawer)
|
||||
const { open } = useNotesSidebar()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside v-show="open" class="hidden w-72 shrink-0 flex-col border-e border-default md:flex">
|
||||
<h1 class="px-4 py-4 text-lg font-semibold text-highlighted">Notes</h1>
|
||||
<NotesList class="flex-1 overflow-y-auto" />
|
||||
</aside>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
// Desktop only: a chevron on the sidebar's right border (or the window's left edge when the
|
||||
// sidebar is hidden) that appears on hover and shows/hides the note list
|
||||
const { open, toggle } = useNotesSidebar()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- The whole hover area is the button. It only reaches to the right of the line (never over the note list's
|
||||
delete buttons), as wide as the note's side padding (md:px-10), so it never covers the text either -->
|
||||
<button
|
||||
type="button"
|
||||
:title="`${open ? 'Hide' : 'Show'} notes (⌘E)`"
|
||||
:aria-label="open ? 'Hide notes' : 'Show notes'"
|
||||
class="group fixed inset-y-0 z-20 hidden w-10 cursor-pointer items-center focus:outline-none md:flex"
|
||||
:class="open ? 'left-72' : 'left-0'"
|
||||
@click="toggle"
|
||||
>
|
||||
<span
|
||||
class="flex size-6 items-center justify-center rounded-full bg-default text-muted opacity-0 shadow-sm ring-1 ring-accented transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100 group-focus-visible:ring-2 group-focus-visible:ring-primary"
|
||||
:class="open ? '-translate-x-1/2' : 'ms-2'"
|
||||
>
|
||||
<UIcon :name="open ? 'i-lucide-chevron-left' : 'i-lucide-chevron-right'" class="size-4" />
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
Reference in New Issue
Block a user