diff --git a/docs/wiki/Development.md b/docs/wiki/Development.md index aa260d0..59167f2 100644 --- a/docs/wiki/Development.md +++ b/docs/wiki/Development.md @@ -30,7 +30,7 @@ real data stays out of it. | `migrations/` | the SQLite schema, one numbered file per change | | `web/src/lib/keymap.ts` | every key: where it is heard, what it is called, its help line | | `web/src/lib/actions.ts`, `modes.ts` | what each key does, and which contexts hear it first | -| `web/src/lib/store.svelte.ts` | the client's state, and every call to the API | +| `web/src/lib/store.svelte.ts`, `state/` | the client's state: the one `workspace` the keys see, made of parts that each keep one thing — `tree`, `tiles`, `drafts`, `cards`, `diary`, `scratch`, `sheets`, `vault`, `prompts` | | `web/src/lib/commands.ts` | the `:` commands | | `web/src/lib/Help.svelte` | the `?` screen | diff --git a/web/src/App.svelte b/web/src/App.svelte index 3329ac1..1eabd5e 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -72,14 +72,14 @@ if (!shareToken) void workspace.boot() - /* Where each tile sits, in the order of `workspace.tiles`. */ - const layout = $derived(dwindle(workspace.tiles.length)) + /* Where each tile sits, in the order of `workspace.tiles.all`. */ + const layout = $derived(dwindle(workspace.tiles.all.length)) /* * The tiles are drawn in the order they were made, whatever order they are * in now, so moving one never moves its element — which would cost it its * scroll position, and a PDF its page. */ - const drawn = $derived([...workspace.tiles].sort((a, b) => a.id - b.id)) + const drawn = $derived([...workspace.tiles.all].sort((a, b) => a.id - b.id)) $effect(() => { folderInput?.setAttribute('webkitdirectory', '') @@ -121,7 +121,7 @@ * window handler instead of being swallowed by the editor underneath. */ $effect(() => { - if (workspace.overlay !== 'none') { + if (workspace.sheets.shown !== 'none') { overlayEl?.focus() return () => (workspace.editing ? hooks.focusEditor() : shell?.focus()) } @@ -134,7 +134,7 @@ */ $effect(() => { const answer = (event: KeyboardEvent) => { - const prompt = workspace.prompt + const prompt = workspace.prompts.prompt if (!prompt || ['Shift', 'Control', 'Alt', 'Meta', 'CapsLock'].includes(event.key)) return event.preventDefault() event.stopPropagation() @@ -153,9 +153,9 @@ if (!workspace.user) return const refresh = () => { if (document.visibilityState !== 'visible') return - void workspace.loadAgenda().then(() => workspace.openAgendaIfDue()) + void workspace.cards.loadAgenda().then(() => workspace.cards.openAgendaIfDue()) // The vault locks itself when left alone; the bar should say so. - void workspace.loadVault() + void workspace.vault.load() } const timer = setInterval(refresh, 5 * 60 * 1000) document.addEventListener('visibilitychange', refresh) @@ -168,9 +168,9 @@ // The vault closes itself after a quiet spell; look again just after it // would have, so the bar does not go on saying it is open. $effect(() => { - const vault = workspace.vault + const vault = workspace.vault.status if (!vault?.unlocked || vault.locks_in === null) return - const timer = setTimeout(() => void workspace.loadVault(), (vault.locks_in + 2) * 1000) + const timer = setTimeout(() => void workspace.vault.load(), (vault.locks_in + 2) * 1000) return () => clearTimeout(timer) }) @@ -183,7 +183,7 @@ // Guard against losing a half-written entry to a stray refresh. $effect(() => { - if (!workspace.tiles.some((tile) => tile.dirty)) return + if (!workspace.tiles.all.some((tile) => tile.dirty)) return const warn = (event: BeforeUnloadEvent) => event.preventDefault() window.addEventListener('beforeunload', warn) return () => window.removeEventListener('beforeunload', warn) @@ -198,7 +198,7 @@ // While writing, they go in at the cursor too; while reading, the // document only holds them. const writing = workspace.editing - const created = await workspace.attach(files) + const created = await workspace.tree.attach(files) if (writing && created.length > 0) hooks.insertText(created.map(embedSnippet).join('\n\n')) }) } @@ -208,7 +208,7 @@ const files = Array.from(input.files ?? []) input.value = '' if (files.length === 0) return - await workspace.guard(() => workspace.putFiles(files, sealUpload)) + await workspace.guard(() => workspace.tree.putFiles(files, sealUpload)) } /* @@ -223,7 +223,7 @@ const root = importRoot importRoot = false if (files.length === 0) return - await workspace.guard(() => workspace.importFolder(files, root)) + await workspace.guard(() => workspace.tree.importFolder(files, root)) } /* @@ -288,10 +288,10 @@
- {#key workspace.spaceId} + {#key workspace.tree.spaceId}
!tile.empty)} + class:reading={workspace.tiles.all.some((tile) => !tile.empty)} class:full={workspace.fullscreen} class:focus-doc={workspace.focus !== 'list'} style="--dir: {workspace.switchDirection}" @@ -302,20 +302,20 @@ class:focused={workspace.focus === 'list'} onpointerdown={() => (workspace.pane = 'list')} >
-
+
{#each drawn as tile (tile.id)} - {@const index = workspace.tiles.indexOf(tile)} + {@const index = workspace.tiles.all.indexOf(tile)} {@const rect = layout[index]}
!tile.empty && workspace.focusTile(index)} - onfocusin={() => !tile.empty && index !== workspace.focused && workspace.focusTile(index)} + onpointerdown={() => !tile.empty && workspace.tiles.focusOn(index)} + onfocusin={() => !tile.empty && index !== workspace.tiles.focused && workspace.tiles.focusOn(index)} > {#if tile.kind === 'board'}
@@ -357,7 +357,7 @@ ((cmdline = null), (cmdGlobal = false))} /> {/if} - {#if workspace.scratchOpen} + {#if workspace.scratch.open} {/if} @@ -368,10 +368,10 @@ (hintRoot = null)} /> {/if} - {#if workspace.overlay !== 'none'} - {@const Shown = sheets[workspace.overlay]} + {#if workspace.sheets.shown !== 'none'} + {@const Shown = sheets[workspace.sheets.shown]} -
e.target === e.currentTarget && workspace.closeSheet()}> +
e.target === e.currentTarget && workspace.sheets.close()}>
{/if} diff --git a/web/src/lib/Actions.svelte b/web/src/lib/Actions.svelte index 52cf834..829351b 100644 --- a/web/src/lib/Actions.svelte +++ b/web/src/lib/Actions.svelte @@ -10,7 +10,7 @@ import { hooks, runCommand } from './commands' import { workspace } from './store.svelte' - const node = $derived(workspace.selected) + const node = $derived(workspace.tree.selected) type Action = { label: string; run: () => unknown; danger?: boolean } @@ -19,17 +19,17 @@ const sealed = node.file?.encrypted ?? node.sealed const holdsFiles = isContainer(node) || node.kind === 'file' const list: Action[] = [ - { label: isContainer(node) ? 'open' : 'read', run: () => workspace.guard(() => workspace.openSelected()) }, + { label: isContainer(node) ? 'open' : 'read', run: () => workspace.guard(() => workspace.tree.openSelected()) }, { label: 'rename', run: () => hooks.openCommandLine(`:name ${node.name}`) }, - workspace.isMarked(node.id) - ? { label: 'put it back — not moving', run: () => workspace.mark(node, false) } - : { label: 'move — then open a folder and drop it there', run: () => workspace.mark(node, false) }, + workspace.tree.isMarked(node.id) + ? { label: 'put it back — not moving', run: () => workspace.tree.mark(node, false) } + : { label: 'move — then open a folder and drop it there', run: () => workspace.tree.mark(node, false) }, ] - if (workspace.marked.length && workspace.parentId !== null && !workspace.isMarked(node.id)) { - const count = workspace.marked.length + if (workspace.tree.marked.length && workspace.tree.parentId !== null && !workspace.tree.isMarked(node.id)) { + const count = workspace.tree.marked.length list.push({ label: `drop the ${count === 1 ? 'marked node' : `${count} marked nodes`} here`, - run: () => workspace.guard(() => workspace.drop()), + run: () => workspace.guard(() => workspace.tree.drop()), }) } if (node.kind === 'document') { @@ -41,7 +41,7 @@ if (node.shared) list.push({ label: 'new share link — the old one stops working', run: () => runCommand('share new') }) } // Inside a sealed folder only that folder opens, as a whole. - if (holdsFiles && node.kind !== 'space' && !(sealed && workspace.parentSealed)) { + if (holdsFiles && node.kind !== 'space' && !(sealed && workspace.tree.parentSealed)) { list.push( sealed ? { label: node.kind === 'file' ? 'take out of the vault' : 'unseal — take every file out', run: () => runCommand('decrypt') } @@ -53,7 +53,7 @@ }) function act(action: Action) { - workspace.closeSheet() + workspace.sheets.close() // The list is what the commands act on while it has the focus. workspace.pane = 'list' void action.run() diff --git a/web/src/lib/Agenda.svelte b/web/src/lib/Agenda.svelte index 892c688..011c494 100644 --- a/web/src/lib/Agenda.svelte +++ b/web/src/lib/Agenda.svelte @@ -16,22 +16,22 @@ const titles = { overdue: 'late', today: 'today', week: 'this week', doing: 'in hand' } as const - const groups = $derived(workspace.agendaGroups.filter((group) => group.cards.length > 0)) - const total = $derived(workspace.agendaCards.length) + const groups = $derived(workspace.cards.agendaGroups.filter((group) => group.cards.length > 0)) + const total = $derived(workspace.cards.agendaCards.length) $effect(() => { - const index = workspace.agendaCursor + const index = workspace.cards.agendaCursor sheet?.querySelector(`[data-index="${index}"]`)?.scrollIntoView({ block: 'nearest' }) }) /** An entry from an earlier year, in place in the diary. */ function open(entry: DiaryEntry) { - void workspace.guard(() => workspace.openPath(entry.path)) + void workspace.guard(() => workspace.tree.openPath(entry.path)) } /** Where a card sits in the walked order, across the groups. */ function indexOf(card: AgendaCard) { - return workspace.agendaCards.findIndex((c) => c.id === card.id) + return workspace.cards.agendaCards.findIndex((c) => c.id === card.id) } @@ -41,7 +41,7 @@ t - {#if workspace.agenda.diary_today === false} + {#if workspace.cards.agenda.diary_today === false}

nothing in the diary today {#if isTouchDevice} @@ -61,10 +61,10 @@

  • {#if isTouchDevice} - {/if} @@ -89,19 +89,19 @@

    nothing late, nothing due this week, nothing in hand. c captures the next thing.

    {/each} - {#if workspace.agenda.on_this_day.length} + {#if workspace.cards.agenda.on_this_day.length}
    -

    on this day {workspace.agenda.on_this_day.length}

    +

    on this day {workspace.cards.agenda.on_this_day.length}

      - {#each workspace.agenda.on_this_day as entry, at (entry.id)} + {#each workspace.cards.agenda.on_this_day as entry, at (entry.id)} {@const index = total + at}
    • @@ -53,7 +53,7 @@ {/each} @@ -62,9 +62,9 @@
  • - {#each workspace.board?.lists ?? [] as list, listIndex (list.id)} + {#each workspace.cards.board?.lists ?? [] as list, listIndex (list.id)} {@const kind = role(listIndex)} -
    +
    pick(listIndex, 0)}> @@ -78,11 +78,11 @@
  • {/each} - {#if kind === 'done' && (hidden > 0 || workspace.showOldDone)} + {#if kind === 'done' && (hidden > 0 || workspace.cards.showOldDone)}
  • -
  • {/if} @@ -132,7 +132,7 @@
    {:else}
    - {#if !workspace.boardSpace} + {#if !workspace.cards.boardSpace} no space has a board — :set board gives the one you are in one. {:else} no lists yet — :list <name> makes a column. then o adds a card to it. diff --git a/web/src/lib/Calendar.svelte b/web/src/lib/Calendar.svelte index dacf386..a1d7a71 100644 --- a/web/src/lib/Calendar.svelte +++ b/web/src/lib/Calendar.svelte @@ -10,7 +10,7 @@ const WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] - const month = $derived(workspace.calendarDay.slice(0, 7)) + const month = $derived(workspace.diary.calendarDay.slice(0, 7)) const title = $derived.by(() => { const [y, m] = month.split('-').map(Number) return new Date(y, m - 1, 1).toLocaleDateString(undefined, { month: 'long', year: 'numeric' }).toLowerCase() @@ -28,21 +28,21 @@ const today = $derived(formatDay(Math.floor(Date.now() / 1000))) const counts = $derived( - workspace.calendarEntries.reduce((all, entry) => all.set(entry.day, (all.get(entry.day) ?? 0) + 1), new Map()), + workspace.diary.calendarEntries.reduce((all, entry) => all.set(entry.day, (all.get(entry.day) ?? 0) + 1), new Map()), ) - const chosen = $derived(workspace.calendarEntries.filter((entry) => entry.day === workspace.calendarDay)) + const chosen = $derived(workspace.diary.calendarEntries.filter((entry) => entry.day === workspace.diary.calendarDay)) function pick(day: string) { - workspace.calendarDay = day - void workspace.guard(() => workspace.openCalendarDay()) + workspace.diary.calendarDay = day + void workspace.guard(() => workspace.diary.openCalendarDay()) }
    - {title} {workspace.calendarEntries.length} entries - - + {title} {workspace.diary.calendarEntries.length} entries + + C
    @@ -55,7 +55,7 @@
  • + {/if}
  • {/if}
    description
    - {#if workspace.describing && workspace.writing} + {#if workspace.cards.describing && workspace.drafts.writing} {#await import('./Editor.svelte')}
    loading editor
    {:then { default: Editor }} - {#key workspace.writing.session} - + {#key workspace.drafts.writing.session} + {/key} {/await} {:else if html} -
    workspace.describe()}> +
    workspace.cards.describe()}> {@html html}
    {:else} - + {/if}
    @@ -112,7 +112,7 @@
      {#each card.checklist as task, index (index)} -
    • pick(index)}> +
    • pick(index)}> {task.text}
    • diff --git a/web/src/lib/CertView.svelte b/web/src/lib/CertView.svelte index 1260fee..1d7ae67 100644 --- a/web/src/lib/CertView.svelte +++ b/web/src/lib/CertView.svelte @@ -41,7 +41,7 @@ try { let question = `password for ${name}` for (;;) { - const password = await workspace.askSecret(question) + const password = await workspace.prompts.askSecret(question) if (password === null) return try { return await load(password) diff --git a/web/src/lib/CommandLine.svelte b/web/src/lib/CommandLine.svelte index 97eb78b..c29ba28 100644 --- a/web/src/lib/CommandLine.svelte +++ b/web/src/lib/CommandLine.svelte @@ -117,7 +117,7 @@ $effect(() => { if (!isSearch) return const query = value.slice(1) - const timer = setTimeout(() => void workspace.guard(() => workspace.search(query, global)), 120) + const timer = setTimeout(() => void workspace.guard(() => workspace.tree.search(query, global)), 120) return () => clearTimeout(timer) }) @@ -146,8 +146,8 @@ // was typed rather than for the line a keystroke ago. const query = line.slice(1) await workspace.guard(async () => { - if (query && workspace.query !== query) await workspace.search(query, global) - if (query && (workspace.selected || workspace.foundCard || workspace.foundNote)) await workspace.openSelected() + if (query && workspace.tree.query !== query) await workspace.tree.search(query, global) + if (query && (workspace.tree.selected || workspace.tree.foundCard || workspace.tree.foundNote)) await workspace.tree.openSelected() }) hooks.focusList() return @@ -202,14 +202,14 @@ } else if (event.key === 'Tab' && isSearch) { // Nothing to complete in a search: Tab walks the results instead. event.preventDefault() - workspace.move(event.shiftKey ? -1 : 1) + workspace.tree.move(event.shiftKey ? -1 : 1) } else if (event.key === 'Tab') { event.preventDefault() if (!retarget(event.shiftKey ? -1 : 1)) complete(event.shiftKey ? -1 : 1) } else if (ctrl && isSearch && (event.key === 'n' || event.key === 'p')) { // The results, walked without leaving the line; Enter opens the one lit. event.preventDefault() - workspace.move(event.key === 'n' ? 1 : -1) + workspace.tree.move(event.key === 'n' ? 1 : -1) } else if (event.key === 'ArrowUp' || (ctrl && event.key === 'p')) { event.preventDefault() recall(1) @@ -244,7 +244,7 @@ {/if}
      - {#if isSearch}{global ? 'all spaces' : (workspace.space?.slug ?? '')}{/if} + {#if isSearch}{global ? 'all spaces' : (workspace.tree.space?.slug ?? '')}{/if} {#if capture}→ {targetNames[capture]} tab{/if}
      diff --git a/web/src/lib/DocumentPane.svelte b/web/src/lib/DocumentPane.svelte index 3aff1d8..380a51a 100644 --- a/web/src/lib/DocumentPane.svelte +++ b/web/src/lib/DocumentPane.svelte @@ -106,15 +106,15 @@ const element = event.target as HTMLElement const media = element.closest('img, video, audio') as HTMLImageElement | HTMLMediaElement | null if (!media || !(media.getAttribute('src') ?? media.querySelector('source')?.getAttribute('src') ?? '').includes('/api/media/')) return - await workspace.loadVault() - if (!workspace.vault?.configured || workspace.vault.unlocked || !media.isConnected) return + await workspace.vault.load() + if (!workspace.vault.status?.configured || workspace.vault.status.unlocked || !media.isConnected) return const button = document.createElement('button') button.className = 'sealed-embed' button.textContent = 'sealed — unlock to show' button.onclick = async (click) => { click.preventDefault() click.stopPropagation() - if (!(await workspace.ensureUnlocked())) return + if (!(await workspace.vault.ensureUnlocked())) return button.replaceWith(media) if (media instanceof HTMLImageElement) media.src = media.src else media.load() @@ -190,7 +190,7 @@ bind:this={title} placeholder="Untitled" bind:value={tile.draft.name} - oninput={() => workspace.touch(tile)} + oninput={() => workspace.drafts.touch(tile)} onkeydown={(event) => { // The title is the line above the text: any of these goes back down. if (['Enter', 'Escape', 'ArrowDown', 'Tab'].includes(event.key) && !event.shiftKey) { diff --git a/web/src/lib/Editor.svelte b/web/src/lib/Editor.svelte index 36d1450..8d32665 100644 --- a/web/src/lib/Editor.svelte +++ b/web/src/lib/Editor.svelte @@ -248,7 +248,7 @@ async function uploadFiles(files: File[]) { if (files.length === 0) return await workspace.guard(async () => { - const created = await workspace.attach(files, tile) + const created = await workspace.tree.attach(files, tile) if (created.length > 0) insertAtCursor(created.map(embedSnippet).join('\n\n')) }) } @@ -257,9 +257,9 @@ const changed = (text: string) => { if (writing) return writing.change(text) tile!.draft.body = text - workspace.touch(tile) + workspace.drafts.touch(tile) } - const save = () => void workspace.guard(() => (writing ? workspace.saveWriting() : workspace.save({ tile }))) + const save = () => void workspace.guard(() => (writing ? workspace.drafts.saveWriting() : workspace.drafts.save({ tile }))) onMount(() => { bindVim() diff --git a/web/src/lib/FilePane.svelte b/web/src/lib/FilePane.svelte index 74599fe..b947169 100644 --- a/web/src/lib/FilePane.svelte +++ b/web/src/lib/FilePane.svelte @@ -14,7 +14,7 @@ const node = $derived(tile.open!) const file = $derived(node.file!) /* A sealed file cannot be fetched while the vault is shut. */ - const locked = $derived(file.encrypted && !workspace.vault?.unlocked) + const locked = $derived(file.encrypted && !workspace.vault.status?.unlocked) const kind = $derived(previewKind(node.name, file.mime)) /* What the browser can show by itself; everything else is a download. */ const viewable = $derived(file.mime !== 'application/octet-stream') diff --git a/web/src/lib/Help.svelte b/web/src/lib/Help.svelte index 6e3f535..d18b456 100644 --- a/web/src/lib/Help.svelte +++ b/web/src/lib/Help.svelte @@ -18,12 +18,12 @@ export function openHelp(all = false) { helpDepth.all = all - workspace.openSheet('help') + workspace.sheets.open('help') } /** `?` in the help: everything, and the help away once it is all on show. */ export function deeperHelp() { - if (helpDepth.all) workspace.closeSheet() + if (helpDepth.all) workspace.sheets.close() else helpDepth.all = true } diff --git a/web/src/lib/Info.svelte b/web/src/lib/Info.svelte index 9077fa9..285fe43 100644 --- a/web/src/lib/Info.svelte +++ b/web/src/lib/Info.svelte @@ -15,42 +15,42 @@ /** Tab and Shift-Tab: the next part along, round the ends. */ export function turnInfo(by: number) { - const at = infoSections.indexOf(workspace.infoSection) - workspace.infoSection = infoSections[(at + by + infoSections.length) % infoSections.length] + const at = infoSections.indexOf(workspace.sheets.infoSection) + workspace.sheets.infoSection = infoSections[(at + by + infoSections.length) % infoSections.length] } /** Enter on a heading: the sheet away, and the text scrolled there. */ - export function goHeading(index = workspace.outlineCursor) { - const heading = workspace.outline[index] - workspace.closeSheet() + export function goHeading(index = workspace.sheets.outlineCursor) { + const heading = workspace.sheets.outline[index] + workspace.sheets.close() heading?.go() } /** What Enter and the button do: the shown revision becomes the document again. */ export async function restoreShown() { - const revision = workspace.revision + const revision = workspace.sheets.revision if (!revision) return - if (!(await workspace.ask(`bring back the version from ${formatStamp(revision.saved_at)}? the text now is kept in the history`))) return - await workspace.guard(() => workspace.restoreRevision(revision)) + if (!(await workspace.prompts.ask(`bring back the version from ${formatStamp(revision.saved_at)}? the text now is kept in the history`))) return + await workspace.guard(() => workspace.sheets.restoreRevision(revision)) } export function moveHistory(by: number) { - const last = Math.max(workspace.history.length - 1, 0) - const index = Math.min(Math.max(workspace.historyCursor + by, 0), last) - if (index !== workspace.historyCursor) void workspace.guard(() => workspace.showRevision(index)) + const last = Math.max(workspace.sheets.history.length - 1, 0) + const index = Math.min(Math.max(workspace.sheets.historyCursor + by, 0), last) + if (index !== workspace.sheets.historyCursor) void workspace.guard(() => workspace.sheets.showRevision(index)) } /** Enter on one of the document's files: the file is opened. */ export function openFile(file: NodeSummary) { - workspace.closeSheet() - return workspace.guard(() => workspace.openNode(file.id)) + workspace.sheets.close() + return workspace.guard(() => workspace.tree.openNode(file.id)) } /** `i`: the file embedded at the cursor, while the document is being written. */ export function embedFile(file: NodeSummary) { if (!workspace.editing) return workspace.say('embedding works while writing — i on the document first', 'error') hooks.insertText(embedSnippet(file)) - workspace.closeSheet() + workspace.sheets.close() } /** `y`: how to embed it, to paste into another document. */ @@ -61,29 +61,29 @@ /** Enter on a backlink: the linking document is opened. */ export function openBacklink(link: Backlink) { - workspace.closeSheet() - return workspace.guard(() => workspace.openPath(link.path)) + workspace.sheets.close() + return workspace.guard(() => workspace.tree.openPath(link.path)) } /** Enter on a card: the sheet away, and the card turned over on its board. */ export function turnCard(card: AgendaCard) { - workspace.closeSheet() - return workspace.guard(() => workspace.turnTo(card)) + workspace.sheets.close() + return workspace.guard(() => workspace.cards.turnTo(card)) } // Sharing is a key away; taking a link back from its readers asks first. export async function toggleSharing(node: NodeSummary | null | undefined) { if (!node) return workspace.say('nothing selected', 'error') - if (node.shared && !(await workspace.ask(`stop sharing "${node.name.trim() || node.slug}"? the link stops working`))) return - await workspace.guard(() => workspace.toggleShare(node.id)) + if (node.shared && !(await workspace.prompts.ask(`stop sharing "${node.name.trim() || node.slug}"? the link stops working`))) return + await workspace.guard(() => workspace.tree.toggleShare(node.id)) } // A new link retires the one readers have, so it asks first. export async function renewLink(node: NodeSummary | null | undefined) { if (!node) return workspace.say('nothing selected', 'error') if (!node.shared) return workspace.say('not shared — :share shares it', 'error') - if (!(await workspace.ask(`new share link for "${node.name.trim() || node.slug}"? the current one stops working`))) return - await workspace.guard(() => workspace.renewShareLink(node.id)) + if (!(await workspace.prompts.ask(`new share link for "${node.name.trim() || node.slug}"? the current one stops working`))) return + await workspace.guard(() => workspace.tree.renewShareLink(node.id)) } const REASONS: Record = { @@ -115,14 +115,14 @@ ] .filter(Boolean) .join(' · ') - const section = $derived(workspace.infoSection) + const section = $derived(workspace.sheets.infoSection) // The shallowest level in the document is the left edge, so a text that // starts at `##` is not indented by a level it never uses. - const top = $derived(Math.min(...workspace.outline.map((heading) => heading.level))) + const top = $derived(Math.min(...workspace.sheets.outline.map((heading) => heading.level))) // Against the text as it is now: what restoring would take out and put back. - const rows = $derived(section === 'history' && workspace.revision ? folded(diffLines(workspace.draft.body, workspace.revision.body)) : []) + const rows = $derived(section === 'history' && workspace.sheets.revision ? folded(diffLines(workspace.draft.body, workspace.sheets.revision.body)) : []) const changed = $derived(rows.some((row) => row.kind === 'gone' || row.kind === 'added')) /* @@ -133,9 +133,9 @@ const sharePath = $derived(doc?.share_token ? `${location.origin}/s/${doc.share_token}#…` : null) const tabs = $derived<[InfoSection, string][]>([ - ['outline', `outline ${workspace.outline.length}`], - ['history', `history ${workspace.history.length}`], - ['backlinks', `linked from ${workspace.backlinks.length}`], + ['outline', `outline ${workspace.sheets.outline.length}`], + ['history', `history ${workspace.sheets.history.length}`], + ['backlinks', `linked from ${workspace.sheets.backlinks.length}`], ['cards', `cards ${doc?.cards.filter((card) => card.done_at === null).length ?? 0}`], ['files', `files ${doc?.files.length ?? 0}`], ['share', doc?.shared ? 'shared' : 'private'], @@ -143,11 +143,11 @@ const cursor = $derived( { - outline: workspace.outlineCursor, - history: workspace.historyCursor, - backlinks: workspace.backlinksCursor, - cards: workspace.cardsCursor, - files: workspace.filesCursor, + outline: workspace.sheets.outlineCursor, + history: workspace.sheets.historyCursor, + backlinks: workspace.sheets.backlinksCursor, + cards: workspace.sheets.cardsCursor, + files: workspace.sheets.filesCursor, share: undefined, }[section], ) @@ -155,28 +155,28 @@ {#snippet actions()} - {#if section === 'history' && workspace.revision} + {#if section === 'history' && workspace.sheets.revision} {/if} {/snippet} {#if section === 'outline'}
        - {#each workspace.outline as heading, index (index)} + {#each workspace.sheets.outline as heading, index (index)}
      1. goHeading(index)} - onpointerenter={() => (workspace.outlineCursor = index)} + onpointerenter={() => (workspace.sheets.outlineCursor = index)} > {heading.text}
      2. @@ -187,9 +187,9 @@ {:else if section === 'history'}
          - {#each workspace.history as summary, index (summary.id)} + {#each workspace.sheets.history as summary, index (summary.id)} -
        • workspace.guard(() => workspace.showRevision(index))}> +
        • workspace.guard(() => workspace.sheets.showRevision(index))}> {formatStamp(summary.saved_at)} {relative(summary.saved_at)} · {reason(summary)} · {summary.size} chars
        • @@ -199,10 +199,10 @@
        - {#if workspace.revision} - {#if workspace.revision.name !== workspace.draft.name} + {#if workspace.sheets.revision} + {#if workspace.sheets.revision.name !== workspace.draft.name}
        - # {workspace.draft.name}
        -
        + # {workspace.revision.name}
        +
        + # {workspace.sheets.revision.name}
        {/if} {#each rows as row, index (index)} {#if row.kind === 'fold'} @@ -219,13 +219,13 @@
        {:else if section === 'backlinks'}
          - {#each workspace.backlinks as link, index (link.id)} + {#each workspace.sheets.backlinks as link, index (link.id)}
        • (workspace.backlinksCursor = index)} + onpointerenter={() => (workspace.sheets.backlinksCursor = index)} onclick={() => openBacklink(link)} > {link.name.trim() || link.slug} @@ -242,10 +242,10 @@
        • (workspace.cardsCursor = index)} + onpointerenter={() => (workspace.sheets.cardsCursor = index)} onclick={() => turnCard(card)} > {card.title} @@ -262,8 +262,8 @@
        • (workspace.filesCursor = index)} + class:at={index === workspace.sheets.filesCursor} + onpointerenter={() => (workspace.sheets.filesCursor = index)} onclick={() => openFile(file)} > {file.name} diff --git a/web/src/lib/Overview.svelte b/web/src/lib/Overview.svelte index 23385e1..859cfd2 100644 --- a/web/src/lib/Overview.svelte +++ b/web/src/lib/Overview.svelte @@ -20,32 +20,32 @@ }) function enter(spaceId: number) { - workspace.closeSheet() - void workspace.guard(() => workspace.enterSpace(spaceId)) + workspace.sheets.close() + void workspace.guard(() => workspace.tree.enterSpace(spaceId)) } /** A recent document opens in place, in its own space. */ function open(doc: NodeSummary) { - workspace.closeSheet() + workspace.sheets.close() void workspace.guard(async () => { const node = await api.getNode(doc.id) - await workspace.openPath(node.path.map((crumb) => crumb.slug).join('/')) + await workspace.tree.openPath(node.path.map((crumb) => crumb.slug).join('/')) }) } - +
            - {#each workspace.overview as { space, recent }, index (space.id)} + {#each workspace.sheets.overview as { space, recent }, index (space.id)}
          1. enter(space.id)} - onpointerenter={() => (workspace.overviewCursor = index)} + onpointerenter={() => (workspace.sheets.overviewCursor = index)} >
            {index < 10 ? (index + 1) % 10 : ''} diff --git a/web/src/lib/Prompt.svelte b/web/src/lib/Prompt.svelte index 8a25dda..c62148b 100644 --- a/web/src/lib/Prompt.svelte +++ b/web/src/lib/Prompt.svelte @@ -1,18 +1,18 @@ -{#if workspace.prompt} -
            - {workspace.prompt.text} +{#if workspace.prompts.prompt} +
            + {workspace.prompts.prompt.text} - - + +
            {/if} diff --git a/web/src/lib/Rail.svelte b/web/src/lib/Rail.svelte index 2ece825..7b1626d 100644 --- a/web/src/lib/Rail.svelte +++ b/web/src/lib/Rail.svelte @@ -33,7 +33,7 @@ const settings: Door = { icon: 'settings', label: 'Settings', keys: keysOf('global', 'settings'), run: () => runCommand('settings') } const focus = workspace.focus - const board: Door = workspace.space?.has_board || focus === 'board' + const board: Door = workspace.tree.space?.has_board || focus === 'board' ? { icon: 'board', label: 'Board', keys: keysOf('global', 'board'), run: () => runCommand('board'), on: focus === 'board' } : { icon: 'board', label: 'Give this space a board', keys: [':set board'], run: () => runCommand('set board'), dim: true } const agenda: Door = { icon: 'agenda', label: 'Agenda — late, due, in hand', keys: keysOf('global', 'agenda'), run: () => runCommand('agenda'), on: focus === 'agenda' } @@ -45,7 +45,7 @@ if (focus === 'board' || focus === 'card') { // The card verbs are doors too, so a phone — which has no H, L or // Space — can still move work along. - const card = !!workspace.card + const card = !!workspace.cards.card return [ [board, agenda, calendar, ...close], [ @@ -53,16 +53,16 @@ { icon: 'column-plus', label: 'New list', keys: [':list'], run: () => hooks.openCommandLine(':list ') }, ], [ - { icon: 'left', label: 'Send the card left', keys: keysOf('board', 'sendLeft'), run: () => workspace.guard(() => workspace.sendCard(-1)), dim: !card }, - { icon: 'check', label: 'Done, or back to the inbox', keys: keysOf('board', 'done'), run: () => workspace.guard(() => workspace.toggleDone()), dim: !card }, - { icon: 'right', label: 'Send the card right', keys: keysOf('board', 'sendRight'), run: () => workspace.guard(() => workspace.sendCard(1)), dim: !card }, + { icon: 'left', label: 'Send the card left', keys: keysOf('board', 'sendLeft'), run: () => workspace.guard(() => workspace.cards.sendCard(-1)), dim: !card }, + { icon: 'check', label: 'Done, or back to the inbox', keys: keysOf('board', 'done'), run: () => workspace.guard(() => workspace.cards.toggleDone()), dim: !card }, + { icon: 'right', label: 'Send the card right', keys: keysOf('board', 'sendRight'), run: () => workspace.guard(() => workspace.cards.sendCard(1)), dim: !card }, { icon: 'calendar', label: 'Due date', keys: keysOf('board', 'due'), run: () => hooks.openCommandLine(':due '), dim: !card }, { icon: 'repeat', label: 'Repeat once done', keys: [':repeat'], run: () => hooks.openCommandLine(':repeat '), dim: !card }, - { icon: 'doc', label: "The card's notes", keys: keysOf('board', 'notes'), run: () => workspace.guard(() => workspace.openCard()), dim: !card }, + { icon: 'doc', label: "The card's notes", keys: keysOf('board', 'notes'), run: () => workspace.guard(() => workspace.cards.openCard()), dim: !card }, ], [ - { icon: 'up', label: 'Card up its list', keys: keysOf('board', 'nudgeUp'), run: () => workspace.guard(() => workspace.nudgeCard(-1)), dim: !card }, - { icon: 'down', label: 'Card down its list', keys: keysOf('board', 'nudgeDown'), run: () => workspace.guard(() => workspace.nudgeCard(1)), dim: !card }, + { icon: 'up', label: 'Card up its list', keys: keysOf('board', 'nudgeUp'), run: () => workspace.guard(() => workspace.cards.nudgeCard(-1)), dim: !card }, + { icon: 'down', label: 'Card down its list', keys: keysOf('board', 'nudgeDown'), run: () => workspace.guard(() => workspace.cards.nudgeCard(1)), dim: !card }, { icon: 'rename', label: card ? 'Rename the card' : 'Rename the list', @@ -84,7 +84,7 @@ [ { icon: 'doc-plus', label: 'New document', keys: keysOf('tree', 'new'), run: () => runCommand('new') }, { icon: 'folder-plus', label: 'New folder', keys: keysOf('tree', 'mkdir'), run: () => hooks.openCommandLine(':mkdir ') }, - ...(workspace.space ? [board] : []), + ...(workspace.tree.space ? [board] : []), ], [ agenda, @@ -96,8 +96,8 @@ [ { icon: 'upload', label: 'Upload files into this folder', keys: [':upload'], run: () => hooks.pickTreeFiles(false) }, { icon: 'trash', label: 'The trash — what was deleted', keys: [':trash'], run: () => runCommand('trash') }, - ...(workspace.marked.length - ? [{ icon: 'drop' as const, label: `Drop ${dropping(workspace.marked)} here`, keys: keysOf('tree', 'drop'), run: () => workspace.guard(() => workspace.drop()), on: true }] + ...(workspace.tree.marked.length + ? [{ icon: 'drop' as const, label: `Drop ${dropping(workspace.tree.marked)} here`, keys: keysOf('tree', 'drop'), run: () => workspace.guard(() => workspace.tree.drop()), on: true }] : []), ], [ diff --git a/web/src/lib/Scratch.svelte b/web/src/lib/Scratch.svelte index d22d770..ec3ef5c 100644 --- a/web/src/lib/Scratch.svelte +++ b/web/src/lib/Scratch.svelte @@ -13,63 +13,63 @@ let list = $state() - const note = $derived(workspace.scratchNote) - const html = $derived(note && !workspace.scratchEditing ? renderMarkdown(note.body) : '') + const note = $derived(workspace.scratch.note) + const html = $derived(note && !workspace.scratch.editing ? renderMarkdown(note.body) : '') $effect(() => { - const at = workspace.scratchCursor + const at = workspace.scratch.cursor list?.querySelector(`[data-index="${at}"]`)?.scrollIntoView({ block: 'nearest' }) }) function pick(index: number) { - if (workspace.scratchEditing) return - workspace.scratchCursor = index + if (workspace.scratch.editing) return + workspace.scratch.cursor = index }
            scratch - {workspace.scratchNotes.length} {workspace.scratchNotes.length === 1 ? 'note' : 'notes'} + {workspace.scratch.notes.length} {workspace.scratch.notes.length === 1 ? 'note' : 'notes'} Q
              - {#if workspace.scratchEditing && workspace.scratchNew} -
            • {headline(workspace.writing?.text ?? '') || 'new note'}now
            • + {#if workspace.scratch.editing && workspace.scratch.writingNew} +
            • {headline(workspace.drafts.writing?.text ?? '') || 'new note'}now
            • {/if} - {#each workspace.scratchNotes as entry, index (entry.id)} + {#each workspace.scratch.notes as entry, index (entry.id)}
            • pick(index)} - ondblclick={() => workspace.editScratchNote()} + ondblclick={() => workspace.scratch.editNote()} > {headline(entry.body) || '—'} {relative(entry.updated_at)}
            • {:else} - {#if !workspace.scratchNew} + {#if !workspace.scratch.writingNew}
            • nothing here — o takes a note
            • {/if} {/each}
            - {#if workspace.scratchEditing && workspace.writing} + {#if workspace.scratch.editing && workspace.drafts.writing} {#await import('./Editor.svelte')}
            loading editor
            {:then { default: Editor }} - {#key workspace.writing.session} - + {#key workspace.drafts.writing.session} + {/key} {/await} {:else if html} -
            workspace.editScratchNote()}> +
            workspace.scratch.editNote()}> {@html html}
            @@ -79,7 +79,7 @@
            - {#if workspace.scratchEditing} + {#if workspace.scratch.editing}