Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 3 additions & 3 deletions apps/docs/docs/web-app/export-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ title: How the export pipeline works
## Building a Word file is for Word files only

Those four are the whole list. Nothing else in the app touches `docx` or
`mammoth`; in particular, **preview does not**. The editor's preview dialog
`mammoth`; in particular, **preview does not**. The editor's preview mode
renders the lesson model directly with `LessonView` — the same component the
public `/hub/:id` page uses — so previewing builds no document, waits on no
chunk, and shows exactly what a reader will see. (It used to build a docx and
Expand All @@ -30,8 +30,8 @@ not the export pipeline.
### On screen it follows the theme

`LessonView` draws a lesson in the app's own theme, light or dark, on both
surfaces that show one — the public lesson page and the editor's preview dialog
exactly as [interactive mode](./interactive-mode.md#what-it-looks-like) does.
surfaces that show one — the public lesson page and the editor's preview mode —
exactly as [interactive mode](./interactive-mode.md#what-it-looks-like) does.
It keeps the export's measurements (the `fitWithin` image maths against
`DOCX_MAX_IMAGE_WIDTH`, the heading sizes, the spacing), so a lesson keeps the
shape it will print in; only the colours and the typeface are the theme's.
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/docs/web-app/mobile-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ actions in an overflow menu. The sidebar holds one copy at any width.
`100vh` is the _large_ viewport: it ignores the browser's retractable address
bar, so a `max-h-[90vh]` dialog can be taller than what's actually on screen.
Page wrappers use `min-h-dvh` and the tall dialogs (history, merge,
collaborate, lesson preview) use `max-h-[85dvh]`/`max-h-[90dvh]`.
collaborate) use `max-h-[85dvh]`/`max-h-[90dvh]`.

`HomePage`'s hero deliberately keeps `min-h-[70vh] md:min-h-[78vh]`: `dvh` there
would resize the hero as the address bar hides and shows during scroll, which
Expand Down
10 changes: 10 additions & 0 deletions apps/docs/docs/web-app/navigating-large-lessons.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,16 @@ Three deliberate limits:
A collapsed section shows "hidden" instead of its block count: folded away in
the document, the outline is the only place it appears at all.

The same outline stands beside the editor's **Preview**, with `readOnly` set:
collapse-all and add-section drop away, and the list keeps working unchanged.
It can, because it addresses a section by `data-section-id` and `LessonView`
publishes that attribute — with the same `scroll-mt-(--header-h)` — on each
`<section>` it renders, exactly as `SectionCard` does in the editor. The two
surfaces never coexist (preview replaces the panes rather than sitting beside
them), so one id always matches one element. A 37-screen lesson is no easier to
move around in when you are reading it back than when you are writing it, which
is the whole reason the preview gets an outline at all.

## What isn't solved yet

The section outline above was one of the answers this page called for. One
Expand Down
5 changes: 5 additions & 0 deletions apps/docs/docs/web-app/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ PDF printing.
block keeps it under the pointer instead of flinging the page, and returning
to the editor puts you back at the block you were last editing (see
[Navigating large lessons](./navigating-large-lessons.md)).
- **Preview** - a toggle, not a window. Pressing **Preview** replaces the editing
panes with the lesson exactly as a reader sees it, in the same place and at the
same width, with the section outline still beside it and still navigating;
pressing it again returns you to editing. Nothing is built to show it — see
[How the export pipeline works](./export-pipeline.md).
- **Collapse sections** - fold any section to its header, or the whole lesson at
once, turning 37 screens into 1.5. A folded section still says what's inside
it, still reorders, still takes a dragged block (dwell over it and it springs
Expand Down
27 changes: 14 additions & 13 deletions apps/docs/docs/web-app/pages-and-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ column — the lesson's sticky tab bar and a Suspense fallback — import
`PAGE_WIDTHS` rather than restating the number.

Two places opt out and say so where they do: the marketing hero (a full-bleed
gradient) and the editor's three panes.
gradient) and the editor's panes.

## Laying out against the container

Expand All @@ -76,27 +76,28 @@ off a `lg:`/`xl:` viewport breakpoint.

This matters because the sidebar is 16rem open and 3rem collapsed, so how much
room a page has is not a function of the window's width. The editor's outline
pane appears once the page column passes 52rem and its preview pane at 70rem; the
lesson's "About" rail moves alongside the lesson at 52rem. Collapse the sidebar
and those thresholds are crossed immediately, with no change to the window:
pane appears once the page column passes 52rem; the lesson's "About" rail moves
alongside the lesson at the same 52rem. Collapse the sidebar and the threshold
is crossed immediately, with no change to the window:

| Window | Sidebar | Page column | Outline | Preview |
| ------ | ------- | ----------- | ------- | ------- |
| 1440px | open | 1184px | yes | yes |
| 1280px | open | 1024px | yes | no |
| 1280px | rail | 1232px | yes | **yes** |
| 1100px | rail | 1052px | yes | no |
| Window | Sidebar | Page column | Outline |
| ------ | ------- | ----------- | ------- |
| 1440px | open | 1184px | yes |
| 1280px | open | 1024px | yes |
| 1024px | open | 768px | no |
| 1024px | rail | 976px | **yes** |

That is what lets one sidebar configuration serve the editor as well as every
other page. Viewport breakpoints could not: at 1280px they have no way to know
whether 256px of the screen is currently a sidebar or not.
other page. Viewport breakpoints could not: at 1024px they have no way to know
whether 256px of the screen is currently a sidebar or not — the last two rows
are the same window, and only one of them has room for an outline.

## The routes

| Route | Page | What it does |
| -------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/` | **Home** | Landing page. Signed out: a marketing splash (animated floating words + feature blurbs). Signed in: a dashboard (latest-lessons feed, your activity, activity from people you follow, notifications). |
| `/editor` | **Editor** | The lesson builder. Three panes — section outline, document, live preview — each appearing once the page column has room for it. |
| `/editor` | **Editor** | The lesson builder. Two panes — section outline and document — the outline appearing once the page column has room for it. **Preview** toggles the document pane for the reader's view of the lesson. |
| `/editor/lessons` | **Editor** | The [lessons this device holds](./local-lessons.md), over the editor — switch between them, copy, rename or delete one. |
| `/editor/history` | **Editor** | The version-history panel, over the editor. |
| `/editor/variations` | **Editor** | The [variations](./lesson-variations.md) panel, over the editor. |
Expand Down
5 changes: 3 additions & 2 deletions apps/docs/docs/web-app/project-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ src/
PageBody.jsx the content column, in two documented widths (wide / reading). Exports PAGE_WIDTHS for the two things that need the width but can't be the column
EditorShell.jsx the editor's nested routes behind one lazy import; mounts no chrome of its own
editor/
SectionOutline.jsx the editor's left-hand section list (52rem+ of page column) — jump to a section, collapse them all
SectionOutline.jsx the editor's left-hand section list (52rem+ of page column) — jump to a section, collapse them all; `readOnly` reuses it beside the preview
LessonPreview.jsx what the editor's document column holds while Preview is on — the eyebrow, the narrow-screen exit, and LessonView in a panel frame
InstallAppButton.jsx the "install app" button in the sidebar footer; renders nothing unless the app is installable (see pwa-and-offline.md)
NotificationBell.jsx sidebar bell that polls for and shows the user's notifications
DisplayNameGate.jsx makes a signed-in user pick a display name before using the app
Expand All @@ -45,7 +46,7 @@ src/
RichTextToolbar.jsx its shadcn ToggleGroup toolbar (bold/italic/underline/lists/link/etc.)
RichText.jsx renders a stored comment/bio: sanitized HTML, or plain text for pre-rich-text values
LiveField.jsx debounced LiveInput/LiveTextarea (commit ~200ms after typing pauses, hold off remote updates while focused)
LessonView.jsx read-only renderer for the lesson page and the editor's preview dialog (blocks straight to React, lazy images, drawn in the app's theme)
LessonView.jsx read-only renderer for the lesson page and the editor's preview mode (blocks straight to React, lazy images, drawn in the app's theme)
LessonSummary.jsx on-device AI summary card on the lesson page (hidden unless the browser supports it)
InteractiveLesson.jsx full-screen step-by-step walkthrough of a lesson, with a field per question and optional read-aloud (see interactive-mode.md)
MyLessonAnswers.jsx the reader's own saved answers on the lesson page — private to them, rendered for nobody else
Expand Down
15 changes: 13 additions & 2 deletions apps/web/src/components/LessonView.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Read-only renderer for a lesson document, used by the public lesson page and
// by the editor's preview dialog.
// by the editor's preview mode.
//
// This replaces the old docx→mammoth preview pipeline: instead of building a
// full Word document in memory, fetching+transcoding every image up front and
Expand Down Expand Up @@ -262,7 +262,18 @@ export default function LessonView({ doc }) {
<style>{LESSON_STYLES}</style>
<h1>{doc?.title || t("lessonView.untitledLesson")}</h1>
{sections.map((section, si) => (
<section key={section.id || si}>
// data-section-id and scroll-mt are the same anchor contract SectionCard
// publishes in the editor, so the section outline can scroll to a
// section here without knowing which of the two surfaces it is looking
// at — see SectionOutline. The two never coexist: preview replaces the
// editing panes rather than sitting beside them, so one id matches one
// element. On the public lesson page nothing queries these, and an
// unused data attribute costs nothing.
<section
key={section.id || si}
data-section-id={section.id}
className="scroll-mt-(--header-h)"
>
<h2>{section.name || t("lessonView.untitledSection")}</h2>
{(section.blocks || []).map((block, bi) => (
<Block key={block.id || bi} block={block} />
Expand Down
60 changes: 60 additions & 0 deletions apps/web/src/components/editor/LessonPreview.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// What the editor's document column holds while Preview is on: the lesson as a
// reader sees it, and nothing else.
//
// It is only the column's *contents*. The page wrapper, the section outline and
// the column itself stay in EditorPage and are the same elements in both modes,
// which is the point — an earlier version of this had preview build its own
// wrapper and column beside the editing one, and the two immediately disagreed:
// the copy here was left unconstrained where the editing column caps at
// max-w-6xl, so on a page column past ~1400px toggling preview silently made
// the lesson wider than the thing it was previewing. Two class strings meant to
// stay equal will not stay equal. One element can't drift from itself.
//
// No outline of its own for the same reason (EditorPage passes `readOnly` to the
// one that is already there), and nothing here is editable: no drag handles, no
// move buttons, no add-section FAB — see the FAB's own guard in EditorPage.

import { useTranslation } from "react-i18next";
import { PencilIcon } from "lucide-react";
import { Button } from "../ui/button.jsx";
import LessonView from "../LessonView.jsx";

/**
* @param {object} props
* @param {object} props.doc The working lesson, rendered read-only.
* @param {() => void} props.onExit Leave preview. Wired to the same toggle the
* bar's Preview button calls, so there is one way out and one state machine.
*/
export default function LessonPreview({ doc, onExit }) {
const { t } = useTranslation("editor");

return (
<>
<div className="mb-2 flex items-center justify-between gap-3">
<p className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">
{t("preview.eyebrow")}
</p>
{/* The narrow-screen copy of the bar's toggle. Below `md` the labelled
cluster is inside the overflow menu, and a mode whose only exit is
behind a hidden menu is a trap. From `md` up the bar has it. */}
<Button
variant="outline"
size="sm"
onClick={onExit}
className="md:hidden"
>
<PencilIcon data-icon="inline-start" />
{t("header.backToEditing")}
</Button>
</div>

<p className="mb-4 text-sm text-muted-foreground">{t("preview.note")}</p>

{/* LessonView draws the lesson in the current theme; the wrapper supplies
only the panel frame. */}
<div className="rounded-panel border border-border bg-card shadow-(--shadow-panel)">
<LessonView doc={doc} />
</div>
</>
);
}
37 changes: 25 additions & 12 deletions apps/web/src/components/editor/SectionOutline.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
// same thing, and the cards' way is the one that keeps the scroll position
// stable (see useScrollAnchor).
//
// Navigating and nothing else is also what lets it stand beside the editor's
// preview unchanged. `readOnly` drops the two controls that edit — collapse-all
// and add-section — and what is left already works, because it addresses
// sections by `data-section-id` and LessonView publishes the same attribute the
// section cards do. A long lesson is exactly as hard to move around in when you
// are reading it back as when you are writing it, so the preview would need an
// outline of its own otherwise; this is that outline, not a copy of it.
//
// It appears once the editor's page column passes 52rem — AppShell's
// @container/page, not the viewport, so collapsing the sidebar can bring it in
// without the window changing size. Below that the editor is a single column
Expand All @@ -36,13 +44,16 @@ export default function SectionOutline({
allCollapsed,
onToggleAll,
onAddSection,
readOnly = false,
}) {
const { t } = useTranslation("editor");

const goTo = (id) => {
const el = document.querySelector(idSelector("data-section-id", id));
// SectionCard sets scroll-mt-(--header-h) on itself, so "start" already
// lands clear of the sticky bar — no offset arithmetic here.
// Whichever surface is mounted answers to the same attribute: SectionCard
// in the editor, <section> in LessonView under preview. Both set
// scroll-mt-(--header-h) on themselves, so "start" already lands clear of
// the sticky bar — no offset arithmetic here.
scrollToElement(el, { block: "start" });
};

Expand All @@ -52,7 +63,7 @@ export default function SectionOutline({
<p className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">
{t("outline.heading")}
</p>
{sections.length > 0 && (
{!readOnly && sections.length > 0 && (
<Button
variant="ghost"
size="icon"
Expand Down Expand Up @@ -107,15 +118,17 @@ export default function SectionOutline({
</ol>
)}

<Button
variant="outline"
size="sm"
className="mt-3 w-full justify-start"
onClick={onAddSection}
>
<PlusIcon data-icon="inline-start" />
{t("emptyState.addSection")}
</Button>
{!readOnly && (
<Button
variant="outline"
size="sm"
className="mt-3 w-full justify-start"
onClick={onAddSection}
>
<PlusIcon data-icon="inline-start" />
{t("emptyState.addSection")}
</Button>
)}
</aside>
);
}
13 changes: 13 additions & 0 deletions apps/web/src/components/layout/AppSidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -355,9 +355,22 @@ export default function AppSidebar() {
{!enabled ? null : user ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
{/* size="lg" drops the button's padding entirely in the rail
(`group-data-[collapsible=icon]:p-0!`), which is right for
the header above, where a size-8 tile fills the 32px
button edge to edge. Here the child is a bare 16px icon,
so p-0 left it pinned to the button's left edge at half
the weight of the utility icons directly above it —
small and visibly off-centre. Centring it and taking it
to 24px in the rail only; expanded, it stays 16px beside
the name and email, where it is a label and not a target.
The `[&>svg]` override outranks the variant's own
`[&>svg]:size-4` on specificity, being nested inside the
group selector. */}
<SidebarMenuButton
size="lg"
tooltip={displayName || t("nav.account")}
className="group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:[&>svg]:size-6"
>
<CircleUserIcon />
{/* Hidden in the rail for the same reason as the header's —
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/components/layout/EditorShell.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
// It mounts no chrome of its own — AppShell is already above it in the route
// table, and the editor gets exactly the same sidebar as every other page. This
// file used to configure that sidebar into a narrow, non-persisting rail
// because three editor panes plus a 16rem sidebar don't fit a 1280px screen;
// because the editor's panes plus a 16rem sidebar don't fit a 1280px screen;
// that is still true, but it is the *panes'* problem to solve, not the
// sidebar's. They size themselves against the `@container/page` AppShell
// publishes, so collapsing the sidebar hands them its 13rem the moment you do
// it — and the sidebar behaves the same here as everywhere else.
// it — and the sidebar behaves the same here as everywhere else. (The editor
// has since given the width back a second way, by retiring the always-on
// preview pane; see the layout comment in EditorPage.)
//
// What this file is still for is the chunk boundary. Everything the editor owns
// — Yjs, lib0, the collaboration client, ~6,000 lines of page — has to stay
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/layout/PageBody.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
// lesson text wider.
//
// Anything that needs neither (the marketing hero's full-bleed gradient, the
// editor's three panes) doesn't use this and says why where it opts out.
// editor's panes) doesn't use this and says why where it opts out.

import { cn } from "../../lib/utils.js";

Expand Down
Loading
Loading