From 8a8ac506fd0ef44ee5c91c34412dc1f1c88fad83 Mon Sep 17 00:00:00 2001 From: mochi Date: Thu, 4 Jun 2026 14:18:01 -0400 Subject: [PATCH] =?UTF-8?q?feat(fields):=20add=20LabelField=20=E2=80=94=20?= =?UTF-8?q?static=20descriptive=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Display-only text that sits alongside other fields (module blurb, status line, bar app-title). No value / onChange, untouched by bind(); consumers drive it with a reactive `text` string (Solid props are already reactive, so no setter API). Single full-width `.stage-label` element via a bespoke `.stage-field-control` skeleton (fieldType="label"), not Field — whose expanded label-row + compact 2-col grid assume a captioned value. Layouts: compact (one line, ellipsis; nowrap collapses newlines so they truncate), expanded (pre-wrap: wraps + keeps newlines, line-height pinned to one control-height so K lines tile to K rows), bar (inline, content- sized, no truncation; align is a no-op). Props: text, align, layout, size, and `class` (style hook appended to `.stage-label` for italic/bold/colour from the consumer's own sheet — no inline-style escape hatch). BREAKING (CSS class): the field caption span is renamed `.stage-field-label` -> `.stage-field-name` (and `.stage-field-label-row` -> `.stage-field-name-row`), matching the `name` prop and freeing `.stage-field-label` for LabelField's per-type discriminator. No TS API change. Consumers targeting the old class must update. Playground: replace all free bar text with LabelField (app title is bar+large, both footers bar) and add a LabelField demo module covering align, italic, compact truncation, expanded wrap/newlines, and a live reactive label. --- CLAUDE.md | 36 ++++++-- .../src/fields/BooleanField.test.tsx | 2 +- .../solid-stage/src/fields/BooleanField.tsx | 4 +- .../src/fields/ButtonGroupField.test.tsx | 2 +- .../solid-stage/src/fields/Field.test.tsx | 6 +- packages/solid-stage/src/fields/Field.tsx | 6 +- .../solid-stage/src/fields/FieldBar.test.tsx | 2 +- .../src/fields/LabelField.test.tsx | 85 +++++++++++++++++++ .../solid-stage/src/fields/LabelField.tsx | 68 +++++++++++++++ .../src/fields/TextareaField.test.tsx | 2 +- packages/solid-stage/src/index.ts | 1 + packages/solid-stage/src/theme/base.css | 2 +- packages/solid-stage/src/theme/fields.css | 70 ++++++++++++++- packages/solid-stage/src/theme/tokens.css | 2 +- playground/src/App.tsx | 42 ++++++++- playground/src/styles.css | 21 +++-- 16 files changed, 318 insertions(+), 33 deletions(-) create mode 100644 packages/solid-stage/src/fields/LabelField.test.tsx create mode 100644 packages/solid-stage/src/fields/LabelField.tsx diff --git a/CLAUDE.md b/CLAUDE.md index 287e345..700d738 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,9 +74,10 @@ Chrome primitives: Plus the **field framework** (`src/fields/`), ported once both consumers grew the same shape: a `Field` layout base + `FieldGroup`, value fields -(Text / Number / Textarea / Color / Select / Boolean / Slider), a button -family (IconButton / IconToggle / ActionButton / ToggleButton / ButtonGroup / -ButtonGroupField), and icon fields (IconButtonField / IconToggleField). +(Text / Number / Textarea / Color / Select / Boolean / Slider), a display-only +`LabelField` (static descriptive text — no value), a button family (IconButton / +IconToggle / ActionButton / ToggleButton / ButtonGroup / ButtonGroupField), and +icon fields (IconButtonField / IconToggleField). Field conventions: - Every row is an `.stage-field-control` carrying a layout class (`stage-compact` | @@ -84,7 +85,30 @@ Field conventions: matching skeleton; compact = label | value grid, expanded = label row (with a trailing rule) above a full-width value. Default is `compact`; a control may pin one (`BooleanField` + `ColorField` compact-only, `TextareaField` - expanded-only). + expanded-only). **The caption span is `.stage-field-name`** (matching the + `name` prop; the expanded caption row is `.stage-field-name-row`) — renamed + from `.stage-field-label` so that name is free for `LabelField`'s per-type + discriminator. Class taxonomy: `.stage-field-*` is the shared row/layout + vocabulary, `.stage-{type}-*` is one control's own internals, and a trailing + segment is a state/variant modifier (`-active`, `-on`, `-center`). +- **`LabelField`** — static, display-only descriptive text (a module blurb, a + status line, a bar's app title). **No value, no `onChange`, untouched by + `bind()`;** the consumer drives it by passing a reactive `text` string (Solid + props are already reactive, so there is no/needs no setter API). It has no + caption/value split — just a single full-width `.stage-label` text element — so + it renders a **bespoke `.stage-field-control` skeleton** (`fieldType="label"`) + instead of going through `Field`, whose expanded label-row + compact 2-col grid + assume a captioned value. Three layouts: `compact` (one line, ellipsis on + overflow — `nowrap` collapses explicit newlines so they truncate too), + `expanded` (`pre-wrap`: wraps *and* keeps newlines; `line-height` pinned to one + control-height so K lines tile to K rows), `bar` (one inline content-sized line, + no truncation — `align` is a no-op inline). Props: `text`, `align` + (`left`/`center`/`right`, compact+expanded only → `.stage-label-{center,right}`), + `layout`, `size`, and `class` (the **style hook** — appended to `.stage-label` + so a consumer pins italic/bold/colour from their own sheet; the library ships + no inline-`style` escape hatch, per the no-inline-styles thesis). Replaces all + free text in bars (the playground's app title is a `bar`+`size="large"` + `LabelField`; both footers are `bar` LabelFields). - **Control-height + the type-scale tiers.** The library owns the type scale, so row height is a *deliberate, font-size-coupled primitive*, not the emergent "whichever control is tallest" it used to be. Two tokens drive it: @@ -125,7 +149,7 @@ Field conventions: Re-declaring per tier re-resolves each at its own control-height. (So a *raw* `--stage-control-height` override on a non-`.stage-size-*` element only resizes the direct-use controls; use a `.stage-size-*` class / `size` prop to move everything.) **Expanded fields** ≈ 2 control-heights + gap: the - `.stage-field-label-row` pins one control-height, the value control below pins + `.stage-field-name-row` pins one control-height, the value control below pins another. **The whole button family** joins the grid too (icon / action / toggle / ButtonGroup segments): the shared `.stage-button` base is `box-sizing: border-box` and each skin sets `height: var(--stage-control-height)` with @@ -151,7 +175,7 @@ Field conventions: not a squeezed compact row, and expanded wraps purely by width (no hard count). **The module header is also one control-height** (`min-height`, horizontal-only padding) so it lines up with its field rows, and is styled as *a field label rendered bold* — same `--stage-color-muted` colour and - `--stage-font-size` (tier) as `.stage-field-label`, `font-weight: 700` (so it + `--stage-font-size` (tier) as `.stage-field-name`, `font-weight: 700` (so it scales with control size; hover/focus brighten the colour). Both header height and font read the *inherited* (pane) tier, so a module re-tiering its own body never resizes its header. **Bar height diff --git a/packages/solid-stage/src/fields/BooleanField.test.tsx b/packages/solid-stage/src/fields/BooleanField.test.tsx index 43c2617..57e6193 100644 --- a/packages/solid-stage/src/fields/BooleanField.test.tsx +++ b/packages/solid-stage/src/fields/BooleanField.test.tsx @@ -40,7 +40,7 @@ describe('BooleanField', () => { )); const row = host.querySelector('.stage-field-control') as HTMLElement; expect(row.classList.contains('stage-compact')).toBe(true); - const label = host.querySelector('label.stage-field-label') as HTMLLabelElement; + const label = host.querySelector('label.stage-field-name') as HTMLLabelElement; const box = host.querySelector('input[type="checkbox"]') as HTMLInputElement; expect(label.getAttribute('for')).toBe(box.id); dispose(); diff --git a/packages/solid-stage/src/fields/BooleanField.tsx b/packages/solid-stage/src/fields/BooleanField.tsx index a3a762a..30bc649 100644 --- a/packages/solid-stage/src/fields/BooleanField.tsx +++ b/packages/solid-stage/src/fields/BooleanField.tsx @@ -50,7 +50,7 @@ export function BooleanField(props: BooleanFieldProps): JSX.Element { onChange={(next) => props.onChange(next)} disabled={disabled()} > - {props.name} + {props.name} @@ -64,7 +64,7 @@ export function BooleanField(props: BooleanFieldProps): JSX.Element { onChange={(next) => props.onChange(next)} disabled={disabled()} > - {props.name} + {props.name} diff --git a/packages/solid-stage/src/fields/ButtonGroupField.test.tsx b/packages/solid-stage/src/fields/ButtonGroupField.test.tsx index a8245a3..27a9ce4 100644 --- a/packages/solid-stage/src/fields/ButtonGroupField.test.tsx +++ b/packages/solid-stage/src/fields/ButtonGroupField.test.tsx @@ -18,7 +18,7 @@ describe('ButtonGroupField', () => { ]} /> )); - expect(host.querySelector('.stage-field-label')?.textContent).toBe('Transport'); + expect(host.querySelector('.stage-field-name')?.textContent).toBe('Transport'); expect(host.querySelector('.stage-button-group')).not.toBeNull(); expect(host.querySelectorAll('.stage-action-button')).toHaveLength(2); dispose(); diff --git a/packages/solid-stage/src/fields/Field.test.tsx b/packages/solid-stage/src/fields/Field.test.tsx index dd65219..ba3ccf8 100644 --- a/packages/solid-stage/src/fields/Field.test.tsx +++ b/packages/solid-stage/src/fields/Field.test.tsx @@ -17,7 +17,7 @@ describe('Field', () => { const row = host.querySelector('.stage-field-control') as HTMLElement; expect(row.classList.contains('stage-compact')).toBe(true); expect(row.classList.contains('stage-field-number')).toBe(true); - expect(row.querySelector('.stage-field-label')?.textContent).toBe('Speed'); + expect(row.querySelector('.stage-field-name')?.textContent).toBe('Speed'); expect(row.querySelector('.stage-field-value .ctl')?.textContent).toBe('5'); expect(row.querySelector('.stage-field-rule')).toBeNull(); dispose(); @@ -31,8 +31,8 @@ describe('Field', () => { )); const row = host.querySelector('.stage-field-control') as HTMLElement; expect(row.classList.contains('stage-expanded')).toBe(true); - const labelRow = row.querySelector('.stage-field-label-row') as HTMLElement; - expect(labelRow.querySelector('.stage-field-label')?.textContent).toBe('Speed'); + const labelRow = row.querySelector('.stage-field-name-row') as HTMLElement; + expect(labelRow.querySelector('.stage-field-name')?.textContent).toBe('Speed'); expect(labelRow.querySelector('.stage-field-rule')).not.toBeNull(); expect(row.querySelector('.stage-field-value .ctl')?.textContent).toBe('5'); dispose(); diff --git a/packages/solid-stage/src/fields/Field.tsx b/packages/solid-stage/src/fields/Field.tsx index d6ee7e9..8ac7d07 100644 --- a/packages/solid-stage/src/fields/Field.tsx +++ b/packages/solid-stage/src/fields/Field.tsx @@ -73,7 +73,7 @@ export function Field(props: FieldProps): JSX.Element { in a row) by passing an empty `name`; the control keeps its own aria-label, so accessibility is unaffected. */} - {props.name} + {props.name}
(props: FieldProps): JSX.Element { } > -
- {props.name} +
+ {props.name}
{props.children(props.value)}
diff --git a/packages/solid-stage/src/fields/FieldBar.test.tsx b/packages/solid-stage/src/fields/FieldBar.test.tsx index a9a1101..0beef2e 100644 --- a/packages/solid-stage/src/fields/FieldBar.test.tsx +++ b/packages/solid-stage/src/fields/FieldBar.test.tsx @@ -82,7 +82,7 @@ describe('FieldBar', () => { )); const row = host.querySelector('.stage-field-control') as HTMLElement; - expect(row.querySelector('.stage-field-label')).toBeNull(); + expect(row.querySelector('.stage-field-name')).toBeNull(); expect(row.querySelector('.ctl')).not.toBeNull(); dispose(); }); diff --git a/packages/solid-stage/src/fields/LabelField.test.tsx b/packages/solid-stage/src/fields/LabelField.test.tsx new file mode 100644 index 0000000..187645a --- /dev/null +++ b/packages/solid-stage/src/fields/LabelField.test.tsx @@ -0,0 +1,85 @@ +// @vitest-environment happy-dom +import { afterEach, describe, expect, it } from 'vitest'; +import { createSignal } from 'solid-js'; +import { mount } from '../test-utils'; +import { LabelField } from './LabelField'; +import { FieldBar } from './FieldBar'; + +afterEach(() => { + document.body.innerHTML = ''; +}); + +describe('LabelField', () => { + it('renders the text in a label-type control row, compact by default', () => { + const { host, dispose } = mount(() => ); + const row = host.querySelector('.stage-field-control') as HTMLElement; + expect(row.classList.contains('stage-field-label')).toBe(true); + expect(row.classList.contains('stage-compact')).toBe(true); + expect(host.querySelector('.stage-label')?.textContent).toBe('A description'); + dispose(); + }); + + it('has no caption/value split — just one text element, no value cell', () => { + const { host, dispose } = mount(() => ); + expect(host.querySelector('.stage-field-value')).toBeNull(); + expect(host.querySelector('.stage-field-name')).toBeNull(); + dispose(); + }); + + it('updates reactively when the text accessor changes (no setter API)', () => { + const [text, setText] = createSignal('before'); + const { host, dispose } = mount(() => ); + expect(host.querySelector('.stage-label')?.textContent).toBe('before'); + setText('after'); + expect(host.querySelector('.stage-label')?.textContent).toBe('after'); + dispose(); + }); + + it('applies the align modifier class (left is the unclassed default)', () => { + const left = mount(() => ); + expect(left.host.querySelector('.stage-label-center')).toBeNull(); + expect(left.host.querySelector('.stage-label-right')).toBeNull(); + left.dispose(); + + const center = mount(() => ); + expect(center.host.querySelector('.stage-label.stage-label-center')).not.toBeNull(); + center.dispose(); + + const right = mount(() => ); + expect(right.host.querySelector('.stage-label.stage-label-right')).not.toBeNull(); + right.dispose(); + }); + + it('honours an explicit layout', () => { + const { host, dispose } = mount(() => ); + expect(host.querySelector('.stage-field-control')?.classList.contains('stage-expanded')).toBe( + true, + ); + dispose(); + }); + + it('inherits the bar layout from an enclosing FieldBar', () => { + const { host, dispose } = mount(() => ( + + + + )); + expect(host.querySelector('.stage-field-control')?.classList.contains('stage-bar')).toBe(true); + dispose(); + }); + + it('applies the size tier class', () => { + const { host, dispose } = mount(() => ); + expect(host.querySelector('.stage-field-control')?.classList.contains('stage-size-large')).toBe( + true, + ); + dispose(); + }); + + it('appends a consumer class onto the text element as a style hook', () => { + const { host, dispose } = mount(() => ); + const label = host.querySelector('.stage-label') as HTMLElement; + expect(label.classList.contains('app-title')).toBe(true); + dispose(); + }); +}); diff --git a/packages/solid-stage/src/fields/LabelField.tsx b/packages/solid-stage/src/fields/LabelField.tsx new file mode 100644 index 0000000..d3f6c49 --- /dev/null +++ b/packages/solid-stage/src/fields/LabelField.tsx @@ -0,0 +1,68 @@ +import { useContext, type JSX } from 'solid-js'; +import { FieldLayoutContext, type FieldLayout } from './Field'; +import { sizeClassList, type ControlSize } from '../panes/size'; + +export type LabelAlign = 'left' | 'center' | 'right'; + +export interface LabelFieldProps { + /** + * The descriptive text. A plain string, but reactive for free — pass a signal + * read (`text={desc()}`) and Solid re-renders on change; no setter API needed. + */ + readonly text: string; + /** + * Text alignment in `compact` / `expanded` layout (default `left`). Ignored in + * `bar` layout, which flows inline and is sized to its content. + */ + readonly align?: LabelAlign | undefined; + readonly layout?: FieldLayout | undefined; + readonly size?: ControlSize | undefined; + /** + * Style hook — appended to the text element's class so a consumer can pin + * italic / bold / colour from their own stylesheet (the library ships no + * inline-style escape hatch). Single or space-separated class tokens. + */ + readonly class?: string | undefined; +} + +/** + * Static descriptive text that sits alongside other fields — a module blurb, a + * status line, a bar's app title. **Display-only:** no value, no `onChange`, + * untouched by `bind()`; the consumer updates it by passing a reactive `text`. + * + * Unlike value fields it has no caption/value split — it's a single full-width + * text element, so it renders a bespoke `.stage-field-control` skeleton rather + * than going through `Field` (whose expanded label-row + compact 2-col grid + * assume a captioned value). The three layouts: + * - `compact` (default): one line, ellipsis-truncated on overflow (explicit + * newlines collapse, so they truncate too). + * - `expanded`: wraps to as many lines as needed, preserving explicit newlines; + * each visual line is one control-height, so the block tiles to whole rows. + * - `bar`: one inline line sized to its content, no truncation (the consumer + * truncates if needed) — `align` does not apply. + */ +export function LabelField(props: LabelFieldProps): JSX.Element { + const ctxLayout = useContext(FieldLayoutContext); + const layout = (): FieldLayout => props.layout ?? ctxLayout ?? 'compact'; + return ( +
+ + {props.text} + +
+ ); +} diff --git a/packages/solid-stage/src/fields/TextareaField.test.tsx b/packages/solid-stage/src/fields/TextareaField.test.tsx index 1196a8b..79e05f1 100644 --- a/packages/solid-stage/src/fields/TextareaField.test.tsx +++ b/packages/solid-stage/src/fields/TextareaField.test.tsx @@ -13,7 +13,7 @@ describe('TextareaField', () => { const { host, dispose } = mount(() => ( '{}'} onChange={onChange} /> )); - expect(host.querySelector('.stage-field-control .stage-field-label')?.textContent).toBe( + expect(host.querySelector('.stage-field-control .stage-field-name')?.textContent).toBe( 'JSON', ); const ta = host.querySelector('textarea') as HTMLTextAreaElement; diff --git a/packages/solid-stage/src/index.ts b/packages/solid-stage/src/index.ts index 1d09e41..821c0cb 100644 --- a/packages/solid-stage/src/index.ts +++ b/packages/solid-stage/src/index.ts @@ -23,6 +23,7 @@ export { export { FieldGroup, type FieldGroupProps } from './fields/FieldGroup'; export { FieldBar, type FieldBarProps } from './fields/FieldBar'; export { TextField, type TextFieldProps } from './fields/TextField'; +export { LabelField, type LabelFieldProps, type LabelAlign } from './fields/LabelField'; export { NumberField, type NumberFieldProps } from './fields/NumberField'; export { TextareaField, type TextareaFieldProps } from './fields/TextareaField'; export { ColorField, type ColorFieldProps } from './fields/ColorField'; diff --git a/packages/solid-stage/src/theme/base.css b/packages/solid-stage/src/theme/base.css index aa27f45..5f4dd1e 100644 --- a/packages/solid-stage/src/theme/base.css +++ b/packages/solid-stage/src/theme/base.css @@ -278,7 +278,7 @@ background: var(--stage-color-module-header-bg); border: none; /* The header is a field label rendered bold: same muted colour and tier - font-size as `.stage-field-label`, so it scales with control size and reads + font-size as `.stage-field-name`, so it scales with control size and reads as the section's label. Both colour + size follow the *inherited* (pane) tier — like the header height — so a module re-tiering its own body never resizes its header. (Hover/focus brighten the colour below.) */ diff --git a/packages/solid-stage/src/theme/fields.css b/packages/solid-stage/src/theme/fields.css index bc4b9d3..9e0b257 100644 --- a/packages/solid-stage/src/theme/fields.css +++ b/packages/solid-stage/src/theme/fields.css @@ -24,7 +24,7 @@ font-size: var(--stage-font-size); } -.stage-field-label { +.stage-field-name { color: var(--stage-color-muted); overflow: hidden; text-overflow: ellipsis; @@ -99,7 +99,7 @@ min-height: var(--stage-control-height); } -.stage-bar > .stage-field-label { +.stage-bar > .stage-field-name { flex: 0 0 auto; } @@ -138,14 +138,14 @@ /* An expanded field's label row is itself one control-height, so an expanded field ≈ 2 control-heights + the row gap (label row + the value control below, which pins its own height). The trailing rule centres in it. */ -.stage-field-label-row { +.stage-field-name-row { display: flex; align-items: center; gap: var(--stage-field-column-gap); min-height: var(--stage-control-height); } -.stage-field-label-row > .stage-field-label { +.stage-field-name-row > .stage-field-name { flex: 0 1 auto; min-width: 0; } @@ -163,6 +163,68 @@ width: 100%; } +/* LabelField: static descriptive text ------------------------------------- + * + * Display-only text with no caption/value split — a single full-width text + * element filling the row, not the label|value grid the other fields use. The + * row carries .stage-field-label (the per-type discriminator, freed by renaming + * the caption span to .stage-field-name) plus the usual layout class. */ +.stage-label { + color: var(--stage-color-label); + min-width: 0; +} + +/* Compact: one line, ellipsis on overflow. white-space: nowrap also collapses + any explicit newline to a space, so a multiline string truncates here too. */ +.stage-field-control.stage-compact.stage-field-label { + display: flex; + align-items: center; + min-height: var(--stage-control-height); +} + +.stage-compact > .stage-label { + flex: 1 1 auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Expanded: wrap to as many lines as needed and preserve explicit newlines + (pre-wrap). line-height pinned to one control-height so K lines occupy + exactly K control-heights — the block tiles to whole rows like .stage-icon-row. */ +.stage-field-control.stage-expanded.stage-field-label { + display: block; +} + +.stage-expanded > .stage-label { + display: block; + white-space: pre-wrap; + overflow-wrap: break-word; + line-height: var(--stage-control-height); +} + +/* Bar: one inline line, content-sized, no truncation (consumer truncates). The + element is inline so text-align (the align prop) is a no-op here, as intended. */ +.stage-field-control.stage-bar.stage-field-label { + display: inline-flex; + align-items: center; + width: auto; + min-height: var(--stage-control-height); +} + +.stage-bar > .stage-label { + white-space: nowrap; +} + +/* Alignment applies to compact / expanded only (bar flows inline). */ +.stage-label-center { + text-align: center; +} + +.stage-label-right { + text-align: right; +} + /* Text-like inputs (text / number / textarea) ------------------------- */ .stage-input { width: 100%; diff --git a/packages/solid-stage/src/theme/tokens.css b/packages/solid-stage/src/theme/tokens.css index 591e249..fb50ac3 100644 --- a/packages/solid-stage/src/theme/tokens.css +++ b/packages/solid-stage/src/theme/tokens.css @@ -96,7 +96,7 @@ /* Max height of SelectField's open listbox before it scrolls. */ --stage-select-listbox-max-height: 240px; /* Colour of the rule that fills the space after an expanded field's label. - Defaults to the field label's colour (.stage-field-label uses --stage-color-muted). */ + Defaults to the field label's colour (.stage-field-name uses --stage-color-muted). */ --stage-field-rule-color: var(--stage-color-muted); --stage-strip-width: 28px; diff --git a/playground/src/App.tsx b/playground/src/App.tsx index 0791c14..1739350 100644 --- a/playground/src/App.tsx +++ b/playground/src/App.tsx @@ -6,6 +6,7 @@ import { FieldGroup, FieldBar, TextField, + LabelField, NumberField, SelectField, BooleanField, @@ -115,7 +116,14 @@ export function App(): JSX.Element { // itself lives in the outer stage's centre; the outer stage has no panes. const top = ( solid-stage playground
} + left={ + + } center={ - {splitEnabled() ? 'two stages — shared state + cache' : 'one stage'} +
); @@ -149,7 +161,7 @@ export function App(): JSX.Element { // toggling either moves both, proving the shared store. const bottomToolbars = ( outer stage footer} + left={} right={} /> ); @@ -351,6 +363,30 @@ export function App(): JSX.Element { /> + + {/* LabelField — static descriptive text, no value. The last entry binds no + slot but reads `state.text.compact`, so it updates live as you type in + the TextField module above: dynamic labels need no setter, just a + reactive `text`. Compact truncates (incl. explicit newlines); expanded + wraps and keeps newlines; italic comes from the class style-hook. */} + + + + + + + + + + + + ); diff --git a/playground/src/styles.css b/playground/src/styles.css index 5f8abce..e5a1e1c 100644 --- a/playground/src/styles.css +++ b/playground/src/styles.css @@ -22,11 +22,10 @@ body, --stage-color-accent: #ff4488; } -.playground-top { - display: flex; - align-items: center; - height: var(--stage-top-bar-height); - padding: 0 8px; +/* App title — a LabelField (bar, large). The library owns size/weight scaling; + the consumer only pins the bold weight via the class style-hook. */ +.playground-title { + font-weight: 700; color: var(--stage-color-label); } @@ -35,8 +34,18 @@ body, align-items: center; height: var(--stage-bottom-bar-height); padding: 0 8px; +} + +/* Muted footer text — LabelField defaults to the primary label colour, so the + muted tone is opted in via the class style-hook. */ +.playground-footer-text { color: var(--stage-color-muted); - font-size: 11px; +} + +/* LabelField demo — italic via the class style-hook (Option A: no inline-style + escape hatch in the library; consumers style through their own classes). */ +.playground-label-italic { + font-style: italic; } .playground-sub { -- knotmirror 0.0.1