Skip to content

feat: add paper theme#12

Merged
rsbh merged 9 commits intomainfrom
feat_paper_theme
Feb 20, 2026
Merged

feat: add paper theme#12
rsbh merged 9 commits intomainfrom
feat_paper_theme

Conversation

@rsbh
Copy link
Member

@rsbh rsbh commented Feb 20, 2026

Summary

  • Add theme registry with dynamic theme selection via config.theme.name
  • Implement paper theme with book-style layout: monospace chapter nav sidebar, serif content area with paper styling, and tick-based reading progress ruler
  • Thin fixed navbar with prev/next arrows, monospace breadcrumb, and search
  • Paper-style content area with borders, shadow, and neutral background
  • Reusable ReadingProgress ruler (Apsara TOC-style) with scroll marker, tick marks, and heading labels on hover
  • API pages: recursive sidebar nav for nested folders, hidden Cmd+K search

Test plan

  • Set theme: { name: paper } in chronicle.yaml and run dev server
  • Verify chapter nav renders numbered groups with active state
  • Verify serif content with paper styling (borders, shadow)
  • Verify reading progress ruler syncs on scroll
  • Verify API pages render with correct sidebar links
  • Verify Cmd+K search works on both docs and API pages
  • Run chronicle build — no TS errors

🤖 Generated with Claude Code

rsbh and others added 7 commits February 19, 2026 09:47
Update tsconfig lib to ES2022 for error.cause support and fix invalid
IconButton props (size/variant) to match Apsara component types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add theme registry for config-driven theme selection and implement
paper theme with chapter nav, serif content, reading progress ruler,
and prev/next navigation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Restructure paper layout with sticky navbar (arrows + breadcrumb + search),
paper-style content area with shadow/borders, neutral background,
and full-height reading progress ruler. Remove PrevNextNav component,
add className prop to Search.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Navbar and ReadingProgress use position fixed. Layout content
is the scroll container with scrollbar at browser edge. Neutral
background on sidebar and content area.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces simple heading list with Apsara-style ruler TOC featuring
tick marks, scroll marker with percentage, heading labels on hover,
and connecting lines. Monospace font, fixed positioning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add heading margins/sizes, paragraph spacing, list indentation,
and blockquote styling for proper content readability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Paper Layout supports classNames prop for overrides
- ChapterNav handles nested folders recursively for API endpoints
- Truncate sidebar text with ellipsis
- Hidden Search in API layout for Cmd+K support
- API layout removes right padding

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@rsbh rsbh requested a review from rohilsurana February 20, 2026 04:14
@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • New "Paper" theme with full layout and page presentation
    • Chapter navigation for hierarchical content browsing
    • Reading progress timeline with clickable section markers
    • Integrated search trigger into layouts and pages, now accept extra styling
  • Refactors

    • Centralized theme registry enabling dynamic theme selection
  • Style

    • Minor UI/spacing and control-size adjustments for consistency

Walkthrough

Adds a new "paper" theme (Layout, Page, ChapterNav, ReadingProgress + CSS), introduces a theme registry and dynamic theme selection, wires Search className through, updates a few IconButton props, and removes next-env declarations from the chronicle package.

Changes

Cohort / File(s) Summary
Build & Config
packages/chronicle/next-env.d.ts, packages/chronicle/tsconfig.json, .gitignore
Removed Next.js env type references, added ES2022/DOM libs to tsconfig, and ignored next-env.d.ts in .gitignore.
Theme Registry
packages/chronicle/src/themes/registry.ts, packages/chronicle/src/themes/paper/index.ts
Added getTheme(name?: string): Theme and exported paperTheme to enable dynamic theme lookup with fallback.
Paper Theme — Core
packages/chronicle/src/themes/paper/Layout.tsx, packages/chronicle/src/themes/paper/Layout.module.css, packages/chronicle/src/themes/paper/Page.tsx, packages/chronicle/src/themes/paper/Page.module.css
New client Layout and Page components and CSS for the paper theme (sidebar, navbar, breadcrumbs, article styling, search integration, reading progress wiring).
Paper Theme — Navigation
packages/chronicle/src/themes/paper/ChapterNav.tsx, packages/chronicle/src/themes/paper/ChapterNav.module.css
New ChapterNav component rendering hierarchical chapter lists from PageTree with active-state handling and styles.
Paper Theme — Reading Progress
packages/chronicle/src/themes/paper/ReadingProgress.tsx, packages/chronicle/src/themes/paper/ReadingProgress.module.css
New ReadingProgress client component mapping TOC headings to a vertical timeline with ticks, labels, interactions, and styles.
App Integration
packages/chronicle/src/app/[[...slug]]/page.tsx, packages/chronicle/src/app/apis/[[...slug]]/layout.tsx, packages/chronicle/src/app/apis/[[...slug]]/layout.module.css
Switched from static defaultTheme to getTheme(config.theme?.name); API layout renders a hidden Search instance; added .hiddenSearch class and adjusted .content padding.
UI Component Adjustments
packages/chronicle/src/components/ui/search.tsx, packages/chronicle/src/components/api/field-row.tsx, packages/chronicle/src/components/api/key-value-editor.tsx
Search now accepts optional className prop and uses cx; several IconButton usages changed size="small"size={1}.
Types / Declarations
packages/chronicle/next-env.d.ts (removed references)
Removed Next.js reference comments/imports from package declaration file.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant App as App (page/layout)
participant Registry as ThemeRegistry
participant ThemeLayout as Theme Layout
participant ThemePage as Theme Page
participant Reading as ReadingProgress
App->>Registry: getTheme(config.theme?.name)
Registry-->>App: Theme { Layout, Page, ... }
App->>ThemeLayout: render Layout(config, tree, children)
ThemeLayout->>ThemePage: render Page(page, config, tree)
ThemePage->>Reading: mount ReadingProgress(items: toc)
Reading-->>ThemePage: scroll/position updates (ticks/marker)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • rohilsurana
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add paper theme' clearly summarizes the main change—introducing a new paper theme to the project.
Description check ✅ Passed The description is directly related to the changeset, detailing the paper theme implementation, layout components, and testing steps.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat_paper_theme

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/chronicle/src/components/api/field-row.tsx (1)

66-71: ⚠️ Potential issue | 🟡 Minor

Icon-only IconButtons are missing aria-label

Both IconButton elements wrap purely visual icons (PlusIcon, TrashIcon) with no accessible name. Screen readers will announce them as unlabelled buttons. key-value-editor.tsx already establishes the correct pattern (aria-label={\Delete ${entry.key || 'entry'}`}`); the same treatment is needed here.

♿ Proposed fix
- <IconButton size={1} onClick={() => {
+ <IconButton size={1} aria-label={`Add item to ${field.name}`} onClick={() => {
    const newItem = itemChildren ? {} : ''
    onChange?.(field.name, [...items, newItem])
  }}>
    <PlusIcon width={14} height={14} />
  </IconButton>
- <IconButton size={1} onClick={() => {
+ <IconButton size={1} aria-label={`Remove item ${i + 1} from ${field.name}`} onClick={() => {
    const updated = items.filter((_, j) => j !== i)
    onChange?.(field.name, updated)
  }}>
    <TrashIcon width={14} height={14} />
  </IconButton>

Also applies to: 108-113

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/components/api/field-row.tsx` around lines 66 - 71,
The IconButton elements wrapping purely visual icons (PlusIcon and TrashIcon)
lack accessible names; update the PlusIcon button in the add handler and the
TrashIcon button (the other occurrence around the remove handler) to include
descriptive aria-labels (e.g., for the add button use aria-label={`Add
${field.name || 'entry'}`} and for the delete button use aria-label={`Delete
${entry.key || 'entry'}`} or similar), so that IconButton, PlusIcon, TrashIcon
and surrounding handlers (onChange, itemChildren, field.name, items, entry)
mirror the accessible pattern used in key-value-editor.tsx.
🧹 Nitpick comments (11)
packages/chronicle/src/app/apis/[[...slug]]/layout.tsx (1)

4-5: Dynamic theming and hidden Search wiring look correct.

getTheme(config.theme?.name) is a safe, fallback-guarded lookup, and a Server Component rendering a Client Component (Search) is valid in the App Router.

One minor point: Search mounts with allowEmpty: true and delayMs: 100, so useDocsSearch fires an initial fetch to /api/search on every API page load even though the button is display: none. This is an extra network round-trip that's never surfaced to the user until Cmd+K is pressed. If load reduction on API pages matters, you could defer the fetch until the dialog opens (e.g., by not rendering <Search> at all and instead only attaching the keydown listener directly in a lightweight client component).

Also applies to: 10-10, 21-21

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/app/apis/`[[...slug]]/layout.tsx around lines 4 - 5,
The Search component mounts on every API page and immediately triggers
useDocsSearch due to allowEmpty: true and delayMs: 100, causing an unnecessary
initial fetch; to fix, stop rendering <Search> on server pages by replacing its
immediate render with a lightweight client-only keyboard-listener component
(e.g., a small client component that attaches the Cmd+K / keydown handler and
only mounts/returns the full Search component when the dialog is opened), or
change the search initialization so useDocsSearch is only invoked on open;
update references around Search, useDocsSearch, allowEmpty and delayMs to ensure
the dialog-triggering client component instantiates Search lazily.
packages/chronicle/src/themes/paper/Layout.module.css (1)

9-15: padding-right: 260px hardcodes the sidebar width — consider a CSS custom property.

Both .sidebar { width: 260px } and .content { padding-right: 260px } share the same magic number. If the sidebar width ever changes, both must be updated manually.

♻️ Suggested refactor using a CSS custom property
+:root {
+  --paper-sidebar-width: 260px;
+}
+
 .sidebar {
-  width: 260px;
+  width: var(--paper-sidebar-width);
   padding: var(--rs-space-7) var(--rs-space-5);
   ...
 }

 .content {
   flex: 1;
   overflow-y: auto;
   background: var(--rs-color-background-neutral-primary);
-  padding-right: 260px;
+  padding-right: var(--paper-sidebar-width);
 }

Also applies to: 26-31

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/themes/paper/Layout.module.css` around lines 9 - 15,
The hard-coded 260px "magic number" is duplicated between .sidebar and .content;
introduce a CSS custom property (e.g., --sidebar-width) at the top of
Layout.module.css and use it for .sidebar { width: var(--sidebar-width, 260px);
} and .content { padding-right: var(--sidebar-width, 260px); } so changing the
sidebar width requires updating only the custom property; update any other
occurrences (lines noted around .sidebar and .content) to reference the same
variable.
packages/chronicle/src/themes/paper/ChapterNav.module.css (1)

34-36: Redundant list-style: none on .item.

.chapterItems (the parent <ul>) already sets list-style: none, so the declaration on .item (<li>) has no additional effect.

♻️ Suggested cleanup
-.item {
-  list-style: none;
-}
+.item {}

Or simply remove the .item rule if it has no other declarations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/themes/paper/ChapterNav.module.css` around lines 34 -
36, The .item rule in ChapterNav.module.css is redundant because the parent
.chapterItems already sets list-style: none; remove the list-style: none
declaration from the .item selector (or delete the entire .item rule if it
contains no other declarations) so styling is not duplicated; update any
references to .item in the component only if you remove the selector entirely to
ensure no dependent styles are lost.
packages/chronicle/src/themes/paper/Page.module.css (1)

73-78: margin-top: 72px on .article is a magic number tied to the fixed navbar height.

The fixed navbar is height: 40px + padding: var(--rs-space-3) 0. If either value changes, 72px will need a manual update. Consider deriving it from a CSS custom property or using padding-top with a CSS variable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/themes/paper/Page.module.css` around lines 73 - 78,
Replace the hard-coded "margin-top: 72px" on the .article rule with a value
derived from the navbar's size using a CSS custom property (e.g.,
--navbar-height) or a calc() that references --rs-space-3 and the fixed navbar
height, so the spacing updates automatically when the navbar's height or padding
changes; update the .article selector to use that CSS variable (or calc
expression) and ensure the fixed navbar sets/exports the same --navbar-height
variable so both elements stay in sync.
packages/chronicle/src/themes/paper/Page.tsx (2)

42-48: Consider memoizing derived navigation data.

flattenTree, findIndex, and findBreadcrumb run on every render. While tree sizes in documentation sites are typically small, wrapping these in useMemo keyed on [tree, pathname, page.slug] would prevent redundant computation during unrelated re-renders (e.g., from ReadingProgress state changes propagating up).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/themes/paper/Page.tsx` around lines 42 - 48, Derived
navigation values in Page (pages, currentIndex, prev, next, crumbs) are
recomputed on every render; wrap the computations that call flattenTree and
findBreadcrumb in a useMemo so they only recompute when inputs change.
Specifically, memoize pages = flattenTree(tree.children) and derive
currentIndex, prev, next from pages and pathname inside a single useMemo keyed
on [tree, pathname, page.slug], and memoize crumbs =
findBreadcrumb(tree.children, page.slug) (or include it in the same memo) to
avoid redundant work during unrelated re-renders; update the Page component to
import/use React.useMemo and replace the direct calls to
flattenTree/findBreadcrumb accordingly.

55-72: Disabled arrows use <span> — not focusable, but acceptable for disabled state.

The disabled prev/next arrows render as <span> elements, which aren't keyboard-focusable. Since they represent disabled navigation, this is a reasonable trade-off. If full ARIA compliance is desired later, consider using <button disabled> or aria-disabled links instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/themes/paper/Page.tsx` around lines 55 - 72, The
disabled arrow rendering currently uses non-semantic <span> elements (in the
prev/next conditional blocks around NextLink and
ChevronLeftIcon/ChevronRightIcon), which harms accessibility; replace those
<span className={styles.arrowDisabled}> elements with proper disabled controls —
either a <button disabled className={styles.arrowDisabled} aria-label="Previous"
or "Next"> wrapping the Chevron*Icon, or keep an anchor but add
aria-disabled="true" and tabIndex={-1} to the non-link state so screenreaders
and keyboard users get correct semantics; update the prev/next conditional
branches that reference prev, next, NextLink, styles.arrowDisabled,
ChevronLeftIcon and ChevronRightIcon accordingly.
packages/chronicle/src/themes/paper/ChapterNav.tsx (1)

23-31: Mutable counter inside render's .map() callback.

chapterIndex is mutated inside .map(), which is a side-effect during rendering. While it works here because the variable is local and re-initialized every render, it's an anti-pattern in React's functional rendering model. A cleaner approach would be to pre-compute the folder count or use reduce.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/themes/paper/ChapterNav.tsx` around lines 23 - 31, The
code mutates chapterIndex inside the render loop (tree.children.map) in
ChapterNav; remove this side-effect by precomputing folder indices before
returning JSX: iterate tree.children once (e.g., with Array.prototype.reduce or
a for loop) to build a list or map of folder items to their sequential chapter
numbers, then in the render map use that precomputed index (referencing
chapterIndex logic, tree.children, and the folder branch) instead of
incrementing a variable during mapping. Ensure the new logic preserves skipping
'separator' items and only counts items with type === 'folder' and children.
packages/chronicle/src/themes/paper/ReadingProgress.module.css (1)

70-80: Inconsistent font-family declarations.

Line 73 hardcodes 'SF Mono', 'Fira Code', monospace while .headingLabel (line 62) uses the design-system variable var(--rs-font-mono). Consider using the CSS variable here as well for consistency.

Proposed fix
 .headingLink {
   color: var(--rs-color-foreground-base-primary);
   text-align: right;
-  font-family: 'SF Mono', 'Fira Code', monospace;
+  font-family: var(--rs-font-mono);
   font-size: var(--rs-font-size-mini);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/themes/paper/ReadingProgress.module.css` around lines
70 - 80, Replace the hardcoded font stack in the .headingLink rule with the
design-system variable used by .headingLabel: use var(--rs-font-mono) for
font-family instead of "'SF Mono', 'Fira Code', monospace" so both .headingLink
and .headingLabel consistently reference the same mono font variable.
packages/chronicle/src/themes/paper/ReadingProgress.tsx (3)

119-163: Direct DOM manipulation in handleScroll bypasses React's rendering model — acceptable for performance but worth documenting.

The imperative classList and style.top manipulation in the scroll handler avoids React re-renders on every scroll event, which is a valid performance optimization. Consider adding a brief comment (e.g., // Imperative updates to avoid re-render on scroll) to signal intent to future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/themes/paper/ReadingProgress.tsx` around lines 119 -
163, handleScroll currently performs imperative DOM updates (style.top,
classList, querySelector) to avoid React re-renders on every scroll; add a
concise inline comment above the function (or at the top of handleScroll)
explaining this is intentional for performance (e.g., "// Imperative updates to
avoid re-render on scroll") and reference the rationale and the related refs
(ARTICLE_SELECTOR, containerRef, scrollMarkerRef, scrollPosRef, snapToTick) so
future maintainers understand this is deliberate rather than a bug.

248-257: Connecting line width can reach 0px for deeply nested headings.

(3 - h.level) * 4 + 12 yields 0px at depth=6. If TOC items at depth 5+ are possible, the connecting line disappears. If depth > 6, the width goes negative (though browsers clamp to 0).

Consider a Math.max guard:

-  width: `${(3 - h.level) * 4 + 12}px`,
+  width: `${Math.max(4, (3 - h.level) * 4 + 12)}px`,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/themes/paper/ReadingProgress.tsx` around lines 248 -
257, The connecting line width calculation for headings in the headings.map
render can produce 0 or negative widths for deep levels; update the inline style
on the element with className styles.connectingLine (the block that maps
headings and uses h.level) to clamp the width using Math.max, e.g. Math.max((3 -
h.level) * 4 + 12, 4) and append "px" so the width never falls below a sensible
minimum.

165-180: Hardcoded 250ms initialization delay is fragile.

The setTimeout(…, 250) assumes the article DOM is ready within 250ms. If content loads slower (e.g., lazy images, code blocks, or server component streaming), heading positions will be calculated against incomplete layout. The resize listener partially mitigates this, but doesn't cover content reflows.

Consider using a ResizeObserver on the article element instead, which would reliably trigger recalculation when the article dimensions settle.

Sketch using ResizeObserver
   useEffect(() => {
-    const init = setTimeout(() => {
-      recalcHeadings()
-      handleScroll()
-      setReady(true)
-    }, 250)
+    // Initial calc
+    recalcHeadings()
+    handleScroll()
+    setReady(true)
+
+    // Re-calc when article layout changes
+    const article = document.querySelector(ARTICLE_SELECTOR)
+    let ro: ResizeObserver | undefined
+    if (article) {
+      ro = new ResizeObserver(() => {
+        recalcHeadings()
+        handleScroll()
+      })
+      ro.observe(article)
+    }
 
     window.addEventListener('resize', recalcHeadings)
     window.addEventListener('scroll', handleScroll, { passive: true })
 
     return () => {
-      clearTimeout(init)
+      ro?.disconnect()
       window.removeEventListener('resize', recalcHeadings)
       window.removeEventListener('scroll', handleScroll)
     }
   }, [recalcHeadings, handleScroll])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/themes/paper/ReadingProgress.tsx` around lines 165 -
180, Replace the fragile setTimeout-based init with a ResizeObserver watching
the article element so heading positions are recalculated when layout changes:
find the element used to derive headings (e.g., via articleRef or
document.querySelector('article')) and create a ResizeObserver that calls
recalcHeadings() and handleScroll() (optionally debounced and then calls
setReady(true) once dimensions have stabilized), attach the observer instead of
relying on the 250ms timer, and on cleanup disconnect the observer; keep the
existing window scroll listener but remove the hardcoded setTimeout and
clearTimeout usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/chronicle/next-env.d.ts`:
- Line 3: This Next.js-generated file next-env.d.ts is tracked but flips imports
between .next/dev/types and .next/types causing git noise; add an entry for
next-env.d.ts to the package's .gitignore so the auto-generated file is not
committed (keep next-env.d.ts in tsconfig.json include as required).

In `@packages/chronicle/src/themes/paper/ChapterNav.tsx`:
- Line 46: Top-level <li> elements are being rendered directly inside <nav> by
the ChapterItem usage in ChapterNav (the line returning ChapterItem for each
tree.children item); wrap the mapped output in a proper list container or ensure
ChapterItem only emits <li> when wrapped by a <ul>/<ol>. Concretely, modify
ChapterNav so the tree.children.map(...) output is enclosed in a <ul> (and
adjust any top-level chapter wrappers to become <li> with nested <ul> for
children) or alter ChapterItem to render a non-<li> element for top-level
non-folder items and only render <li> when inside a list, so all <li> elements
are descendants of a <ul> or <ol>.

In `@packages/chronicle/src/themes/paper/Page.module.css`:
- Around line 119-122: The CSS selectors .content h5 and .content h6 in
Page.module.css only set margins and rely on browser defaults for font-size; add
explicit font-size declarations for both selectors to match the paper theme's
typographic scale (choose sizes consistent with existing h1–h4 definitions in
this file) so h5 and h6 no longer inherit inconsistent browser defaults; update
the .content h5, .content h6 rule to include the chosen font-size values and
adjust line-height if needed for visual consistency.
- Around line 7-18: The fixed .navbar is starting at the viewport edge and
overlapping the 260px sidebar; update the .navbar rules in Page.module.css (the
.navbar selector) to offset it from the left by the sidebar width and constrain
its width accordingly (e.g., set left: 260px and adjust width to calc(100% -
260px) and/or max-width to calc(1024px - 260px)); if a CSS variable for the
sidebar width exists (e.g., --sidebar-width), use that instead of hardcoding
260px.

In `@packages/chronicle/src/themes/paper/ReadingProgress.module.css`:
- Around line 30-36: The two CSS classes tickLineBefore and tickLineAfter in
ReadingProgress.module.css are identical, so the scroll progress ticks never
visually change; update tickLineBefore to use the "read"/accent color (e.g., the
cobalt/accent token used elsewhere) instead of
var(--rs-color-background-neutral-tertiary) so the component in
ReadingProgress.tsx (which toggles these classes) shows read vs unread ticks;
locate the tickLineBefore and tickLineAfter selectors in
ReadingProgress.module.css and change tickLineBefore's background-color to the
appropriate accent variable (matching the scroll marker color) while leaving
tickLineAfter as the neutral color.

---

Outside diff comments:
In `@packages/chronicle/src/components/api/field-row.tsx`:
- Around line 66-71: The IconButton elements wrapping purely visual icons
(PlusIcon and TrashIcon) lack accessible names; update the PlusIcon button in
the add handler and the TrashIcon button (the other occurrence around the remove
handler) to include descriptive aria-labels (e.g., for the add button use
aria-label={`Add ${field.name || 'entry'}`} and for the delete button use
aria-label={`Delete ${entry.key || 'entry'}`} or similar), so that IconButton,
PlusIcon, TrashIcon and surrounding handlers (onChange, itemChildren,
field.name, items, entry) mirror the accessible pattern used in
key-value-editor.tsx.

---

Nitpick comments:
In `@packages/chronicle/src/app/apis/`[[...slug]]/layout.tsx:
- Around line 4-5: The Search component mounts on every API page and immediately
triggers useDocsSearch due to allowEmpty: true and delayMs: 100, causing an
unnecessary initial fetch; to fix, stop rendering <Search> on server pages by
replacing its immediate render with a lightweight client-only keyboard-listener
component (e.g., a small client component that attaches the Cmd+K / keydown
handler and only mounts/returns the full Search component when the dialog is
opened), or change the search initialization so useDocsSearch is only invoked on
open; update references around Search, useDocsSearch, allowEmpty and delayMs to
ensure the dialog-triggering client component instantiates Search lazily.

In `@packages/chronicle/src/themes/paper/ChapterNav.module.css`:
- Around line 34-36: The .item rule in ChapterNav.module.css is redundant
because the parent .chapterItems already sets list-style: none; remove the
list-style: none declaration from the .item selector (or delete the entire .item
rule if it contains no other declarations) so styling is not duplicated; update
any references to .item in the component only if you remove the selector
entirely to ensure no dependent styles are lost.

In `@packages/chronicle/src/themes/paper/ChapterNav.tsx`:
- Around line 23-31: The code mutates chapterIndex inside the render loop
(tree.children.map) in ChapterNav; remove this side-effect by precomputing
folder indices before returning JSX: iterate tree.children once (e.g., with
Array.prototype.reduce or a for loop) to build a list or map of folder items to
their sequential chapter numbers, then in the render map use that precomputed
index (referencing chapterIndex logic, tree.children, and the folder branch)
instead of incrementing a variable during mapping. Ensure the new logic
preserves skipping 'separator' items and only counts items with type ===
'folder' and children.

In `@packages/chronicle/src/themes/paper/Layout.module.css`:
- Around line 9-15: The hard-coded 260px "magic number" is duplicated between
.sidebar and .content; introduce a CSS custom property (e.g., --sidebar-width)
at the top of Layout.module.css and use it for .sidebar { width:
var(--sidebar-width, 260px); } and .content { padding-right:
var(--sidebar-width, 260px); } so changing the sidebar width requires updating
only the custom property; update any other occurrences (lines noted around
.sidebar and .content) to reference the same variable.

In `@packages/chronicle/src/themes/paper/Page.module.css`:
- Around line 73-78: Replace the hard-coded "margin-top: 72px" on the .article
rule with a value derived from the navbar's size using a CSS custom property
(e.g., --navbar-height) or a calc() that references --rs-space-3 and the fixed
navbar height, so the spacing updates automatically when the navbar's height or
padding changes; update the .article selector to use that CSS variable (or calc
expression) and ensure the fixed navbar sets/exports the same --navbar-height
variable so both elements stay in sync.

In `@packages/chronicle/src/themes/paper/Page.tsx`:
- Around line 42-48: Derived navigation values in Page (pages, currentIndex,
prev, next, crumbs) are recomputed on every render; wrap the computations that
call flattenTree and findBreadcrumb in a useMemo so they only recompute when
inputs change. Specifically, memoize pages = flattenTree(tree.children) and
derive currentIndex, prev, next from pages and pathname inside a single useMemo
keyed on [tree, pathname, page.slug], and memoize crumbs =
findBreadcrumb(tree.children, page.slug) (or include it in the same memo) to
avoid redundant work during unrelated re-renders; update the Page component to
import/use React.useMemo and replace the direct calls to
flattenTree/findBreadcrumb accordingly.
- Around line 55-72: The disabled arrow rendering currently uses non-semantic
<span> elements (in the prev/next conditional blocks around NextLink and
ChevronLeftIcon/ChevronRightIcon), which harms accessibility; replace those
<span className={styles.arrowDisabled}> elements with proper disabled controls —
either a <button disabled className={styles.arrowDisabled} aria-label="Previous"
or "Next"> wrapping the Chevron*Icon, or keep an anchor but add
aria-disabled="true" and tabIndex={-1} to the non-link state so screenreaders
and keyboard users get correct semantics; update the prev/next conditional
branches that reference prev, next, NextLink, styles.arrowDisabled,
ChevronLeftIcon and ChevronRightIcon accordingly.

In `@packages/chronicle/src/themes/paper/ReadingProgress.module.css`:
- Around line 70-80: Replace the hardcoded font stack in the .headingLink rule
with the design-system variable used by .headingLabel: use var(--rs-font-mono)
for font-family instead of "'SF Mono', 'Fira Code', monospace" so both
.headingLink and .headingLabel consistently reference the same mono font
variable.

In `@packages/chronicle/src/themes/paper/ReadingProgress.tsx`:
- Around line 119-163: handleScroll currently performs imperative DOM updates
(style.top, classList, querySelector) to avoid React re-renders on every scroll;
add a concise inline comment above the function (or at the top of handleScroll)
explaining this is intentional for performance (e.g., "// Imperative updates to
avoid re-render on scroll") and reference the rationale and the related refs
(ARTICLE_SELECTOR, containerRef, scrollMarkerRef, scrollPosRef, snapToTick) so
future maintainers understand this is deliberate rather than a bug.
- Around line 248-257: The connecting line width calculation for headings in the
headings.map render can produce 0 or negative widths for deep levels; update the
inline style on the element with className styles.connectingLine (the block that
maps headings and uses h.level) to clamp the width using Math.max, e.g.
Math.max((3 - h.level) * 4 + 12, 4) and append "px" so the width never falls
below a sensible minimum.
- Around line 165-180: Replace the fragile setTimeout-based init with a
ResizeObserver watching the article element so heading positions are
recalculated when layout changes: find the element used to derive headings
(e.g., via articleRef or document.querySelector('article')) and create a
ResizeObserver that calls recalcHeadings() and handleScroll() (optionally
debounced and then calls setReady(true) once dimensions have stabilized), attach
the observer instead of relying on the 250ms timer, and on cleanup disconnect
the observer; keep the existing window scroll listener but remove the hardcoded
setTimeout and clearTimeout usage.

- Add next-env.d.ts to .gitignore and remove from tracking
- Fix invalid HTML: wrap ChapterNav items in <ul>, precompute chapter indices
- Use CSS variables for sidebar width and navbar height (no magic numbers)
- Add explicit font-size for h5/h6 in prose styles
- Use semantic accent token for ReadingProgress tick/marker colors
- Use var(--rs-font-mono) consistently in ReadingProgress
- Replace setTimeout init with ResizeObserver for reliable layout detection
- Add Math.max guard for connecting line width at deep heading levels
- Add useMemo for derived navigation data in Page
- Use <button disabled> with aria-label for disabled prev/next arrows
- Remove redundant .item { list-style: none } in ChapterNav

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
packages/chronicle/src/themes/paper/ReadingProgress.tsx (2)

233-240: Optional: simplify the zIndex expression.

10 * (h.level < 4 ? 1 : 0) is more clearly expressed as a direct ternary.

♻️ Proposed simplification
-  zIndex: 10 * (h.level < 4 ? 1 : 0),
+  zIndex: h.level < 4 ? 10 : 0,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/themes/paper/ReadingProgress.tsx` around lines 233 -
240, The inline style for styles.headingLabel uses an unnecessary multiplication
for zIndex; replace the expression 10 * (h.level < 4 ? 1 : 0) with a direct
ternary returning 10 or 0 (e.g., h.level < 4 ? 10 : 0) inside the style object
in ReadingProgress.tsx where the heading label div is rendered (referencing h
and the inline style block).

233-240: Optional: simplify the zIndex expression.

10 * (h.level < 4 ? 1 : 0) is needlessly roundabout.

♻️ Proposed simplification
-  zIndex: 10 * (h.level < 4 ? 1 : 0),
+  zIndex: h.level < 4 ? 10 : 0,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/themes/paper/ReadingProgress.tsx` around lines 233 -
240, The zIndex expression in ReadingProgress.tsx is overly indirect: replace 10
* (h.level < 4 ? 1 : 0) with a clearer expression such as h.level < 4 ? 10 : 0
(or Number(h.level < 4) * 10) in the inline style on the element using
styles.headingLabel so the intent (assign zIndex 10 when h.level < 4, otherwise
0) is explicit and easier to read.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/chronicle/src/themes/paper/Layout.module.css`:
- Around line 1-4: The CSS rule .layout violates stylelint's
declaration-empty-line-before by placing the regular declaration min-height
immediately after the custom property --paper-sidebar-width; fix it in
Layout.module.css by inserting a single blank line between the custom property
declaration (--paper-sidebar-width: 260px;) and the following regular
declaration (min-height: 100vh;) so that there is an empty line separating the
custom property from min-height.
- Around line 1-4: In the .layout rule, add a blank line between the custom
property --paper-sidebar-width and the normal declaration min-height so the
declaration-empty-line-before Stylelint rule is satisfied; update the .layout
block containing --paper-sidebar-width and min-height to insert an empty line
immediately before the min-height declaration.

In `@packages/chronicle/src/themes/paper/Page.module.css`:
- Around line 1-8: In the .main rule in Page.module.css, add a single blank line
between the last custom property (--paper-navbar-total) and the first standard
declaration (flex) to satisfy the Stylelint declaration-empty-line-before rule;
update the .main block so the custom properties remain grouped together, then a
blank line, then the rest of declarations (flex, max-width, margin) in the same
rule.
- Around line 1-8: The .main rule in Page.module.css places several custom
properties (--paper-*) followed immediately by normal declarations (flex,
max-width, margin) which violates the stylelint declaration-empty-line-before
rule; open the .main selector and insert a single blank line between the last
custom property (--paper-navbar-total) and the next declaration (flex), matching
the fix used in Layout.module.css so custom-property blocks are separated from
other declarations.

In `@packages/chronicle/src/themes/paper/ReadingProgress.tsx`:
- Around line 31-58: The backward pass in resolveOverlaps wrongly reads mutated
neighbor positions which lets multiple headings collapse to 0; fix by replacing
the backward loop with a single-pass clamp that uses a running "nextPos"
initialized to maxPosition and for each heading (iterate i from end to start)
set its yPosition to Math.min(resolved[i].yPosition, nextPos) then update
nextPos = Math.max(0, resolved[i].yPosition - TICK_HEIGHT) so the allowed slot
is propagated backward without reading already-clamped neighbors; keep
references to resolveOverlaps, resolved, maxPosition, and TICK_HEIGHT when
making the change.
- Around line 31-58: The backward pass in resolveOverlaps can leave multiple
headings at the same yPosition when there are more headings than tick slots; fix
this by turning the clamp into a clamp-and-shift: inside resolveOverlaps's
backward loop where you compute maxAllowed and detect resolved[i].yPosition >
maxAllowed, set resolved[i].yPosition = Math.max(0, maxAllowed) and then walk j
from i-1 down to 0; for each earlier resolved[j], if resolved[j].yPosition >
resolved[j+1].yPosition - TICK_HEIGHT, set resolved[j].yPosition = Math.max(0,
resolved[j+1].yPosition - TICK_HEIGHT) so earlier items are pulled along and no
two items end up with the same yPosition. Ensure you update the objects in the
resolved array (e.g., resolved[i] = { ...resolved[i], yPosition: newPos }) when
modifying positions.

---

Duplicate comments:
In `@packages/chronicle/src/themes/paper/Page.module.css`:
- Around line 10-21: The fixed .navbar is missing a left offset for the sidebar;
update the .navbar rule to set left: var(--paper-sidebar-width) and adjust its
width (e.g., width or max-width to calc(100% - var(--paper-sidebar-width))) so
it no longer sits under the sidebar defined by --paper-sidebar-width in
Layout.module.css; ensure the change is applied in Page.module.css where the
.navbar selector is defined.
- Around line 10-21: The .navbar rule uses position: fixed with width: 100% but
no left offset, so it starts at viewport left and covers the sidebar; update the
.navbar CSS to account for the sidebar by adding a left offset and adjusting
width (e.g., set left to the sidebar width variable or calc and change width to
calc(100% - sidebarWidthVar)) so the fixed navbar clears the 260px sidebar;
modify the .navbar selector in Page.module.css accordingly.

---

Nitpick comments:
In `@packages/chronicle/src/themes/paper/ReadingProgress.tsx`:
- Around line 233-240: The inline style for styles.headingLabel uses an
unnecessary multiplication for zIndex; replace the expression 10 * (h.level < 4
? 1 : 0) with a direct ternary returning 10 or 0 (e.g., h.level < 4 ? 10 : 0)
inside the style object in ReadingProgress.tsx where the heading label div is
rendered (referencing h and the inline style block).
- Around line 233-240: The zIndex expression in ReadingProgress.tsx is overly
indirect: replace 10 * (h.level < 4 ? 1 : 0) with a clearer expression such as
h.level < 4 ? 10 : 0 (or Number(h.level < 4) * 10) in the inline style on the
element using styles.headingLabel so the intent (assign zIndex 10 when h.level <
4, otherwise 0) is explicit and easier to read.

- Fix resolveOverlaps backward pass to propagate clamp-and-shift,
  preventing multiple headings from collapsing to same position
- Add blank line after CSS custom properties (stylelint)
- Simplify zIndex ternary in ReadingProgress

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/chronicle/src/themes/paper/Page.module.css (1)

32-52: .arrowDisabled duplicates .arrow's flex setup — consider CSS composition.

Both .arrow and .arrowDisabled independently declare display: flex; align-items: center. In a CSS-modules context, extract the shared layout into a composable utility or merge them with a modifier class:

♻️ Suggested refactor
+.arrowBase {
+  display: flex;
+  align-items: center;
+}
+
 .arrow {
-  display: flex;
-  align-items: center;
   color: var(--rs-color-foreground-base-primary);
   text-decoration: none;
 }

 .arrowDisabled {
-  display: flex;
-  align-items: center;
   color: var(--rs-color-foreground-base-tertiary);
   opacity: 0.4;
   cursor: default;
   border: none;
   background: none;
   padding: 0;
 }

Then in Page.tsx, compose the classes:

// For active arrow:
className={`${styles.arrowBase} ${styles.arrow}`}
// For disabled arrow:
className={`${styles.arrowBase} ${styles.arrowDisabled}`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/themes/paper/Page.module.css` around lines 32 - 52,
Extract the shared layout rules (display: flex; align-items: center) from .arrow
and .arrowDisabled into a new CSS class (e.g., .arrowBase) in Page.module.css,
leave color/hover/disabled-specific rules in .arrow and .arrowDisabled, then
update Page.tsx to compose classes so active arrows use styles.arrowBase +
styles.arrow and disabled ones use styles.arrowBase + styles.arrowDisabled
(ensure you reference the existing .arrow and .arrowDisabled class names when
composing).
packages/chronicle/src/themes/paper/ReadingProgress.tsx (1)

191-192: Window resize listener calls recalcHeadings without debouncing.

recalcHeadings performs several getBoundingClientRect() calls (each a forced reflow) and multiple setState dispatches. The resize event fires on every pixel change during a drag-resize. On pages with many TOC items or on lower-end devices this can cause noticeable jank.

Consider debouncing via requestAnimationFrame or a short setTimeout:

♻️ Suggested debounce wrapper (rAF-based)
+  const rafRef = useRef<number | null>(null)
+  const debouncedRecalc = useCallback(() => {
+    if (rafRef.current !== null) cancelAnimationFrame(rafRef.current)
+    rafRef.current = requestAnimationFrame(() => {
+      recalcHeadings()
+    })
+  }, [recalcHeadings])

   window.addEventListener('resize', debouncedRecalc)
   ...
   return () => {
     ...
-    window.removeEventListener('resize', recalcHeadings)
+    if (rafRef.current !== null) cancelAnimationFrame(rafRef.current)
+    window.removeEventListener('resize', debouncedRecalc)
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/themes/paper/ReadingProgress.tsx` around lines 191 -
192, The resize listener currently calls recalcHeadings directly causing layout
thrash; replace the direct window.addEventListener('resize', recalcHeadings)
with a debounced wrapper (e.g., an rAF-based or short setTimeout debounce) that
batches calls to recalcHeadings, ensure you keep a stable reference to the
debounced function so the cleanup removes the exact same listener, and update
the effect/unmount logic to remove this debounced resize handler instead of the
raw recalcHeadings; target the window.addEventListener('resize', ...) site and
the recalcHeadings function for the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/chronicle/src/themes/paper/ReadingProgress.tsx`:
- Around line 97-98: The hasScroll calculation underestimates scrollability by
not accounting for the top navbar height; update the logic in ReadingProgress
(where hasScroll is computed and setIsScrollable is called) to compare
articleBox.height against (window.innerHeight - NAV_HEIGHT) so pages whose
content exceeds the visible area beneath the navbar are treated as scrollable;
ensure you reference the NAV_HEIGHT constant and keep the rest of the
setIsScrollable flow unchanged.
- Around line 234-239: The clickable tick divs rendered in ticks.map (the
element using className styles.tickClickable and the click handler
scrollToTick(y)) are not keyboard/AT accessible; update that element to be an
accessible button by adding role="button", tabIndex={0}, a descriptive
aria-label (e.g. indicating the tick position or percent), and an onKeyDown
handler that listens for Enter/Space and calls scrollToTick(y) (preserve the
existing onClick). Ensure the handlers use the same scrollToTick function and
that aria-label text is meaningful for screen readers.

---

Duplicate comments:
In `@packages/chronicle/src/themes/paper/Page.module.css`:
- Around line 11-22: The .navbar CSS rule is missing a left offset so the fixed
navbar overlaps the 260px sidebar; update the .navbar declaration to include a
left offset (e.g., left: var(--paper-sidebar-width) or left: 260px) and adjust
its width (e.g., width: calc(100% - var(--paper-sidebar-width)) or calc(100% -
260px)) so it lines up to the right of the sidebar; modify the .navbar rule
(class name .navbar) to use the chosen CSS variable (or hard value) for left and
recalc width accordingly.

In `@packages/chronicle/src/themes/paper/ReadingProgress.tsx`:
- Around line 31-68: The review note shows an accidental duplicate comment
marker; remove the stray "[duplicate_comment]" token from the review text (or
any duplicated approval tag) so the PR comment is not duplicated, and ensure the
code change message only contains the intended approval
"[approve_code_changes]"; this is a non-code cleanup tied to the resolveOverlaps
change and its inner propagation loop (the j-based backward propagation that
updates resolved[i] and resolved[j]).

---

Nitpick comments:
In `@packages/chronicle/src/themes/paper/Page.module.css`:
- Around line 32-52: Extract the shared layout rules (display: flex;
align-items: center) from .arrow and .arrowDisabled into a new CSS class (e.g.,
.arrowBase) in Page.module.css, leave color/hover/disabled-specific rules in
.arrow and .arrowDisabled, then update Page.tsx to compose classes so active
arrows use styles.arrowBase + styles.arrow and disabled ones use
styles.arrowBase + styles.arrowDisabled (ensure you reference the existing
.arrow and .arrowDisabled class names when composing).

In `@packages/chronicle/src/themes/paper/ReadingProgress.tsx`:
- Around line 191-192: The resize listener currently calls recalcHeadings
directly causing layout thrash; replace the direct
window.addEventListener('resize', recalcHeadings) with a debounced wrapper
(e.g., an rAF-based or short setTimeout debounce) that batches calls to
recalcHeadings, ensure you keep a stable reference to the debounced function so
the cleanup removes the exact same listener, and update the effect/unmount logic
to remove this debounced resize handler instead of the raw recalcHeadings;
target the window.addEventListener('resize', ...) site and the recalcHeadings
function for the change.

@rsbh rsbh merged commit c4798e0 into main Feb 20, 2026
1 check passed
@coderabbitai coderabbitai bot mentioned this pull request Feb 20, 2026
7 tasks
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.

2 participants