From a4c236d5bb1ecc66e72aa679dfced94a6e26c313 Mon Sep 17 00:00:00 2001 From: sawradip Date: Tue, 3 Nov 2026 01:57:12 +0600 Subject: [PATCH 1/2] feat(reader): a progress bar while a book loads for editing, and a smooth start of author mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The "Loading the book for editing… n of m" notice gets a bar that fills as the pages arrive (role=progressbar); when the book is whole the bar fills and fades out instead of vanishing. - While the book loads, the editor's layout is already in place: quiet placeholders where the tool rail and the sidebar will be, and the sidebar's room kept, so the pages do not shrink and jump when editing begins. The real rail and sidebar fade in over the placeholders. Reduced motion: no animation, still no jump. Co-Authored-By: Claude Opus 5.5 --- packages/reader/src/ChalkReader.tsx | 27 ++++++++++++++++++++++----- packages/reader/src/styles.css | 23 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/packages/reader/src/ChalkReader.tsx b/packages/reader/src/ChalkReader.tsx index 1536072..6e2f305 100644 --- a/packages/reader/src/ChalkReader.tsx +++ b/packages/reader/src/ChalkReader.tsx @@ -530,7 +530,7 @@ function Reader(props: Omit & { book: Book; config: }, [streamed, page, pages.length, config.streaming.window]); const wantsAuthor = authoringOn && mode === 'author'; // while the rest of a streamed book loads for editing, say how far it is (not a notice that fades: it stays until the book is whole) - const [loadProgress, setLoadProgress] = useState<{ done: number; total: number } | null>(null); + const [loadProgress, setLoadProgress] = useState<{ done: number; total: number; finished?: boolean } | null>(null); useEffect(() => { if (!streamed || !wantsAuthor || streamed.complete) { setLoadProgress(null); return; } let last = 0; @@ -538,11 +538,13 @@ function Reader(props: Omit & { book: Book; config: show(); // pages arrive many times a second: draw the count a few times a second const off = streamed.subscribe(() => { const now = Date.now(); if (now - last >= 200) { last = now; show(); } }); + let fade: ReturnType | undefined; streamed.loadAll().then( - () => setLoadProgress(null), + // the bar fills, then fades away while the editor's tools fade in over their placeholders (nothing on the page moves) + () => { setLoadProgress((p) => (p ? { done: p.total, total: p.total, finished: true } : null)); fade = setTimeout(() => setLoadProgress(null), 450); }, (e: unknown) => { setLoadProgress(null); setToast(e instanceof Error ? e.message : String(e)); }, ); - return off; + return () => { off(); clearTimeout(fade); }; }, [streamed, wantsAuthor]); // eslint-disable-line react-hooks/exhaustive-deps // search a streamed book with its text file: pages not loaded yet are searched by their text runs useEffect(() => { if (streamed && (panel === 'search' || query)) void streamed.searchText().then(setStreamText); }, [streamed, panel, query]); @@ -809,6 +811,9 @@ function Reader(props: Omit & { book: Book; config: return lesson.pages.find((p) => p.id === sel.pageId)?.objects.find((o) => o.id === sel.id) ?? null; }, [sel, lesson]); const sidebar = author && (rootWidth >= 720 || !!selectedObject || !!adding); + // the book is loading before editing begins: the editor's layout is already in place (placeholders for the rail and the sidebar), + // so when it is ready its tools fade in where the placeholders were and the pages do not move or change size + const preparing = wantsAuthor && !author; // a selection that was undone away is gone useEffect(() => { if (sel && !selectedObject) setSel(null); }, [sel, selectedObject]); // on a phone the properties sheet covers the lower part: bring a newly selected object up above it @@ -835,7 +840,7 @@ function Reader(props: Omit & { book: Book; config: return ( -
+
= 720) ? '' : undefined} data-mode={authoringOn ? mode : undefined} lang={locale} style={props.style} onKeyDown={onKey}> & { book: Book; config: requestAnimationFrame(() => root.current?.querySelector('.chalk-sidebar :is(input, button, select):not([disabled])')?.focus({ preventScroll: true })); }} /> )} + {preparing &&
} + {preparing && rootWidth >= 720 &&
} {author && } {sidebar && ( x.id === sel.pageId) : pages[page]} selected={selectedObject} @@ -896,7 +903,17 @@ function Reader(props: Omit & { book: Book; config: { const f = e.target.files?.[0]; if (f) void importNotes(f); e.target.value = ''; }} /> {loadProgress - ?
{at_.loadingProgress.replace('{done}', num(authorLocale, loadProgress.done)).replace('{total}', num(authorLocale, loadProgress.total))}
+ ? (() => { + // a bar that fills as the pages arrive, with the count under it in words + const label = at_.loadingProgress.replace('{done}', num(authorLocale, loadProgress.done)).replace('{total}', num(authorLocale, loadProgress.total)); + const pct = loadProgress.total ? Math.min(100, (loadProgress.done / loadProgress.total) * 100) : 0; + return ( +
+ {label} + +
+ ); + })() : (toast ?? saveNote) &&
{toast ?? saveNote}
} {lookup.missing.size > 0 && goTo(i)} />} {dockCfg && ( diff --git a/packages/reader/src/styles.css b/packages/reader/src/styles.css index 589ce84..750d98a 100644 --- a/packages/reader/src/styles.css +++ b/packages/reader/src/styles.css @@ -477,6 +477,29 @@ button.chalk-box:focus-visible { outline: 3px solid var(--chalk-focus); outline- font-size: var(--chalk-text-sm); box-shadow: var(--chalk-float-shadow); animation: chalk-fade-in var(--chalk-dur) var(--chalk-ease-out); } @keyframes chalk-fade-in { from { opacity: 0; } to { opacity: 1; } } +/* the whole book loading for editing: a bar that fills, with the count over it */ +.chalk-load { display: grid; gap: 8px; width: min(340px, calc(100% - 32px)); } +.chalk-load-track { display: block; height: 6px; border-radius: 999px; background: color-mix(in oklch, var(--chalk-surface) 22%, transparent); overflow: hidden; } +.chalk-load-fill { display: block; height: 100%; border-radius: inherit; background: var(--chalk-accent-soft); transition: width 200ms linear; } +/* filled: it fades away while the editor's tools fade in */ +.chalk-load[data-finished] { animation: chalk-fade-out 450ms var(--chalk-ease-out) forwards; } +@keyframes chalk-fade-out { from { opacity: 1; } to { opacity: 0; } } +@media (prefers-reduced-motion: reduce) { .chalk-load-fill { transition: none; } .chalk-load[data-finished] { animation: none; } } + +/* the editor getting ready (the whole book loading): its rail and sidebar as quiet placeholders, in the places they will take */ +.chalk-ghost-rail { width: 46px; height: min(320px, 60%); padding: 0; box-shadow: none; background: color-mix(in oklch, var(--chalk-surface) 60%, transparent); } +.chalk-ghost-sidebar { gap: 14px; padding: var(--chalk-space-lg); box-shadow: none; background: color-mix(in oklch, var(--chalk-surface) 60%, transparent); } +.chalk-ghost-sidebar span { display: block; height: 12px; border-radius: 6px; background: var(--chalk-surface-2); } +.chalk-ghost-sidebar span:first-child { width: 55%; height: 16px; } +.chalk-ghost-sidebar span:nth-child(3) { width: 80%; } +.chalk-ghost-sidebar span:nth-child(4) { width: 65%; } +.chalk-ghost-rail, .chalk-ghost-sidebar span { animation: chalk-ghost-pulse 1.6s ease-in-out infinite; } +@keyframes chalk-ghost-pulse { 50% { opacity: 0.55; } } +/* and the real ones fade in over them */ +.chalk-reader[data-mode="author"] .chalk-rail:not(.chalk-ghost-rail), .chalk-reader[data-mode="author"] .chalk-sidebar:not(.chalk-ghost-sidebar) { animation: chalk-fade-in 260ms var(--chalk-ease-out); } +@media (prefers-reduced-motion: reduce) { + .chalk-ghost-rail, .chalk-ghost-sidebar span, .chalk-reader[data-mode="author"] .chalk-rail, .chalk-reader[data-mode="author"] .chalk-sidebar { animation: none; } +} /* in a side dock the page number stacks: the current page over the count, a button wide */ .chalk-dock.is-left .chalk-dock-page, .chalk-dock.is-right .chalk-dock-page { flex-direction: column; align-items: center; justify-content: center; gap: 0; width: var(--chalk-btn); height: 48px; padding: 0; line-height: 1.15; From dae9defee6ed342117bf8a3ea893a9ef649f86fd Mon Sep 17 00:00:00 2001 From: sawradip Date: Tue, 3 Nov 2026 02:02:28 +0600 Subject: [PATCH 2/2] chore(release): 0.1.6 Co-Authored-By: Claude Opus 5.5 --- release.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.json b/release.json index 23efdfb..9655a75 100644 --- a/release.json +++ b/release.json @@ -1,3 +1,3 @@ { - "version": "0.1.5" + "version": "0.1.6" }