Notes
StableNotes and reminders in one window: a searchable list beside a block editor, and reminders typed in plain words.
NotesApp is the whole tool in the RChursin_ app window, with two views. Notes keeps a searchable list (pinned first, then by last edit, grouped by day) beside the editor, which saves as you type. Reminders reads dates from plain words and lists what is coming. Its one prop, hrefFor, says where the window’s links to the other tools go. It saves notes, display options and reminders to this browser until you connect a backend. In a narrow window the list and the editor take turns.
Notes and reminders
import { useRouter } from '@rchursin/product/lib/router'import { NotesApp } from '@rchursin/product/notes'import type { KitSlug } from '@rchursin/product/shell/pages'
/** The window's tool links go to each tool's page in this Library (Today has none, so it shows as text) */const toolPages: Partial<Record<KitSlug, string>> = { tasks: '/library/tools/tasks-app', calendar: '/library/tools/calendar-app', timer: '/library/tools/timer-app', notes: '/library/tools/notes-app',}
/** * Notes and reminders on a page of their own, on sample data. Click inside, then N starts a note * and R jumps to a new reminder. In the list, ↑ ↓ move between notes, Enter opens one, P pins it. * Everything saves to this browser; "Reset sample data" in the display options puts it back. */export default function NotesAppPage() { const { href } = useRouter() const hrefFor = (slug: KitSlug) => { const path = toolPages[slug] return path && href(path) } return ( <main className="min-h-dvh bg-sunken/40 px-4 pt-6 pb-24 sm:px-8 sm:pt-10"> <div className="mx-auto flex max-w-6xl flex-col gap-5"> <header className="flex flex-col gap-1"> <h1 className="text-xl font-semibold tracking-[-0.02em] text-balance">Notes</h1> <p className="text-[13px] text-pretty text-fg-3">A sample workspace. Everything you do here stays in this browser.</p> </header> {/* Keeps the outline in order: the app's own headings (the open note, the reminder groups) are h3 */} <h2 className="sr-only">Notes and reminders</h2> <NotesApp hrefFor={hrefFor} /> </div> </main> )}Usage
import { focusNoteRow, groupNotes, NotesApp, NotesList } from '@rchursin/product/notes'<NotesApp />Examples
The notes list on its own
NotesList on local state and a fixed clock. Tab into the list, then ↑ ↓ to move, P to pin, Delete to remove (with Undo), or type in the search.
import { useState } from 'react'import type { Note } from '@rchursin/product/data/sample'import { useAnnounce } from '@rchursin/product/lib/announce'import { focusNoteRow, groupNotes, NotesList } from '@rchursin/product/notes'import { SampleBadge } from '@rchursin/product/shell/layout'import { useToast } from '@rchursin/product/ui'
/** A fixed clock, so the dates and groups read the same on every visit */const NOW = new Date(2026, 9, 13, 15, 30).getTime()const HOUR = 3_600_000
const start: Note[] = [ { id: 'ex-audit', title: 'Skill audit: October', body: 'Five skills do 90% of the work.\n\n- [x] Export runs for the last 30 days\n- [ ] Decide keep / merge / cut', pinned: true, tags: ['skills'], updatedAt: NOW - 2 * HOUR, },API
NotesApp props
| Prop | Type | Default | Description |
|---|---|---|---|
hrefFor | (slug: KitSlug) => string | undefined | – | Where the window’s tool links (Today, Tasks, Calendar, Focus) go: your own pages. The default is the product kit’s own #tasks, #calendar … anchors; return undefined to show a tool as plain text. |
NotesList props
| Prop | Type | Default | Description |
|---|---|---|---|
notesrequired | Note[] | – | { id, title, body (Markdown), pinned, tags, updatedAt }. |
selectedIdrequired | string | null | – | The note shown beside the list. If it is filtered out, the first note holds the Tab stop. |
onSelectrequired | (id: string) => void | – | Arrows, Home, End or a click chose a note. |
onOpenrequired | (id: string, withKeyboard: boolean) => void | – | Enter or a click: show the note. withKeyboard says to move focus into it. |
queryrequired | string | – | The search text. It matches titles, bodies and #tags. |
onQueryChangerequired | (query: string) => void | – | Typing, or Esc to clear. |
groupByDaterequired | boolean | – | Group under Today, Yesterday, Previous 7 days, Previous 30 days and Older; otherwise one Notes group. |
onNewrequired | () => void | – | The “New note” button in the empty state. |
onTogglePinrequired | (id: string) => void | – | P on a note. |
onDeleterequired | (id: string) => void | – | Delete or Backspace on a note. Move focus to its neighbour yourself with focusNoteRow(id); NotesApp does, and offers Undo. |
nowrequired | number | – | For the dates under each title and the day groups. |
searchRef | Ref<HTMLInputElement> | – | The search field, so a / shortcut can focus it. |
className | string | – | Classes on the wrapper. Give it a height: the list scrolls inside. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| N | New note; focus goes to its title. |
| R | Show Reminders and focus the quick capture field. |
| / | Show Notes and focus the search field. |
| Down arrowUp arrow | In the list: next or previous note, shown beside the list straight away. From the search field, ↓ moves into the list. |
| HomeEnd | In the list: first or last note. |
| Enter | In the list: move into the note’s title. In the title: move into the body. |
| P | In the list: pin or unpin the note. |
| BackspaceDel | In the list: delete the note, with Undo in the toast. |
| Esc | In the search field: clear it. In the note: go back to its row in the list. |
- The window is a region named “RChursin_ app: notes and reminders (sample workspace)”. Its shortcuts only fire while focus is inside it and you aren’t typing.
- The notes list is a listbox with one Tab stop. Each note is an option named by its title and described by its date, first line, checklist progress (“Checklist: 2/4 done”), tags and pin.
- Pinning is announced (“Pinned to the top”); deleting and duplicating end in a toast, deleting with Undo. After a delete, focus moves to the next note.
- Enter on a note always moves focus into its title, also when that note is already shown beside the list.
- When the window’s body is under 672px (a container query, so a narrow column counts too), opening a note hides the list and a “Notes” back button returns to the same row.
- Reminders that come due while the page is open appear in a bar at the top whose text is a polite status region. Handling the last one moves focus to the next control after the bar. The browser’s Notifications API is not used.
- The sample notes’ days and times follow the visitor’s clock, which a prerendered page can’t know: the static HTML shows the window with an empty body, and the notes fill in as soon as the page runs.
- The site search also listens for “/” on the whole page, but while focus is inside the window the app’s own “/” runs first (scoped shortcuts win over page-wide ones), so it focuses the notes search.
Guidelines
Do
- Give it about 900px: the list sits beside the editor once the window’s body is 672px wide, and below that they take turns. The list and the editor scroll inside the window.
- Pass hrefFor, so the window’s Tasks, Calendar and Focus links go to your own pages.
- Keep notes as Markdown when you connect a backend: the editor reads and writes it, so notes look the same anywhere.
Don’t
- Don’t put two NotesApps on a page: they save under the same keys and their list rows use fixed ids (“note-n1”).
- Don’t ship “Reset sample data” to real users; it is for demos.
In use
Related
- Note editorA note’s title, a Markdown block body, tags, word count and an honest save status; it saves 800 ms after typing stops.
- RemindersReminders grouped from Overdue to Done, checked off, snoozed or deleted with Undo, plus a compact widget for a dashboard.
- Quick captureOne field that reads a date, time or repeat out of plain English and shows what it understood before you save.
- BannersIn-page messages for things that happen on their own: a timer that finished, a reminder that came due.
- App windowThe RChursin_ app frame: a sidebar with the tools, a title row with a toolbar, and your tool in the body.
- Markdown blocksTurns a note’s Markdown into flat blocks (headings, lists, checklists, quotes) and back, so notes stay plain Markdown in storage.
Source
The component’s own files, exactly as they run on this site.
/** * The Notes app in the RChursin_ window: notes (list beside the editor) and reminders, as two * views of one tool. Everything persists per viewer; "Reset sample data" puts the story back. * When the window is narrow (under 672px of body) the list and the editor take turns, with a back button. * * Shortcuts (only while focus is inside the window, and never while typing): * N new note, / search, R new reminder. */import { useEffect, useMemo, useRef, useState } from "react"import { cx } from "../../components/cx"import { sampleNotes, type Note } from "../data/sample"import { useAnnounce } from "../lib/announce"import { useHotkeys } from "../lib/hotkeys"import { useHydrated } from "../lib/hydration"import { AppWindow } from "../shell/AppWindow"import type { KitSlug } from "../shell/pages"import { Button, IconButton } from "../ui/button"import { Segmented } from "../ui/fields"import { Menu } from "../ui/menu"import { EmptyState } from "../ui/misc"