Bee Box documentation · directory: https://beebox.run/docs/dev/ · index: https://beebox.run/docs/dev/index.md · root: https://beebox.run/llms.txt # Frontend Conventions UI palette, primitives, and the `className` rule. Backend code never needs to load this; code-style.md covers conventions that apply to both halves. ## Card themes Card presentation is theme-driven and independent of view selection. Theme surfaces live under `src/frontend/src/themes/`; keep theme IDs and stocks in the shared catalog and use literal class maps so Tailwind includes them in the build. The user-facing selection and read-only origin details belong in card Properties; see [`docs/box/card-themes.md`](docs/box/card-themes.md) for the authoring contract. ## Data Source Tagging UI elements that display data from a known source (card, commit, session, etc.) must be tagged with `data-cb-source` attributes for traceability. Use the `cbSource()` and `cbSourceItem()` helpers from `src/frontend/src/lib/source-tag.ts`. Tag the outermost meaningful data boundary — not every inner element. See `docs/data-source-tagging.md` for the full convention. ## Browser Tab Titles Every route declares its tab title in `staticData`, and the augmented `StaticDataRouteOption` in `router.tsx` makes that mandatory: a new route does not typecheck until it says what it is called (`title: null` for a layout or a redirect). A page whose real name is data it loads — a chat's label, a card's title — publishes it with `usePageTitle` from `components/DocumentTitle.tsx`, and the route's static title stands in until it arrives. The composed title is ` `, most-distinguishing first because browsers truncate tabs hard; the app name appears only where there is no box. Nothing else writes `document.title`. The **box half comes from the box itself** — the landmark card at the box root, whose `navigation.label` is the box's display name and whose `symbol` is its mark (`core/landmark/box-identity.ts`). There is no separate box-name or box-icon store: an agent renames a box by editing that card, the same way it writes any other box fact. **The icon says which box; the title says which place.** `components/ DocumentIcon.tsx` shows the box's own mark, always, and `DocumentPlace.tsx` publishes the current directory's landmark emoji to lead the title (`🍳 Recipes — Kitchen`). The icon was landmark-first at one point and the cost was the thing a tab icon exists to prevent: two boxes' tabs wearing the same mark. A landmarked directory is titled by its landmark rather than its folder name, so the tab's two halves name the same place. **A working indicator rides in the same slot.** `useWorking(true)` from anywhere puts a spinning glyph at the front of the title; the chat calls it while a turn streams, so a chat left in a background tab says whether the box is still going. Any caller is enough — the provider counts owners, so one finishing does not answer for another. **Presence is the signal; the spin is decoration.** A hidden tab throttles timers to once a second, and Chrome drops that to once a MINUTE once a silent page has been hidden five minutes (developer.chrome.com/blog/timer-throttling-in-chrome-88). So the animation is exactly what stops being trustworthy during a long background turn — which is the case the indicator exists for. The glyph's presence carries the meaning and no throttle can take it away; built the other way round, a frozen spinner would read as "stuck" at the moment the honest answer is "still going". Wherever a surface needs a real raster rather than a character — the Apple touch icon, the manifest icons, a notification — it comes from box-scoped routes that render the mark on demand (`webapp/routes/box-identity-assets.ts`, `core/box/box-icon.ts`). Rendering the emoji as *text* was measured and rejected; see `docs/attribution.md` for why the artwork is a dependency. The box server also stamps the box's name and mark into the served `index.html` (`webapp/index-html.ts`) so a tab is identifiable before React boots. That stamping happens only in the box server: **in local dev Vite serves the document**, so dev shows the built title for the moment before the app takes over. ## Frontend Color Palette Components reach for **semantic color roles**, not brand names. Defined in `src/frontend/tailwind.config.js`. | Role | Use for | Shades | |------|---------|--------| | `primary` | Main brand actions, links, selected state | DEFAULT, dark (hover), light, 50, 100 | | `accent` | Highlights, CTAs, focus rings, warning-lite | DEFAULT, dark, light, 50, 100 | | `info` | Informational callouts, metadata badges | DEFAULT, dark, light, muted, 50, 100 | | `danger` | Destructive actions, error messages | DEFAULT, dark, light, 50, 100 | | `success` | Positive confirmations, connected state | DEFAULT, dark, light, 50, 100 | | `warning` | Caution, dirty state, notes | DEFAULT, dark, light, 50, 100 | | `coral` | App-nav gradient only (no semantic role) | DEFAULT, dark, light, 50 | | `warm-50`..`warm-900` | Neutral surface + text scale (backgrounds, body text, borders) | Self-documenting scale | **Rules:** - **Never** use Tailwind defaults (`red-*`, `green-*`, `yellow-*`, `amber-*`, `purple-*`, `gray-*`, `blue-*`, etc.) for appearance. They're not part of the brand palette. The one exception is `gray-*` in dark-overlay UI (CapturePage, CommandRunner) that's intentionally Tailwind-default dark-theme. - **Never** add new colors to `tailwind.config.js` without a semantic role. If you need a new color category, define a new role. - **For shade variation** on an existing color, use opacity syntax (`bg-primary/50`, `text-danger/70`) before reaching for additional numeric shades. - **Components in `components/`** own their appearance — they reference these semantic names directly. Page-level code (outside `components/`) can only use *outer-layout* classes via `className` (enforced by `restrict-component-classes` ESLint rule). When a color role doesn't feel right for a use case, that's usually a sign that a new semantic role is warranted — discuss before inventing a one-off color. ## Workspace presentation Ordinary cards and built-in interface instruments render in the workspace. Open them through the workspace target rather than adding a parallel page, generic preview overlay, or page-level conversation shell. Passive navigation changes the visible attention target while preserving the conversation recipient, draft, and send destination; only an explicit conversation action selects a recipient. Source editing, image lightboxes, capture, and other task-specific dialogs remain appropriate overlays for their distinct interactions. ## Frontend UI Primitives Reach for a primitive from `src/frontend/src/components/ui/` before writing appearance classes inline. The primitives own appearance (colors, borders, shadows); callers own outer layout (margin, padding, flex-self, sizing, position) via a `className` prop. **Actions** - ``. - `` — inline action embedded in prose ("(reset)", "retry", "+ Add comment"). Intents: emphatic/subtle/danger. Has `flash` for clipboard copies etc. **Inputs** (all in `ui/fields.tsx`) - ``, ``, `` — label + input + error + helper, pass-through of native validation attrs (`required`, `minLength`, `pattern`, etc.). `hideLabel` for sr-only labels. Value/onChange are controlled. - `` — inline checkbox with label to the right. - `` — dropdown select with options array. - `` — stacked (`variant="list"`) or card-style (`variant="cards"`) radios. - `` — iOS-style pill switch for immediate-apply settings (distinct from CheckboxField, which is form-bound). Props: `checked`, `onChange`, `disabled`, `label`. **Navigation** - `` — use TanStack Router's Link for any internal nav. - `` — styled inline internal link (primary + underline-on-hover). - `` — external-url link with target=_blank, rel=noopener, trailing icon. Variants: inline / plain / button. - `` — icon-only "open in new tab" affordance with required `label` for aria. `onDark` variant. - `` — icon-only button that opens a file in the chat's companion side panel; sibling to ``'s open-in-new-tab action. Sizes `sm`/`md`. **Display** - `` — src/alt/size with built-in lightbox, error placeholder, bbox overlay, rotation. Lightbox and onClick are mutually exclusive at the type level. - `` — responsive 16:9 lazy-loaded video iframe (privacy-friendly nocookie domain), block-level like a figure; used for embedded video in rendered markdown. - `` — user profile image with initial fallback. `fallbackClassName` for dark nav contexts. - `` — renders an ISO timestamp as a localized date/time (the browser's locale and zone) in a semantic `