Context. surface customer · domain nisaba · route /nisaba/notebook/new · auth signed-in · source apps/oshun/web/src/app/nisaba/notebook/new/page.tsx
Last walked. 2026-05-29 automated runtime walk (Playwright headless) — render, /v1 data (2xx), console/page-errors, expected content, screenshot verified; live screen-reader, touch, offline, and telemetry-delivery checks pending a manual AT pass. Evidence: WALKTHROUGH/results/runtime-sweep-2026-05-29.md. 2026-06-30 notebook-create addendum — Playwright real-dev-infra coverage now exercises the authenticated browser create form against the live BFF, verifies CSRF header attachment, BFF response/readback visibility contract, radio keyboard semantics, mobile containment, and text-safe submit/success telemetry. Evidence: WALKTHROUGH/results/v1-real-infra-run-2026-06-22.md; spec: apps/oshun/web/e2e/nisaba-notebook-new.spec.ts.
Purpose#
Start a reading notebook: give it a name, a visibility, and an optional first
passage. "A notebook is a quiet thread of passages and your annotations on them.
Private by default; share with a link only when you want a reader." The page is
a server shell hosting the client form NisabaNewNotebookForm, which POSTs to
/v1/nisaba/notebooks. See
V1/features.md#nisaba.
Entry points#
/nisaba/notebooksfooter — "Start a notebook" primary button → here/nisaba/notebooksempty state — verify the empty-notebooks card's "Start one to thread your first passage" copy links here (currently plain text — see gaps)- Direct URL / bookmark — yes
(
alternates.canonical = '/nisaba/notebook/new'); signed-in - Shell nav — arrives under the
librarytab (LCustomerNav active="library")
Layout regions#
page.tsx is a synchronous server component rendering the shell + form.
Centered column, maxWidth: 640, data-responsive-page.
- Top chrome:
LCustomerNav(Today / Explore / Activity / Library; Library active) with "Lilith" wordmark + "⌕ Search" - Masthead (
LMasthead): leftNisaba · notebook, rightnew, kicker "A reading thread, started", title "Begin a notebook." - "How this opens" note: "A notebook is a quiet thread of passages and your annotations on them. Private by default; share with a link only when you want a reader."
- Main:
<NisabaNewNotebookForm />— the create form, or after success the "Created" confirmation card
States#
- Default / empty form — title empty, visibility defaulting to "Private", first-passage textarea empty
- Validating — title input is
required,minLength={2},maxLength={120}; the submit handler trims title and blocks whitespace or short values before the BFF write - Submitting —
pendingtrue: submit button reads "Creating…",opacity 0.6,cursor: progress,disabled - Created — POST 2xx returns the live BFF notebook mutation envelope;
form normalizes
notebook.notebookId,title,summary, andvisibility, then swaps to arole="status" aria-live="polite"card titled "Created" showing "“” — ready for its first passage." and the mono notebook id - Error (recoverable) — non-2xx renders a
role="alert"box: "Nisaba could not create the notebook ()."; entered title/passage preserved; unit coverage asserts the failure telemetry path - Offline —
catchrenders "Nisaba is offline. Try again in a moment." in the alert box; no write queueing - Gated — middleware signed-in; POST sends
credentials: include, bearer auth fromtryGetApiAuthToken(), andcsrfHeaders()into the create request - Standalone PWA — route-specific Playwright coverage launches the page at a 390 px standalone viewport, checks the Library shell context, form visibility, radio group semantics, Back link, no horizontal overflow, and 44 px controls
Interactions#
Form fields#
- "The notebook, named" (text input
#nb-title)- Function: sets
title; placeholder "e.g. On the citadel" - Validation:
required,minLength 2,maxLength 120 - Screen reader: label "The notebook, named" via
htmlFor
- Function: sets
- Visibility radio buttons (3 cell buttons inside
role="radiogroup"aria-labelledby="nb-visibility-label")- Options: Private ("only you can read"), Unlisted ("link-only, no listing"), Public ("in the open library")
- Function: each
type="button" role="radio"setsvisibility; selected getsaria-checked="true", accent border, italic label, andtabIndex=0 - Keyboard: ArrowRight/ArrowDown advance, ArrowLeft/ArrowUp reverse, Home/End jump to edges with roving focus
- Mobile:
repeat(auto-fit, minmax(92px, 1fr))grid avoids horizontal overflow in the 390 px Playwright pass
- "First passage (optional)" (textarea
#nb-seed)- Function: sets
seedPassage;rows={4},maxLength 1200; placeholder "A line, a paragraph, a footnote — whatever the notebook begins with."
- Function: sets
- "Back" (
LBtnghost, lg) →/nisaba/notebooks(cancel path) - "Start the notebook" / "Creating…" (submit button)
- Function: POSTs
{ title, summary, visibility, tags: [] }to/v1/nisaba/notebooks;summaryis the trimmed first passage, or the honest fallback "Notebook created from the new notebook form." - Disabled when:
pending→ opacity 0.6, cursor progress
- Function: POSTs
Confirmation card (after create)#
- "All notebooks" (
LBtnghost) →/nisaba/notebooks - "Open it" (
LBtnprimary) →/nisaba/notebook?id=<encodeURIComponent(notebookId)>— opens the new notebook in the reading desk
Data & contracts#
- Reads: none (pure create form)
- Writes:
POST /v1/nisaba/notebooksviabuildOshunBffUrl,credentials: 'include', headerscontent-type: application/json, bearer auth fromtryGetApiAuthToken(), pluscsrfHeaders(). Body:{ title, summary, visibility, tags: [] }. Response on success: live BFF mutation envelope{ generatedAt, userId, revision, notebook: { notebookId, title, summary, visibility, ... } }. - Realtime: none
- Caching: none (write-only client interaction)
- Auth/role check: middleware signed-in
- Privacy:
visibilitydefaults toprivate; "unlisted" is link-only, "public" lists in the open library - Telemetry: dispatches
nisaba_notebook_create_submitted,nisaba_notebook_created, andnisaba_notebook_create_failedwith lengths, visibility, success/failure metadata, and no title or passage text
E2E coverage#
The Playwright route spec is the authoritative browser proof for this walkthrough: it runs against the dev Next/BFF stack, verifies the authenticated browser create form sends bearer auth and CSRF headers, reads the created notebook back from the live BFF, proves radio keyboard semantics and text-safe telemetry, and now covers the route-specific standalone mobile containment and touch-target contract. Component Vitest coverage backs the same create/error contracts at the form layer.
apps/oshun/web/e2e/nisaba-notebook-new.spec.ts— authenticated notebook create form against the live BFF, CSRF and authorization header attachment, mutation response/readback visibility contract, roving radio keyboard behavior, success links, no horizontal overflow, text-safe submit/success telemetry, and standalone mobile launch checks.apps/oshun/web/src/app/nisaba/notebook/new/NisabaNewNotebookForm.test.tsx— form-level radio semantics, create payload/CSRF/auth headers, success telemetry, HTTP error state, and failure telemetry.
Cross-references#
- Feature spec:
V1/features.md#nisaba - Architecture:
V1/ARCHITECTURE.md#nisaba - Sibling Nisaba routes:
nisaba.md,nisaba-notebook.md,nisaba-notebooks.md,nisaba-scholar.md - Journeys:
journeys/nisaba-notebook-capture-and-cite.md(creating a notebook is the first leg) - Component sources:
apps/oshun/web/src/app/nisaba/notebook/new/NisabaNewNotebookForm.tsx
Known downstream boundaries#
- This form now attaches bearer auth and
csrfHeaders()to the BFF create request; the browser spec verifies theauthorizationandx-csrf-tokenheaders on the real POST - Visibility control now uses
role="radio",aria-checked, and roving focus; unit and browser coverage verify ArrowRight behavior - Submit / create / error telemetry now exists and excludes raw title or passage text
- On the notebooks index, the empty-state copy ("Start one to thread your first passage") is not itself a link to this route — verify the intended affordance
- The form normalizes the real BFF mutation envelope and the browser spec
reads the created notebook back by
notebook.notebookId, proving the BFF returns a usable id