Documentation
¶
Overview ¶
Package theme owns kb's TUI design tokens: the semantic palette of docs/design/tui-design-spec.md section 1, the cached style factory of section 6, and the layout metrics and glyphs of section 2.
Nothing outside this package names a hex. Nothing outside this package constructs a lipgloss style; a seam test enforces that rule across internal/tui.
Index ¶
- Constants
- Variables
- func Downsample(content string, profile colorprofile.Profile) string
- func PinColor() tea.ProgramOption
- func PinProfile(profile colorprofile.Profile) tea.ProgramOption
- func PinStructure() tea.ProgramOption
- func Tick(d time.Duration, msg tea.Msg) tea.Cmd
- func WheelIndex(tag string) int
- type Band
- type BoardStyles
- type ButtonSet
- type ButtonStyles
- type ButtonVariant
- type CardStyles
- type ChipStyles
- type ColumnStyles
- type Density
- type Fidelity
- type Glyphs
- type Metrics
- func (m Metrics) BrandBlockH() int
- func (m Metrics) BrandFits(frameWidth, frameHeight int) bool
- func (m Metrics) BrandMetaWidth(frameWidth int) int
- func (m Metrics) CardGapRows(density Density) int
- func (m Metrics) CardInner(width int, density Density) int
- func (m Metrics) CardPad(density Density) int
- func (m Metrics) CardRows(frameHeight int, density Density) int
- func (m Metrics) ColumnPad(density Density) int
- func (m Metrics) DensityFor(frameHeight, columnInnerWidth int) Density
- func (m Metrics) DescLines(frameHeight int, density Density) int
- func (m Metrics) InnerPadRows(frameHeight int, density Density) int
- func (m Metrics) LabelRows(frameHeight int, density Density) int
- func (m Metrics) OverlayContent(paneWidth int) int
- func (m Metrics) OverlayElevated(paneWidth, paneHeight int) bool
- func (m Metrics) OverlayFocusContent(paneWidth int) int
- func (m Metrics) OverlayPane(frameWidth, frameHeight int) (paneWidth, paneHeight int)
- func (m Metrics) PageMargin(frameWidth int) int
- func (m Metrics) PagePad(density Density) int
- func (m Metrics) TitleRows(frameHeight int, density Density) int
- type OverlayMetrics
- type OverlayStyles
- type Palette
- type Ramp
- type Slot
- type StatusStyles
- type Styles
- func (s *Styles) BandRun(band Band, content string) string
- func (s *Styles) ChipRuns(fill, surface Slot) ChipStyles
- func (s *Styles) ChipRunsTint(fill, surface Slot) ChipStyles
- func (s *Styles) Fg(foreground Slot) lipgloss.Style
- func (s *Styles) Grad(ramp Ramp, text string) string
- func (s *Styles) GradBold(ramp Ramp, text string) string
- func (s *Styles) GradCell(ramp Ramp, column, width int, cluster string) string
- func (s *Styles) Graded() bool
- func (s *Styles) HoverRun(surface, raised Slot, content string) string
- func (s *Styles) HuhTheme() huh.Theme
- func (s *Styles) On(foreground, background Slot) lipgloss.Style
- func (s *Styles) OnBold(foreground, background Slot) lipgloss.Style
- func (s *Styles) OnItalic(foreground, background Slot) lipgloss.Style
- func (s *Styles) PressedRun(content string) string
- func (s *Styles) RowSurface(hovered bool) Slot
- func (s *Styles) Surface(selected, alternate bool) Slot
- func (s *Styles) SurfaceRun(surface Slot, content string) string
- type TableStyles
- type Timing
- type WorkStyles
Constants ¶
const BandRest = Raised
BandRest is the unfocused column header band tier. Spec section 1.1 puts the band one step above the card surface and names that tier Raised, so BandRest is a same-hex alias and not a collision: selection exists only in the focused column, whose band is a solid hue fill, so a Raised card and a BandRest band can never share a column.
const ColorProfile = colorprofile.TrueColor
Color profile pinning for goldens, spec section 6.4. The renderer downsamples per cell against the detected profile, so a golden cell grid depends on the profile the program ran under. Production pins nothing; tests pin explicitly.
ColorProfile is the profile for palette goldens. An ASCII-pinned golden of a design whose entire depth model is background color asserts nothing about the design, so every board and overlay color golden pins truecolor.
const GradSteps = 24
GradSteps is the length of every prebuilt ramp. Spec section 10.1.1: a ramp's interior colors are not palette slots, so no per-slot style exists for them and section 6.2 forbids blending one per frame. The ramp is built once at a fixed length and resampled by index instead. Twenty-four is roughly 2.7x the longest gradient-bearing run in the spec; a longer run repeats colors, which is correct rather than an error.
const LabelWheel = 5
LabelWheel is the size of the label pill wheel of spec section 1.6.
const StructureProfile = colorprofile.ASCII
StructureProfile is the profile for goldens that assert layout, truncation and drop order rather than color. They strip ANSI anyway and stay cheap.
Variables ¶
var DefaultTiming = Timing{ FPS: 20, PlainStride: 2, BirthDelay: 5, BirthSteps: 20, ScrambleSteps: 3, EllipsisStride: 8, SuffixAfter: 60, BrandBirthSteps: 12, CelebrateSteps: 12, DialogGraceQuiet: 425 * time.Millisecond, DialogGraceMax: 1500 * time.Millisecond, DialogGraceReopen: 500 * time.Millisecond, ScrollActiveLinger: 2000 * time.Millisecond, DoubleClickWindow: 400 * time.Millisecond, InputCoalesce: 16 * time.Millisecond, KeyboardNavigationInterval: 50 * time.Millisecond, KeyboardNavigationQuiet: 100 * time.Millisecond, NoticeTTL: 5000 * time.Millisecond, PollInterval: 1000 * time.Millisecond, AutoShipDelay: 350 * time.Millisecond, SimilarDelay: 400 * time.Millisecond, GeometrySliceTarget: 8 * time.Millisecond, GeometrySliceLimit: 16 * time.Millisecond, }
DefaultTiming is the table of spec section 10.3.1. crush is the donor for every value: each governs when a message is scheduled rather than how a frame is drawn, so the donor's cell-buffer architecture does not reach this set.
DialogGraceQuiet is deliberately longer than DoubleClickWindow, so the trailing half of a double-click that opened a destructive prompt lands inside the grace by construction. Neither may be reduced without the other.
var TimingCollapsed = Timing{}
TimingCollapsed is the test configuration: the struct zero value. Every one-shot fires immediately and no frame clock runs. It is not "fast" timing, it is no timing, which is the only kind a golden can assert against.
The rate/count split against the duration split is what makes this work: zero means "do not run" to a clock and "run now" to a one-shot, and conflating the two produces a test that spins the CPU.
Functions ¶
func Downsample ¶
func Downsample(content string, profile colorprofile.Profile) string
Downsample rewrites rendered content as the given profile would emit it. It is the string-level counterpart of PinProfile, for widget goldens that render directly instead of running a program. An unrecognized profile leaves the content untouched rather than dropping it.
func PinColor ¶
func PinColor() tea.ProgramOption
PinColor pins a program to the truecolor profile of the reference design.
func PinProfile ¶
func PinProfile(profile colorprofile.Profile) tea.ProgramOption
PinProfile returns the program option that pins a golden's color profile.
func PinStructure ¶
func PinStructure() tea.ProgramOption
PinStructure pins a program to the colorless profile used by layout goldens.
func Tick ¶
Tick schedules msg after d. A non-positive d dispatches immediately instead of scheduling: tea.Tick(0, ...) still round-trips through the runtime timer, which is a wall-clock dependency wearing a zero. Every tea.Tick call site under internal/tui goes through here.
func WheelIndex ¶
WheelIndex is the label color hash of spec section 1.6: the hash the board has always used, carried over unchanged so existing label colors stay stable relative to each other. It lives here beside LabelSlot because the per-board accent of section 10.7.2 derives from the same wheel, and the derivation must not fork the hash.
Types ¶
type BoardStyles ¶
type BoardStyles struct {
Canvas lipgloss.Style // page ground
TopBar lipgloss.Style // brand row
Toolbar lipgloss.Style // filter and action row
PagePad lipgloss.Style // the canvas row between the toolbar and the columns
}
BoardStyles are the page-level surfaces of spec section 2.1.
type ButtonSet ¶
type ButtonSet [numButtonVariants]ButtonStyles
ButtonSet is the button token matrix: one ButtonStyles per variant, built once beside every other style.
func (ButtonSet) Variant ¶
func (b ButtonSet) Variant(variant ButtonVariant) ButtonStyles
Variant returns the styles of one variant. An out-of-range variant resolves to Neutral rather than panicking a render path.
type ButtonStyles ¶
type ButtonStyles struct {
Rest lipgloss.Style
Focused lipgloss.Style
Hovered lipgloss.Style
Armed lipgloss.Style
Pressed lipgloss.Style
}
ButtonStyles are the states of one button variant. Armed is kb's addition for the purge and remove two-step; Pressed is the promoted reverse-video feedback that pointer.State.Render writes by hand today.
type ButtonVariant ¶
type ButtonVariant uint8
ButtonVariant names what a button does, never what it looks like. Spec section 5.4 assigns one to every button surface in the TUI.
const ( ButtonNeutral ButtonVariant = iota // dismissal, navigation, a side action ButtonPrimary // the pane's main affirmative ButtonSuccess // the state-advancing action ButtonDanger // the destructive action )
The variants of spec section 1.9. Neutral is the zero value: a caller that states no meaning gets the calmest surface, not an accidental accent.
type CardStyles ¶
type CardStyles struct {
Rest lipgloss.Style
Zebra lipgloss.Style
Raised lipgloss.Style
Title lipgloss.Style
TitleSel lipgloss.Style
Desc lipgloss.Style
Seq lipgloss.Style
}
CardStyles are the card surfaces and text roles of spec section 3. Rest, Zebra and Raised are the three surfaces a card can sit on; the text roles are cached against Rest and recomposed with On for the other two.
type ChipStyles ¶
type ChipStyles struct {
Pad lipgloss.Style
ScopedPad lipgloss.Style
Body lipgloss.Style
BodyHover lipgloss.Style
BodyFocus lipgloss.Style
BodyFocusHover lipgloss.Style
ScopedKey lipgloss.Style
Flat lipgloss.Style
FlatHover lipgloss.Style
}
ChipStyles are the runs of one pill (spec section 3.6): a padding cell on the pill's own ground at each end, the text body on the fill, the dark half of a scoped pill, and the compact flat form.
Pad and ScopedPad are the padding runs that replaced the half-block end caps (issue #227). A cap drew the fill color as a *glyph* over the ground behind, so it read as a colored bar wherever the font did not draw a half block flush to the cell edge, and as a bar by construction on the inactive pill, which has no fill for it to fuse into. The padding cell is the same column spent as a colored cell instead of a glyph: Pad is the body's own ground, ScopedPad the key half's, so the pill is a flat span of color that owes the font nothing.
BodyHover and FlatHover are the hovered forms of spec section 10.5.1: an underline on the body run, zero cells, no color moved. BodyFocus is the keyboard-traversal cue that replaced the thickened caps - bold on the body run, likewise zero cells. Bold is available here for the same reason the underline of section 10.4.2 is: section 2.6 step 7 spends bold on the *compact flat* chip, which has no focus state and never shares a widget with a padded pill, so the two boldings can never be read in one place. BodyFocusHover is the composition, because focus and hover are orthogonal axes and the filter bar can hold both at once.
type ColumnStyles ¶
type ColumnStyles struct {
Panel lipgloss.Style // panel body
BandRest lipgloss.Style // unfocused band, non-hued cells
BandFocus lipgloss.Style // focused band on the brand hue
BandLabel lipgloss.Style // band label on the unfocused surface
Meta lipgloss.Style // the "N cards - N blocked" row
More lipgloss.Style // the "+N more" overflow cue
}
ColumnStyles are the panel and header band surfaces of spec section 2.2. The hued halves of a band are composed with On and OnBold: BandRest carries the unfocused band's surface and BandFocus the focused band's foreground, so the four column hues do not each need their own cached style.
type Density ¶
type Density uint8
Density is the layout density the frame resolved to. Spec section 2.6: compaction is not gradual, crossing the threshold applies all of it at once.
type Fidelity ¶
type Fidelity uint8
Fidelity is the terminal floor of spec section 10.7.5, resolved once from the detected color profile so no view ever branches on the profile itself. Truecolor is the reference target and everything below it is a degradation, never a second design.
The word is deliberately not "tier": spec section 1.1 already spends that on the shade depth model.
const ( FidelityFlat Fidelity = iota // no color at all: glyph, weight and geometry only FidelityIndexed // flat palette slots, no blends FidelityFull // gradients, blends and color-carried effects run )
The three floors of spec section 10.7.5.
func FidelityFor ¶
func FidelityFor(profile colorprofile.Profile) Fidelity
FidelityFor resolves a detected profile to its floor.
colorprofile.Unknown maps to FidelityFull because it only ever occurs before detection completes - bubbletea always resolves a profile before it sends ColorProfileMsg - and the reference target is the correct assumption until the terminal says otherwise. This mirrors spec section 6.3's rule of defaulting isDark to true until the background message lands.
type Glyphs ¶
type Glyphs struct {
Rail string // U+258C, resting card rail, unfocused band rail, meter fill
RailFull string // U+2588, selected card rail, scrollbar thumb
CapL string // U+2590, meter left end cap (spec section 10.1.3; the pill lost its caps to issue #227)
CapR string // U+258C, meter right end cap
Dot string // U+25CF, column status dot
Check string // U+2610, unchecked checklist row
CheckOn string // U+2611, checked checklist row
CheckOff string // U+2612, dropped checklist row
Tick string // U+2713, resolved-blocker mark inside a blocker chip
Diamond string // U+25C7, effort marker for a value outside the S/M/L scale
Focus string // U+25B8, focused band caret
More string // overflow cue prefix, rendered as "+N more"
Ellipsis string // U+2026, the truncation tail of spec section 3.3
// Blocked is the card's blocked alarm, drawn beside the sequence number of
// spec section 3.2. It is the vocabulary's only pictograph, and it answers
// to the emoji admission rule of section 10.4.1: a single code point with
// Emoji_Presentation=Yes, no variation selector, no zero-width joiner and no
// modifier, so the terminal has one rune to draw and the width every layout
// calculation assumes is the width it takes. It is East Asian Wide and so
// two cells, and it is bound by the adjacency rule - the render site writes
// a space after it.
//
// It is admitted where the three effort squares were retired (issue #232)
// because it carries a binary alarm rather than a value: a font with no
// pictograph draws tofu beside the sequence number, and tofu beside a
// sequence number still says this card is flagged. The squares carried one
// of three values, and a partial or substituted glyph there lost which.
Blocked string // U+26D4, the blocked alarm of spec section 3.2
Track string // U+2591, progress meter and scrollbar track (section 10.1.3)
Empty string // U+25CB, empty-state mark (section 10.8.3)
Alert string // U+25B2, failure mark (section 10.8.5)
Bullet string // U+00B7, meta separator and card description list marker
HintSep string // hint ladder separator, three cells (section 10.4.6)
// The half-block pair of spec section 10.6.1. These are the only glyphs in
// the vocabulary that widen the block-glyph risk section 3.6 records rather
// than inheriting it, and they are accepted for the launch mark alone: the
// mark is decoration, and every fact the launch screen carries lives in the
// meta row as plain text (section 10.6.1, glyph vocabulary cost).
HalfTop string // U+2580, brand letterform upper half
HalfBottom string // U+2584, brand letterform lower half
// Markers are the ASCII prefixes of spec section 10.4.1 that are display
// vocabulary rather than prose. MarkSeq and MarkTag are a same-text alias,
// deliberate and not a collision: they answer to different sections and
// either may be re-spelled without the other.
MarkSeq string // card reference prefix, "#142" (section 3.2)
MarkTag string // plain label pill prefix, "#tag" (section 3.5)
MarkDue string // compact due prefix, "!2d" (section 3.4)
// The filter bar's toggle marks. A filter label pill is the section 3.6
// pill plus one of these inside its caps, so the toggle survives the flat
// fidelity floor of section 10.7.5, where the filled/tinted distinction that
// carries the state everywhere else has no color to carry it with. Both are
// two cells - the mark and the column that separates it from the pill body -
// so the pill's width is the same in both states (section 10.4.4).
MarkFilterOff string // unselected filter label pill, "+ #tag"
MarkFilterOn string // selected filter label pill, "x #tag"
}
Glyphs are the accent vocabulary of the design language. They are tokens and live beside the colors.
Known risk, carried consciously (map #136): the accent vocabulary is built from U+2588 / U+258C / U+2590. On fonts without block glyphs it degrades worse than a border would. Accepted at the issue #137 resolution. Spec section 10.4.1 makes this struct the only place under internal/tui where a display glyph or a separator may be written as a literal: views and widgets name tokens, and a view that needs a mark the vocabulary does not carry has found a missing token, not a reason for a literal.
type Metrics ¶
type Metrics struct {
WideFrame int // frame width at or above which the page keeps a margin
PageMarginX int // left/right page margin on a wide frame
PagePadTop int // canvas row between the toolbar and the columns, normal only
ColumnGutter int // columns between panels
ColumnPadX int // inset of the card stack inside its panel, normal only
ColumnMetaInset int // meta line and "+N more" inset from the panel edge
CardGap int // rows between stacked cards, normal only
CardRail int // reserved on the card's left edge, always
CardPadLeft int // between rail and content, normal only
CardPadRight int // always
CardMinInner int // below this a card renders surface and rail only
MinColumnWidth int // narrowest panel a column may shrink to and still hold a title
BandHeadW int // band prefix before its label, fixed across focus (section 10.4.4)
ButtonPadX int // left and right padding of one button (section 10.4.2)
ButtonGap int // surface-filled gap between two buttons in a row
FocusGutterW int // gutter column, reserved on every focusable non-card row
FocusGutterGap int // column between the gutter and the row's content
MeterCells int // default bar width of the progress meter (section 10.1.3)
MeterMinCells int // below this a meter renders its label only, no bar
// The empty-state row of section 10.8.3. The two minimums are the rungs of
// its width ladder: the headline is dropped before the action tail, because
// the tail is the actionable half.
EmptyHeadlineMin int // inner width at or above which an empty row keeps its headline
EmptyActionMin int // inner width at or above which an empty row keeps its action tail
ActionGap int // columns before an empty row's action tail
// The busy and error rows of section 10.8.4 and 10.8.5.
BusyGap int // columns between a spinner frame and its label
ErrorMaxLines int // lines an error message may wrap to inside a panel
// The brand mark of spec section 10.6.8. The reveal's span is a timing
// token and lives in Timing.BrandBirthSteps; the two half-block glyphs are
// vocabulary and live in Glyphs.
BrandMarkW int // unstretched mark width, k(4) + BrandKern(1) + b(5)
BrandMarkH int // mark height, both letterforms
BrandKern int // blank columns between letterforms
BrandStretchMax int // inclusive upper bound of the memoized stretch
BrandMetaW int // meta row width before the frame cap
BrandMetaGap int // minimum columns between the meta row's two slots
BrandMetaGapRows int // blank Canvas rows between the mark and the meta row
BrandMinW int // frame width below which the full mark is dropped
BrandMinH int // frame height below which it is dropped
OverlayInsetX int // overlay content inset from the panel edge
OverlayLabelW int // fixed label gutter of an overlay field row
TableGutter int // columns between two cells of a lipgloss table row
CompactBelow int // frame height below which density compacts
CompactInnerW int // column inner width below which density compacts
DescTwoLines int // frame height at or above which the snippet gets a second line
// The card row ceilings of spec section 2.6, rewritten by issue #232, given
// interior vertical rhythm by issue #240 and re-cut into ceilings by issue
// #243. Each is a function of density and frame height and bounds what its
// section may spend; how much of it the section actually takes is a function
// of the card's own content.
CardTitleLines int // title rows a card carries on a frame at or above DescTwoLines
CardDescMax int // description rows a card may carry at its tallest
CardDescStep int // frame rows that buy one more description line
CardLabelRows int // label rows a card carries on a frame tall enough for them
CardInnerPadRows int // interior blank rows a card carries at its tallest
CardInnerPadTwo int // frame height at or above which the card carries both
Overlay OverlayMetrics
}
Metrics are the gutter, padding and threshold tokens of spec section 2.5, plus the compaction thresholds of section 2.6 and the overlay width caps of section 4. Every number here is normative; a slice that needs a value not written here has found a spec gap.
func (Metrics) BrandBlockH ¶
BrandBlockH is the launch block height of spec section 10.6.7: the mark, the blank rows under it, and the meta row.
func (Metrics) BrandFits ¶
BrandFits reports whether a frame is large enough for the full mark. Below either floor the launch screen drops the mark and renders the meta row alone (spec section 10.6.7).
func (Metrics) BrandMetaWidth ¶
BrandMetaWidth is the meta row's width on a frame of this width: the section 10.6.5 token capped to the frame less a page margin either side. It never goes below zero, so a one-column frame renders an empty row rather than a negative allotment.
func (Metrics) CardGapRows ¶
CardGapRows is the number of rows between stacked cards.
func (Metrics) CardInner ¶
CardInner is the content width of a card of this total width. Spec section 2.5: normal spends rail, left pad and right pad; compact drops the left pad.
func (Metrics) CardRows ¶
CardRows is the tallest one card may be: every section of spec section 3.1 at its ceiling, in one place.
Until issue #243 this was the card's height outright, and columnStackHeight reserved a column from it before any card was rendered. Content-sized cards retired the reservation - a column measures the cards it is about to draw (widget.CardHeight) and packs against that - and what is left here is the bound: no card at this density and frame height can draw more rows than this, whatever its content, because every section is capped and the two interior separators are the only other rows on it.
func (Metrics) DensityFor ¶
DensityFor resolves the frame against the compaction thresholds. Spec section 2.6: compaction fires when the frame is short or a column is narrow.
func (Metrics) DescLines ¶
DescLines is the most description rows a card may carry. Spec section 3.3 as issue #232 rewrote it: none when compact, one on a short normal frame, and one more for every CardDescStep rows above DescTwoLines up to CardDescMax.
The ladder spends height the frame has rather than height it might have: a 45-row terminal buys the second line the original rule bought, and only a terminal tall enough to keep the same number of cards on screen buys the third, fourth and fifth.
Issue #243 made it a ceiling rather than an allotment. The frame decides how much description a card may show; the description decides how much of that it takes, and a card with none takes no rows at all instead of holding blank ones open under the title.
func (Metrics) InnerPadRows ¶
InnerPadRows is the number of blank interior rows a card carries: the vertical rhythm of spec section 3.1 as issue #240 added it. The rows carry the card's own fill rather than the panel's, so the card still reads as one slab and the blank rows highlight, stripe and take a click with every other row of it.
The count is a rung of its own. Compact carries none - compact exists to be dense (section 2.6). Below CardInnerPadTwo the card affords exactly one, and it goes between the shared title/description block and the meta row, which is the boundary that carries the grouping: title and description are one unit of prose, and everything under them is chips. At or above CardInnerPadTwo the frame has the surplus for the second, which goes between the meta row and the label rows - data above it, navigation below.
func (Metrics) LabelRows ¶
LabelRows is the most label rows a card may carry. Spec section 3.5 as issue #232 rewrote it: labels own rows of their own below the meta line and wrap onto the second when one row does not hold them. Compact reports zero, because step 5 of the section 2.6 drop order merges the labels onto the meta row instead of giving them one.
Issue #243 made it a ceiling too: a card whose pills fit one row draws one, and a card with no labels draws none and loses the separator above them.
func (Metrics) OverlayContent ¶
OverlayContent is the readable measure inside a panel of this width: the panel grows with the frame, the prose column inside it does not grow past the point where a line stops being scannable.
func (Metrics) OverlayElevated ¶
OverlayElevated reports whether a panel of this size renders as an elevated panel. Spec section 4: below the minimum the overlay falls back to the full frame, which is what keeps the frozen dismissal behaviors reachable on a terminal too small to center anything in.
func (Metrics) OverlayFocusContent ¶
OverlayFocusContent is the readable measure inside a panel for a row that can take focus. Spec section 10.4.3: the focus gutter and its gap are reserved in every state, so a focusable row's prose column is two narrower than a static row on the same panel rather than reflowing when focus arrives.
func (Metrics) OverlayPane ¶
OverlayPane is the panel geometry of spec section 4. It has two regimes, split at the same WideFrame threshold the board collapses on, which is how a responsive modal behaves: a narrow frame has no width to give away, so the panel takes all of it but the slack; a wide frame gets a proportional panel with real backdrop around it instead of a fixed cap stranded in dead canvas.
func (Metrics) PageMargin ¶
PageMargin is the left/right page margin for a frame of this width.
func (Metrics) TitleRows ¶
TitleRows is the most title rows a card may carry - one, or two where the frame affords the wrap and the title needs it (issue #243). Spec section 3.2 as issue #232 rewrote it: the title wraps rather than being ellipsized to one line, and it is the last allotted row that carries the ellipsis when the title still does not fit. Compact keeps the single row it always had.
Amended by issue #240. The short normal rung - a frame below DescTwoLines - now keeps a single title row too, and spends the continuation row it gives up on the interior separator of InnerPadRows. That is step 3 of the section 2.6 drop order applied one rung early, and it is the cheapest row on the card to spend: the spec already ranks a wrapped title's second line below the description, the first row still carries the whole of what a scan reads, and the ellipsis says the rest exists. The trade is exact - one row out, one row in - so the shortest normal frame keeps the card count it had before the rhythm arrived.
type OverlayMetrics ¶
type OverlayMetrics struct {
WidthPct int // percent of the frame width a content panel spans
HeightPct int // percent of the frame height a content panel spans
FrameSlackW int // columns a proportional panel always leaves free
FrameSlackH int // rows a proportional panel always leaves free
NarrowSlackW int // columns a narrow-frame panel leaves free
NarrowSlackH int // rows a narrow-frame panel leaves free
MinPaneW int // narrowest panel the proportional rule will produce
MinPaneH int // shortest panel the proportional rule will produce
MinW int // below this the overlay falls back to full frame
MinH int
ContentMax int // readable measure cap for prose inside a panel
TaskAction int
Help int
}
OverlayMetrics is the proportional panel geometry of spec section 4: every content overlay spans a percentage of the frame rather than a fixed cap, so a laptop-sized terminal gets a laptop-sized panel. The two content-sized dialogs (the task action confirm and the keyboard help) keep their own width caps because their height is their content and blowing them up would frame a handful of rows in a screenful of surface.
type OverlayStyles ¶
type OverlayStyles struct {
Surf lipgloss.Style
HeaderBand lipgloss.Style
HeaderBandArmed lipgloss.Style
SectionBand lipgloss.Style
Shadow lipgloss.Style
FieldLabel lipgloss.Style
FieldValue lipgloss.Style
}
OverlayStyles are the elevation surfaces of spec section 4.
HeaderBandArmed is the one state in the TUI that recolors a header band (spec section 10.1.4, ratified call 6): an armed two-step re-fills the band to StatusAlarm with FgBase bold, the same pair section 1.9 gives the armed button, so the frame and the button say the same thing in the same color. A destructive prompt that is merely pending does not reach it - it re-ramps the section label and leaves the frame alone.
type Ramp ¶
type Ramp uint8
Ramp names one prebuilt foreground gradient. Spec section 10.1: a gradient is chrome that says something about state, never decoration laid over content, and the budget is four surfaces and five named ramps. A fifth surface is a spec change, not a slice decision.
type Slot ¶
type Slot uint8
Slot names one semantic role in the palette. Roles, not hues: a slot says what a color is for, never what it looks like.
const ( // Depth tiers (section 1.1). Depth is carried entirely by background // shade; there are no box-drawing borders on cards or columns. Shadow Slot = iota // #05070a x256 232 overlay drop shadow Canvas // #0b0e14 x256 233 page ground Surface // #171d27 x256 234 column panel body, footer bar, filter field Zebra // #1e2632 x256 235 alternating card tier, compact density only Card // #252f3d x256 236 card surface Raised // #35404f x256 238 selected card surface and the unfocused band OverlaySurf // #3c495c x256 239 overlay panel body OverlayBand // #4a5970 x256 59 overlay header/footer/section bands // Foreground scale (section 1.2). FgBase // #e3e9f2 x256 255 primary text FgSubtle // #9aa5b6 x256 248 secondary text FgMuted // #6b7686 x256 243 tertiary text FgOnAccent // #0b0e14 x256 233 text on a saturated fill Brand // #4f8ef7 x256 69 wordmark pill, overlay header band, focus accent HueTodo // #7aa2f7 x256 111 TO DO column identity HueDoing // #f2a33c x256 215 DOING column identity HueDone // #3fbf7f x256 72 DONE column identity HueCancelled // #7b8494 x256 102 CANCELLED column, hidden-count chip // Priority scale (section 1.4). Prio1 // #ff5a48 x256 203 Prio2 // #ffb020 x256 214 Prio3 // #4f8ef7 x256 69 low, and the fallback for any unknown priority // Status colors (section 1.5). StatusOK // #3fbf7f x256 72 StatusWarn // #ffb020 x256 214 StatusDanger // #ff5a48 x256 203 StatusInfo // #4f8ef7 x256 69 StatusAlarm // #b31f14 x256 124 armed two-step fill // Button variant tints (section 1.9): the readable-on-Raised form of a // variant's hue, and the fill its hovered state carries. TintPrimary // #a8b6ff x256 147 TintSuccess // #7fe0b0 x256 115 TintDanger // #ffa7a0 x256 217 // Label pill wheel (section 1.6), selected by the labelColor hash. Label1 // #ff7b54 x256 209 Label2 // #4f8ef7 x256 69 Label3 // #3f9d58 x256 71 Label4 // #b98af7 x256 141 Label5 // #ffb020 x256 214 )
The palette slots of spec section 1. Order is depth tiers, foreground scale, brand and column hues, priority scale, status colors, label wheel.
func AccentSlot ¶
AccentSlot is the per-project accent hue of spec section 10.7.2: a deterministic recognition cue for the board the user named, and nothing else. No state, count, severity or affordance may ever be encoded in it.
The accent derives from the section 1.6 label wheel rather than from free HSL, so it is total over {Brand, Label1..Label5} and can emit no hex the section 1.7 audit has not already cleared. The default and empty titles resolve to Brand: a board that declared no name looks exactly like kb looks without one.
func EffortSlot ¶
EffortSlot resolves the fill hue of the section 3.4 effort pill for one effort value: the scale is S, M and L, and the ramp runs cool to warm.
Issue #232 retired the three colored squares the chip wore between #223 and #230. A pictograph proved font-dependent on real terminals however carefully its width was pinned, and the value it carried was one of three rather than a yes or no, so a substituted or clipped glyph lost information. The letter now sits on the fill instead of beside a square: ASCII on background color, which is the one thing section 3.6 says a pill owes a terminal font.
The three hues are palette slots that already exist and are already audited against FgOnAccent as pill fills. No new hex enters section 1.7's table for a three-value scale, and the letter - not the hue - is what carries the value.
An effort value the scale does not name resolves false, and the render site falls back to the Diamond mark it wore before the squares.
func LabelSlot ¶
LabelSlot maps a label wheel position onto its palette slot. Spec section 1.6: five colors, selected by the label hash the board already uses.
func PrioritySlot ¶
PrioritySlot maps a task priority onto its palette slot. Spec section 1.4: the scale is three values - high, medium, low - so exact match on 1 and 2, and everything else is low.
Issue #234 migrated the store onto the same three values, so the fourth hue is gone from the palette and the four-value data this seam was written to tolerate no longer reaches it: migrate runs inside store.Open before any caller can read a task, and the write paths reject a value off the scale. The default arm stays because Slot must be total - a zero-valued struct that never reached the store still has to render something, and low is what an unset priority means everywhere else.
func RampStops ¶
RampStops returns the lead and tail slot of a ramp, so a caller that has to pair a flat run with a graded one - the meter's end caps of section 10.1.3 - names the ramp rather than restating its endpoints. An out-of-range ramp resolves to GradSection rather than panicking a render path.
type StatusStyles ¶
type StatusStyles struct {
OK lipgloss.Style
Warn lipgloss.Style
Danger lipgloss.Style
Info lipgloss.Style
Dot lipgloss.Style
}
StatusStyles are the semantic status roles of spec section 1.5.
type Styles ¶
type Styles struct {
Pal Palette
Dimmed *Styles // nil on the dimmed instance itself; spec section 4 step 1
Board BoardStyles
Column ColumnStyles
Card CardStyles
Rail [4]lipgloss.Style // resting card rail by priority 1..3; index 0 unused
RailSel [4]lipgloss.Style // the same rail on a selected card's Raised surface
Chip ChipStyles
Label [5]ChipStyles // the section 1.6 wheel
Status StatusStyles
Overlay OverlayStyles
Button ButtonSet
Pressed lipgloss.Style
Table TableStyles
Input textinput.Styles
Area textarea.Styles
Help help.Styles
Spinner spinner.Spinner
Progress progress.Model
Markdown glamour.StyleConfig
Huh *huh.Styles
Work WorkStyles
Metrics Metrics
Glyph Glyphs
Timing Timing
Fidelity Fidelity
// contains filtered or unexported fields
}
Styles is the whole design system, resolved once. Spec section 6.2: every lipgloss style in here is constructed inside New, exactly once. New is called on program start and on tea.BackgroundColorMsg, nowhere else. The result is threaded down from the root model; there is no package-level mutable style state and no use of Style.Inherit, which silently skips padding and margins.
func New ¶
New resolves the palette for a terminal background and builds every style exactly once. Both the base and the dimmed variant of spec section 1.8 are built here, so the overlay backdrop never blends a color per frame.
isDark arrives from tea.BackgroundColorMsg.IsDark(). Default to true until the message lands, then rebuild once.
func NewFor ¶
func NewFor(isDark bool, profile colorprofile.Profile) *Styles
NewFor is New with the terminal floor of spec section 10.7.5 resolved from a detected color profile. New is NewFor at the truecolor reference target, so the huh ThemeFunc seam of spec section 6.3 keeps its exact signature.
func NewWith ¶
NewWith is New with the timing set of spec section 10.3 injected. Injection follows the cached factory of spec section 6.2 rather than mutating a built *Styles: the same Timing lands on the base instance and on Dimmed, so an overlay schedules against the same clock as the board behind it.
Production calls New. Tests that must collapse timing call NewWith with TimingCollapsed, which is the only configuration a golden can assert against.
func (*Styles) BandRun ¶
BandRun lays an already-composed run into an overlay band row. An overlay band is rendered as one styled run over the whole row, so a colored fragment inside it - the branded spinner frame of spec section 10.2.5, which is the one part of a busy row that is supposed to carry a color - would close with a reset and drop the band's own foreground and background for every cell after it. Each reset in the run is followed by the band's own sequence instead.
Content with no reset in it is returned untouched, so a band row of plain text renders exactly the bytes it always did.
func (*Styles) ChipRuns ¶
func (s *Styles) ChipRuns(fill, surface Slot) ChipStyles
ChipRuns returns the runs of one pill fill over one surface. Styles.Chip and Styles.Label are this composition against the resting card surface; a chip on any other surface resolves here, which costs struct copies and never a style construction.
The pill has no end caps since issue #227. Each end spends its column on a padding cell filled with the ground that run already owns - Pad is the body's fill, ScopedPad the Surface tier the scoped key half sits on - so the whole pill is a run of colored cells and nothing in it depends on a font drawing a half block flush to a cell edge. Width is unchanged: the space costs the cell the cap used to.
func (*Styles) ChipRunsTint ¶
func (s *Styles) ChipRunsTint(fill, surface Slot) ChipStyles
ChipRunsTint returns the runs of one inactive pill over one surface: the same section 3.6 anatomy with the wheel hue withdrawn from the fill but kept on the text. The fill drops to Surface - the tier the scoped pill's dark half already uses - so the pill cannot be misread as selected, and the body run takes the fill hue as its foreground instead of FgOnAccent, which is the section 1.9 blurred-button pattern: a variant that has lost its state still carries its identity as a tint on the resting surface. An unselected filter label therefore reads as an offer, and still matches by eye the wheel-hued label pill on the card it filters for (issue #208).
The tint form keeps the two-tone split of the scoped pill: the key run stays one step below the body - FgSubtle under the hue - so the key/value hierarchy survives the withdrawal of the fill rather than collapsing into one run. That run also carries the toggle marker (section 10.4.1), which is why it sits at the secondary role rather than the tertiary one: the marker is the affordance a user must read before acting, so it takes the section 1.9 AA floor with it.
Both padding cells are the withdrawn Surface ground the tinted body and key runs already sit on (issue #227), so an inactive pill is one flat Surface span carrying hued text. The end caps this replaced were the worst case of the cap problem: an inactive pill has no fill for a hued half-block to fuse into, so the caps rendered as two colored bars flanking the word by construction rather than only on an unlucky font. The toggle affordance was never theirs - it is hue-on-fill against hue-on-surface plus the equal-width marker.
The compact run keeps the hue it already had: the flat form has no fill and no padding to spend, so there the marker is the whole toggle affordance, the same rule section 10.7.5 applies at FidelityFlat. No cell count changes in any form, which is what lets the filter bar toggle a pill in place (section 10.4.4).
func (*Styles) Fg ¶
Fg returns the cached blank style carrying a foreground slot only, for runs that inherit the surface they are composed onto.
func (*Styles) Grad ¶
Grad paints text cluster by cluster along the named ramp, foreground only. Spec section 10.1.1: the run is split into grapheme clusters and each cluster is styled whole, because rune splitting recolors the inside of an emoji ZWJ sequence and puts an SGR change between a base character and its combining mark, which some terminals then draw as two cells.
The caller wraps the result in SurfaceRun: per-cluster output is the worst case of the hazard SurfaceRun exists for, and a gradient run that is not wrapped in it is a bug (spec section 10.1.1).
func (*Styles) GradCell ¶
GradCell renders one already-split cluster at the ramp index column takes in a run of width columns. Spec section 10.2.5: the branded engine rebuilds one cell of one frame at a time rather than a whole run, so it needs the ramp resampled by position instead of Grad's whole-run walk.
The indexing is the section's own: column * (GradSteps-1) / max(width-1, 1), which shares a ramp style across neighbouring columns on a run longer than the ramp rather than blending a new one per cell.
func (*Styles) Graded ¶
Graded reports whether color-carried effects run on this terminal.
Spec section 10.7.5: this may be read for exactly one purpose, deciding whether to *start* an effect - arm a tick chain, allocate a prerender cache. It may never be read to pick a color. A view that writes "if styles.Graded() { colorA } else { colorB }" has reintroduced the bespoke 256-color design the section forbids.
func (*Styles) HoverRun ¶
HoverRun raises one inline run a depth tier inside an already-styled row. Spec section 10.5.1 gives hover a one-tier raise in the section 1.1 depth order; an inline reference is not a row, so the raise spans the run's own cells and the row keeps its surface on both sides. The foreground is left alone - a rendered link keeps the section 5.2 link color - and no cell is added or removed, which is section 10.4.4 applied to the one state driven by cell coordinates.
A composed run may close with a reset that would drop the raise for the rest of the run, so the tier is re-armed after each one the way PressedRun does. The run closes by restoring the row's own surface rather than by resetting, because it is substituted into the middle of a styled line.
func (*Styles) HuhTheme ¶
HuhTheme hands the already-built huh styles to a huh field. Spec section 6.3 registers the factory itself as huh.ThemeFunc; a field rendered from a *Styles that New has already resolved must not rebuild the palette per frame, so the func closes over the built styles and ignores the background argument it was resolved for.
func (*Styles) On ¶
On returns the cached blank style carrying a foreground and background slot. The widget API of spec section 5.1 is slot-parameterized (chip fills, column hues), so the surface a run lands on is only known at render time; resolving it here keeps lipgloss.NewStyle out of every render path while still costing nothing but a struct copy.
func (*Styles) OnItalic ¶
OnItalic is On with the italic attribute already set, for the emphasis run of the card description's markdown (spec section 3.3, issue #232). Italic is an attribute and costs no cell, so a description that carries emphasis wraps to the same rows as one that does not.
func (*Styles) PressedRun ¶
PressedRun wraps an already-composed run in the Pressed token. Spec section 9.1: the reverse-video feedback is a theme token, not a raw escape written by the pointer package.
A themed control renders its own styles inside this run, and every one of them ends in a reset that would cancel the feedback for the rest of the row, so the attribute is re-armed after each reset. The run closes by clearing the attribute alone rather than resetting the whole style, because the caller may be substituting it into the middle of a styled line.
func (*Styles) RowSurface ¶
RowSurface returns the surface for one overlay choice row. Spec section 10.5.1: hover raises the whole row one tier in the section 1.1 depth order, panel edge to panel edge, and OverlayBand under FgBase is the pair section 1.9 already measured as the Neutral hovered button, so a hovered row and a hovered Neutral button in the same panel read as one system.
The row does not bold: bolding a full-width run is a shout where bolding a six-cell label is a nudge. A row's own selected state is the focus gutter glyph of section 10.4.3, which does not spend the tier step, so the full-row raise collides with nothing.
func (*Styles) Surface ¶
Surface returns the card surface for a card in this state. Spec section 2.1: selection steps Card to Raised, and the alternating tier is compact-only.
func (*Styles) SurfaceRun ¶
SurfaceRun lays an already-composed run onto a surface slot. An adopted charm component paints its own runs and closes each one with a reset, which drops the panel surface for every cell the component itself did not paint - the single space bubbles/help writes between its key and description columns is the case this exists for. The background is armed once at the front and re-armed after every reset, so a component's output carries the surface edge to edge without kb reformatting what the component rendered.
The run closes with a full reset because it is laid down as a whole row, not substituted into the middle of one the way PressedRun is.
type TableStyles ¶
type TableStyles struct {
Cell lipgloss.Style // every column but the last: carries the gutter
Last lipgloss.Style // the last column: no trailing gutter to spend
}
TableStyles are the cell styles of an adopted lipgloss/v2 table. They carry layout only - the column gutter of spec section 2.5 and nothing else - so a table lays out plain text that the view then paints with the token its row role names. A cell style that carried a color would fight the surface the row is composed onto.
type Timing ¶
type Timing struct {
FPS int
PlainStride int
BirthDelay int
BirthSteps int
ScrambleSteps int
EllipsisStride int
SuffixAfter int
BrandBirthSteps int
// CelebrateSteps is the span of the ship celebration (issue #191). Spec
// section 10.3.1 names no such token, so this is the twentieth and the one
// the section's own rule sends back here rather than to a call site. It is
// a tick count for the same reason BrandBirthSteps is: both index a
// class-B effect that collapses to nothing at zero.
CelebrateSteps int
DialogGraceQuiet time.Duration
DialogGraceMax time.Duration
DialogGraceReopen time.Duration
ScrollActiveLinger time.Duration
DoubleClickWindow time.Duration
InputCoalesce time.Duration
NoticeTTL time.Duration
PollInterval time.Duration
AutoShipDelay time.Duration
SimilarDelay time.Duration
// GeometrySliceTarget is the preferred amount of offscreen layout work per
// worker command. GeometrySliceLimit is the hard ceiling used to report an
// overrun. Both are worker budgets, not paint-path timing.
GeometrySliceTarget time.Duration
GeometrySliceLimit time.Duration
}
Timing is the clock of spec section 10. Every duration the TUI schedules against is named here; nothing under internal/tui writes one inline. Rates and tick counts are int; one-shots are time.Duration.
Spec section 10.3.1 is the normative table and this struct is its Go shape. Two hazards bound the whole family and both are enforced rather than documented: a duration may never be read on a paint path, and every effect must collapse to zero. TimingCollapsed is the second one; theme/seam_test.go is the first.
func (Timing) CelebrateBeat ¶
CelebrateBeat is the tick length of one phase of the ship celebration, or 0 when the effect is collapsed. The beat is derived rather than named so the span stays the one reviewable number: shortening the flourish shortens every phase of it by construction.
func (Timing) PlainFrame ¶
PlainFrame is the tick period of a plain-tier spinner.
type WorkStyles ¶
type WorkStyles struct {
Label lipgloss.Style // plain-tier spinner and static busy text
Birth lipgloss.Style // the branded engine's pre-birth cells
Suffix lipgloss.Style // the elapsed-time field right of the label
}
WorkStyles are the busy-state text roles of spec section 10.2. They are foreground only: the branded engine composes a run of them and the caller lays that run onto its own shade tier with SurfaceRun, so a style that carried a background would fight the surface behind the row.