Vanilla JS components vs React primitives — inconsistencies and decisions for Milestone 5
| 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">×</button> |
Same | Identical structure. React uses × 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 |
| Aspect | Vanilla | React | Winner |
|---|---|---|---|
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 |
× (multiplication sign) |
React — × is semantically correct and looks better at small sizes |
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.
| Aspect | Codebook <input> | React contentEditable | Recommendation |
|---|---|---|---|
| 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 |
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).
| Feature | Codebook (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) |
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.
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.
These are fundamentally different visualisations serving different purposes.
| Option | Effort | Quality | Notes |
|---|---|---|---|
| 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 |
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.
| Selector | Property | Current value | Token equivalent | Should 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 |
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:
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.
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 tokenB. Use a new token --bn-colour-accent-bgC. 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 |