Codebook Migration Audit

Vanilla JS components vs React primitives — inconsistencies and decisions for Milestone 5

13
Same / aligned
9
Different
4
Problems to fix
5
Opportunities
Component / Area Aspect Vanilla JS (codebook.js) React primitive Status Notes
Badge Base font size 0.72rem (badge.css) 0.72rem (same CSS) Same React reuses exact same CSS
Codebook override size 0.82rem + 0.2rem 0.5rem pad
(codebook-panel.css .codebook-group .badge)
No equivalent — Badge has no size prop Different Codebook bumps badges to 0.82rem via CSS scope. React Badge will inherit this if wrapped in .codebook-group, but if the React island doesn't use that class, badges will be small
Delete button (user badge) <button class="badge-delete">x</button>
via createUserTagBadge()
<button class="badge-delete">&times;</button> Same Identical structure. React uses &times; entity, vanilla uses literal x — renders identically
AI badge delete (::after pseudo) CSS ::after with \00d7 content React Badge variant="ai" — same CSS, no ::after override Same Both rely on the CSS pseudo-element for the delete chip
Data attributes data-badge-type, data-tag-name Neither attribute emitted Different Vanilla JS modules (tag-filter, transcript-annotations) query [data-badge-type] and [data-tag-name]. React Badge doesn't emit these. Not a problem if those modules are also migrated, but a problem during coexistence
EditableText Editing mechanism contentEditable on span contentEditable on span/p Same Both use contentEditable. React adds suppressContentEditableWarning
Codebook title editing Uses <input class="group-title-input">not contentEditable Uses contentEditable span Different Codebook uses traditional <input> elements for group title/subtitle editing, while React EditableText uses contentEditable. Different UX: input has native border/focus ring, contentEditable blends into text flow
CSS editing state .group-title-input — has visible border, font-family: var(--bn-font-body), inherits size/weight .editing class — uses --bn-colour-editing-bg + outline, no border Different Codebook inputs look like form fields (border). Quote/heading editing looks like highlighted text (background+outline). The React way is more subtle and modern
Keyboard handling Enter = commit, Escape = cancel, blur = commit Enter (no shift) = commit, Escape = cancel, blur = commit Same Identical behaviour
Committed indicator .edited class (dashed underline) committedClassName="edited" (same CSS) Same Same visual
TagInput Codebook add-tag mechanism Plain <input class="tag-add-input"> — no autocomplete, no ghost text, max 40 chars Full autocomplete with ghost text, suggestion dropdown, auto-resize Different Codebook's tag input is primitive compared to the React TagInput. The React version is a significant upgrade
Auto-suggest in codebook No autocomplete — just a plain text input Full vocabulary-based autocomplete with prefix matching Opportunity Switching to React TagInput gives codebook free autocomplete. Vocabulary = all existing tags from the project
Font/size for tag input 0.82rem, var(--bn-font-mono) Inherits via .tag-input class in tag-input.css — 0.72rem, var(--bn-font-mono) Different Codebook's .tag-add-input is 0.82rem (matching upscaled badges). React TagInput's .tag-input class would be 0.72rem. Need a CSS scope override or size prop
Commit behaviour Enter/blur = commit, Escape = cancel Enter/blur = commit, Escape = cancel, Tab = commit+reopen Same React adds Tab-to-reopen for rapid tagging — a bonus feature
Sparkline vs Micro Bar Visualization type Single horizontal bar per tag (.tag-micro-bar), width proportional to count Vertical stacked bars (Sparkline), height proportional to count Different Completely different visualisations. Codebook uses horizontal micro-bars (frequency indicators), sessions table uses vertical sparklines (sentiment distribution). Different data, different purpose — both are correct for their context
Reuse for codebook? Horizontal bar: width 2-48px, colour from codebook palette, single value Sparkline: vertical bars, multiple values, generic colours Problem React Sparkline doesn't fit the codebook use case. Codebook needs a simple horizontal frequency bar, not a multi-bar chart. Options: (a) build MicroBar as a new primitive, (b) just use a styled div with inline width, (c) make Sparkline support horizontal mode
Bar colour system Uses var(--bn-bar-{set}) tokens from pentadic palette Accepts colour string prop — consumer passes the token Same Both are token-driven, just at different abstraction levels
Layout Grid system CSS columns: 240px (masonry) N/A — no codebook composition yet Same Will reuse existing codebook-panel.css
Hardcoded spacing padding: 12px, margin-bottom: 6px, gap: 4px, gap: 6px, padding: 3px 4px React components use tokens or let CSS handle it Problem codebook-panel.css uses hardcoded px values where the design system has tokens. 12px should be var(--bn-space-md), 6px could be var(--bn-space-sm). Not blocking but worth cleaning up during migration
Merge target colour rgba(37, 99, 235, 0.06) — hardcoded blue N/A Problem Should use color-mix(in srgb, var(--bn-colour-accent) 6%, transparent) or a token. Will look wrong in dark mode
Border radius var(--bn-radius-lg) (group), var(--bn-radius-sm) (tags, inputs) Same tokens via CSS Same Fully aligned
Colour system Pentadic palette tokens 5 colour sets, each with slots: --bn-{set}-{N}-bg, --bn-group-{set}, --bn-bar-{set} React Badge accepts colour prop — consumer passes the resolved var(--bn-ux-2-bg) Same The colour resolution happens in the codebook data layer, not in the component. Both worlds pass the resolved CSS var as a string
Dark mode support Tokens have light/dark values in tokens.css Same tokens — React doesn't override Same Fully aligned. light-dark() function in tokens.css handles both
Interactions Drag and drop Native HTML5 drag API (draggable="true", dragstart/dragover/drop) No drag support in any React primitive Opportunity Need to implement drag-and-drop in the React Codebook island. Options: (a) native HTML5 drag API (like vanilla), (b) @dnd-kit, (c) custom pointer events. Native is simplest and matches existing behaviour
Tag merge (drag onto tag) Drag tag onto another tag = merge confirmation modal No equivalent Opportunity Unique to codebook. The merge flow (highlight target, confirmation modal, combine quote counts) needs to be built from scratch in React
Modal system createModal() / showConfirmModal() from modal.js No React modal primitive yet Different Codebook uses the vanilla JS modal system (modal.js). React has no modal component. Options: (a) call vanilla modal from React (bridge), (b) build React modal, (c) use <dialog> element directly
Cross-window sync window.addEventListener('storage', ...) re-renders on external changes No equivalent Opportunity In serve mode, the API is the source of truth — no localStorage needed. Cross-tab sync becomes server-push (websocket or polling) rather than storage events. This is a genuine improvement
Toggle AI tag visibility body.hide-ai-tags class toggle — hides all .badge-ai via CSS React Toggle component + CSS class on body Same Both approaches work. In React codebook, the toggle state would be local React state, but could also toggle the body class for backward compat with non-React badge rendering
Codebook-specific New group placeholder border border: 1.5px dashed — non-standard sub-pixel width N/A Problem 1.5px is rendered as 2px on 1x screens, 1.5px on retina. Should be 1px or 2px for consistency. Minor but worth fixing
Colour set selector Auto-assigned (round-robin through COLOUR_SET_ORDER) No manual colour picker UI OK Auto-assignment is fine for now. Manual colour picker is a future enhancement

Badge: Vanilla JS vs React

Structure comparison

Vanilla JS (badge-utils.js)
<span class="badge badge-user" data-badge-type="user" data-tag-name="confusion" style="background: var(--bn-emo-1-bg)"> confusion <button class="badge-delete" aria-label="Delete tag">x</button> </span>
React (Badge.tsx)
<span class="badge badge-user" style="backgroundColor: var(--bn-emo-1-bg)"> confusion <button class="badge-delete" aria-label="Remove confusion"> &times; </button> </span>

Key differences

AspectVanillaReactWinner
data-badge-type Always present Not emitted Vanilla — needed during coexistence with tag-filter.js which queries [data-badge-type="user"]
data-tag-name Always present on user badges Not emitted Vanilla — needed by applyCodebookColours() in codebook.js which updates badge colours by querying [data-tag-name]
aria-label on delete Generic: "Delete tag" Specific: "Remove {name}" React — more accessible, screen readers know which tag is being removed
Delete character Literal x &times; (multiplication sign) React× is semantically correct and looks better at small sizes
Recommendation

Add data-badge-type and data-tag-name to React Badge during the coexistence period. These are cheap to add and prevent breakage of vanilla JS modules that query for them. Can be removed once tag-filter and codebook are both React.

Keep the React improvements (specific aria-label, × character) — they're strictly better.

EditableText: Codebook <input> vs React contentEditable

Codebook group title editing (vanilla)

Vanilla: <input> replaces text
// codebook.js: _editGroupTitle() span.style.display = 'none'; // hide text const inp = document.createElement('input'); inp.className = 'group-title-input'; inp.value = currentTitle; // CSS: border, bg, width: 100%, outline: none // focus ring on :focus
React: contentEditable on same element
// EditableText.tsx <span contentEditable={isEditing} className="group-title-text" > {isEditing ? undefined : value} </span> // CSS: editing-bg + outline (no border) // text stays in flow, no layout shift

Visual difference

AspectCodebook <input>React contentEditableRecommendation
Visual treatment Form field: visible border, --bn-colour-bg background, full-width Inline highlight: --bn-colour-editing-bg (subtle tint), 1px outline React — more refined, less disruptive. The form field look is dated
Layout shift Yes — text element hidden, input element shown at full width No — same element becomes editable, dimensions unchanged React — zero layout shift is strictly better UX
Font inheritance font: inherit on input — generally works but input elements have quirks Same element, same font — no inheritance issues React — more reliable
Select-all on focus No (just focuses input, cursor at end) Yes — selects all text on edit start React — faster to replace, can also just position cursor
Multi-line editing Not possible (input is single-line) Possible with Shift+Enter (contentEditable) React — useful for subtitle descriptions
Recommendation

Use React EditableText for codebook titles and subtitles. The contentEditable approach is superior: no layout shift, no font inheritance quirks, select-all on focus. The .group-title-input / .group-subtitle-input CSS can be retired.

For codebook, use trigger="click" so titles become editable on click (matching current behaviour).

TagInput: Codebook plain input vs React autocomplete

Feature comparison

FeatureCodebook (vanilla)React TagInput
Autocomplete None Prefix-match dropdown with ghost text
Max length maxLength=40 No limit
Auto-resize No — fixed max-width: 160px Yes — resizes to content + ghost text, min 48px
Suggestion dropdown None Up to 8 suggestions, keyboard navigable
Tab behaviour Default (moves to next focusable) onCommitAndReopen — commit then reopen for rapid multi-tag entry
Ghost text None Grey suffix showing best prefix match, accept with ArrowRight
Duplicate prevention Manual check in JS exclude prop filters out already-applied tags
Font size 0.82rem (codebook scale) 0.72rem (default badge scale)
Font size gap

The codebook upscales all badges and inputs to 0.82rem via .codebook-group .badge. The React TagInput would render at 0.72rem by default.

Fix: Either add a .codebook-group .tag-input CSS override (like the badge override), or add a size prop to TagInput. The CSS override is simpler and follows the existing pattern.

Recommendation

Use React TagInput. It's a strict superset of the vanilla input — every feature is preserved, plus autocomplete, ghost text, and rapid-entry Tab flow.

Add CSS scope override for the codebook context: .codebook-group .tag-input { font-size: 0.82rem; }

Set vocabulary to all known tags from the project (from API response).

Consider maxLength. The vanilla input had maxLength=40, which is a reasonable guard. Could add this to TagInput as an optional prop, or just let it be — 40 chars is generous enough that it rarely triggers.

Sparkline (React) vs Micro Bar (Codebook vanilla)

These are fundamentally different visualisations serving different purposes.

Side-by-side

Codebook: Micro Bar
Purpose: Per-tag frequency indicator Direction: Horizontal (left to right) Data: Single value (quote count per tag) Width: 2px to 48px proportional Height: 0.6rem (fixed) Colour: Group palette bar colour var(--bn-bar-{set}) or var(--bn-bar-none) Corner: rounded right side only 0 var(--bn-radius-sm) var(--bn-radius-sm) 0 Count label: right-aligned, tabular-nums
Sessions Table: Sparkline
Purpose: Sentiment distribution per session Direction: Vertical (bottom to top) Data: Multiple values (7 sentiments) Height: 2px to 20px proportional Width: 5px per bar (fixed) Colour: Sentiment palette var(--bn-sentiment-{name}) Corner: rounded top only (1px 1px 0 0) Container: 54px tall, inline-flex

Can Sparkline be reused for codebook?

OptionEffortQualityNotes
A. Keep micro-bar as plain CSS/div Low Fine A simple <div style="width: Npx; background: var(...)"> inside the React component. No primitive needed for a single proportional bar
B. Build MicroBar primitive Medium Good Reusable horizontal bar with value, max, colour props. Clean but maybe over-engineering for one use case
C. Make Sparkline support horizontal mode High Confusing Would conflate two different visualisations under one component. Sparklines are conventionally vertical
Recommendation

Option A: Inline div. The micro-bar is a single styled <div> with proportional width. It doesn't need a primitive — it's 3 lines of JSX. If it's used in multiple places later, extract it then.

The existing .tag-micro-bar CSS class handles height, border-radius, and transition. Just apply it.

Layout & Spacing Inconsistencies

Hardcoded values in codebook-panel.css

SelectorPropertyCurrent valueToken equivalentShould change?
.codebook-group padding 12px var(--bn-space-md) (12px) Match — same value, but should use token for consistency
.group-header margin-bottom 6px var(--bn-space-sm) (8px) Different — 6px is between xs (4px) and sm (8px). Intentional?
.group-header gap 4px var(--bn-space-xs) (4px) Match — use token
.tag-row gap 6px var(--bn-space-sm) (8px) Different — same 6px oddity
.tag-row padding 3px 4px No exact token OK — tight spacing is intentional for dense tag lists
.tag-list gap 2px No token (smallest is --bn-space-xs = 4px) OK — tighter than the scale, but needed for compact tag lists
.group-total-row gap 6px var(--bn-space-sm) (8px) Different
.tag-bar-area gap 4px var(--bn-space-xs) (4px) Match
.new-group-placeholder border 1.5px dashed Standard: 1px or 2px Problem — sub-pixel renders inconsistently
.tag-row.merge-target background rgba(37, 99, 235, 0.06) color-mix(in srgb, var(--bn-colour-accent) 6%, transparent) Problem — hardcoded blue, wrong in dark mode
.codebook-grid columns 240px N/A (layout-specific) OK — layout values don't need tokens
.codebook-grid max-width 1200px N/A (layout-specific) OK
The 6px question

Three places use 6px for gap/margin. The design system scale is: 4px (xs), 8px (sm), 12px (md), 16px (lg), 24px (xl). There's no 6px token.

Options:

  • Round up to 8px (--bn-space-sm) — slightly more spacious, fully on-scale
  • Round down to 4px (--bn-space-xs) — tighter, may feel cramped
  • Keep 6px — hardcoded but visually tuned for this specific context

Verdict: The codebook is a dense information display. 6px was likely chosen by eye to balance density with readability. Round to var(--bn-space-sm) (8px) — the 2px difference is imperceptible but puts us on-scale.

Decision Tracker

Use the dropdowns to record decisions. These will inform the Codebook React island implementation.

# Decision Options Choice
1 Badge data-badge-type / data-tag-name A. Add to React Badge now
B. Skip (not needed in serve mode)
C. Add only during coexistence phase
2 Codebook editing: <input> vs contentEditable A. Use React EditableText (contentEditable)
B. Keep <input> pattern (build new component)
C. Mix: titles=contentEditable, subtitles=input
3 TagInput for codebook add-tag A. Use React TagInput (with autocomplete)
B. Keep plain input (match current UX)
C. Use TagInput but disable autocomplete initially
4 Micro bar visualisation A. Inline div (simplest)
B. New MicroBar primitive
C. Extend Sparkline with horizontal mode
5 6px spacing: tokenise or keep? A. Round to 8px (--bn-space-sm)
B. Round to 4px (--bn-space-xs)
C. Keep 6px hardcoded
6 Merge target background colour A. Use color-mix() with accent token
B. Use a new token --bn-colour-accent-bg
C. Keep hardcoded rgba
7 New group placeholder border width A. Change to 1px
B. Change to 2px
C. Keep 1.5px
8 Modal system for codebook confirmations A. Bridge to vanilla modal.js
B. Build React modal component
C. Use native <dialog> element
9 Drag and drop implementation A. Native HTML5 drag API
B. @dnd-kit library
C. Custom pointer events
10 Read-only first or interactive? A. Read-only first, mutations later
B. Interactive from the start