From ecfbab0eaaf19086e4db6d72d0bd5e6eec599dca Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 17 Oct 2026 17:27:59 +0200 Subject: [PATCH] ui: share view in the shell, icon rail, phone polish - shared view uses the bar, porthole and status band; outline gets its own window on wide screens; j/k, gg/G, Ctrl-d/u scroll with a vim-style ruler - rail doors are line icons in groups with hover tooltips and keycaps - phone: number-only space pills, no nested rims, icon dock, toast messages, back button and one-line meta in the document header - keycaps (.kbd) wherever keys are shown, styled scrollbars in Chrome, outline tracks position on jumps, breakpoints unified at 767px --- web/src/App.svelte | 110 +++------------ web/src/app.css | 229 +++++++++++++++++++++++++++++++- web/src/lib/Bar.svelte | 108 +++------------ web/src/lib/Clock.svelte | 21 +++ web/src/lib/DocumentPane.svelte | 68 ++++++---- web/src/lib/Icon.svelte | 53 ++++++++ web/src/lib/Outline.svelte | 40 ++++-- web/src/lib/Overview.svelte | 20 ++- web/src/lib/Rail.svelte | 222 ++++++++++++++++++++++--------- web/src/lib/Shared.svelte | 205 +++++++++++++++++++++------- web/src/lib/Sidebar.svelte | 6 +- web/src/lib/StatusBar.svelte | 66 ++++----- web/src/lib/commands.ts | 5 +- web/src/lib/util.ts | 25 ++++ 14 files changed, 796 insertions(+), 382 deletions(-) create mode 100644 web/src/lib/Clock.svelte create mode 100644 web/src/lib/Icon.svelte diff --git a/web/src/App.svelte b/web/src/App.svelte index 5647dc1..0c5be2f 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -91,15 +91,16 @@ } // What the empty pane offers, in the key/value shape of a status readout. - const offers: [string, string][] = [ - ['o', 'write a new document'], - ['O', 'make a folder'], - ['j / k', 'browse, l opens'], - ['1 … 9', 'switch space, Tab cycles'], - ['w', 'overview of every space'], - ['b', "this space's board"], - ['/', 'search this space'], - ['?', 'help'], + // Keys are keycaps, so `o` and `O` cannot be read as the same key. + const offers: [string[], string][] = [ + [['o'], 'write a new document'], + [['⇧ O'], 'make a folder'], + [['j', 'k'], 'browse, l opens'], + [['1…9'], 'switch space, Tab cycles'], + [['w'], 'overview of every space'], + [['b'], "this space's board"], + [['/'], 'search this space'], + [['?'], 'help'], ] /* @@ -520,8 +521,8 @@

nothing open

- {#each offers as [key, what] (key)} -
{key}
+ {#each offers as [keys, what] (what)} +
{#each keys as key (key)}{key}{/each}
{what}
{/each}
status
@@ -564,78 +565,9 @@ diff --git a/web/src/lib/Clock.svelte b/web/src/lib/Clock.svelte new file mode 100644 index 0000000..8d801ac --- /dev/null +++ b/web/src/lib/Clock.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/web/src/lib/DocumentPane.svelte b/web/src/lib/DocumentPane.svelte index 991bba4..cb6e91f 100644 --- a/web/src/lib/DocumentPane.svelte +++ b/web/src/lib/DocumentPane.svelte @@ -3,7 +3,8 @@ import { renderMarkdown } from './markdown' import Outline from './Outline.svelte' import { workspace } from './store.svelte' - import { formatStamp, formatWeekday, isTouchDevice, relative } from './util' + import Icon from './Icon.svelte' + import { formatStamp, formatWeekday, isTouchDevice, relative, scrollPage } from './util' const doc = $derived(workspace.open!) const html = $derived(renderMarkdown(workspace.editing ? workspace.draft.body : doc.body)) @@ -30,24 +31,7 @@ // after the new one has set the hook — so it only clears its own. const scroll = (amount: ScrollAmount) => { if (!reader) return false - const line = parseFloat(getComputedStyle(reader).lineHeight) || 24 - const half = reader.clientHeight / 2 - switch (amount) { - case 'top': - reader.scrollTo({ top: 0 }) - break - case 'bottom': - reader.scrollTo({ top: reader.scrollHeight }) - break - case 'halfdown': - reader.scrollBy({ top: half }) - break - case 'halfup': - reader.scrollBy({ top: -half }) - break - default: - reader.scrollBy({ top: amount * line * 3 }) - } + scrollPage(reader, amount) return true } hooks.scrollReader = scroll @@ -74,6 +58,10 @@
+ + {workspace.here}/{doc.slug} {#if workspace.dirty}modified{/if} @@ -86,13 +74,10 @@
edited
{relative(doc.updated_at)}
words
-
{words}
+
{words}words
- {#if isTouchDevice} - - {/if} {#if workspace.editing} @@ -104,7 +89,7 @@ {doc.shared ? 'unshare' : 'share'} - +
@@ -277,7 +262,7 @@ position: sticky; top: 0; max-width: 30ch; - padding: 6px 0 0 44px; + padding: 6px 0 0 32px; } @container (max-width: 1000px) { .side { display: none; } @@ -302,9 +287,36 @@ } .mobilebar button { flex: 1; padding: 8px; } - @media (max-width: 760px) { - header { padding: 14px 16px 10px; } - .row { flex-direction: column; align-items: flex-start; } + .back { display: none; } + .unit { display: none; } + + /* + * A phone: a back button where the bar's crumbs would be, the readout on one + * line without its captions, and the actions as one row of equal buttons + * big enough for a thumb. Closing is the back button's job. + */ + @media (max-width: 767px) { + header { padding: 10px 14px 10px 8px; gap: 8px; } + .crumb { gap: 8px; } + .back { + display: grid; + place-items: center; + width: 32px; + height: 32px; + flex: none; + padding: 0; + border: 0; + color: var(--light); + } + .row { flex-direction: column; align-items: stretch; gap: 10px; padding-left: 6px; } + .meta { flex-wrap: nowrap; gap: 0; overflow: hidden; white-space: nowrap; } + .meta dt { display: none; } + .meta dd { margin: 0; } + .meta dd + dt + dd::before { content: '·'; margin: 0 8px; color: var(--muted); } + .unit { display: inline; margin-left: 0.5ch; } + .actions { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 6px; } + .actions button { height: 36px; } + .actions .close { display: none; } .sharebar { margin: 0 16px 8px; } .title { margin: 8px 16px 4px; } .reading { padding: 14px 16px 30vh; grid-template-columns: minmax(0, 1fr); } diff --git a/web/src/lib/Icon.svelte b/web/src/lib/Icon.svelte new file mode 100644 index 0000000..193c556 --- /dev/null +++ b/web/src/lib/Icon.svelte @@ -0,0 +1,53 @@ + + + + + + + diff --git a/web/src/lib/Outline.svelte b/web/src/lib/Outline.svelte index f307d70..a343059 100644 --- a/web/src/lib/Outline.svelte +++ b/web/src/lib/Outline.svelte @@ -41,18 +41,31 @@ headings = found current = found[0]?.id ?? '' - // The section whose heading last crossed the upper third is the one being - // read; a band rather than a line, so a short section still registers. - const observer = new IntersectionObserver( - (entries) => { - for (const entry of entries) { - if (entry.isIntersecting) current = entry.target.id - } - }, - { root: scroller, rootMargin: '-10% 0px -70% 0px' }, - ) - for (const element of elements) observer.observe(element) - return () => observer.disconnect() + // The section being read is the last one whose heading has passed the + // upper third of the view — worked out from positions on every scroll, + // so a jump with G or Ctrl-d lands on the right one too. Scrolled to the + // very end, the last heading wins even if it never got that high. + let frame = 0 + const spy = () => { + frame = 0 + const top = scroller.getBoundingClientRect().top + const line = top + scroller.clientHeight / 3 + const room = scroller.scrollHeight - scroller.clientHeight + const end = room > 2 && scroller.scrollTop >= room - 2 + let id = found[0]?.id ?? '' + for (const element of elements) { + if (end || element.getBoundingClientRect().top <= line) id = element.id + else break + } + current = id + } + const onscroll = () => (frame ||= requestAnimationFrame(spy)) + scroller.addEventListener('scroll', onscroll, { passive: true }) + spy() + return () => { + scroller.removeEventListener('scroll', onscroll) + cancelAnimationFrame(frame) + } }) function jump(id: string) { @@ -105,5 +118,6 @@ button:focus-visible { background: var(--light); color: var(--ink); } .marker { width: 5px; height: 5px; transform: translateY(-2px); } - .text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + /* A margin is narrow: long headings wrap rather than lose their ends. */ + .text { min-width: 0; line-height: 1.4; overflow-wrap: anywhere; } diff --git a/web/src/lib/Overview.svelte b/web/src/lib/Overview.svelte index e0d9706..75fde50 100644 --- a/web/src/lib/Overview.svelte +++ b/web/src/lib/Overview.svelte @@ -86,7 +86,13 @@ {/each} - + diff --git a/web/src/lib/Rail.svelte b/web/src/lib/Rail.svelte index 1e28bbc..5c0dcff 100644 --- a/web/src/lib/Rail.svelte +++ b/web/src/lib/Rail.svelte @@ -1,49 +1,92 @@ -