diff --git a/examples/css-tty/README.md b/examples/css-tty/README.md new file mode 100644 index 0000000..f3c7918 --- /dev/null +++ b/examples/css-tty/README.md @@ -0,0 +1,66 @@ +# `@examples/css-tty` + +CSS + `@media` queries driven by terminal capabilities: a `@bomb.sh/dom` +document is snapshotted into `@bomb.sh/css`, whose environment is built from +`@bomb.sh/tty`'s capability maps. + +``` +detectTerminal() ──▶ createTerm({ terminfo }) + │ scan() events ─▶ term.update(events) + ▼ (capability events fold in) + term.capabilities ──▶ environmentFromCapabilities(caps, size) ──▶ env + │ │ +computeTree(sheet, { nodes, parents, env }) ◀───────────────────────────────────┘ +``` + +`@bomb.sh/dom` has no run loop by design — frame scheduling belongs to +consumers — so the demo owns a minimal one (probe window, repaint on +resize, Ctrl-C exit). Real applications get the loop from the platform: +`@clack/ui`'s `createUI().main()` mounts roots and drives frames for them. + +## The wiring + +| Terminal capability | CSS surface | +| ---------------------------- | ------------------------------------------------ | +| columns / rows | `env.viewportWidth` / `Height` (8×16px per cell) | +| probed background color | `env.colorScheme` (background luminance) | +| host motion switch | `env.reducedMotion` (terminals expose none) | +| cell aspect | `env.resolution: 1` | +| color depth, kitty protocols | root custom properties via `capabilityVariables` | + +The capability map is `term.capabilities` — the merged view `@bomb.sh/tty` +maintains as `scan()` events flow through `term.update()` (the full events +array passes straight through). The demo folds nothing itself. + +Two paths carry capabilities into values: + +- **`@media`** reads the environment record: `@media (min-width: 80ch)` + flips the board to two columns (1ch = 8px, so `80ch` = 80 columns), + `@media (orientation: …)` picks the main axis, and + `@media (prefers-reduced-motion: reduce)` responds to the host switch. +- **`light-dark()`** resolves at computed-value time against the color + scheme derived from the probed background. Resolution covers `color` and + `*-color` properties, so the stylesheet uses `background-color` and + `border-color` rather than the shorthands. + +## Run + +``` +pnpm --filter @examples/css-tty demo +``` + +With a TTY the demo probes the terminal (raw-mode stdin for ~250ms), folds +the responses into the capability map, and re-evaluates the `@media` queries +on every resize; Ctrl-C exits. Piped, it performs one evaluation with the +static capabilities and a dark fallback scheme. + +## Test + +``` +pnpm --filter @examples/css-tty test +``` + +The suite covers the adapter in isolation and the stylesheet's behavior +through the real engine: the 80ch breakpoint flip, the orientation axis, +the motion switch, `light-dark()` under both schemes, and the computed-value +drop for malformed inline calls. diff --git a/examples/css-tty/package.json b/examples/css-tty/package.json new file mode 100644 index 0000000..6fc651e --- /dev/null +++ b/examples/css-tty/package.json @@ -0,0 +1,28 @@ +{ + "name": "@examples/css-tty", + "private": true, + "license": "MIT", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "demo": "node --experimental-transform-types src/demo.ts", + "typecheck": "tsgo --noEmit -p tsconfig.json", + "test": "vitest run", + "format": "bsh format", + "format:check": "bsh format --check", + "lint": "bsh lint" + }, + "dependencies": { + "@bomb.sh/css": "workspace:*", + "@bomb.sh/dom": "workspace:*", + "@bomb.sh/tty": "catalog:" + }, + "devDependencies": { + "@bomb.sh/tools": "^0.6.1", + "@types/node": "^22", + "@typescript/native-preview": "7.0.0-dev.20260623.1", + "vitest": "^4.1.2" + } +} diff --git a/examples/css-tty/src/css-types.d.ts b/examples/css-tty/src/css-types.d.ts new file mode 100644 index 0000000..75e3833 --- /dev/null +++ b/examples/css-tty/src/css-types.d.ts @@ -0,0 +1,63 @@ +// Type stub for `@bomb.sh/css`: the package ships untyped JS, so this file +// pins the surface the example uses. Wired up through `paths` in the +// example tsconfig (type-checking only; runtime always loads the real +// package). The WIT world `bombshell:css@0.4.0` is the source of truth. + +/// Rendering environment consulted by `@media` evaluation. +export interface Environment { + viewportWidth?: number; + viewportHeight?: number; + colorScheme?: 'light' | 'dark'; + reducedMotion?: boolean; + resolution?: number; +} + +/// One element of the host tree: trees ship as a flat, document-ordered +/// list plus a parallel parent-index list (`NO_PARENT` marks a root). +export interface CssNode { + tag: string; + id?: string; + classes?: string[]; + attributes?: [string, string][]; + /// WIT `node-state` bits: focus = 1, hover = 2, active = 4. + state?: number; + width?: number; + height?: number; + /// Inline style declarations; a real cascade slot. + style?: string; +} + +export interface Diagnostic { + /// 0 = error, 1 = warning. + severity: 0 | 1; + code: string; + message: string; +} + +/// A `[name, value]` computed declaration. +export type ComputedProperty = [string, string]; + +export interface Sheet { + readonly ir: Uint8Array; + readonly diagnostics: Diagnostic[]; + compute( + nodes: CssNode[], + options: { parents: number[]; target: number; env?: Environment }, + ): ComputedProperty[]; + computeTree( + nodes: CssNode[], + options: { parents: number[]; env?: Environment }, + ): ComputedProperty[][]; + release(): void; + [Symbol.dispose](): void; +} + +export declare function compile(css: string, origin?: number): Sheet; + +export declare function computeTree( + sheet: Sheet, + options: { nodes: CssNode[]; parents: number[]; env?: Environment }, +): ComputedProperty[][]; + +/// Parent index marking a root with no parent (u32::MAX). +export declare const NO_PARENT: number; diff --git a/examples/css-tty/src/demo.ts b/examples/css-tty/src/demo.ts new file mode 100644 index 0000000..d160c7c --- /dev/null +++ b/examples/css-tty/src/demo.ts @@ -0,0 +1,233 @@ +// CSS + @media queries driven by terminal capabilities, rendered live. +// +// Detects the terminal (terminfo, plus a bounded probe round-trip when +// stdin is a TTY), keeps the capability maps in a `@bomb.sh/tty` term — +// `scan()` events flow through `term.update()`, which folds them into +// `term.capabilities` — snapshots a `@bomb.sh/dom` document, computes +// styles with `@bomb.sh/css`, and renders the computed styles as real +// frames: resize the terminal and the `@media` queries re-evaluate, the +// board relayouts, the accent color changes, and the status bar prints +// the numbers. +// +// `@bomb.sh/dom` has no run loop by design (frame scheduling belongs to +// consumers), so the demo owns a minimal one: probe window, then +// repaint-on-resize (the render itself diffs inside `Term.render`, so +// only changed cells hit the wire). Real applications get the loop from +// the platform (`@clack/ui`'s `createUI().main()`). +// +// Usage: pnpm --filter @examples/css-tty demo +import { compile, type ComputedProperty, type CssNode } from '@bomb.sh/css'; +import { + alternateBuffer, + createInput, + createTerm, + cursor, + detectTerminal, + settings, + type RuntimeCapabilities, + type Setting, + type TerminalInfo, +} from '@bomb.sh/tty'; +import { capabilityVariables, environmentFromCapabilities, type TerminalSize } from './index.ts'; +import { createDemoDocument, snapshotDocument, type DocumentSnapshot } from './tree.ts'; +import { renderTree } from './render.ts'; +import { DEMO_CSS } from './styles.ts'; + +/// How long to wait for probe responses before proceeding with whatever +/// arrived. Terminals that never answer keep the static capabilities. +const PROBE_TIMEOUT_MS = 250; + +function currentSize(): TerminalSize { + // `||` rather than `??`: a degenerate pty reports 0×0, and a zero-sized + // term lays out nothing. + return { + columns: process.stdout.columns || 80, + rows: process.stdout.rows || 24, + }; +} + +/// Write the terminfo probe and feed responses through the input parser +/// into `term.update()` until the theme answers (or the timeout lapses), +/// inside a short raw-mode window so responses reach the parser +/// byte-for-byte. +async function probeCapabilities( + terminfo: TerminalInfo, + term: Awaited>, +): Promise { + const input = await createInput({ terminfo }); + const stream = process.stdin; + const wasRaw = stream.isRaw ?? false; + + stream.setRawMode(true); + try { + await new Promise((resolve) => { + let done = false; + const finish = () => { + if (done) { + return; + } + done = true; + clearTimeout(timer); + stream.off('data', onData); + // A response ending mid-burst can leave a buffered lone ESC; + // one argument-less scan flushes it. + term.update(input.scan().events); + resolve(); + }; + const onData = (chunk: Buffer | string): void => { + const bytes = typeof chunk === 'string' ? Buffer.from(chunk) : chunk; + const { events } = input.scan(new Uint8Array(bytes)); + // The full events array passes straight through: capability + // events fold into term.capabilities, everything else is a + // no-op. Bytes come back only when something must be written + // now. + const out = term.update(events); + if (out.length > 0) { + process.stdout.write(out); + } + if (term.capabilities.theme.background !== undefined) { + finish(); + } + }; + const timer = setTimeout(finish, PROBE_TIMEOUT_MS); + stream.resume(); + stream.on('data', onData); + process.stdout.write(terminfo.probe); + }); + } finally { + // Restore the terminal even if the probe window fails midway. + stream.setRawMode(wasRaw); + stream.pause(); + } +} + +interface Frame extends DocumentSnapshot { + env: ReturnType; + computed: ComputedProperty[][]; + numbers: string; +} + +/// Snapshot the demo document and compute every node's style against the +/// environment built from the capability map. The status bar prints the +/// numbers, and the numbers themselves are computed values — so the frame +/// is computed twice: once to read what the CSS decided (axis, gap, +/// breakpoint), once with those numbers written into the tree. +function evaluate(sheet: ReturnType, capabilities: RuntimeCapabilities): Frame { + const size = currentSize(); + const env = environmentFromCapabilities(capabilities, { size }); + + const draft = snapshotDocument(createDemoDocument(), { + rootStyle: capabilityVariables(capabilities), + viewport: size, + }); + const draftComputed = sheet.computeTree(draft.nodes, { parents: draft.parents, env }); + const root = Object.fromEntries(draftComputed[0] ?? []); + const numbers = + `${size.columns}×${size.rows} cells → ${env.viewportWidth}×${env.viewportHeight}px` + + ` · ${env.colorScheme} · ${capabilities.colors} colors${capabilities.trueColor ? ' · truecolor' : ''}` + + ` · ${root['--axis']} board · gap ${root['--gap']} · ${root['--columns']}-column` + + ` · motion ${root['--motion']}`; + + const frame = snapshotDocument(createDemoDocument(numbers), { + rootStyle: capabilityVariables(capabilities), + viewport: size, + }); + return { + ...frame, + env, + computed: sheet.computeTree(frame.nodes, { parents: frame.parents, env }), + numbers, + }; +} + +const formatDeclarations = (declarations: ComputedProperty[]): string => + declarations.map(([name, value]) => `${name}: ${value}`).join('; '); + +const formatNodeLabel = (node: CssNode): string => + [node.tag, node.id ? `#${node.id}` : '', (node.classes ?? []).map((c) => `.${c}`).join('')].join( + '', + ); + +function report(frame: Frame): string { + const { env, nodes, computed } = frame; + const lines = [ + `env viewport=${env.viewportWidth}×${env.viewportHeight}px color-scheme=${env.colorScheme}`, + ` reduced-motion=${env.reducedMotion ? 'reduce' : 'no-preference'} resolution=${env.resolution}`, + ` ${frame.numbers}`, + '', + ]; + for (let index = 0; index < nodes.length; index++) { + lines.push( + `${String(index).padStart(2, ' ')} ${formatNodeLabel(nodes[index]!)}`, + ` ${formatDeclarations(computed[index]!)}`, + ); + } + return lines.join('\n'); +} + +const sheet = compile(DEMO_CSS, 2); +for (const diagnostic of sheet.diagnostics) { + console.warn(`css ${diagnostic.severity === 0 ? 'error' : 'warning'}: ${diagnostic.message}`); +} + +const terminfo = await detectTerminal(); +const size = currentSize(); +const term = await createTerm({ width: size.columns, height: size.rows, terminfo }); +if (process.stdin.isTTY) { + await probeCapabilities(terminfo, term); +} + +const visual = Boolean(process.stdin.isTTY); +let setup: Setting | undefined; +if (visual) { + // Full-screen app mode: alternate buffer, hidden cursor. Reverted on exit. + setup = settings(alternateBuffer({ clear: true }), cursor(false)); + process.stdout.write(setup.apply); +} + +const repaint = () => { + const frame = evaluate(sheet, term.capabilities); + if (!visual) { + return frame; + } + const ops = renderTree(frame, frame.computed); + const { output } = term.render(ops); + process.stdout.write(output); + return frame; +}; + +const firstFrame = repaint(); + +if (visual) { + // Live loop: fold the new size into the term (no WASM reinit), then + // re-evaluate the stylesheet and re-render. Diffing happens inside + // Term.render, so only changed cells hit the wire. + process.stdout.on('resize', () => { + const next = currentSize(); + term.update([{ type: 'resize', width: next.columns, height: next.rows }]); + repaint(); + }); + // Liveness, belt and suspenders: stdin referenced AND a timer anchor, + // so the event loop cannot drain no matter how the underlying streams + // behave. Cooked mode is restored after the probe, so Ctrl-C still + // delivers SIGINT. + process.stdin.ref(); + const keepAlive = setInterval(() => {}, 2_147_483_647); + const exit = () => { + clearInterval(keepAlive); + if (setup) { + process.stdout.write(setup.revert); + } + sheet.release(); + process.exit(0); + }; + process.on('SIGINT', exit); + process.on('SIGTERM', exit); +} else { + // Piped or non-interactive: one evaluation of the numbers + computed + // declarations, then done. + console.info(`\n${firstFrame.numbers}`); + console.info(report(firstFrame)); + sheet.release(); + console.info(`\nno TTY on stdin (isTTY=${process.stdin.isTTY}) — one-shot evaluation`); +} diff --git a/examples/css-tty/src/index.ts b/examples/css-tty/src/index.ts new file mode 100644 index 0000000..4d93c51 --- /dev/null +++ b/examples/css-tty/src/index.ts @@ -0,0 +1,90 @@ +// Adapter: wire `@bomb.sh/tty` capability maps into `@bomb.sh/css`. +// +// Two paths carry terminal capabilities into a stylesheet: +// +// 1. `environmentFromCapabilities` builds the `env` record handed to +// `computeTree({ env })`. `@media` evaluation reads it: terminal size +// becomes the viewport, the probed background color becomes +// `prefers-color-scheme`, and `prefers-reduced-motion` comes from a host +// switch (terminals expose none). +// 2. `capabilityVariables` renders capability facts that have no +// environment slot (color depth, kitty protocols) as root custom +// properties, consumable with `var()` through the root's inline style. +// +// The capability map itself is `term.capabilities` — the merged view +// `@bomb.sh/tty` maintains when `scan()` events flow through +// `term.update()`. Nothing is folded here. +import type { Rgb, RuntimeCapabilities } from '@bomb.sh/tty'; +import type { Environment } from '@bomb.sh/css'; + +/// One terminal cell is 8×16 CSS px. The width matches the engine's `ch` +/// unit (1ch = 8px), so `@media (min-width: 80ch)` selects an 80-column +/// terminal; the 16px cell makes `em`-based queries (16px root) read as +/// lines. +export const CELL_WIDTH_PX = 8; +export const CELL_HEIGHT_PX = 16; + +export interface TerminalSize { + columns: number; + rows: number; +} + +/// WCAG relative luminance of an 8-bit sRGB color. +export function relativeLuminance({ r, g, b }: Rgb): number { + const channel = (component: number): number => { + const srgb = component / 255; + return srgb <= 0.04045 ? srgb / 12.92 : ((srgb + 0.055) / 1.055) ** 2.4; + }; + return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b); +} + +/// `prefers-color-scheme` from the probed background color: `light` when the +/// terminal paints a bright background, `dark` otherwise. `undefined` when +/// the terminal never answered the probe — the caller chooses the fallback. +export function colorSchemeOf(background: Rgb | undefined): 'light' | 'dark' | undefined { + if (background === undefined) { + return undefined; + } + return relativeLuminance(background) >= 0.5 ? 'light' : 'dark'; +} + +export interface EnvironmentOptions { + /// Terminal size in cells; becomes the viewport at 8×16px per cell. + size: TerminalSize; + /// Used when the terminal did not answer the background-color probe. + /// Defaults to `dark`, the usual terminal default. + fallbackColorScheme?: 'light' | 'dark'; + /// The host's motion switch; terminals expose no such capability. + reducedMotion?: boolean; +} + +/// The capability map → env wiring for `computeTree({ env })`. +export function environmentFromCapabilities( + capabilities: RuntimeCapabilities, + options: EnvironmentOptions, +): Environment { + const { size } = options; + return { + viewportWidth: size.columns * CELL_WIDTH_PX, + viewportHeight: size.rows * CELL_HEIGHT_PX, + colorScheme: + colorSchemeOf(capabilities.theme.background) ?? options.fallbackColorScheme ?? 'dark', + reducedMotion: options.reducedMotion ?? false, + // Terminals are one device pixel per cell. + resolution: 1, + }; +} + +/// Capability facts without an environment slot reach CSS through root +/// custom properties: put the returned string in the root node's inline +/// `style` and consume the values with `var()`. +export function capabilityVariables(capabilities: RuntimeCapabilities): string { + const declarations = [`--tty-colors: ${capabilities.colors}`]; + if (capabilities.trueColor) { + declarations.push('--tty-truecolor: 1'); + } + if (capabilities.kittyGraphics) { + declarations.push('--tty-kitty-graphics: 1'); + } + return declarations.join('; '); +} diff --git a/examples/css-tty/src/render.ts b/examples/css-tty/src/render.ts new file mode 100644 index 0000000..0ab0cea --- /dev/null +++ b/examples/css-tty/src/render.ts @@ -0,0 +1,152 @@ +// Bridge: computed declarations → `@bomb.sh/tty` render ops. +// +// `@bomb.sh/css` resolves the stylesheet per node; this file turns each +// node's computed style into Clay render ops, so the terminal draws what +// the CSS says. The mapping conventions: +// +// - `width`/`height` percentages fill the parent (`100%` → percent(1)); +// `flex: 1` grows a box to share the free space with its siblings. +// - `flex-direction: row|column` picks the main axis (`ltr`/`ttb`). +// - `gap`/`padding`/`border` lengths map to cells: the demo's cell metric +// is 8×16 CSS px, and the quantities involved are small enough to read +// as cells. +// - `border: px` with a `border-color` draws an n-cell border. +// - `color` paints text, `background-color` paints the element's cells. +// +// Colors arrive canonicalized (`#rgb`, `#rrggbb`); lengths arrive as +// canonical text like `2px` or `100%`. +import { close, open, rgba, text, type OpenElement, type Op } from '@bomb.sh/tty'; +import type { ComputedProperty, CssNode } from '@bomb.sh/css'; +import { NO_PARENT } from '@bomb.sh/css'; + +const HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i; + +/// Canonical engine color text → packed rgba for the render ops. +export function colorOf(value: string | undefined): number | undefined { + const match = HEX.exec(value?.trim() ?? ''); + if (!match) { + return undefined; + } + let hex = match[1]!; + if (hex.length === 3) { + hex = [...hex].map((c) => c + c).join(''); + } + return rgba( + parseInt(hex.slice(0, 2), 16), + parseInt(hex.slice(2, 4), 16), + parseInt(hex.slice(4, 6), 16), + ); +} + +/// A canonical length (`2px`) → cells. Percentages are handled separately +/// by the sizing rules. +function cells(value: string | undefined): number | undefined { + const match = /^(\d+(?:\.\d+)?)px$/.exec(value?.trim() ?? ''); + return match ? Number(match[1]) : undefined; +} + +/// `100%` → 1 (Clay percentages are 0..1). +function fraction(value: string | undefined): number | undefined { + const match = /^(\d+(?:\.\d+)?)%$/.exec(value?.trim() ?? ''); + return match ? Number(match[1]) / 100 : undefined; +} + +type Declarations = Record; + +const declarationsOf = (computed: ComputedProperty[]): Declarations => Object.fromEntries(computed); + +function sizing(style: Declarations, axis: 'width' | 'height'): OpenElement['layout'] { + const sizingAxis: NonNullable = {}; + // `flex: 1` grows the box into the free space (both axes), overriding + // any percentage on the same axis. + if (style.flex !== undefined) { + return { [axis]: { type: 'grow' } }; + } + const percent = fraction(style[axis]); + if (percent !== undefined) { + return { [axis]: { type: 'percent', value: percent } }; + } + const px = cells(style[axis]); + if (px !== undefined) { + return { [axis]: { type: 'fixed', value: px } }; + } + return sizingAxis; +} + +/// Element ops for one box: open (styled) + children + close. +function boxOps({ + id, + style, + children, +}: { + id: string; + style: Declarations; + children: Op[]; +}): Op[] { + const props: Omit = {}; + const layout: NonNullable = { + ...sizing(style, 'width'), + ...sizing(style, 'height'), + }; + const direction = style['flex-direction']; + if (direction === 'row') { + layout.direction = 'ltr'; + } else if (direction === 'column') { + layout.direction = 'ttb'; + } + const gap = cells(style.gap); + if (gap !== undefined) { + layout.gap = gap; + } + const padding = cells(style.padding); + if (padding !== undefined) { + layout.padding = { left: padding, right: padding, top: padding, bottom: padding }; + } + props.layout = layout; + + const bg = colorOf(style['background-color']); + if (bg !== undefined) { + props.bg = bg; + } + const borderWidth = cells(style.border); + const borderColor = colorOf(style['border-color']); + if (borderWidth !== undefined && borderColor !== undefined) { + props.border = { + color: borderColor, + left: borderWidth, + right: borderWidth, + top: borderWidth, + bottom: borderWidth, + }; + } + return [open(id, props), ...children, close()]; +} + +/// Render the whole snapshot: one styled op stream per node, in document +/// order, styled by the index-aligned computed declarations. +export function renderTree( + { nodes, parents, texts }: { nodes: CssNode[]; parents: number[]; texts: (string | undefined)[] }, + computed: ComputedProperty[][], +): Op[] { + const styles = computed.map(declarationsOf); + const childIndices: number[][] = nodes.map(() => []); + nodes.forEach((_, index) => { + const parent = parents[index]!; + if (parent !== NO_PARENT) { + childIndices[parent]?.push(index); + } + }); + + const build = (index: number): Op[] => { + const node = nodes[index]!; + const style = styles[index]!; + const children = (childIndices[index] ?? []).flatMap(build); + if (node.tag === 'text') { + const color = colorOf(style.color); + return [text(texts[index] ?? '', color !== undefined ? { color } : {})]; + } + return boxOps({ id: node.id ?? `node-${index}`, style, children }); + }; + + return nodes[0] ? build(0) : []; +} diff --git a/examples/css-tty/src/styles.ts b/examples/css-tty/src/styles.ts new file mode 100644 index 0000000..6f54867 --- /dev/null +++ b/examples/css-tty/src/styles.ts @@ -0,0 +1,108 @@ +// The demo stylesheet. +// +// Two mechanisms carry terminal capabilities into values: +// +// - `@media` queries read the environment record built from the terminal's +// capability maps: `(min-width: 80ch)` keys off the cell-mapped viewport, +// `(orientation)` off its aspect, `(prefers-reduced-motion)` off the +// host's switch. +// - `light-dark()` resolves at computed-value time against the color scheme +// derived from the probed background color. Note the property choice: +// resolution covers `color` and `*-color` properties, so panels use +// `background-color` and borders use `border-color` rather than the +// `background` / `border` shorthands. +// +// The render bridge (src/render.ts) maps the computed declarations to +// terminal cells: `width`/`height` percentages fill the terminal, `flex: 1` +// grows children to share space, `flex-direction` picks the main axis, +// `gap`/`padding` become cell counts, `border` + `border-color` draw +// borders, and `color`/`background-color` paint cells. +export const DEMO_CSS = ` + :root { + --fg: light-dark(#1b1b22, #e2e2e8); + --panel: light-dark(#ffffff, #1f1f28); + --muted: light-dark(#5f5f6b, #9a9aa8); + /* Amber while narrow; the wide breakpoint hands the accent to violet. */ + --accent: light-dark(#b45309, #f59e0b); + --axis: column; + --columns: 1; + --gap: 1px; + --motion: steady; + } + + /* 80 columns (80ch = 640px at 8px cells) or wider: a row board, and the + accent changes with it — resize across the breakpoint to see both. */ + @media (min-width: 80ch) { + :root { + --axis: row; + --columns: 2; + --gap: 2px; + --accent: light-dark(#6d28d9, #8b5cf6); + } + } + + /* Shape of the terminal decides the board's main axis. */ + @media (orientation: portrait) { + :root { + --axis: column; + } + } + + /* Hosts wire their own motion switch; terminals expose none. */ + @media (prefers-reduced-motion: reduce) { + :root { + --motion: none; + } + } + + .app { + width: 100%; + height: 100%; + color: var(--fg); + background-color: var(--panel); + display: flex; + flex-direction: column; + gap: 1px; + padding: 1px; + } + .header { + border: 1px; + border-color: var(--accent); + color: var(--accent); + gap: 1px; + } + .title { + color: var(--accent); + } + .status { + color: var(--muted); + } + .board { + flex: 1; + display: flex; + flex-direction: var(--axis); + gap: var(--gap); + } + .card { + flex: 1; + background-color: var(--panel); + border: 1px; + border-color: var(--accent); + color: var(--fg); + gap: var(--gap); + padding: 1px; + } + .heading { + color: var(--accent); + } + .body { + color: var(--muted); + } + .statusbar { + border: 1px; + border-color: var(--muted); + } + .numbers { + color: var(--muted); + } +`; diff --git a/examples/css-tty/src/tree.ts b/examples/css-tty/src/tree.ts new file mode 100644 index 0000000..920bdf2 --- /dev/null +++ b/examples/css-tty/src/tree.ts @@ -0,0 +1,117 @@ +// The demo document and its snapshot into `@bomb.sh/css` node form. +import { Document, type Element } from '@bomb.sh/dom'; +import { NO_PARENT, type CssNode } from '@bomb.sh/css'; +import { CELL_HEIGHT_PX, CELL_WIDTH_PX, type TerminalSize } from './index.ts'; + +/// WIT `node-state` flags in declaration order: focus, hover, active. +export const NODE_STATE = { focus: 1, hover: 2, active: 4 } as const; + +export interface SnapshotOptions { + /// Inline style for the root element (e.g. capability custom properties). + rootStyle?: string; + /// Recorded on the root node so container queries can size against the + /// terminal viewport. + viewport?: TerminalSize; +} + +export interface DocumentSnapshot { + nodes: CssNode[]; + parents: number[]; + /// Text content per node: leaf elements carry their `textContent`, boxes + /// are `undefined`. The CSS node record has no text slot, so hosts keep + /// the content channel alongside the snapshot. + texts: (string | undefined)[]; +} + +/// Snapshot a document into the flat, document-ordered node/parent form +/// `computeTree` consumes: `NO_PARENT` marks a root, sibling order follows +/// document order, and the focus flag reflects `document.activeElement`. +export function snapshotDocument( + document: Document, + options: SnapshotOptions = {}, +): DocumentSnapshot { + const nodes: CssNode[] = []; + const parents: number[] = []; + const texts: (string | undefined)[] = []; + const visit = (element: Element, parent: number): void => { + const index = nodes.length; + const isRoot = parent === NO_PARENT; + nodes.push({ + tag: element.localName, + id: element.id === '' ? undefined : element.id, + classes: element.className.split(/\s+/).filter(Boolean), + attributes: Array.from(element.attributes, (attr): [string, string] => [ + attr.name, + attr.value, + ]), + state: document.activeElement === element ? NODE_STATE.focus : 0, + width: isRoot && options.viewport ? options.viewport.columns * CELL_WIDTH_PX : undefined, + height: isRoot && options.viewport ? options.viewport.rows * CELL_HEIGHT_PX : undefined, + style: + isRoot && options.rootStyle !== undefined + ? options.rootStyle + : (element.getAttribute('style') ?? undefined), + }); + parents.push(parent); + texts.push(element.childElementCount === 0 ? (element.textContent ?? undefined) : undefined); + for (const child of element.children) { + visit(child, index); + } + }; + if (document.documentElement) { + visit(document.documentElement, NO_PARENT); + } + return { nodes, parents, texts }; +} + +const CARDS: [string, string][] = [ + ['layout', 'One column below 80 columns, a row above — @media (min-width: 80ch).'], + ['scheme', 'Colors come from light-dark(), resolved against the probed background.'], + ['motion', 'The host wires its own reduced-motion switch into the environment.'], +]; + +/// Build the demo document: a header, a board of cards, and a status bar +/// that prints the numbers. +export function createDemoDocument(numbers = ''): Document { + const document = new Document(); + const app = document.createElement('box'); + app.id = 'app'; + app.className = 'app'; + + const header = document.createElement('box'); + header.className = 'header'; + const title = document.createElement('text'); + title.className = 'title'; + title.textContent = 'bombshell css'; + const status = document.createElement('text'); + status.className = 'status'; + status.textContent = '@media over tty capabilities'; + header.append(title, status); + + const board = document.createElement('box'); + board.className = 'board'; + for (const [name, blurb] of CARDS) { + const card = document.createElement('box'); + card.className = 'card'; + card.tabIndex = 0; + const heading = document.createElement('text'); + heading.className = 'heading'; + heading.textContent = name; + const body = document.createElement('text'); + body.className = 'body'; + body.textContent = blurb; + card.append(heading, body); + board.append(card); + } + + const statusbar = document.createElement('box'); + statusbar.className = 'statusbar'; + const readout = document.createElement('text'); + readout.className = 'numbers'; + readout.textContent = numbers; + statusbar.append(readout); + + app.append(header, board, statusbar); + document.append(app); + return document; +} diff --git a/examples/css-tty/test/css-tty.test.ts b/examples/css-tty/test/css-tty.test.ts new file mode 100644 index 0000000..8dc2f60 --- /dev/null +++ b/examples/css-tty/test/css-tty.test.ts @@ -0,0 +1,199 @@ +// Acceptance tests: the capability-map wiring in isolation, and the demo +// stylesheet's @media / light-dark behavior through the real CSS engine. +import { describe, expect, it } from 'vitest'; +import { compile, computeTree, NO_PARENT } from '@bomb.sh/css'; +import type { Rgb, RuntimeCapabilities } from '@bomb.sh/tty'; +import { + capabilityVariables, + colorSchemeOf, + environmentFromCapabilities, + CELL_HEIGHT_PX, + CELL_WIDTH_PX, +} from '../src/index.ts'; +import { createDemoDocument, NODE_STATE, snapshotDocument } from '../src/tree.ts'; +import { DEMO_CSS } from '../src/styles.ts'; + +/// A baseline `term.capabilities` snapshot: the 256-color static defaults +/// plus the empty merged view a term has before any probe responses fold +/// in. +const caps = (over: Partial = {}): RuntimeCapabilities => ({ + colors: 256, + trueColor: false, + bce: true, + autoMargin: true, + xenl: true, + altScreen: true, + styledUnderline: false, + syncOutput: false, + pointerShape: false, + kittyKeyboard: false, + kittyGraphics: false, + theme: {}, + ...over, +}); + +const WHITE: Rgb = { r: 255, g: 255, b: 255 }; +const NEAR_BLACK: Rgb = { r: 30, g: 30, b: 40 }; + +const rootDeclarations = (computed: [string, string][][]): Map => + new Map(computed[0]); + +describe('capability map wiring', () => { + it('derives the color scheme from the probed background', () => { + expect(colorSchemeOf(WHITE)).toBe('light'); + expect(colorSchemeOf(NEAR_BLACK)).toBe('dark'); + expect(colorSchemeOf(undefined)).toBeUndefined(); + }); + + it('maps cell metrics and scheme into the environment', () => { + const env = environmentFromCapabilities(caps({ theme: { background: WHITE } }), { + size: { columns: 80, rows: 24 }, + reducedMotion: true, + }); + expect(env.viewportWidth).toBe(80 * CELL_WIDTH_PX); + expect(env.viewportHeight).toBe(24 * CELL_HEIGHT_PX); + expect(env.colorScheme).toBe('light'); + expect(env.reducedMotion).toBe(true); + expect(env.resolution).toBe(1); + }); + + it('falls back to a host-chosen scheme when the probe never answers', () => { + const defaults = environmentFromCapabilities(caps(), { size: { columns: 80, rows: 24 } }); + expect(defaults.colorScheme).toBe('dark'); + const forced = environmentFromCapabilities(caps(), { + size: { columns: 80, rows: 24 }, + fallbackColorScheme: 'light', + }); + expect(forced.colorScheme).toBe('light'); + }); + + it('exposes depth facts as root custom properties', () => { + expect(capabilityVariables(caps())).toBe('--tty-colors: 256'); + expect(capabilityVariables(caps({ trueColor: true }))).toBe( + '--tty-colors: 256; --tty-truecolor: 1', + ); + }); +}); + +describe('document snapshot', () => { + it('snapshots into the flat node/parent form', () => { + const { nodes, parents } = snapshotDocument(createDemoDocument(), { + rootStyle: '--tty-colors: 256', + viewport: { columns: 80, rows: 24 }, + }); + expect(parents[0]).toBe(NO_PARENT); + expect(parents[1]).toBe(0); + expect(nodes[0]!.tag).toBe('box'); + expect(nodes[0]!.id).toBe('app'); + expect(nodes[0]!.style).toBe('--tty-colors: 256'); + expect(nodes[0]!.width).toBe(80 * CELL_WIDTH_PX); + expect(nodes[0]!.height).toBe(24 * CELL_HEIGHT_PX); + // Document order: app, header, title, status, board, then each + // card with its heading and body, then the status bar's readout. + expect(nodes.map((node) => node.classes?.[0])).toEqual([ + 'app', + 'header', + 'title', + 'status', + 'board', + 'card', + 'heading', + 'body', + 'card', + 'heading', + 'body', + 'card', + 'heading', + 'body', + 'statusbar', + 'numbers', + ]); + }); + + it('marks the focused element in the state bits', () => { + const document = createDemoDocument(); + const firstCard = document.documentElement!.children[1]!.children[0]!; + firstCard.focus(); + const { nodes } = snapshotDocument(document); + const focused = nodes.findIndex((node) => node.state === NODE_STATE.focus); + expect(focused).toBe(5); + }); +}); + +describe('demo stylesheet through the engine', () => { + const style = ({ + columns, + rows, + background, + }: { + columns: number; + rows: number; + background?: Rgb; + }) => { + const capabilities = caps(background ? { theme: { background } } : {}); + const size = { columns, rows }; + const { nodes, parents } = snapshotDocument(createDemoDocument(), { + rootStyle: capabilityVariables(capabilities), + viewport: size, + }); + const env = environmentFromCapabilities(capabilities, { size }); + return computeTree(compile(DEMO_CSS, 2), { nodes, parents, env }); + }; + + it('compiles without diagnostics', () => { + expect(compile(DEMO_CSS, 2).diagnostics).toEqual([]); + }); + + it('flips the layout across the 80ch breakpoint', () => { + // 40×25: 320×400px — narrow and portrait. + const narrow = rootDeclarations(style({ columns: 40, rows: 25 })); + expect(narrow.get('--columns')).toBe('1'); + expect(narrow.get('--axis')).toBe('column'); + // 80×24: 640×384px — exactly the 80ch breakpoint, landscape. + const wide = rootDeclarations(style({ columns: 80, rows: 24 })); + expect(wide.get('--columns')).toBe('2'); + expect(wide.get('--axis')).toBe('row'); + }); + + it('honors the host reduced-motion switch', () => { + const size = { columns: 80, rows: 24 }; + const { nodes, parents } = snapshotDocument(createDemoDocument(), { + viewport: size, + }); + const env = environmentFromCapabilities(caps(), { size, reducedMotion: true }); + const computed = computeTree(compile(DEMO_CSS, 2), { nodes, parents, env }); + expect(rootDeclarations(computed).get('--motion')).toBe('none'); + }); + + it('resolves light-dark() against the probed scheme', () => { + // .card's color is var(--fg), and --fg is light-dark(...): the dark + // probed background picks the dark branch. The card is node 5; its + // heading (node 6) resolves var(--accent) instead. + const dark = style({ columns: 80, rows: 24, background: NEAR_BLACK }); + expect(dark[5]).toContainEqual(['color', '#e2e2e8']); + const light = style({ columns: 80, rows: 24, background: WHITE }); + expect(light[5]).toContainEqual(['color', '#1b1b22']); + }); + + it('changes the accent color across the 80ch breakpoint', () => { + // The wide breakpoint hands the accent from amber to violet — this + // is what makes the rendered demo change colors on resize. + const narrow = style({ columns: 40, rows: 25, background: NEAR_BLACK }); + expect(narrow[6]).toContainEqual(['color', '#f59e0b']); + const wide = style({ columns: 80, rows: 24, background: NEAR_BLACK }); + expect(wide[6]).toContainEqual(['color', '#8b5cf6']); + const wideLight = style({ columns: 80, rows: 24, background: WHITE }); + expect(wideLight[6]).toContainEqual(['color', '#6d28d9']); + }); + + it('drops malformed inline light-dark() at computed-value time', () => { + const size = { columns: 80, rows: 24 }; + const { nodes, parents } = snapshotDocument(createDemoDocument(), { + viewport: size, + }); + nodes[0]!.style = `${nodes[0]!.style}; color: light-dark(#fff)`; + const env = environmentFromCapabilities(caps(), { size }); + const computed = computeTree(compile(DEMO_CSS, 2), { nodes, parents, env }); + expect(rootDeclarations(computed).has('color')).toBe(false); + }); +}); diff --git a/examples/css-tty/tsconfig.json b/examples/css-tty/tsconfig.json new file mode 100644 index 0000000..d4d9993 --- /dev/null +++ b/examples/css-tty/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@bomb.sh/tools/tsconfig.json", + "compilerOptions": { + "composite": false, + "types": ["node"], + "lib": ["ESNext", "DOM"], + "skipLibCheck": false, + "paths": { + "@bomb.sh/css": ["./src/css-types.d.ts"] + } + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79b38a6..926f53d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,8 +10,8 @@ catalogs: specifier: ^0.3.1 version: 0.3.1 '@bomb.sh/tty': - specifier: ^0.8.0 - version: 0.8.0 + specifier: ^0.10.0 + version: 0.10.0 '@clack/prompts': specifier: ^1.7.0 version: 1.7.0 @@ -61,11 +61,36 @@ importers: specifier: ^4.1.2 version: 4.1.9(@types/node@22.20.1)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) + examples/css-tty: + dependencies: + '@bomb.sh/css': + specifier: workspace:* + version: link:../../experiments/css + '@bomb.sh/dom': + specifier: workspace:* + version: link:../../packages/dom + '@bomb.sh/tty': + specifier: 'catalog:' + version: 0.10.0 + devDependencies: + '@bomb.sh/tools': + specifier: ^0.6.1 + version: 0.6.1(@types/node@22.20.1)(oxc-resolver@11.21.3)(unrun@0.2.39)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) + '@types/node': + specifier: ^22 + version: 22.20.1 + '@typescript/native-preview': + specifier: 7.0.0-dev.20260623.1 + version: 7.0.0-dev.20260623.1 + vitest: + specifier: ^4.1.2 + version: 4.1.9(@types/node@22.20.1)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) + examples/demo: dependencies: '@bomb.sh/tty': specifier: 'catalog:' - version: 0.8.0 + version: 0.10.0 examples/dom-realm: dependencies: @@ -90,7 +115,7 @@ importers: dependencies: '@bomb.sh/tty': specifier: 'catalog:' - version: 0.8.0 + version: 0.10.0 '@clack/ui': specifier: 'catalog:' version: https://pkg.pr.new/bombshell-dev/ui/@clack/ui@a60ac12 @@ -228,6 +253,7 @@ importers: vitest: specifier: ^4.1.2 version: 4.1.9(@types/node@22.20.1)(vite@8.1.0(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.9.0)) + experiments/css: {} experiments/ghostwright: @@ -287,6 +313,10 @@ packages: resolution: {integrity: sha512-p3dLXau2JygQ9DTErUvHSn7GwqpRraIjiXdj/xeAU0+7opUZrtiHe18FLrU5opCSr16WZep0p0/OBX1HgMjgJQ==} hasBin: true + '@bomb.sh/tty@0.10.0': + resolution: {integrity: sha512-ZOfMnVMhlik9CtSv5NJV4409T6SitS9GC93YZrbq1Be1nmbv83gf5wQHdOc+ic9QOb2kgpgAXKrMV6WiHLSTPA==} + engines: {node: '>= 22'} + '@bomb.sh/tty@0.7.0': resolution: {integrity: sha512-htTavc3o84vR6uSoyF+Y9u81GG8NoTJkwOovahcNOY4nVeiM86vodCHdPvAXoj2ucmx04frpwMrOJ4ipLSSJfw==} engines: {node: '>= 22'} @@ -304,7 +334,7 @@ packages: engines: {node: '>= 20.12.0'} '@clack/ui@https://pkg.pr.new/bombshell-dev/ui/@clack/ui@a60ac12': - resolution: {tarball: https://pkg.pr.new/bombshell-dev/ui/@clack/ui@a60ac12, integrity: sha512-RuGTYiIwK+nsC7WxBdPm3636wUJtfyvzPoPWpzXkeV3KgCASLlsF3Y5ZG/UN2yWviceDHFu9Ou15JUBs3VJEjw==} + resolution: {integrity: sha512-RuGTYiIwK+nsC7WxBdPm3636wUJtfyvzPoPWpzXkeV3KgCASLlsF3Y5ZG/UN2yWviceDHFu9Ou15JUBs3VJEjw==, tarball: https://pkg.pr.new/bombshell-dev/ui/@clack/ui@a60ac12} version: 0.0.0 '@emnapi/core@1.10.0': @@ -581,48 +611,56 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-arm64-musl@0.137.0': resolution: {integrity: sha512-EGJ+Bs8iXx8KBH8DQ5BLoEm5lnHaYjlh4/8j8vFhrr/6z4tqONy5BZDzLpKmmNWlN6Hlc5r8YOuBVHqZ9vRFEQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxc-parser/binding-linux-ppc64-gnu@0.137.0': resolution: {integrity: sha512-vzFUQENy/fnbSe5DZWovq6tIBc1uhuMztanSW6rz1e9WdQE4gHwYuD7ZII6JnrJifd1R3RSoqiZbgRFlVL2tYQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-riscv64-gnu@0.137.0': resolution: {integrity: sha512-SfVI14HBQs9gtLcUD5hTt5hsNbdrqSUNg9S8muN+LhVQ5nf1WwH3hAoK6B9NKgdYgWAQSXFXGiiBedQ4r/BKuw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-riscv64-musl@0.137.0': resolution: {integrity: sha512-e7Ppy4FCIFNQxT/ikSeIWFoQ0l+N9vgtRBtLcyZXeolTzApyVoPqEXsYPrcdM/9i0Bwk8knvYd37vaEMxHyi6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxc-parser/binding-linux-s390x-gnu@0.137.0': resolution: {integrity: sha512-Bho5qFwdhqsIFR7gipYEUlqvi3SRrY8sugxXig380MIaakBB1PyU9+7dBiBVScfImTNWhijUxdBwqrprGdq5WA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-x64-gnu@0.137.0': resolution: {integrity: sha512-36mGWtg7PyFzjJwGDkH6/F4o2nIDEoKXLPr/X/lwqklkomQwJJt1I5GJVmGhovUEmgPK5WAeAZMqlFCehwiy9Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-x64-musl@0.137.0': resolution: {integrity: sha512-/Jqx6+N7A44n2BdvUr7pXhVr2vFjs6WGH3unZRczwrfiH0H1zY0QwKQMG/dtRiTlKGDKGukznPT8lx84/oEsZg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxc-parser/binding-openharmony-arm64@0.137.0': resolution: {integrity: sha512-9Uj0qHNNl+OgT1UTGwF7ixIXU6T1u2SbMidmgPy/h1h/fl2gRS6YpAxxY1gwHofcWjoTwkoMFd8xs5Vuj6GOFA==} @@ -701,41 +739,49 @@ packages: resolution: {integrity: sha512-X0AqNZgcD07Q4V3RDK18/vYOj/HQT/FnmEFGYS2jTWqY7JO13ryE3TEs3eAIgUJhBnNkpEaiXqz3VK8M7qQhWQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@11.21.3': resolution: {integrity: sha512-YkaQnaKYdbuaXvRt5Qd0GpbihzVnyfR6z1SpYfIUC6RTu4NF7lDKPjVkYb+jRI2gedVO2rVpN35Y6akG6ud4Lw==} cpu: [arm64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-ppc64-gnu@11.21.3': resolution: {integrity: sha512-gB9HwhrPiFqUzDeEq+y/CgAijz1YdI6BnXz5GaH2Pa9cWdutchlkGFAiAuGb/PjVQpiK6NFKzFuztxrweoit7A==} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-riscv64-gnu@11.21.3': resolution: {integrity: sha512-zjDWBlYk8QGv0H8dsPUWqkfjYIIjG2TvspGkzXL0eImbgxtZorA/klKeHyolevoT3Kvbi+1iMr9Lhrh7jf54Og==} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-riscv64-musl@11.21.3': resolution: {integrity: sha512-4UfsQvacV388y1zpXL7C1x1FNYaV52JtuNRiuzrfQA2z1z6ElVrsidkGsrvQ5EgeSq1Pj7kaKqrgGkvFuxJ/tw==} cpu: [riscv64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-s390x-gnu@11.21.3': resolution: {integrity: sha512-b5uH+HKH0MP5mNBYaK75SKsJbw52URqrx2LavYdq6wb0l3ExAG5niYRP9DWUNHdKilpaBVM2bXk9HNWrH3ew7Q==} cpu: [s390x] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-gnu@11.21.3': resolution: {integrity: sha512-PjYlmilBpNRh2ntXNYAK3Am5w/nPfEpnU/96iNx7CI8EzAn12J4JRiec63wHJTH31nLoCNxBg/829pN+3CfG3Q==} cpu: [x64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@11.21.3': resolution: {integrity: sha512-QTBAb7JuHlZ7JUEyM8UiQi2f7m/L4swBhP2TNpYIDc9Wp/wRw1G/8sl6i13aIzQAXH7LKIm294LeOHd0lQR8zA==} cpu: [x64] os: [linux] + libc: [musl] '@oxc-resolver/binding-openharmony-arm64@11.21.3': resolution: {integrity: sha512-4j1DFwjwv36ec9kds0jU/ucQ5Ha4ERO/H95BxR5JFf0kqUUAJ1kwII7XhTc1vZrkdJkvLGC9Q2MbpObpum8RBg==} @@ -804,48 +850,56 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-arm64-musl@0.47.0': resolution: {integrity: sha512-IxtQC/sbBi4ubbY+MdwdanRWrG9InQJVZqyMsBa5IUaQcnSg86gQme574HxXMC1p4bo4YhV99zQ+wNnGCvEgzw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxfmt/binding-linux-ppc64-gnu@0.47.0': resolution: {integrity: sha512-EWXEhOMbWO0q6eJSbu0QLkU8cKi0ljlYLngeDs2Ocu/pm1rrLwyQiYzlFbdnMRURI4w9ndr1sI9rSbhlJ5o23Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-riscv64-gnu@0.47.0': resolution: {integrity: sha512-tZrjS11TUiDuEpRaqdk8K9F9xETRyKXfuZKmdeW+Gj7coBnm7+8sBEfyt033EAFEQSlkniAXvBLh+Qja2ioGBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-riscv64-musl@0.47.0': resolution: {integrity: sha512-KBFy+2CFKUCZzYwX2ZOPQKck1vjQbz+hextuc19G4r0WRJwadfAeuQMQRQvB+Ivc8brlbOVg7et8K7E467440g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxfmt/binding-linux-s390x-gnu@0.47.0': resolution: {integrity: sha512-REUPFKVGSiK99B+9eaPhluEVglzaoj/SMykNC5SUiV2RSsBfV5lWN7Y0iCIc251Wz3GaeAGZsJ/zj3gjarxdFg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-x64-gnu@0.47.0': resolution: {integrity: sha512-KVftVSVEDeIfRW3TIeLe3aNI/iY4m1fu5mDwHcisKMZSCMKLkrhFsjowC7o9RoqNPxbbglm2+/6KAKBIts2t0Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-x64-musl@0.47.0': resolution: {integrity: sha512-DTsmGEaA2860Aq5VUyDO8/MT9NFxwVL93RnRYmpMwK6DsSkThmvEpqoUDDljziEpAedMRG19SCogrNbINSbLUQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxfmt/binding-openharmony-arm64@0.47.0': resolution: {integrity: sha512-8r5BDro7fLOBoq1JXHLVSs55OlrxQhEso4HVo0TcY7OXJUPYfjPoOaYL5us+yIwqyP9rQwN+rxuiNFSmaxSuOQ==} @@ -918,48 +972,56 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-arm64-musl@1.74.0': resolution: {integrity: sha512-3tFSjBxc5D8/zvjEuLvOqcA8ZXKD0+6NuaVO/edeamNc49MoAsbfaC9s1UiwODwgF6slGaF8yJA2TPkukd77tg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxlint/binding-linux-ppc64-gnu@1.74.0': resolution: {integrity: sha512-9QggtPkSPXOCTu8Szis7auOK/sC7KdQaN+/TujP7YVVhzCAOhgdRfgv8uEz0r2tk5xdgus5rLYUrCDoZNtiRUw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-riscv64-gnu@1.74.0': resolution: {integrity: sha512-VM5VPUJ4DJIWiK+AZn8FScUqMr6OFrCAYybMYjEEi7W13ParI64MByiXTkKMqZpBmvQ9zxl9Ebq2VUOiZRJYUg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-riscv64-musl@1.74.0': resolution: {integrity: sha512-SaDY1gh9rOA592J54g+gu5hkOFFQBZsMmIYHs+NRHG+Uq0OxtuuCXMWQ3vu1830Eugv5uMXyjG+bv2Z9y4IXjw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxlint/binding-linux-s390x-gnu@1.74.0': resolution: {integrity: sha512-ZATQeHZCyr6MbDveg0obD5sxLHFOghtOdC5jwVwYlvFWqtFOxctgFEG6Ef/64hYvZrWyhyCckB10AelqLopeDA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxlint/binding-linux-x64-gnu@1.74.0': resolution: {integrity: sha512-+aIvJyrdeD7LwCQ2WYLMUWNmnbeDRSPb40aBYtPjD9+PTqUwgJnk+HK5yLfSMeqXrMrDhE9uTmtt2y50tvjhHw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-x64-musl@1.74.0': resolution: {integrity: sha512-XyktaR8lhK2qWiCK0Tk8oYD+/cgn+oHA6ddRnxSSXUKkkojkV78CmShZUxQF+yrBFs0SuW+JBOPG6hecyc/iZg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxlint/binding-openharmony-arm64@1.74.0': resolution: {integrity: sha512-mzbjrPl4neaVUiJ1fUiEUxTGaSZBoiKtaoB6jmIpz9S+VOA2vDYmJpihQ82w6178V5jxziclTg8Cgj5yF6tTDg==} @@ -1057,72 +1119,84 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-gnu@1.1.5': resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-arm64-musl@1.1.5': resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-ppc64-gnu@1.1.5': resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.1.5': resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.1.5': resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-linux-x64-musl@1.1.5': resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} @@ -1290,111 +1364,145 @@ packages: resolution: {integrity: sha512-pbDcFygFmbvo0jGFq5U0m5Sa9U8aVttVJWbBHZDZ68w/X48HdDWS1V4XvBacs8XkmWbTr/ef5fMGG7HsngqTmg==} cpu: [arm64] os: [darwin] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-darwin-x64@0.6.3': resolution: {integrity: sha512-qZMrnA4i8OfqL3NMGoOvLdh1vby8cCGsmNo+tJQIIezXO557m3fdQLenz/57GqtBnnGWzWqd/aDp+faNxWlLwQ==} cpu: [x64] os: [darwin] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-freebsd-x64@0.6.3': resolution: {integrity: sha512-2+SFpfem2GBH6BlCTAq6R44bZwuieduwRWHkCQnSgbK8tdEjMwB0Ix0IchryVBn6hdiWCZSTkSE3UILliRXsRQ==} cpu: [x64] os: [freebsd] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-linux-arm-gnu@0.6.3': resolution: {integrity: sha512-fbxg3cBPdJ++36DXtdzcoKw2xzFov91Wxvmn1khX9MXQbDqJQLJmITZhtokcZsj4uGJe32sUmxAZnKbUtZLjmA==} cpu: [arm] os: [linux] + libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-linux-arm-musl@0.6.3': resolution: {integrity: sha512-Jk4P7kocGEisSvUFIm1VuHO3hC01LvS3sYAAmVVu1/ve5TuZ0iXyl9kIGtd1ZrgUvchgvZWNOaB+/Kq/RO63FA==} cpu: [arm] os: [linux] + libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-linux-arm64-gnu@0.6.3': resolution: {integrity: sha512-i1xE8Bx1YZLheWtBZHD0Mq3nAIDrhgiH7o8VB4GiCbHufKb4XKj4CqSDMWSC0RYPmn//E+UEd1NsE2NbOux1tQ==} cpu: [arm64] os: [linux] + libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-linux-arm64-musl@0.6.3': resolution: {integrity: sha512-ZeLkC6xZrlDoIJTadHfqTABTmsj2f6wCCtYBYx/RPGgdmQcGLA0NALRl7m0tnK2CT45eNRuOYzsfEZyF0XWM/A==} cpu: [arm64] os: [linux] + libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-linux-x64-gnu@0.6.3': resolution: {integrity: sha512-HNYt7zjIChPcnjZRG42CZq3Zn5mqaRo4UcFLr4mIbmGqdhX5hDDE8O8US8YgYyOUosfbBTBFdsbvFwVAx1TOkQ==} cpu: [x64] os: [linux] + libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-linux-x64-musl@0.6.3': resolution: {integrity: sha512-/1ttT31dAQc7hGtXWSEYEgzGtakAyO2C+/GqAzIuKXlGLpNPZgXdR8LZ0iDHDalbEr3AuHIPRV43sWLUrHWdsA==} cpu: [x64] os: [linux] + libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-win32-arm64@0.6.3': resolution: {integrity: sha512-oAArRDU1lkKg+xFEtiQ7C+/wghpqrkBrFWM05W73S+3sZz8JIHH79Q+Qh5gWls3i8vccitUgCnvln5V7xKn3XQ==} cpu: [arm64] os: [win32] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-win32-x64@0.6.3': resolution: {integrity: sha512-bhFDcDsvmp5KuBfWTt4carNo1E4LXH7++S8qqZgDtXVqJxs0xMm7gbuqPihA8kBbphM+NzAUm5qmq6ocfqM6kg==} cpu: [x64] os: [win32] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-parser/binding-darwin-arm64@0.6.3': resolution: {integrity: sha512-Xate6yyZgvi7da/gdnZy+Vu5jlFB0LRlb5m4MY6Y98KSQeJPZIhoVXMK2Vsl48XOmPlDIS8lge414HUVUEo+hg==} cpu: [arm64] os: [darwin] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-darwin-x64@0.6.3': resolution: {integrity: sha512-WKMZ5UU2HBdGZDEpQoLq+21f1FlS+BjroH1FaVE6zCSeqKmZ7xRP5jIRGtQ4vCYj/k2KHgyABZ16lgK9mTe2Sg==} cpu: [x64] os: [darwin] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-freebsd-x64@0.6.3': resolution: {integrity: sha512-OWX8V2k4bmtl/DNwU3yz3PZa2QXiSqWN3Xk7pNB5IPt7FcJBDlnpkOcX6h3tjMS7CyKE0lMvPUwwcmWSdbjkyA==} cpu: [x64] os: [freebsd] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-linux-arm-gnu@0.6.3': resolution: {integrity: sha512-/4LzmPXPaCWqIpY1j3+XVb8rbXIratqCte3A4sGEjua6aVhvVxEVAqeKlBsoGkORWLeqbpcxhgxKwOGM17eexA==} cpu: [arm] os: [linux] + libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-linux-arm-musl@0.6.3': resolution: {integrity: sha512-Df4jk0M/eNKKQfYzXBBKhKkmJBpB+XoX2LkMxmlK3GN+fxUdeb8EM78wX+1+eLVl5dZNo6f7gOd6oDV0gChevw==} cpu: [arm] os: [linux] + libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-linux-arm64-gnu@0.6.3': resolution: {integrity: sha512-sRCtDktUgIbbV78SYX3wdGVVm1Hz/nSUS24JgXB4MzUeGNwBNB+eQAWMtBxCGriyJNXK3zwfj+SSgvTmUdPf/A==} cpu: [arm64] os: [linux] + libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-linux-arm64-musl@0.6.3': resolution: {integrity: sha512-3J/jV3ROSqlhLyB/6i5EUHxjkom5i59iPvrtiAsnAjHzMsZJJPEke9LSaOsB0rf4MJFH9AmjvsK8gDahTjZy+A==} cpu: [arm64] os: [linux] + libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-linux-x64-gnu@0.6.3': resolution: {integrity: sha512-a5mPn/OMSq2Aa2i7eJXcc37Jtw0b89gDO2mDpXN769b06IirEiqOzLNNJd6R7R8DxoadHzY0nLFhYNChE+jyAg==} cpu: [x64] os: [linux] + libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-linux-x64-musl@0.6.3': resolution: {integrity: sha512-kQSjfa6zdvotuXEGNKQ9vZZxE+lcEEbTUMSuvY/5+0crlwBCjEqrf9/W9ViFDoQAEt8WJiu/mtVNYkKAOkjLmA==} cpu: [x64] os: [linux] + libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-win32-arm64@0.6.3': resolution: {integrity: sha512-ZawdN3R0YKr48BeXCpbax+WDWbgEG6nWyDosVeZasrT5TjgfF4XMP5SfuyMNvRJ4gbTitrcYHZkENSXZ9ncqcg==} cpu: [arm64] os: [win32] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-win32-x64@0.6.3': resolution: {integrity: sha512-NcqZwBXQhKyfC0Eb+yBxXQcPjZoUstD1Dbr3X4UlOD1QiYfnvAYRKCZJ6nQ6KQ5p30dGaKkQeBL4t3qQtDQNWA==} cpu: [x64] os: [win32] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-toolchain/types@0.5.43': resolution: {integrity: sha512-kSpvPntnXw5+lYjO71ffBEnQ5ycQ74KGIYknh0TS4xeyCuBkOqxyJumxZkMhLBBUCLjDAbx2+Icnr3Zh4ftjpQ==} @@ -1590,24 +1698,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -2102,6 +2214,8 @@ snapshots: - vite-plus - vue-tsc + '@bomb.sh/tty@0.10.0': {} + '@bomb.sh/tty@0.7.0': {} '@bomb.sh/tty@0.8.0': {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b5ce6aa..2f0d0bc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,7 +6,7 @@ packages: - vendor/* catalog: - '@bomb.sh/tty': '^0.8.0' + '@bomb.sh/tty': '^0.10.0' '@bomb.sh/args': '^0.3.1' '@clack/ui': 'https://pkg.pr.new/bombshell-dev/ui/@clack/ui@a60ac12' '@clack/prompts': '^1.7.0'