Two-layer architecture: atomic CSS design tokens (shared by all render paths) +
React components (the active development surface in serve mode). CSS custom
properties are the single source of truth for visual decisions. Dark mode uses
light-dark() — no JS required.
Bristlenose has two rendering paths that share the same CSS tokens and stylesheets.
React serve mode (bristlenose serve) is the actively developed
experience. Static HTML (bristlenose render) is the frozen
offline snapshot — correct data, no new design work.
tokens.css — design tokensatoms/ — 18 filesmolecules/ — 16 filesorganisms/ — 9 filestemplates/ — 3 CSS + 14 HTML
Static HTML contains comment markers (<!-- bn-toolbar -->).
At serve time, re.sub in app.py replaces them with
React mount divs (<div id="bn-toolbar-root">).
main.tsx calls createRoot() on each mount point.
main.js boot array references functions at parse time)useSyncExternalStore (React Context can’t span separate createRoot() calls)showToast, copyToClipboard, switchToTab) remain on window for interop
The single source of truth for all visual decisions. tokens.css
defines colours, spacing, radii, typography, transitions, and dark mode overrides
via light-dark(). Override these CSS custom properties to theme the
entire application.
File: tokens.css. Dark mode via light-dark() +
color-scheme: light dark on :root. User override via
[data-theme="light|dark"] attribute. ~87% browser support (mid-2024+).
Smallest indivisible building blocks. Each atom is a single CSS file in
bristlenose/theme/atoms/. Atoms reference only design tokens,
never other atoms. 18 files.
Remove “usability” from all quotes?
Also: atoms/input.css (text input base), atoms/logo.css,
atoms/footer.css, atoms/interactive.css,
atoms/span-bar.css (vertical extent bars),
atoms/context-expansion.css, atoms/journey-label.css,
atoms/moderator-question.css, atoms/autocode-toast.css.
Groups of atoms forming single functional units. Files live in
bristlenose/theme/molecules/. 16 files.
Also: molecules/bar-group.css, molecules/quote-actions.css,
molecules/tag-input.css, molecules/name-edit.css,
molecules/help-overlay.css, molecules/feedback.css,
molecules/autocode-report.css, molecules/threshold-review.css,
molecules/sparkline.css, molecules/person-badge.css.
Complex components composed of molecules and atoms, forming distinct sections
of the interface. Files live in bristlenose/theme/organisms/. 9 files.
Also: organisms/sentiment-chart.css, organisms/coverage.css,
organisms/toc.css, organisms/settings.css.
Islands are React components mounted into static HTML via comment markers.
Each gets its own createRoot() call. Cross-island state shares via
a module-level store (useSyncExternalStore), not React Context.
| Island | Mount marker | Purpose |
|---|---|---|
| Dashboard | <!-- bn-dashboard --> |
Project tab: stat cards, featured quotes, session summary |
| SessionsTable | <!-- bn-session-table --> |
Sessions tab: interview table with thumbnails, sparklines, drill-down |
| Toolbar | <!-- bn-toolbar --> |
Quotes tab: sticky search + tag filter + view switcher + CSV export |
| QuoteSections | <!-- bn-quote-sections --> |
Quotes tab: screen-specific insights with quote cards |
| QuoteThemes | <!-- bn-quote-themes --> |
Quotes tab: cross-session thematic patterns with quote cards |
| CodebookPanel | <!-- bn-codebook --> |
Codebook tab: masonry tag groups, drag-drop, inline editing |
| AnalysisPage | <!-- bn-analysis --> |
Analysis tab: signal cards, OKLCH heatmap, drill-down |
| TranscriptPage | <!-- bn-transcript-page --> |
Standalone transcript pages: segments, timecodes, annotations |
| SettingsPanel | <!-- bn-settings --> |
Settings tab: appearance (auto/light/dark), speaker badge display |
| AboutPanel | <!-- bn-about --> |
About tab: version, documentation links, design mockups |
Page-level layout that places organisms into a structure. Templates define the skeleton — where things go, not what they contain.
| File | Page | Key layout decisions |
|---|---|---|
templates/report.css |
Main report | Max-width (52rem), article centering, header + metadata, section spacing, blockquote grid, tabbed panels |
templates/transcript.css |
Per-session transcript | Segment layout with speaker codes, right-margin annotations (≥ 1100px), anchor-highlight animation, moderator de-emphasis |
templates/print.css |
Print / PDF export | Forces light colour-scheme, hides toolbar/footer/navigation, adjusts page margins |
| Page | File | Render path | Description |
|---|---|---|---|
| Report | bristlenose-{slug}-report.html |
Static + Serve (10 React islands) | Main research report: 7 tabs, extracted quotes, themes, sentiment, search, editing |
| Transcript | sessions/transcript_s{n}.html |
Static + Serve (1 React island) | Per-session transcript with timecoded segments, speaker codes, annotations |
| Layer | File | Primary components |
|---|---|---|
| Tokens | tokens.css | All CSS custom properties (colours, spacing, radii, transitions, dark mode via light-dark()) |
| Atoms | atoms/activity-chip.css | Background job status chips (pending, complete, error) |
atoms/autocode-toast.css | AutoCode progress notification | |
atoms/badge.css | Badge base, 7 sentiment variants, user/AI tags, proposed + accept/deny pill, animations | |
atoms/bar.css | Histogram bars, counts, labels | |
atoms/button.css | Toolbar button (.toolbar-btn) | |
atoms/context-expansion.css | Expandable context sections | |
atoms/footer.css | Footer layout, links, logo | |
atoms/input.css | Text input base styles | |
atoms/interactive.css | Generic interactive element states | |
atoms/journey-label.css | User journey step labels | |
atoms/logo.css | Logo image + logotype | |
atoms/modal.css | Overlay, modal card, action buttons | |
atoms/moderator-question.css | Moderator context pill | |
atoms/span-bar.css | Vertical extent indicator bars | |
atoms/thumbnail.css | Video session thumbnail with play icon | |
atoms/timecode.css | Timecode links, brackets | |
atoms/toast.css | Toast notification | |
atoms/toggle.css | Star, hide, toolbar toggle buttons | |
| Molecules | molecules/autocode-report.css | AutoCode proposal triage table |
molecules/badge-row.css | Flex row of badges | |
molecules/bar-group.css | Histogram bar group (display: contents) | |
molecules/editable-text.css | Inline edit + crop bracket handles + undo | |
molecules/feedback.css | Feedback form | |
molecules/help-overlay.css | Keyboard shortcut help | |
molecules/hidden-quotes.css | Hidden quote badge + dropdown preview | |
molecules/name-edit.css | Participant name cells with pencil-on-hover | |
molecules/person-badge.css | Split speaker badge (two-tone code+name pill) | |
molecules/quote-actions.css | Bulk action hover preview states | |
molecules/search.css | Expanding search field | |
molecules/sparkline.css | Micro sentiment bar chart | |
molecules/tag-filter.css | Tag filter dropdown with codebook groups | |
molecules/tag-input.css | Tag input with autocomplete | |
molecules/threshold-review.css | Confidence threshold slider + zones | |
molecules/transcript-annotations.css | Right-margin annotations with span bars | |
| Organisms | organisms/analysis.css | Signal cards, OKLCH heatmap grid, cell tooltips |
organisms/blockquote.css | Full quote card with all actions | |
organisms/codebook-panel.css | Codebook group grid, drag-drop, inline edit | |
organisms/coverage.css | Transcript coverage details | |
organisms/global-nav.css | Tab navigation + dashboard + featured quotes + session drill-down | |
organisms/sentiment-chart.css | Side-by-side histogram layout | |
organisms/settings.css | Settings panel layout | |
organisms/toc.css | Table of contents | |
organisms/toolbar.css | Sticky toolbar, view switcher, scroll-margin offsets | |
| Templates | templates/report.css | Report page layout |
templates/transcript.css | Transcript page layout | |
templates/print.css | Print overrides |
| Layer | File | Purpose |
|---|---|---|
| Hooks & utils | hooks/useCropEdit.ts | Quote text crop editing (trim start/end with bracket handles) |
hooks/useDropdown.ts | Shared dropdown dismiss (Escape + click-outside) | |
hooks/useTranscriptCache.ts | Session transcript segment caching | |
utils/filter.ts | filterQuotes() — search + view mode + tag filter | |
utils/highlight.tsx | highlightText() — search match <mark> wrapping | |
utils/colours.ts | Codebook colour mapping (mirrors vanilla JS) | |
utils/toast.ts | Imperative toast() function (detached React root) | |
utils/sequences.ts | Consecutive quote detection for signal cards | |
| Primitives | components/Badge.tsx | Sentiment, user tag, proposed (accept/deny) badges |
components/PersonBadge.tsx | Split speaker badge (code + name, display toggle) | |
components/SearchBox.tsx | Toolbar search input (150ms debounce, min 3 chars) | |
components/TagFilterDropdown.tsx | Codebook-grouped tag filter with counts | |
components/ViewSwitcher.tsx | All / Starred view mode dropdown | |
components/ToolbarButton.tsx | Styled button atom (icon + label + arrow) | |
components/Toast.tsx | Notification component | |
| Store | contexts/QuotesContext.tsx | Module-level store: quotes, filters, mutations (useSyncExternalStore) |