jackin'
Behind jackin' — InternalsTUI Design

Visual Design

PHOSPHOR color palette, border color semantics, left sidebar conventions, and tab bar specifications.

PHOSPHOR Color Palette

The canonical RGB values live in the jackin-tui crate palette (crates/jackin-tui/src/lib.rs) so the host TUI and the in-container multiplexer cannot drift; crates/jackin-tui/src/theme.rs adapts them into ratatui Color constants that the rest of the host TUI imports. Never define local color constants.

All colors come from named theme tokens; no inline Color::Rgb(...) literals outside theme.rs. Every new color must be added as a named constant in lib.rs and re-exported from theme.rs as a ratatui Color. Inline RGB literals are a design-decision violation.

ConstantValueUsage
PHOSPHOR_GREENRgb(0, 255, 65)Active/focused elements, selected text
PHOSPHOR_DIMRgb(0, 140, 30)Inactive text, scrollbar thumbs
PHOSPHOR_DARKRgb(0, 80, 18)Borders (unfocused), separators
WHITERgb(255, 255, 255)Labels, keys, headings
WARNING_YELLOWRgb(255, 216, 94)Warning notes in confirmation dialogs
PREVIEW_CARDRgb(28, 28, 28)Lookbook preview canvas background
CAPSULE_PANE_FOCUSEDRgb(180, 180, 180)Capsule pane focused border

Every lookbook story preview card and lookbook background must use the PREVIEW_CARD token, never an inline Color::Rgb(28, 28, 28) literal. The general rule (no inline Color::Rgb(...) outside theme.rs) already covers this, but PREVIEW_CARD is the token to reach for whenever a dark card background is needed.

Background fills use the terminal default, never forced black

Modal backdrops and dialog surfaces paint the terminal's default background, not a fixed black. The Ratatui tokens theme::DIALOG_BACKDROP and theme::DIALOG_SURFACE are Color::Reset; the raw-ANSI token ansi::BG_DARK is \x1b[49m (the default-background SGR). A dialog backdrop or surface must never set Color::Black, Color::Rgb(0, 0, 0), or a 48;2;0;0;0m fill. Forcing pure black makes overlays stand out as a dark rectangle against a themed (non-black) terminal background; emitting the default background lets every overlay match the operator's terminal theme. Occlusion still holds — a Color::Reset cell with a space overwrites the chrome behind it, just on the operator's background instead of black. This applies to every surface (console, launch cockpit, capsule) because all three consume the same shared tokens. Black as a foreground (e.g. black text on a light status chip) is unaffected by this rule; it governs background fills only.

Panel Title Spacing

Every titled block passes its title through Panel::title(), which normalizes the string to " {trimmed_title} " (one space, trimmed title, one space) internally. Callers must not pre-pad the title string with spaces. The resulting display is ┌ Title ┐ with exactly one space on each side of the title text.

A title passed as " Title " (extra spaces) or "Title" (no spaces) is a design-decision violation — pass the bare trimmed title and let Panel::title() apply the canonical padding.


Panel Body Inset

Panel body content must be inset from the border by exactly 1 cell horizontally. Use panel_body_area(block_area, inner_area) from jackin_tui::components::panel instead of rendering content directly into block.inner(area). The 1-cell inset prevents text from touching the left and right border characters and is a visual requirement across all titled panels.

render_scrollable_block applies this inset automatically. Direct callers that render text content into a titled panel via the ratatui Block API must call panel_body_area to obtain the inset rect and render into that, not into the raw inner rect.


Action Rows (+ Add … / + Override …)

Every row that begins with + is an add/create action and must render through the one shared action_row_style(selected) function in jackin-console. Rules:

  • Same ACTION_ACCENT foreground on every + … row app-wide, on every screen and tab.
  • Bold when selected, normal weight when not.
  • No trailing parenthetical description (no (all roles overridden), no (0 remaining), no any-other-parenthetical). The action label alone is sufficient.
  • One shared style function for all + … rows; no surface hand-rolls its own style.

Block Border Colors

  • Active interaction container: PHOSPHOR_GREEN border — dialogs, pickers, forms, tab panels, and the currently active list/sidebar use the bright border whenever they own keyboard or scroll interaction.
  • Passive scroll block, focused and scrollable: PHOSPHOR_GREEN border — both focused = true and content overflows in at least one axis.
  • Passive scroll block, focused but not scrollable: PHOSPHOR_DARK border — focused = true is ignored when content fits; no false scroll affordance.
  • Unfocused / background container: PHOSPHOR_DARK border regardless of scrollability.

render_scrollable_block enforces the passive-scroll logic internally. Callers only supply focused: bool from state; the renderer decides whether to use green. Active interaction containers that are not passive scroll blocks must use the same token rule directly or through their shared component: bright border for the active container, dark border for inactive/background containers.


  • Horizontal scroll only — no vertical scroll.
  • Uses the shared fixed-prefix line renderer (render_line_with_fixed_prefix_scroll) so the disclosure/cursor prefix stays visible while only the workspace label scrolls. The renderer must slice by display columns through jackin_tui::fixed_prefix_scroll_segments, not by Rust char or byte count.
  • H/L keys and ScrollLeft/ScrollRight both update state.list_names_scroll_x.
  • Focus set on click via update_scroll_focus; persists until user clicks the right pane.
  • Focus persists until the user explicitly clicks the right pane. Horizontal scroll is clamped on resize, but focus state is not cleared (focus-sustainability rule).
  • Selected and hovered rows must paint their background across the full visible row after horizontal scroll, even when the scrolled label is shorter than the viewport. Do not let generic line scrolling clip the highlighted prefix or leave unpainted cells inside the row.

Tab Bar

W3C ARIA Tabs pattern (see AGENTS.md → TUI navigation conventions for full spec). Summary:

  • Tab bar has its own focus area (tab_bar_focused: bool in state).

  • Tab chrome is shared with the in-container multiplexer status bar (jackin-capsule): inactive tabs render on a dark-grey background (TAB_BG_INACTIVE), the active tab lifts to graphite (TAB_BG_ACTIVE — never the brand green, so it stays distinct from the jackin' pill), both with WHITE text and the active tab bold. These backgrounds and the Ratatui renderer live in the shared jackin-tui crate (TAB_BG_*, components::TabStrip) and are consumed by the console tab strips (workspace editor, settings) and the capsule Ratatui status bar.

  • Tab bar focused: the active tab shows a PHOSPHOR_GREEN underline bar — the keyboard-focus cue. GREEN = "tab bar owns focus."

  • Content focused (tab bar NOT focused): the active tab still shows a underline bar, but in WHITE — a dim context indicator. This tells the operator which tab is selected without implying the tab bar is the focus owner. The GREEN border on the content block below is the real focus signal.

  • Neither: no underline bar rendered (edge case only; one of the two areas always owns focus in normal operation).

  • Content focused (tab bar not focused): same graphite active tab, no underline bar.

  • / cycle tabs when tab bar is focused.

  • Tab/ moves focus from tab bar into the first content block.

  • BackTab/Esc returns focus to the tab bar from content.

  • Tab hover lifts the cell under the pointer (TAB_BG_INACTIVE_HOVER / TAB_BG_ACTIVE_HOVER in the shared palette), matching the in-container multiplexer's pointer feedback. The console enables any-event motion tracking (?1003h alongside ?1000h/?1002h/?1015h/?1006h) so a MouseEventKind::Moved event repaints the hovered tab; moving off the strip clears it. The motion-flood concern that deferred this earlier is moot host-side — events are local (no pty) and the manager coalesces renders at 20Hz, so a stream of motion events costs one repaint per frame. The hit-test that maps a column to a tab index is the shared jackin_tui::lay_out_tabs + jackin_tui::tab_at_column, the same geometry the multiplexer uses, so click-selection and hover stay in lock-step with the rendered cells.


Lookbook SVG Exports — Padded Preview Canvas

Rule: lookbook SVG exports use the same PREVIEW_CARD padded canvas as the interactive preview. The SVG export path and the interactive preview must render identically — each story floats on a uniform charcoal card, never edge-to-edge against a black background.

Concretely, render_story_to_buffer allocates a buffer of (story.width + 2·STORY_PAD) × (story.height + 2·STORY_PAD), fills the whole area with PREVIEW_CARD (Rgb(28, 28, 28)), Clears the inset story rect, and renders the story into that inset. The resulting SVG has a #1c1c1c ring around every component.

Two paths that must always agree:

  • Interactive preview (main.rs preview_inner): fills with PREVIEW_CARD, applies Margin { horizontal: 1, vertical: 1 }.
  • SVG export (svg.rs render_story_to_buffer): same PREVIEW_CARD fill, same STORY_PAD = 1 padding ring.

If one path is updated (different padding, different background), the other must be updated in the same PR. DRY miss here = every screenshot on the docs site drifts from the interactive browser.


Panel and Dialog Title Capitalization

Every panel border title, dialog title, and box border title must start with an uppercase letter. The first character of the title string (after Panel::title() normalizes spacing) must be uppercase.

Rule: pass titles with an uppercase first character at the call site. Panel::title() does not normalize letter case — it only normalizes spacing. A title "workspace" produces ┌ workspace ┐ (wrong); "Workspace" produces ┌ Workspace ┐ (correct).

Data-derived titles (workspace names, role names, story group labels) must be capitalized at their source, not with a post-hoc to_uppercase() call. If a data source can produce a lowercase first character that ends up as a panel border title, capitalize it at the point where the string is constructed.

Examples of correctly capitalized titles: "General", "Environments", "Debug info", "Stories", "About", "Preview". A lowercase first letter in any title visible to the operator is a violation of this rule.


Capsule ANSI color encoders: static allowlist vs dynamic runtime

The capsule's ANSI renderer uses two flavors of the color-encoding helpers in crates/jackin-tui/src/lib.rs:

  • rgb_fg(color) / rgb_bg(color)const fn, compile-time only. These return a &'static str from a fixed allowlist of named palette colors. An unlisted color is a compile error (not a runtime panic), which is the desired behavior for constant tables: if you add a named color to the palette and forget to add it to the allowlist, the build fails loudly. Use these for const COLOR_NAME: &str = rgb_fg(PHOSPHOR_GREEN) table entries.

  • rgb_fg_dyn(Rgb) -> String / rgb_bg_dyn(Rgb) -> String — dynamic, runtime. These emit the standard truecolor SGR sequence \x1b[38;2;r;g;bm / \x1b[48;2;r;g;bm for any Rgb value and never panic. Use these whenever the color value is not a compile-time constant — any place a color is chosen at runtime (e.g. the debug chip which is conditionally rendered with DANGER_RED).

Rule: capsule render code must never call rgb_fg or rgb_bg with a value that is not a const palette constant. Any non-const color use must route through rgb_fg_dyn / rgb_bg_dyn. A const call with an unlisted color is a build error (correct!); a non-const call with an unlisted color is an exit-101 panic (bug!).


Any value the operator can click to copy (a file path, run id, container id, URL) must read as a link and behave identically on every surface, following the W3C native-link convention:

  • Colour: LINK_FG (cyan) on a dark dialog surface — never blue, never the same as plain or emphasised text. LINK_BLUE is reserved for clickable text on the white status bar, where cyan lacks contrast; do not use it on dark dialog surfaces.
  • Underline: always underlined, with or without an OSC 8 hyperlink. Underline is the resting affordance that marks the value as a link; underlining only the hyperlinked rows is a violation.
  • Hover: the value brightens to LINK_FG_HOVER while the pointer is over it, and the pointer switches to the clickable shape where supported. A copyable value with no hover colour change is a violation — the hover feedback is what tells the operator it is interactive.
  • Click: copies the full value to the clipboard (OSC 52) and shows the transient "Copied!" badge on that row. The visible text may be abbreviated; the copied payload and the OSC 8 href carry the full value.

These rules are enforced by the shared component (crates/jackin-tui/src/components/container_info.rs): every copyable row renders through the same styling, so the affordance never drifts between the console, launch cockpit, and capsule. New copyable surfaces reuse that component rather than re-implementing the colour/underline/hover rules.

On this page