---
path: /nisaba/notebook/new
surface: customer
domain: nisaba
auth: signed-in
source: apps/oshun/web/src/app/nisaba/notebook/new/page.tsx
status: walked + e2e-covered
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.
---

# Nisaba · new notebook

## 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`](../../../V1/features.md#nisaba).

## Entry points

- **`/nisaba/notebooks` footer** — "Start a notebook" primary button → here
- **`/nisaba/notebooks` empty 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 `library` tab
  (`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`): left `Nisaba · notebook`, right `new`, 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

- [x] **Default / empty form** — title empty, visibility defaulting to
      "Private", first-passage textarea empty
- [x] **Validating** — title input is `required`, `minLength={2}`,
      `maxLength={120}`; the submit handler trims title and blocks whitespace or
      short values before the BFF write
- [x] **Submitting** — `pending` true: submit button reads "Creating…",
      `opacity 0.6`, `cursor: progress`, `disabled`
- [x] **Created** — POST 2xx returns the live BFF notebook mutation envelope;
      form normalizes `notebook.notebookId`, `title`, `summary`, and
      `visibility`, then swaps to a `role="status" aria-live="polite"` card
      titled "Created" showing "&ldquo;<title>&rdquo; — ready for its first
      passage." and the mono notebook id
- [x] **Error (recoverable)** — non-2xx renders a `role="alert"` box: "Nisaba
      could not create the notebook (<status>)."; entered title/passage
      preserved; unit coverage asserts the failure telemetry path
- [x] **Offline** — `catch` renders "Nisaba is offline. Try again in a moment."
      in the alert box; no write queueing
- [x] **Gated** — middleware signed-in; POST sends `credentials: include`,
      bearer auth from `tryGetApiAuthToken()`, and `csrfHeaders()` into the
      create request
- [x] **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

- [x] **"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`
- [x] **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"` sets `visibility`; selected gets
    `aria-checked="true"`, accent border, italic label, and `tabIndex=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
- [x] **"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."
- [x] **"Back"** (`LBtn` ghost, lg) → `/nisaba/notebooks` (cancel path)
- [x] **"Start the notebook" / "Creating…"** (submit button)
  - Function: POSTs `{ title, summary, visibility, tags: [] }` to
    `/v1/nisaba/notebooks`; `summary` is the trimmed first passage, or the
    honest fallback "Notebook created from the new notebook form."
  - Disabled when: `pending` → opacity 0.6, cursor progress

### Confirmation card (after create)

- [x] **"All notebooks"** (`LBtn` ghost) → `/nisaba/notebooks`
- [x] **"Open it"** (`LBtn` primary) →
      `/nisaba/notebook?id=<encodeURIComponent(notebookId)>` — opens the new
      notebook in the reading desk

## Data & contracts

- **Reads**: none (pure create form)
- **Writes**: `POST /v1/nisaba/notebooks` via `buildOshunBffUrl`,
  `credentials: 'include'`, headers `content-type: application/json`, bearer
  auth from `tryGetApiAuthToken()`, plus `csrfHeaders()`. 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**: `visibility` defaults to `private`; "unlisted" is link-only,
  "public" lists in the open library
- **Telemetry**: dispatches `nisaba_notebook_create_submitted`,
  `nisaba_notebook_created`, and `nisaba_notebook_create_failed` with 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`](../../../V1/features.md#nisaba)
- Architecture:
  [`V1/ARCHITECTURE.md#nisaba`](../../../V1/ARCHITECTURE.md#nisaba)
- Sibling Nisaba routes: [`nisaba.md`](./nisaba.md),
  [`nisaba-notebook.md`](./nisaba-notebook.md),
  [`nisaba-notebooks.md`](./nisaba-notebooks.md),
  [`nisaba-scholar.md`](./nisaba-scholar.md)
- Journeys:
  [`journeys/nisaba-notebook-capture-and-cite.md`](../../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

- [x] This form now attaches bearer auth and `csrfHeaders()` to the BFF create
      request; the browser spec verifies the `authorization` and `x-csrf-token`
      headers on the real POST
- [x] Visibility control now uses `role="radio"`, `aria-checked`, and roving
      focus; unit and browser coverage verify ArrowRight behavior
- [x] 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
- [x] 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
