Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions packages/reader/src/ChalkReader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -530,19 +530,21 @@ function Reader(props: Omit<ChalkReaderProps, 'config'> & { 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;
const show = () => setLoadProgress({ done: streamed.loadedPages, total: streamed.totalPages });
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<typeof setTimeout> | 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]);
Expand Down Expand Up @@ -809,6 +811,9 @@ function Reader(props: Omit<ChalkReaderProps, 'config'> & { 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
Expand All @@ -835,7 +840,7 @@ function Reader(props: Omit<ChalkReaderProps, 'config'> & { book: Book; config:

return (
<PresenterProvider value={pr}>
<div ref={root} className={`chalk-reader ${props.className ?? ''}`} data-theme={dark} data-dock={dockPos ?? 'none'} data-panel={panel ?? undefined} data-sidebar={sidebar ? '' : undefined} data-mode={authoringOn ? mode : undefined} lang={locale} style={props.style} onKeyDown={onKey}>
<div ref={root} className={`chalk-reader ${props.className ?? ''}`} data-theme={dark} data-dock={dockPos ?? 'none'} data-panel={panel ?? undefined} data-sidebar={sidebar || (preparing && rootWidth >= 720) ? '' : undefined} data-mode={authoringOn ? mode : undefined} lang={locale} style={props.style} onKeyDown={onKey}>
<Viewer
ref={viewer} book={book} pages={pages} layout={layout} fit={fit} zoom={zoom} zoomRange={config.zoom}
onZoom={setZoom} onPage={changePage} page={page} renderHtmlObject={author ? renderPreview : renderHtmlObject}
Expand Down Expand Up @@ -867,6 +872,8 @@ function Reader(props: Omit<ChalkReaderProps, 'config'> & { book: Book; config:
requestAnimationFrame(() => root.current?.querySelector<HTMLElement>('.chalk-sidebar :is(input, button, select):not([disabled])')?.focus({ preventScroll: true }));
}} />
)}
{preparing && <div className="chalk-rail chalk-ghost-rail" aria-hidden />}
{preparing && rootWidth >= 720 && <div className="chalk-sidebar chalk-ghost-sidebar" aria-hidden><span /><span /><span /><span /></div>}
{author && <AuthorRail tool={authorTool} onTool={setAuthorTool} adding={adding} onMedia={pickMedia} locale={authorLocale} t={at_} />}
{sidebar && (
<AuthorSidebar lesson={au.lesson} page={sel ? au.lesson.pages.find((x) => x.id === sel.pageId) : pages[page]} selected={selectedObject}
Expand Down Expand Up @@ -896,7 +903,17 @@ function Reader(props: Omit<ChalkReaderProps, 'config'> & { book: Book; config:
<input ref={notesFile} type="file" accept=".chalk,application/vnd.chalk+zip,application/zip" hidden
onChange={(e) => { const f = e.target.files?.[0]; if (f) void importNotes(f); e.target.value = ''; }} />
{loadProgress
? <div className="chalk-toast" role="status">{at_.loadingProgress.replace('{done}', num(authorLocale, loadProgress.done)).replace('{total}', num(authorLocale, loadProgress.total))}</div>
? (() => {
// 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 (
<div className="chalk-toast chalk-load" data-finished={loadProgress.finished ? '' : undefined} role="progressbar" aria-label={label} aria-valuemin={0} aria-valuemax={loadProgress.total} aria-valuenow={loadProgress.done}>
<span className="chalk-load-text">{label}</span>
<span className="chalk-load-track" aria-hidden><span className="chalk-load-fill" style={{ width: `${pct}%` }} /></span>
</div>
);
})()
: (toast ?? saveNote) && <div className="chalk-toast" role="status">{toast ?? saveNote}</div>}
{lookup.missing.size > 0 && <MissingChip lesson={lesson} missing={lookup.missing} t={t} locale={locale} onGo={(i) => goTo(i)} />}
{dockCfg && (
Expand Down
23 changes: 23 additions & 0 deletions packages/reader/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading