Skip to content

Editor button-style normalization, batch photo upload, and real TOC page numbers - #39

Merged
important-new merged 32 commits into
mainfrom
feat/editor-buttons-toc-batch-upload
Jul 13, 2026
Merged

important-new merged 32 commits into
mainfrom
feat/editor-buttons-toc-batch-upload

Conversation

@important-new

Copy link
Copy Markdown
Owner

Three independent editor/report follow-ups plus an enabling worker-bundle diet.

Editor button normalization

  • New shared-ui affordances: Button link/danger-link variants + selected toggle state, IconButton, MenuItem, vertical TabStrip, and a cn() tailwind-merge helper so consumer classNames reliably win conflicts; plus an app-level RatingSegment consolidating the rating-tile copies.
  • Migrated ~135 bespoke controls across the inspection + template editors onto these primitives (behavior-preserving). Publish normalized to the primary fill. SpeedMode and BurstCamera are now theme-aware (BurstCamera keeps an over-video treatment by design — its background is the live camera feed).

Batch photo upload

  • Bounded-concurrency BFF fan-out (mapPool) returning per-file results; a dedicated multi-select "Add from library" input alongside "Take photo"; results-based "N of M uploaded" messaging.

TOC page numbers (commercial PCA)

  • Two-pass render: pass 1 → read Chrome's intra-doc named destinations with pdf-lib (extractAnchorPages) → re-render with the page map injected into the TOC. Self-short-circuits to a single pass when a report has no anchors. Replaces the previous Paged.js path. RENDER_VERSION → r7.

Worker bundle diet

  • Replaced the full twilio/telnyx SDKs with fetch-based REST clients so the new pdf-lib dependency stays under the Workers Free 3 MiB limit (101.7% → 83.2%).

Full gate green on Node 22 (type-check, lint, bundle, db:check, unit, workers, web). Two new E2Es (batch-upload, TOC-numbers) run in CI.

🤖 Generated with Claude Code

important-new and others added 30 commits July 12, 2026 21:50
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
…code gate)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
Domain rating-tile component (app, not shared-ui — severity tone map is
domain-coupled) that will consolidate RatingButtonRow, BatchActionBar, and
SpeedMode's hand-rolled rating tiles in a later phase. Radiogroup a11y
pattern mirrors SegmentedControl (role=radio/aria-checked, roving tabindex,
Arrow/Home/End). Union of the three call sites' needs: optional shortLabel
(RatingButtonRow's abbreviation), hint (keyboard-shortcut digit shown by
RatingButtonRow/SpeedMode), and color (BatchActionBar's dynamic
tenant-color override, applied regardless of selection). warn tone maps to
the real bg-ih-watch token — there is no ih-warn token in tailwind.css.

Bumps scripts/knip-baseline.json by 2 entries (RatingOption/RatingTone) —
intended public API for Phase-2 adoption, currently unused since no call
site has adopted the component yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
…utton, MenuItem, vertical TabStrip)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
…d-ui primitives

Group A of the editor button-normalization pass. Behavior-preserving:
Publish green bg-ih-ok normalized to primary; secondary actions, item-filter
SegmentedControl, batch/fullscreen toggles, and toolbar icon buttons moved onto
Button/IconButton/SegmentedControl.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
…Name wins

A migration reviewer proved (via the compiled Tailwind config) that appending a
className to Button/IconButton did not reliably override the primitive's own
base/size utilities — Tailwind resolves same-property conflicts by stylesheet
order, not string order, so responsive-hide (hidden xl:inline-flex) and size
overrides silently lost. Introduce cn() (tailwind-merge) and use it in Button,
IconButton, MenuItem, SegmentedControl, TabStrip, RatingSegment with className
last, so consumer classes win. Also restore EditorHeader Preview/Preview PDF/
Sign now to size=md (h-9), correcting a Group A height regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
…red-ui primitives

Group B of the editor button-normalization pass. Behavior-preserving:
link/danger-link text actions, MenuItem catalog rows, and PSQ-status + report-tier
SegmentedControls; retires CompliancePanel's bg-ih-bad-fg Decline in favor of the
single danger token.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
…ed-ui primitives

Group C of the editor button-normalization pass. Behavior-preserving: ⋯ menu
triggers to IconButton, popover rows to MenuItem (Delete unified as tone=danger
across SectionRail/ItemList), scope/template/clone triggers to secondary Button.
Nav trees, the SideRail vertical icon strip, and the photo-download <a> are
intentionally left as-is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
…t, Button type default

- BreadcrumbDropdown scope options revert from MenuItem to plain button (they are
  listbox options, not menu items — MenuItem's hardcoded role=menuitem conflicted
  with the parent li role=option).
- TemplateMenu trigger size sm->md (h-9) to align with sibling header controls.
- Button now defaults type=button (consistent with IconButton/MenuItem); explicit
  type still wins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
…itives

Group D (normal surfaces) of the editor button-normalization pass. ItemPhotoStrip
select-mode controls to ghost/link/danger-link Button; RatingButtonRow and
BatchActionBar rating tiles consolidated onto RatingSegment (values + onChange
preserved; BatchActionBar keeps dynamic per-option colors via RatingSegment color).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
…abels

Group D review: TONE_FILLED.neutral paired bg-ih-bg-muted with text-ih-fg-inverse
(~1:1 contrast, invisible) — the common minor/Not-Inspected tier maps here. Use
bg-ih-bg-inverse (solid, high-contrast in both themes). Also restore the pre-
migration responsive abbreviation: at md/lg, shortLabel shows below sm breakpoint
and the full label at sm+; sm stays compact-short. Accessible name stays the full
label.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
…trols to primitives

Per product direction, SpeedMode and BurstCamera must be consistent with the rest
of the (theme-aware) editor. SpeedMode: convert its fixed-dark slate palette to
theme-aware ih-* tokens (works in light + dark), Prev/Next/Exit onto Button, large
number-hinted rating tiles kept bespoke but theme-colored. BurstCamera: controls
onto IconButton/Button while intentionally keeping the over-video translucent-dark
treatment (the frame is live camera video, not a themed surface — light-on-dark is
the correct, theme-independent camera-UI pattern); shutter stays special.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
… light theme)

bg-ih-bg-app/80 is near-white at 80% in light theme -> the first-run coach scrim
vanished. Use the dedicated ih-backdrop token (the app's standard overlay scrim,
alpha baked in) like every other full-screen overlay.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
…conButton

Group E of the editor button-normalization pass. Cancel/Stay/Close -> ghost,
confirm -> primary (PublishModal green bg-ih-ok normalized to primary), Delete/Leave/
Crop&clear -> danger, hand-rolled modal close glyphs -> IconButton; KeyboardHud raw
rgba backdrop -> ih-backdrop token. SectionPickerModal (listbox pattern) and
TagPickerModal (dynamic-color chips) intentionally left bespoke.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
…red-ui primitives

Group G of the editor button-normalization pass. UnitsManager/CommentLibraryDrawer
filters -> SegmentedControl; unit + progress-dot + clear icons -> IconButton;
InspectionSettingsSheet Save keeps type=submit/form; ItemEditor + CannedCommentTabs
+ TagChipRow text/dashed actions -> Button; InspectorToolsDock tool tiles -> MenuItem
(gradient FAB left as a documented DS exception). Dynamic-color toggle chips and
content cards intentionally kept bespoke.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
…tent layout

Group G review: the secondary variant leaked a solid bg-ih-bg-card fill onto the
transparent dashed +Add empty-state CTAs (SectionRail/ItemList/CannedCommentTabs/
ItemEditor/TagChipRow) -> switch to ghost + dashed border + preserved per-control
hover. MenuItem wrapped children in a span, breaking rich flex-1 label + trailing
badge layouts (InspectorToolsDock hotkeys) -> render children directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
Group F (final migration group). Save/Rating System actions -> Button, rail tabs ->
TabStrip, RatingSystemEditor move/remove/preset controls -> IconButton/Button;
raw hex page-bg leaks (bg-[#f8fafc] dark:bg-[#0f172a]) -> theme-aware bg-ih-bg-app;
navigation Links kept as links (tokens fixed). Preview watch-tone toggle kept bespoke.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
…umed)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
… results

Replace the sequential for-await photo-upload loop in the BFF action with mapPool
(a generic order-preserving bounded-concurrency pool, concurrency 4). The action
now returns a per-file results[] ({index, ok, key?, error?}) alongside the existing
keys[] contract, so the client can report N-of-M and retry only failed files. The
concurrency cap bounds R2-write/isolate-memory concurrency; total subrequests stay
client-chunked. Also tightens file-size baselines to the branch's post-migration
line counts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
Add a dedicated 'Add from library' input (multiple, no capture) alongside the
'Take photo' camera input; handlePhotoUpload now bakes and submits every selected
file as one batch (repeated file keys, mirroring burst), capped at 20/submission,
with the offline branch enqueuing each file. The result effect uses the action's
per-file results[] to report 'N of M uploaded' on partial failure. Adds an E2E
spec (runs against the seeded D1 harness in CI).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
Chrome encodes intra-doc <a href=#id> TOC links as named destinations whose name
equals the element id; pdf-lib resolves each via the /Names /Dests tree to the
correct 1-based page. Validates the two-pass approach (render -> extractAnchorPages
-> inject -> re-render). Verified with Playwright Chromium (same engine as CF
Browser Rendering).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
…ces Paged.js)

Chrome encodes intra-doc <a href=#id> TOC links as named destinations whose name
is the element id. New server/lib/toc-pages.ts:extractAnchorPages reads the PDF
/Names /Dests tree to map anchor id -> 1-based page. server/lib/pdf.ts gains
generatePdfWithTocPages: render pass 1, extract the page map, re-render pass 2 with
the map injected via a &tocpages= param; it self-short-circuits to a single pass
when there are no anchors, so report-pdf.service calls it unconditionally with no
per-caller plumbing and non-commercial reports are unchanged. ReportToc fills each
reserved slot from the tocPages map (stable pagination); the report loader parses
the param. Removes the falsified/gated Paged.js path (component, synthetic e2e,
pagedjs dep, vendor entry, target-counter CSS). RENDER_VERSION r6 -> r7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
…worker bundle diet)

The full twilio + telnyx npm SDKs added ~567 KiB gzip and, together with the new
pdf-lib TOC dependency, pushed the worker past the Workers Free 3 MiB limit. The
compliance providers already program against narrow local client interfaces
(TwilioComplianceClient / TelnyxComplianceClient) with the SDK instances duck-typed
in, so this swaps the construction seam only: new twilio-rest-client.ts /
telnyx-rest-client.ts implement those interfaces over fetch (Twilio Basic-auth
form-encoded; Telnyx Bearer JSON), wired in resolve-compliance-provider.ts. Provider
logic + their mocked tests are unchanged. Removes twilio/telnyx deps + the dead
fetch-http-client shim. Worker bundle 101.7% -> 83.2% of the Free limit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
twilio-compliance header now describes the fetch REST client (not the dropped SDK);
remove the dangling twilio-sdk-fetch.spec.ts reference; correct ReportToc's
pagination-stability rationale (no added TOC lines, not 'same width').

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
RepairItemsPanel's search-filtered repair-item picker is a listbox, not a menu —
revert its rows from MenuItem (role=menuitem) to plain buttons inside role=option
li's under a role=listbox ul. CloneLastButton's dropdown IS a menu (clone-scope
actions) but its container lacked role=menu while the trigger declared
aria-haspopup=menu — add role=menu so its MenuItems aren't orphaned.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
important-new and others added 2 commits July 13, 2026 10:47
… CI)

Fork CI caught two E2E failures the local env (JWT-keypair gap) couldn't run:
- inspection-edit-hotkeys: RatingButtonRow is now a RatingSegment radiogroup, so
  the 'press 4' assertion targets the checked radio (role=radio + aria-checked),
  not a button with aria-pressed.
- report-toc-numbers: loginApi omitted the double-submit CSRF token /api/auth/login
  requires; send it as header + __Host-csrf_token cookie (mirrors editor-seed). The
  existing 503 skip still covers CI's missing BROWSER binding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
The wizard create returns 200 (not 201) with { data: { id } } (not data.inspection.id);
assert 2xx via ok() and read data.id. Broaden the render-unavailable skip from
exactly-503 to any non-ok status, so CI (no live BROWSER binding, possible pipeline
gate) skips cleanly — the real two-pass proof runs only on deployed CF, never a
false pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iM1JG433tMu7MGJjEC49X
@important-new
important-new merged commit c0294b2 into main Jul 13, 2026
5 checks passed
@important-new
important-new deleted the feat/editor-buttons-toc-batch-upload branch July 13, 2026 03:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant