From d24e58dd29b97f7ae34bbd6caa1dd516925e0b91 Mon Sep 17 00:00:00 2001 From: "Domscribe Staff SWE (bot)" Date: Sun, 7 Jun 2026 06:58:44 -0700 Subject: [PATCH 1/3] feat(runtime,relay,core): runtime StyleCapturer + componentStyles surface (RFC 0001) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the runtime half of RFC 0001's two-tier component-style attribution. * @domscribe/core: extends RuntimeContextSchema with optional componentStyles (additive — schema bump deferred until Task A's build-time styleSource lands). * @domscribe/runtime: new StyleCapturer reads a ≤32-property computed allowlist + resolves --* CSS custom properties from element through ancestors. Wired into ContextCapturer behind the domscribe.config.captureStyles flag (off by default). Honors the ≤4KB per-element serialization budget. * @domscribe/relay: extends QueryBySourceResponse and the MCP query.bySource tool to surface componentStyles. Tool description nudges agents to call this first for styling annotations. Tests: 16 new (12 StyleCapturer unit + 4 ContextCapturer integration), 544 runtime tests pass total, 306 relay tests pass. Co-Authored-By: Claude Opus 4.7 --- .../src/lib/types/annotation.ts | 33 ++ .../mcp/tools/query-by-source.tool.spec.ts | 52 +++- .../src/mcp/tools/query-by-source.tool.ts | 42 ++- packages/domscribe-relay/src/schema.ts | 11 + .../server/routes/v1/query-by-source.route.ts | 1 + .../__snapshots__/style-capturer.spec.ts.snap | 38 +++ .../src/capture/style-capturer.spec.ts | 221 +++++++++++++ .../src/capture/style-capturer.ts | 291 ++++++++++++++++++ .../domscribe-runtime/src/capture/types.ts | 8 + .../src/core/context-capturer.spec.ts | 86 ++++++ .../src/core/context-capturer.ts | 71 ++++- .../src/core/runtime-manager.ts | 12 +- packages/domscribe-runtime/src/core/types.ts | 11 + packages/domscribe-runtime/src/index.ts | 7 + 14 files changed, 875 insertions(+), 9 deletions(-) create mode 100644 packages/domscribe-runtime/src/capture/__snapshots__/style-capturer.spec.ts.snap create mode 100644 packages/domscribe-runtime/src/capture/style-capturer.spec.ts create mode 100644 packages/domscribe-runtime/src/capture/style-capturer.ts diff --git a/packages/domscribe-core/src/lib/types/annotation.ts b/packages/domscribe-core/src/lib/types/annotation.ts index 8e426ff..b83bd57 100644 --- a/packages/domscribe-core/src/lib/types/annotation.ts +++ b/packages/domscribe-core/src/lib/types/annotation.ts @@ -54,11 +54,43 @@ export const EnvironmentSchema = z.object({ packageManager: z.string().optional().describe('Package manager used'), }); +/** + * Component-style snapshot captured at runtime. + * + * `computed` is a bounded subset of CSS properties resolved via + * `getComputedStyle()` against the picked element (see the + * `STYLE_CAPTURE_ALLOWLIST` in `@domscribe/runtime`). `customProperties` are + * the resolved `--*` CSS variables visible from the element up to `:root`, + * used as the runtime token boundary for design-system attribution. + * + * Both fields are optional so older clients can ignore them. Companion + * build-time `styleSource` attribution lives on `ManifestEntry` and is + * intentionally separate — runtime context here is ground truth for what is + * rendered; the manifest field is ground truth for where it came from. + */ +export const ComponentStylesSchema = z.object({ + computed: z + .record(z.string(), z.string()) + .optional() + .describe( + 'Computed CSS properties from the allowlist (≤32 entries: layout, spacing, typography, visual, positioning)', + ), + customProperties: z + .record(z.string(), z.string()) + .optional() + .describe( + 'Resolved `--*` CSS custom properties visible from the element through its ancestors up to `:root`', + ), +}); + export const RuntimeContextSchema = z.object({ componentProps: z.unknown().optional().describe('Component props snapshot'), componentState: z.unknown().optional().describe('Component state snapshot'), eventFlow: z.unknown().optional().describe('Event flow breadcrumbs'), performance: z.unknown().optional().describe('Performance metrics'), + componentStyles: ComponentStylesSchema.optional().describe( + 'Captured computed styles + resolved CSS custom properties for the picked element. Populated when `domscribe.config.captureStyles` is enabled.', + ), }); export const AnnotationIdSchema = z @@ -196,3 +228,4 @@ export type BoundingRect = z.infer; export type Viewport = z.infer; export type Environment = z.infer; export type RuntimeContext = z.infer; +export type ComponentStyles = z.infer; diff --git a/packages/domscribe-relay/src/mcp/tools/query-by-source.tool.spec.ts b/packages/domscribe-relay/src/mcp/tools/query-by-source.tool.spec.ts index 2411944..94859bc 100644 --- a/packages/domscribe-relay/src/mcp/tools/query-by-source.tool.spec.ts +++ b/packages/domscribe-relay/src/mcp/tools/query-by-source.tool.spec.ts @@ -71,13 +71,63 @@ describe('QueryBySourceTool', () => { }, browserConnected: true, error: undefined, - hint: undefined, + // The runtime is rendered but `componentStyles` is absent — the + // hint nudges the agent to enable `captureStyles` so the next + // styling query lands in one round trip (RFC 0001). + hint: expect.stringContaining('captureStyles'), }); expect(JSON.parse(getResultText(result))).toEqual( result.structuredContent, ); }); + it('emits no captureStyles hint when componentStyles is already present', async () => { + // Arrange — simulate the post-RFC 0001 happy path: runtime is configured + // with captureStyles, the query returns componentStyles, agent has full + // ground truth and needs no follow-on nudge. + const mockClient = createMockRelayClient({ + queryBySource: vi.fn().mockResolvedValue({ + found: true, + entryId: 'aB3dEf7h', + sourceLocation: { + file: 'src/components/Button.tsx', + start: { line: 10, column: 4 }, + }, + runtime: { + rendered: true, + componentProps: { label: 'Submit' }, + componentStyles: { + computed: { padding: '16px', color: 'rgb(15, 23, 42)' }, + customProperties: { '--color-fg': 'rgb(15, 23, 42)' }, + }, + }, + browserConnected: true, + }), + }); + const tool = new QueryBySourceTool(mockClient); + + // Act + const result: CallToolResult = await tool.toolCallback({ + file: 'src/components/Button.tsx', + line: 10, + }); + + // Assert + expect( + (result.structuredContent as { hint?: string }).hint, + ).toBeUndefined(); + expect( + ( + result.structuredContent as { + runtime?: { componentStyles?: unknown }; + } + ).runtime?.componentStyles, + ).toEqual({ + computed: { padding: '16px', color: 'rgb(15, 23, 42)' }, + customProperties: { '--color-fg': 'rgb(15, 23, 42)' }, + }); + }); + it('should handle not-found responses', async () => { // Arrange const mockClient = createMockRelayClient({ diff --git a/packages/domscribe-relay/src/mcp/tools/query-by-source.tool.ts b/packages/domscribe-relay/src/mcp/tools/query-by-source.tool.ts index f96c731..4f554d7 100644 --- a/packages/domscribe-relay/src/mcp/tools/query-by-source.tool.ts +++ b/packages/domscribe-relay/src/mcp/tools/query-by-source.tool.ts @@ -55,6 +55,25 @@ const QueryBySourceToolOutputSchema = McpToolOutputSchema.extend({ rendered: z.boolean(), componentProps: z.unknown().optional(), componentState: z.unknown().optional(), + componentStyles: z + .object({ + computed: z + .record(z.string(), z.string()) + .optional() + .describe( + 'Computed CSS properties from the allowlist (display, padding, color, font-*, etc.). Use this to confirm what the element actually renders before editing.', + ), + customProperties: z + .record(z.string(), z.string()) + .optional() + .describe( + "Resolved `--*` CSS custom properties visible at the element. When a computed value matches a token's resolved value, prefer the token name in the fix.", + ), + }) + .optional() + .describe( + 'Runtime style snapshot (RFC 0001). Present only when the runtime is configured with `captureStyles: true`. Use this on styling annotations to ground the agent edit in what the user sees, not just what the source says.', + ), domSnapshot: z .object({ tagName: z.string().optional(), @@ -79,11 +98,13 @@ export class QueryBySourceTool implements McpToolDefinition< > { name = MCP_TOOLS.QUERY_BY_SOURCE; description = - 'Get live DOM snapshot, component props, and state for a source location (file + line). ' + + 'Get live DOM snapshot, component props, state, and computed styles for a source location (file + line). ' + 'Call this when fixing visual/styling bugs, debugging conditional rendering, tracing prop values, or verifying UI changes after editing. ' + + 'For styling annotations specifically — padding, color, spacing, typography, layout — always call this first: `runtime.componentStyles.computed` is the ground truth for what the user sees, and `runtime.componentStyles.customProperties` exposes the resolved design-system tokens (`--*` vars) at the element. Prefer token names over raw values in your edit when the resolved value matches. ' + "Skip for pure logic changes, new files, refactoring, or type fixes — runtime context won't help there. " + 'If browserConnected is false, ask the user to open the page in their browser and retry. ' + - 'Returns manifest data even when the browser is not connected.'; + 'Returns manifest data even when the browser is not connected. ' + + 'NOTE: `componentStyles` requires the runtime to be configured with `captureStyles: true` (off by default in v0.x for payload-size reasons).'; inputSchema = QueryBySourceToolInputSchema; outputSchema = QueryBySourceToolOutputSchema; @@ -92,7 +113,10 @@ export class QueryBySourceTool implements McpToolDefinition< private buildHint(result: { found: boolean; browserConnected?: boolean; - runtime?: { rendered?: boolean }; + runtime?: { + rendered?: boolean; + componentStyles?: unknown; + }; }): string | undefined { if (!result.found) { return ( @@ -113,6 +137,18 @@ export class QueryBySourceTool implements McpToolDefinition< 'Ask the user to navigate to a page that renders this component, then retry.' ); } + if ( + result.runtime && + result.runtime.rendered && + !result.runtime.componentStyles + ) { + return ( + 'componentStyles is not in this response. If the user is asking about a styling change ' + + '(padding, color, spacing, typography, layout), ask the user to set `captureStyles: true` ' + + "in their Domscribe runtime config and retry — you'll get the computed-style + token snapshot " + + 'for the element, which lets the edit land in one round trip.' + ); + } return undefined; } diff --git a/packages/domscribe-relay/src/schema.ts b/packages/domscribe-relay/src/schema.ts index 07e7c2c..33fde0b 100644 --- a/packages/domscribe-relay/src/schema.ts +++ b/packages/domscribe-relay/src/schema.ts @@ -446,6 +446,17 @@ export const QueryBySourceResponseSchema = z.object({ rendered: z.boolean(), componentProps: z.unknown().optional(), componentState: z.unknown().optional(), + componentStyles: z + .object({ + computed: z.record(z.string(), z.string()).optional(), + customProperties: z.record(z.string(), z.string()).optional(), + }) + .optional() + .describe( + 'Runtime computed styles + resolved CSS custom properties (RFC 0001). ' + + 'Populated when the runtime is configured with `captureStyles: true`. ' + + 'Use this to verify what the user actually sees before editing styling source.', + ), domSnapshot: z .object({ tagName: z.string().optional(), diff --git a/packages/domscribe-relay/src/server/routes/v1/query-by-source.route.ts b/packages/domscribe-relay/src/server/routes/v1/query-by-source.route.ts index 53d847d..ef0e4d1 100644 --- a/packages/domscribe-relay/src/server/routes/v1/query-by-source.route.ts +++ b/packages/domscribe-relay/src/server/routes/v1/query-by-source.route.ts @@ -109,6 +109,7 @@ export class QueryBySourceRoute implements RelayRoute { rendered: wsResult.rendered ?? false, componentProps: wsResult.context?.componentProps, componentState: wsResult.context?.componentState, + componentStyles: wsResult.context?.componentStyles, domSnapshot: wsResult.elementInfo, }; } diff --git a/packages/domscribe-runtime/src/capture/__snapshots__/style-capturer.spec.ts.snap b/packages/domscribe-runtime/src/capture/__snapshots__/style-capturer.spec.ts.snap new file mode 100644 index 0000000..68541a0 --- /dev/null +++ b/packages/domscribe-runtime/src/capture/__snapshots__/style-capturer.spec.ts.snap @@ -0,0 +1,38 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`StyleCapturer > allowlist invariants > orders entries deterministically (snapshot for CI stability) 1`] = ` +[ + "display", + "position", + "box-sizing", + "margin", + "padding", + "width", + "height", + "top", + "right", + "bottom", + "left", + "z-index", + "color", + "background-color", + "background-image", + "font-family", + "font-size", + "font-weight", + "line-height", + "letter-spacing", + "text-align", + "border", + "border-radius", + "box-shadow", + "opacity", + "transform", + "transition", + "cursor", + "flex", + "gap", + "grid-template-columns", + "overflow", +] +`; diff --git a/packages/domscribe-runtime/src/capture/style-capturer.spec.ts b/packages/domscribe-runtime/src/capture/style-capturer.spec.ts new file mode 100644 index 0000000..55e23b8 --- /dev/null +++ b/packages/domscribe-runtime/src/capture/style-capturer.spec.ts @@ -0,0 +1,221 @@ +/** + * Tests for StyleCapturer + * + * Validates the bounded computed-style allowlist, CSS custom property + * resolution from element through ancestors, the 4 KB serialization budget, + * and detached-element error handling. + * + * @vitest-environment jsdom + */ + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { StyleCapturer, STYLE_CAPTURE_ALLOWLIST } from './style-capturer.js'; + +function makeElement(html: string): HTMLElement { + const host = document.createElement('div'); + host.innerHTML = html.trim(); + const el = host.firstElementChild as HTMLElement; + document.body.appendChild(host); + return el; +} + +function makeStyle(css: string): HTMLStyleElement { + const style = document.createElement('style'); + style.textContent = css; + document.head.appendChild(style); + return style; +} + +describe('StyleCapturer', () => { + let toRemove: Element[] = []; + + beforeEach(() => { + toRemove = []; + }); + + afterEach(() => { + for (const el of toRemove) { + el.remove(); + } + document.body.innerHTML = ''; + document.head.querySelectorAll('style').forEach((s) => s.remove()); + }); + + describe('allowlist invariants', () => { + it('exports a frozen allowlist of at most 32 entries', () => { + expect(Object.isFrozen(STYLE_CAPTURE_ALLOWLIST)).toBe(true); + expect(STYLE_CAPTURE_ALLOWLIST.length).toBeLessThanOrEqual(32); + }); + + it('orders entries deterministically (snapshot for CI stability)', () => { + // Updating the allowlist requires updating this snapshot — the RFC + // commits us to a fixed shape so agent prompts can rely on it. + expect([...STYLE_CAPTURE_ALLOWLIST]).toMatchSnapshot(); + }); + }); + + describe('basic capture', () => { + it('captures computed allowlist properties for the target element', () => { + makeStyle(` + .box { + display: block; + color: rgb(15, 23, 42); + padding: 16px; + } + `); + const el = makeElement('
box
'); + toRemove.push(el); + + const capturer = new StyleCapturer(); + const result = capturer.capture(el); + + expect(result.success).toBe(true); + expect(result.data?.computed).toBeDefined(); + expect(result.data?.computed?.['display']).toBe('block'); + expect(result.data?.computed?.['color']).toBe('rgb(15, 23, 42)'); + expect(result.data?.computed?.['padding']).toBe('16px'); + }); + + it('omits properties whose computed value is empty string', () => { + const el = makeElement('
'); + toRemove.push(el); + + const capturer = new StyleCapturer({ allowlist: ['nonexistent-prop'] }); + const result = capturer.capture(el); + + expect(result.success).toBe(true); + expect(result.data?.computed).toBeUndefined(); + }); + + it('respects a custom allowlist override', () => { + makeStyle('.tiny { color: red; }'); + const el = makeElement('x'); + toRemove.push(el); + + const capturer = new StyleCapturer({ allowlist: ['color'] }); + const result = capturer.capture(el); + + expect(result.success).toBe(true); + // Only the override property is captured. + expect(Object.keys(result.data?.computed ?? {})).toEqual(['color']); + }); + }); + + describe('CSS custom properties', () => { + it('resolves --* vars defined on the element', () => { + makeStyle('.themed { --token-fg: #0f172a; }'); + const el = makeElement('
x
'); + toRemove.push(el); + + const capturer = new StyleCapturer(); + const result = capturer.capture(el); + + expect(result.success).toBe(true); + expect(result.data?.customProperties?.['--token-fg']).toBe('#0f172a'); + }); + + it('walks ancestors so :root tokens are visible at a leaf', () => { + makeStyle(` + :root { --color-fg: rgb(15, 23, 42); } + .leaf { color: var(--color-fg); } + `); + const root = makeElement('
x
'); + const leaf = root.querySelector('.leaf') as HTMLElement; + toRemove.push(root); + + const capturer = new StyleCapturer(); + const result = capturer.capture(leaf); + + expect(result.success).toBe(true); + expect(result.data?.customProperties?.['--color-fg']).toBeDefined(); + }); + + it('caps custom properties at maxCustomProperties', () => { + // Define many tokens; cap at 3. + const css = [':root {']; + for (let i = 0; i < 50; i++) { + css.push(` --t${i}: ${i}px;`); + } + css.push('}'); + makeStyle(css.join('\n')); + + const el = makeElement('
x
'); + toRemove.push(el); + + const capturer = new StyleCapturer({ maxCustomProperties: 3 }); + const result = capturer.capture(el); + + expect(result.success).toBe(true); + expect( + Object.keys(result.data?.customProperties ?? {}).length, + ).toBeLessThanOrEqual(3); + }); + }); + + describe('serialization budget', () => { + it('drops custom-property tail entries until payload fits maxBytes', () => { + // Each --pad-N value is a long string; build a budget that forces drops. + const css = [':root {']; + const longValue = 'x'.repeat(200); + for (let i = 0; i < 10; i++) { + css.push(` --pad-${i}: ${longValue};`); + } + css.push('}'); + makeStyle(css.join('\n')); + + const el = makeElement('
x
'); + toRemove.push(el); + + const capturer = new StyleCapturer({ maxBytes: 512 }); + const result = capturer.capture(el); + + expect(result.success).toBe(true); + const serializedLength = JSON.stringify(result.data).length; + expect(serializedLength).toBeLessThanOrEqual(512); + }); + + it('preserves computed allowlist values even when over budget', () => { + // Tighter budget than even the computed section alone — the trimming + // strategy never drops allowlist values, so the result will exceed the + // budget. This is the explicit RFC tradeoff: the allowlist is the + // ground truth. + makeStyle(` + .box { display: block; color: red; padding: 16px; } + `); + const el = makeElement('
x
'); + toRemove.push(el); + + const capturer = new StyleCapturer({ maxBytes: 32 }); + const result = capturer.capture(el); + + expect(result.success).toBe(true); + expect(result.data?.computed).toBeDefined(); + }); + + it('disables budget enforcement when maxBytes is 0', () => { + makeStyle(':root { --giant: ' + 'a'.repeat(5000) + '; }'); + const el = makeElement('
x
'); + toRemove.push(el); + + const capturer = new StyleCapturer({ maxBytes: 0 }); + const result = capturer.capture(el); + + expect(result.success).toBe(true); + expect(JSON.stringify(result.data).length).toBeGreaterThan(4096); + }); + }); + + describe('error handling', () => { + it('returns a CaptureResult error for a detached element with no owner window', () => { + const detached = document.createElement('div'); + // Simulate a fully detached node: clobber ownerDocument. + Object.defineProperty(detached, 'ownerDocument', { value: null }); + + const capturer = new StyleCapturer(); + const result = capturer.capture(detached); + + expect(result.success).toBe(false); + expect(result.error?.message).toMatch(/no owner window/i); + }); + }); +}); diff --git a/packages/domscribe-runtime/src/capture/style-capturer.ts b/packages/domscribe-runtime/src/capture/style-capturer.ts new file mode 100644 index 0000000..d6d57d2 --- /dev/null +++ b/packages/domscribe-runtime/src/capture/style-capturer.ts @@ -0,0 +1,291 @@ +/** + * StyleCapturer - Captures runtime computed styles and CSS custom properties + * + * Reads a fixed, bounded allowlist of computed CSS properties from the picked + * element via `window.getComputedStyle()`, and resolves the `--*` custom + * properties visible from the element through its ancestors up to `:root`. + * + * The allowlist (≤32 entries) is the runtime ground truth for "what the user + * sees" — layout, spacing, typography, visual, positioning. The custom + * properties are the runtime token boundary: the design system surfaces tokens + * as `--*` vars, and capturing them is what lets an agent attribute a hex value + * to a token (e.g. `color: rgb(15, 23, 42)` → `--color-fg`) without re-reading + * the design-system config. + * + * Companion build-time `styleSource` attribution lives on `ManifestEntry` and + * is a separate package's responsibility (`@domscribe/transform`). + * + * @module @domscribe/runtime/capture/style-capturer + */ + +import type { ComponentStyles } from '@domscribe/core'; +import type { CaptureResult } from './types.js'; +import { ContextCaptureError } from '../errors/index.js'; + +/** + * Bounded computed-property allowlist for runtime style capture (≤32 entries). + * + * Grouped by intent: + * - layout: display, position, box-sizing + * - spacing: margin/padding (4-side shorthands kept; the agent reads + * the resolved per-side values from these — capturing all 16 + * would double the budget for marginal value) + * - typography: font-family, font-size, font-weight, line-height, + * letter-spacing, text-align, color + * - visual: background-color, background-image, border, border-radius, + * box-shadow, opacity + * - dimensions: width, height + * - positioning: top, right, bottom, left, z-index, transform + * + * Order is stable; CI snapshots of these strings must be deterministic. + */ +export const STYLE_CAPTURE_ALLOWLIST: readonly string[] = Object.freeze([ + 'display', + 'position', + 'box-sizing', + 'margin', + 'padding', + 'width', + 'height', + 'top', + 'right', + 'bottom', + 'left', + 'z-index', + 'color', + 'background-color', + 'background-image', + 'font-family', + 'font-size', + 'font-weight', + 'line-height', + 'letter-spacing', + 'text-align', + 'border', + 'border-radius', + 'box-shadow', + 'opacity', + 'transform', + 'transition', + 'cursor', + 'flex', + 'gap', + 'grid-template-columns', + 'overflow', +]); + +/** + * Options for the StyleCapturer. + */ +export interface StyleCaptureOptions { + /** + * Computed-property allowlist override. Defaults to {@link STYLE_CAPTURE_ALLOWLIST}. + * + * If you pass a custom list, keep it ≤32 entries — the RFC's serialization + * budget assumes that ceiling. + */ + allowlist?: readonly string[]; + + /** + * Maximum number of `--*` CSS custom properties to surface. + * @default 64 + */ + maxCustomProperties?: number; + + /** + * Approximate maximum serialized bytes for the entire `ComponentStyles` + * payload. The RFC's per-element budget is ≤4 KB. Captured strings beyond + * this limit are dropped (rather than throwing) so a single oversize page + * cannot poison a whole annotation. + * + * Set to 0 to disable the budget (not recommended). + * + * @default 4096 + */ + maxBytes?: number; + + /** + * Enable debug logging. + * @default false + */ + debug?: boolean; +} + +const DEFAULT_MAX_CUSTOM_PROPERTIES = 64; +const DEFAULT_MAX_BYTES = 4096; + +/** + * StyleCapturer class + * + * Captures a bounded snapshot of computed CSS properties and resolved CSS + * custom properties for a single DOM element. Designed to be cheap enough to + * run on every annotation: one `getComputedStyle()` call on the target, plus + * one per ancestor while collecting `--*` vars (early-exits once the cap is + * reached). + */ +export class StyleCapturer { + private readonly allowlist: readonly string[]; + private readonly maxCustomProperties: number; + private readonly maxBytes: number; + private readonly debug: boolean; + + constructor(options: StyleCaptureOptions = {}) { + this.allowlist = options.allowlist ?? STYLE_CAPTURE_ALLOWLIST; + this.maxCustomProperties = + options.maxCustomProperties ?? DEFAULT_MAX_CUSTOM_PROPERTIES; + this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES; + this.debug = options.debug ?? false; + } + + /** + * Capture styles for an element. + * + * Returns `CaptureResult`. On failure (e.g. element + * detached or no Window) the result is `{ success: false, error }` — the + * caller decides whether to swallow or surface. + */ + capture(element: HTMLElement): CaptureResult { + try { + const ownerWindow = element.ownerDocument?.defaultView; + if (!ownerWindow) { + return { + success: false, + error: new ContextCaptureError( + 'StyleCapturer: element has no owner window — element may be detached or running outside a DOM', + ), + }; + } + + const computed = ownerWindow.getComputedStyle(element); + const computedValues: Record = {}; + for (const prop of this.allowlist) { + const value = computed.getPropertyValue(prop); + if (value !== '') { + computedValues[prop] = value.trim(); + } + } + + const customProperties = this.collectCustomProperties( + element, + ownerWindow, + ); + + const result: ComponentStyles = {}; + if (Object.keys(computedValues).length > 0) { + result.computed = computedValues; + } + if (Object.keys(customProperties).length > 0) { + result.customProperties = customProperties; + } + + const trimmed = this.enforceBudget(result); + + if (this.debug) { + console.log('[domscribe-runtime][style-capturer] Captured styles', { + computedCount: Object.keys(trimmed.computed ?? {}).length, + customPropCount: Object.keys(trimmed.customProperties ?? {}).length, + approxBytes: this.approximateSize(trimmed), + }); + } + + return { success: true, data: trimmed }; + } catch (error) { + const err = new ContextCaptureError( + 'Failed to capture component styles', + error instanceof Error ? error : undefined, + ); + if (this.debug) { + console.error('[domscribe-runtime][style-capturer]', err); + } + return { success: false, error: err }; + } + } + + /** + * Walk from the element to `:root` collecting `--*` custom properties. + * + * We collect from the leaf up so leaf-scoped overrides win — the resolved + * value visible at the picked element is what the user actually sees. + * + * Early-exits once {@link maxCustomProperties} is reached. Token systems + * that define hundreds of `--*` vars at `:root` (e.g. shadcn) would + * otherwise blow through the 4 KB budget on tokens that are not even + * relevant to the picked element. + */ + private collectCustomProperties( + element: HTMLElement, + win: Window, + ): Record { + const customProperties: Record = {}; + let count = 0; + + let current: Element | null = element; + while (current && count < this.maxCustomProperties) { + const computed = win.getComputedStyle(current); + // `computed.length` enumerates *all* property names visible on the + // element, including custom properties whose computed value is set. + for ( + let i = 0; + i < computed.length && count < this.maxCustomProperties; + i++ + ) { + const name = computed[i]; + if (!name.startsWith('--')) continue; + if (Object.prototype.hasOwnProperty.call(customProperties, name)) + continue; + const value = computed.getPropertyValue(name).trim(); + if (value === '') continue; + customProperties[name] = value; + count++; + } + current = current.parentElement; + } + + return customProperties; + } + + /** + * Enforce {@link maxBytes} by dropping entries from the tail until the + * payload fits. Computed-allowlist entries are preserved; only custom + * properties are trimmed, because the allowlist is small (≤32) and + * deterministic while custom-properties can be unbounded. + */ + private enforceBudget(styles: ComponentStyles): ComponentStyles { + if (this.maxBytes <= 0) return styles; + + if (this.approximateSize(styles) <= this.maxBytes) { + return styles; + } + + const customProperties = styles.customProperties + ? { ...styles.customProperties } + : undefined; + + if (customProperties) { + const names = Object.keys(customProperties); + while ( + names.length > 0 && + this.approximateSize({ ...styles, customProperties }) > this.maxBytes + ) { + const dropped = names.pop(); + if (dropped !== undefined) { + delete customProperties[dropped]; + } + } + } + + return { + ...(styles.computed ? { computed: styles.computed } : {}), + ...(customProperties && Object.keys(customProperties).length > 0 + ? { customProperties } + : {}), + }; + } + + private approximateSize(styles: ComponentStyles): number { + // JSON length is a reasonable approximation of the wire size on the + // bridge. We do not double-stringify for the budget check; this is the + // same approximation `serializeValue` uses elsewhere. + return JSON.stringify(styles).length; + } +} diff --git a/packages/domscribe-runtime/src/capture/types.ts b/packages/domscribe-runtime/src/capture/types.ts index 123e54d..05545a5 100644 --- a/packages/domscribe-runtime/src/capture/types.ts +++ b/packages/domscribe-runtime/src/capture/types.ts @@ -37,6 +37,14 @@ export interface CaptureOptions { */ includeState?: boolean; + /** + * Include the computed-style + custom-property snapshot + * (`ComponentStyles`) in capture. The manager-level `captureStyles` flag is + * the canonical opt-in; this per-call override exists so a styling-aware + * MCP query can request style context without changing the manager config. + */ + includeStyles?: boolean; + /** * Maximum depth for object serialization */ diff --git a/packages/domscribe-runtime/src/core/context-capturer.spec.ts b/packages/domscribe-runtime/src/core/context-capturer.spec.ts index e5e1f72..73ce52d 100644 --- a/packages/domscribe-runtime/src/core/context-capturer.spec.ts +++ b/packages/domscribe-runtime/src/core/context-capturer.spec.ts @@ -67,6 +67,14 @@ vi.mock('../capture/state-capturer.js', () => ({ }, })); +const mockStyleCapture = vi.fn(); + +vi.mock('../capture/style-capturer.js', () => ({ + StyleCapturer: class { + capture = (element: unknown) => mockStyleCapture(element); + }, +})); + // Test Helpers function createMockAdapter( overrides?: Partial, @@ -214,6 +222,84 @@ describe('ContextCapturer', () => { }); }); + describe('captureForElement - style capture (RFC 0001)', () => { + beforeEach(() => { + adapter.getComponentInstance.mockReturnValue({ type: 'TestComponent' }); + mockPropsCapture.mockReturnValue({ success: true, data: { ok: 1 } }); + mockStateCapture.mockReturnValue({ success: true, data: { ok: 2 } }); + }); + + it('does not invoke StyleCapturer when captureStyles is unset (default off)', async () => { + // Arrange + const capturer = new ContextCapturer({ adapter }); + const element = document.createElement('div'); + + // Act + const result = await capturer.captureForElement(element); + + // Assert + expect(mockStyleCapture).not.toHaveBeenCalled(); + expect(result?.componentStyles).toBeUndefined(); + }); + + it('attaches componentStyles when captureStyles is true and StyleCapturer succeeds', async () => { + // Arrange + const styles = { + computed: { padding: '16px' }, + customProperties: { '--color-fg': 'rgb(15, 23, 42)' }, + }; + mockStyleCapture.mockReturnValue({ success: true, data: styles }); + const capturer = new ContextCapturer({ adapter, captureStyles: true }); + const element = document.createElement('div'); + + // Act + const result = await capturer.captureForElement(element); + + // Assert + expect(mockStyleCapture).toHaveBeenCalledWith(element); + expect(result?.componentStyles).toEqual(styles); + }); + + it('swallows StyleCapturer failure without breaking the props/state pipeline', async () => { + // Arrange — style capture fails (e.g., a detached element); the rest of + // the context still has to come through cleanly. This is the explicit + // RFC requirement: style capture is best-effort, never load-bearing. + mockStyleCapture.mockReturnValue({ + success: false, + error: new Error('No owner window'), + }); + const capturer = new ContextCapturer({ adapter, captureStyles: true }); + const element = document.createElement('div'); + + // Act + const result = await capturer.captureForElement(element); + + // Assert + expect(result?.componentStyles).toBeUndefined(); + expect(result?.componentProps).toBeDefined(); + expect(result?.componentState).toBeDefined(); + }); + + it('honors per-call includeStyles=false even when manager-level captureStyles is on', async () => { + // Arrange + mockStyleCapture.mockReturnValue({ + success: true, + data: { computed: { padding: '16px' } }, + }); + const capturer = new ContextCapturer({ adapter, captureStyles: true }); + const element = document.createElement('div'); + + // Act + const result = await capturer.captureForElement(element, { + includeStyles: false, + }); + + // Assert + expect(mockStyleCapture).not.toHaveBeenCalled(); + expect(result?.componentStyles).toBeUndefined(); + }); + }); + describe('captureForElement - error handling', () => { it('should throw ContextCaptureError when adapter throws', async () => { // Arrange diff --git a/packages/domscribe-runtime/src/core/context-capturer.ts b/packages/domscribe-runtime/src/core/context-capturer.ts index 12c4dfa..8ec9d4d 100644 --- a/packages/domscribe-runtime/src/core/context-capturer.ts +++ b/packages/domscribe-runtime/src/core/context-capturer.ts @@ -14,6 +14,10 @@ import type { } from '../capture/types.js'; import { PropsCapturer } from '../capture/props-capturer.js'; import { StateCapturer } from '../capture/state-capturer.js'; +import { + StyleCapturer, + type StyleCaptureOptions, +} from '../capture/style-capturer.js'; import { ContextCaptureError } from '../errors/index.js'; /** @@ -47,6 +51,23 @@ export interface ContextCapturerOptions { * Enable debug logging */ debug?: boolean; + + /** + * Enable computed-style + custom-property capture + * (RFC 0001 `domscribe.config.captureStyles`). + * + * Off by default; flips on the {@link StyleCapturer} pass and makes + * `RuntimeContext.componentStyles` available to `query.bySource` and the + * annotation payload. Respects the existing ≤4 KB per-element budget via + * `styleOptions.maxBytes`. + */ + captureStyles?: boolean; + + /** + * Tuning knobs forwarded to {@link StyleCapturer}. Ignored when + * {@link captureStyles} is false. + */ + styleOptions?: StyleCaptureOptions; } /** @@ -58,8 +79,11 @@ export interface ContextCapturerOptions { export class ContextCapturer { private propsCapturer: PropsCapturer; private stateCapturer: StateCapturer; - private options: Required> & - Pick; + private styleCapturer: StyleCapturer | null; + private options: Required< + Omit + > & + Pick; constructor(options: ContextCapturerOptions) { this.options = { @@ -68,6 +92,8 @@ export class ContextCapturer { serialization: options.serialization, redactPII: options.redactPII ?? true, debug: options.debug ?? false, + captureStyles: options.captureStyles ?? false, + styleOptions: options.styleOptions, }; const s = this.options.serialization; @@ -90,6 +116,12 @@ export class ContextCapturer { redactPII: this.options.redactPII, debug: this.options.debug, }); + this.styleCapturer = this.options.captureStyles + ? new StyleCapturer({ + ...(this.options.styleOptions ?? {}), + debug: this.options.debug, + }) + : null; } /** @@ -117,7 +149,12 @@ export class ContextCapturer { return null; } - return this.captureForComponent(componentInstance, options); + const context = await this.captureForComponent( + componentInstance, + options, + ); + this.attachStyles(context, element, options); + return context; } catch (error) { throw new ContextCaptureError( 'Failed to capture context for element', @@ -126,6 +163,33 @@ export class ContextCapturer { } } + /** + * Run the style capture pass and attach the result onto `context.componentStyles`. + * + * Skipped when the manager-level `captureStyles` flag is off OR the + * per-call `includeStyles` override explicitly opts out. Style-capture + * failures are swallowed (logged at debug) — props/state capture must + * still succeed even on a page where computed-style resolution is broken. + */ + private attachStyles( + context: RuntimeContext, + element: HTMLElement, + options: CaptureOptions, + ): void { + if (!this.styleCapturer) return; + if (options.includeStyles === false) return; + + const result = this.styleCapturer.capture(element); + if (result.success && result.data) { + context.componentStyles = result.data; + } else if (this.options.debug && result.error) { + console.warn( + '[domscribe-runtime][context-capturer] Style capture failed:', + result.error, + ); + } + } + /** * Capture runtime context for a component instance * @@ -167,6 +231,7 @@ export class ContextCapturer { console.log('[domscribe-runtime][context-capturer] Captured context:', { hasProps: !!context.componentProps, hasState: !!context.componentState, + hasStyles: !!context.componentStyles, }); } diff --git a/packages/domscribe-runtime/src/core/runtime-manager.ts b/packages/domscribe-runtime/src/core/runtime-manager.ts index 6837c34..d3e5cb5 100644 --- a/packages/domscribe-runtime/src/core/runtime-manager.ts +++ b/packages/domscribe-runtime/src/core/runtime-manager.ts @@ -30,8 +30,10 @@ export class RuntimeManager { private elementTracker: ElementTracker | null = null; private contextCapturer: ContextCapturer | null = null; - private options: Required> & - Pick; + private options: Required< + Omit + > & + Pick; private isInitialized = false; private constructor() { @@ -43,6 +45,8 @@ export class RuntimeManager { redactPII: true, blockSelectors: [], serialization: undefined, + captureStyles: false, + styleOptions: undefined, }; } @@ -99,6 +103,8 @@ export class RuntimeManager { redactPII: options.redactPII ?? true, blockSelectors: options.blockSelectors ?? [], serialization: options.serialization, + captureStyles: options.captureStyles ?? false, + styleOptions: options.styleOptions, }; try { @@ -118,6 +124,8 @@ export class RuntimeManager { serialization: this.options.serialization, redactPII: this.options.redactPII, debug: this.options.debug, + captureStyles: this.options.captureStyles, + styleOptions: this.options.styleOptions, }); this.isInitialized = true; diff --git a/packages/domscribe-runtime/src/core/types.ts b/packages/domscribe-runtime/src/core/types.ts index 4a4a54f..3ec8c65 100644 --- a/packages/domscribe-runtime/src/core/types.ts +++ b/packages/domscribe-runtime/src/core/types.ts @@ -6,6 +6,7 @@ import type { ManifestEntryId, RuntimeContext } from '@domscribe/core'; import type { FrameworkAdapter } from '../adapters/adapter.interface.js'; import type { SerializationConstraints } from '../capture/types.js'; +import type { StyleCaptureOptions } from '../capture/style-capturer.js'; /** * User-facing runtime configuration options (minus adapter, which is framework-specific). @@ -22,6 +23,16 @@ export interface DomscribeRuntimeOptions { blockSelectors?: string[]; /** Serialization constraints for captured props and state. */ serialization?: SerializationConstraints; + /** + * Capture computed-style + custom-property snapshot for every annotation + * (RFC 0001 `domscribe.config.captureStyles`). Off by default so existing + * v0.x integrations continue to ship a leaner runtime payload. Flip on for + * styling-aware agent workflows. + * @default false + */ + captureStyles?: boolean; + /** Tuning knobs for the style capturer. Ignored when `captureStyles` is false. */ + styleOptions?: StyleCaptureOptions; } /** diff --git a/packages/domscribe-runtime/src/index.ts b/packages/domscribe-runtime/src/index.ts index f0927e9..c3c141d 100644 --- a/packages/domscribe-runtime/src/index.ts +++ b/packages/domscribe-runtime/src/index.ts @@ -20,3 +20,10 @@ export type { IRuntimeTransport } from './bridge/transport.interface.js'; // Configuration types export type { DomscribeRuntimeOptions } from './core/types.js'; export type { SerializationConstraints } from './capture/types.js'; + +// Style capture (RFC 0001) +export { + StyleCapturer, + STYLE_CAPTURE_ALLOWLIST, +} from './capture/style-capturer.js'; +export type { StyleCaptureOptions } from './capture/style-capturer.js'; From 65731bf3214ab5d8bf881cd291e63289268c2dc4 Mon Sep 17 00:00:00 2001 From: "Domscribe Staff SWE (bot)" Date: Sun, 7 Jun 2026 06:59:00 -0700 Subject: [PATCH 2/3] =?UTF-8?q?test(test-fixtures):=20RFC=200001=20styling?= =?UTF-8?q?=20falsifier=20=E2=80=94=20Tailwind=20+=20styled=20apps=20+=20h?= =?UTF-8?q?arness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the sprint 2734 hard-gate measurement instrument. Two Vite React fixture apps (Tailwind v3, styled-components v6), ten styling annotations, and a Playwright + pixelmatch harness that grades agent output against canonical baseline screenshots. * styling/tailwind-app, styling/styled-app: pnpm workspaces, deterministic Vite output, animations + caret disabled for pixel-diff stability. * styling/annotations.json: 10 annotations (5 per fixture) covering padding, color tokens, border-radius, typography, and flex-gap fixes. * styling/baselines/: canonical-after PNGs committed (recorded via --mode=record). * styling/scripts/falsifier.ts: three modes (self-test default for CI, record, measure --agent-output=). Emits machine-readable JSON with oneShotRate metric to stdout. Exits non-zero on fails. * package.json: test:falsifier + test:falsifier:record scripts. * project.json: nx falsifier + falsifier:record targets. Self-test currently reports 10/10 oneShotRate=1.0, verifying the mechanism. Real-agent integration is plumbing for a follow-on sprint (--mode=measure already wired). Co-Authored-By: Claude Opus 4.7 --- packages/domscribe-test-fixtures/package.json | 12 +- packages/domscribe-test-fixtures/project.json | 14 + .../domscribe-test-fixtures/styling/README.md | 113 +++ .../styling/annotations.json | 142 ++++ .../styling/baselines/styled/A101.png | Bin 0 -> 3629 bytes .../styling/baselines/styled/A102.png | Bin 0 -> 8014 bytes .../styling/baselines/styled/A103.png | Bin 0 -> 4472 bytes .../styling/baselines/styled/A104.png | Bin 0 -> 10735 bytes .../styling/baselines/styled/A105.png | Bin 0 -> 11188 bytes .../styling/baselines/tailwind/A001.png | Bin 0 -> 8832 bytes .../styling/baselines/tailwind/A002.png | Bin 0 -> 7040 bytes .../styling/baselines/tailwind/A003.png | Bin 0 -> 4877 bytes .../styling/baselines/tailwind/A004.png | Bin 0 -> 6616 bytes .../styling/baselines/tailwind/A005.png | Bin 0 -> 5782 bytes .../styling/scripts/falsifier.ts | 533 ++++++++++++ .../styling/styled-app/index.html | 35 + .../styling/styled-app/package.json | 22 + .../styling/styled-app/src/App.tsx | 82 ++ .../src/components/A101-badge-padding.tsx | 37 + .../src/components/A102-hero-background.tsx | 44 + .../components/A103-toggle-border-radius.tsx | 43 + .../components/A104-callout-typography.tsx | 51 ++ .../src/components/A105-stack-gap.tsx | 44 + .../styling/styled-app/src/main.tsx | 9 + .../styling/styled-app/tsconfig.json | 17 + .../styling/styled-app/vite.config.ts | 18 + .../styling/tailwind-app/index.html | 31 + .../styling/tailwind-app/package.json | 24 + .../styling/tailwind-app/postcss.config.js | 6 + .../styling/tailwind-app/src/App.tsx | 74 ++ .../src/components/A001-padding.tsx | 53 ++ .../src/components/A002-color-token.tsx | 48 ++ .../src/components/A003-rounded.tsx | 29 + .../src/components/A004-font-weight.tsx | 21 + .../tailwind-app/src/components/A005-gap.tsx | 29 + .../styling/tailwind-app/src/index.css | 10 + .../styling/tailwind-app/src/main.tsx | 10 + .../styling/tailwind-app/tailwind.config.js | 15 + .../styling/tailwind-app/tsconfig.json | 17 + .../styling/tailwind-app/vite.config.ts | 20 + pnpm-lock.yaml | 779 +++++++++++++++++- pnpm-workspace.yaml | 4 +- 42 files changed, 2375 insertions(+), 11 deletions(-) create mode 100644 packages/domscribe-test-fixtures/styling/README.md create mode 100644 packages/domscribe-test-fixtures/styling/annotations.json create mode 100644 packages/domscribe-test-fixtures/styling/baselines/styled/A101.png create mode 100644 packages/domscribe-test-fixtures/styling/baselines/styled/A102.png create mode 100644 packages/domscribe-test-fixtures/styling/baselines/styled/A103.png create mode 100644 packages/domscribe-test-fixtures/styling/baselines/styled/A104.png create mode 100644 packages/domscribe-test-fixtures/styling/baselines/styled/A105.png create mode 100644 packages/domscribe-test-fixtures/styling/baselines/tailwind/A001.png create mode 100644 packages/domscribe-test-fixtures/styling/baselines/tailwind/A002.png create mode 100644 packages/domscribe-test-fixtures/styling/baselines/tailwind/A003.png create mode 100644 packages/domscribe-test-fixtures/styling/baselines/tailwind/A004.png create mode 100644 packages/domscribe-test-fixtures/styling/baselines/tailwind/A005.png create mode 100644 packages/domscribe-test-fixtures/styling/scripts/falsifier.ts create mode 100644 packages/domscribe-test-fixtures/styling/styled-app/index.html create mode 100644 packages/domscribe-test-fixtures/styling/styled-app/package.json create mode 100644 packages/domscribe-test-fixtures/styling/styled-app/src/App.tsx create mode 100644 packages/domscribe-test-fixtures/styling/styled-app/src/components/A101-badge-padding.tsx create mode 100644 packages/domscribe-test-fixtures/styling/styled-app/src/components/A102-hero-background.tsx create mode 100644 packages/domscribe-test-fixtures/styling/styled-app/src/components/A103-toggle-border-radius.tsx create mode 100644 packages/domscribe-test-fixtures/styling/styled-app/src/components/A104-callout-typography.tsx create mode 100644 packages/domscribe-test-fixtures/styling/styled-app/src/components/A105-stack-gap.tsx create mode 100644 packages/domscribe-test-fixtures/styling/styled-app/src/main.tsx create mode 100644 packages/domscribe-test-fixtures/styling/styled-app/tsconfig.json create mode 100644 packages/domscribe-test-fixtures/styling/styled-app/vite.config.ts create mode 100644 packages/domscribe-test-fixtures/styling/tailwind-app/index.html create mode 100644 packages/domscribe-test-fixtures/styling/tailwind-app/package.json create mode 100644 packages/domscribe-test-fixtures/styling/tailwind-app/postcss.config.js create mode 100644 packages/domscribe-test-fixtures/styling/tailwind-app/src/App.tsx create mode 100644 packages/domscribe-test-fixtures/styling/tailwind-app/src/components/A001-padding.tsx create mode 100644 packages/domscribe-test-fixtures/styling/tailwind-app/src/components/A002-color-token.tsx create mode 100644 packages/domscribe-test-fixtures/styling/tailwind-app/src/components/A003-rounded.tsx create mode 100644 packages/domscribe-test-fixtures/styling/tailwind-app/src/components/A004-font-weight.tsx create mode 100644 packages/domscribe-test-fixtures/styling/tailwind-app/src/components/A005-gap.tsx create mode 100644 packages/domscribe-test-fixtures/styling/tailwind-app/src/index.css create mode 100644 packages/domscribe-test-fixtures/styling/tailwind-app/src/main.tsx create mode 100644 packages/domscribe-test-fixtures/styling/tailwind-app/tailwind.config.js create mode 100644 packages/domscribe-test-fixtures/styling/tailwind-app/tsconfig.json create mode 100644 packages/domscribe-test-fixtures/styling/tailwind-app/vite.config.ts diff --git a/packages/domscribe-test-fixtures/package.json b/packages/domscribe-test-fixtures/package.json index d1290a5..be382e5 100644 --- a/packages/domscribe-test-fixtures/package.json +++ b/packages/domscribe-test-fixtures/package.json @@ -5,7 +5,17 @@ "type": "module", "generators": "./generator/generators.json", "description": "Kitchen-sink test fixtures and integration tests for Domscribe", + "scripts": { + "test:falsifier": "tsx styling/scripts/falsifier.ts", + "test:falsifier:record": "tsx styling/scripts/falsifier.ts --mode=record" + }, "devDependencies": { - "@playwright/test": "^1.49.0" + "@playwright/test": "^1.49.0", + "@types/pixelmatch": "^5.2.6", + "@types/pngjs": "^6.0.5", + "pixelmatch": "^7.1.0", + "playwright": "^1.49.0", + "pngjs": "^7.0.0", + "tsx": "^4.21.0" } } diff --git a/packages/domscribe-test-fixtures/project.json b/packages/domscribe-test-fixtures/project.json index 666b163..5c6a526 100644 --- a/packages/domscribe-test-fixtures/project.json +++ b/packages/domscribe-test-fixtures/project.json @@ -43,6 +43,20 @@ "cwd": "{projectRoot}" } }, + "falsifier": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm test:falsifier", + "cwd": "{projectRoot}" + } + }, + "falsifier:record": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm test:falsifier:record", + "cwd": "{projectRoot}" + } + }, "build": { "executor": "nx:noop" }, diff --git a/packages/domscribe-test-fixtures/styling/README.md b/packages/domscribe-test-fixtures/styling/README.md new file mode 100644 index 0000000..41dc515 --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/README.md @@ -0,0 +1,113 @@ +# Styling-annotation falsifier (RFC 0001) + +This directory is the measurement instrument for sprint 2734's thesis: + +> ≥70% agent one-shot styling-completion rate on `@domscribe/test-fixtures` +> by sprint 2734+6. + +It is **not** a product feature. It is the rig that lets us answer "did +the agent's edit actually produce the right pixels" in CI. + +## Layout + +``` +styling/ + tailwind-app/ # Tailwind v3 + Vite React fixture (annotations A001–A005) + styled-app/ # styled-components + Vite React fixture (annotations A101–A105) + annotations.json # the 10 styling annotations the falsifier grades against + baselines/ # canonical-after PNGs (per fixture/per id) + scripts/falsifier.ts # Playwright + pixelmatch harness +``` + +Each annotation component has two exports — `Foo` (the as-shipped, broken +state) and `FooFixed` (the canonical correct state). The fixture App +exposes them as `#A001/before` and `#A001/after` hash routes. + +## Why two states per component? + +The falsifier's job is to grade an agent edit. To do that it needs a +known-correct rendering to diff against. By committing both states in +source, we get: + +1. A reproducible baseline (no "what did this render last week?" drift). +2. A clean grading signal — pixel-diff against `/after` is a binary + pass/fail. +3. A self-test that proves the harness mechanism works + independently of any agent. + +## Running + +```bash +# CI default — verifies the mechanism (should be 100% pass). +pnpm test:falsifier + +# Re-record baselines after editing an /after route. +pnpm test:falsifier:record + +# Grade an external agent's output directory. +pnpm test:falsifier -- --mode=measure --agent-output=/path/to/agent/png/dir +``` + +The harness writes one JSON object to stdout: + +```json +{ + "mode": "self-test", + "total": 10, + "passes": 10, + "fails": 0, + "oneShotRate": 1.0, + "annotations": [ + { + "id": "A001", + "fixture": "tailwind", + "passed": true, + "pixelDiffRatio": 0, + "diffPixels": 0 + } + ] +} +``` + +Exit code is non-zero when `fails > 0`, except in `--mode=record` which +always exits 0. + +## Determinism + +Pixel-diff stability requires removing every source of jitter. We: + +- Disable animations and transitions globally via `index.html`. +- Lock viewport, scale, locale, timezone, and reduced-motion in Playwright. +- Use deterministic asset filenames in the Vite output. +- Force the default system font stack on `body` (no remote fonts). +- Set `caret: 'hide'` and `animations: 'disabled'` on every screenshot. + +We allow up to 0.5% pixel-diff to absorb sub-pixel AA jitter on text. The +canonical-after path normally diffs at 0 — the floor exists for CI worker +quirks, not as license for "close enough" agent edits. + +## Adding a new annotation + +1. Add a component pair `FooAnnX / FooAnnXFixed` under + `-app/src/components/`. +2. Register a `before`/`after` route in `-app/src/App.tsx`. +3. Add an entry to `annotations.json` with the source location and intent. +4. Run `pnpm test:falsifier:record` to update the baseline. +5. Run `pnpm test:falsifier` and confirm the new entry passes. + +## What this harness deliberately does NOT do + +- It does not invoke an agent. The agent-integration loop is built on + top of this — see `--mode=measure`. +- It does not assert on `componentStyles` or `styleSource` output. Those + are runtime-capture concerns (validated in `@domscribe/runtime` and + `@domscribe/relay` unit tests). The falsifier asks one question only: + "do the after-state pixels match the baseline". + +## Task A dependency note + +The corpus references `styleSource` shape in the intent text only as +forward-context for the agent. The falsifier itself does not require +`styleSource` to be populated; it grades on pixels. Once Task A's +build-time `styleSource` lands, the agent-side prompts can lean on it +without any harness change. diff --git a/packages/domscribe-test-fixtures/styling/annotations.json b/packages/domscribe-test-fixtures/styling/annotations.json new file mode 100644 index 0000000..4ed2cf0 --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/annotations.json @@ -0,0 +1,142 @@ +{ + "$schema": "./annotation.schema.json", + "description": "Styling-annotation falsifier corpus for RFC 0001. The agent is graded one-shot: given the intent + sourceFile + sourceLine, can it produce an edit that, when applied to the before route, renders pixel-identical to the after route's baseline?", + "schemaVersion": 1, + "annotations": [ + { + "id": "A001", + "fixture": "tailwind", + "fixtureDir": "tailwind-app", + "beforeRoute": "#A001/before", + "afterRoute": "#A001/after", + "intent": "Card padding is too cramped. Bump it to 32px (Tailwind p-8) to match the spacious card style used elsewhere on the page.", + "sourceFile": "src/components/A001-padding.tsx", + "sourceLine": 33, + "targetTestId": "A001", + "expectedComputedStyles": { "padding": "32px" }, + "tags": ["padding", "utility-class-swap"] + }, + { + "id": "A002", + "fixture": "tailwind", + "fixtureDir": "tailwind-app", + "beforeRoute": "#A002/before", + "afterRoute": "#A002/after", + "intent": "The alert is using a generic Tailwind palette color (text-red-500). Switch to the design-system token `text-brand-accent` (Tailwind theme extension; resolves to var(--color-accent)).", + "sourceFile": "src/components/A002-color-token.tsx", + "sourceLine": 33, + "targetTestId": "A002", + "expectedComputedStyles": { "color": "rgb(59, 130, 246)" }, + "expectedCustomProperties": ["--color-accent"], + "tags": ["color", "design-token"] + }, + { + "id": "A003", + "fixture": "tailwind", + "fixtureDir": "tailwind-app", + "beforeRoute": "#A003/before", + "afterRoute": "#A003/after", + "intent": "The Continue button has square corners. Make it a pill (rounded-full) to match the action-button style.", + "sourceFile": "src/components/A003-rounded.tsx", + "sourceLine": 25, + "targetTestId": "A003", + "expectedComputedStyles": { "border-radius": "9999px" }, + "tags": ["border-radius", "utility-class-swap"] + }, + { + "id": "A004", + "fixture": "tailwind", + "fixtureDir": "tailwind-app", + "beforeRoute": "#A004/before", + "afterRoute": "#A004/after", + "intent": "The section heading is too light. Bump it to font-bold for visual hierarchy.", + "sourceFile": "src/components/A004-font-weight.tsx", + "sourceLine": 16, + "targetTestId": "A004", + "expectedComputedStyles": { "font-weight": "700" }, + "tags": ["typography", "font-weight"] + }, + { + "id": "A005", + "fixture": "tailwind", + "fixtureDir": "tailwind-app", + "beforeRoute": "#A005/before", + "afterRoute": "#A005/after", + "intent": "Items in the panel are touching. Add gap-4 (16px) for breathing room.", + "sourceFile": "src/components/A005-gap.tsx", + "sourceLine": 27, + "targetTestId": "A005", + "expectedComputedStyles": { "gap": "16px" }, + "tags": ["layout", "flex-gap"] + }, + { + "id": "A101", + "fixture": "styled", + "fixtureDir": "styled-app", + "beforeRoute": "#A101/before", + "afterRoute": "#A101/after", + "intent": "The NEW badge is too cramped — increase padding to 8px 16px for breathing room. This is a styled-components hash-class case; you must edit the `styled.span` source block directly.", + "sourceFile": "src/components/A101-badge-padding.tsx", + "sourceLine": 19, + "targetTestId": "A101", + "expectedComputedStyles": { "padding": "8px 16px" }, + "tags": ["padding", "css-in-js"] + }, + { + "id": "A102", + "fixture": "styled", + "fixtureDir": "styled-app", + "beforeRoute": "#A102/before", + "afterRoute": "#A102/after", + "intent": "The hero is using a hardcoded background color (#e0f2fe). Switch to var(--color-surface) so it tracks the theme. The token is resolved on :root.", + "sourceFile": "src/components/A102-hero-background.tsx", + "sourceLine": 15, + "targetTestId": "A102", + "expectedComputedStyles": { "background-color": "rgb(248, 250, 252)" }, + "expectedCustomProperties": ["--color-surface"], + "tags": ["color", "design-token", "css-in-js"] + }, + { + "id": "A103", + "fixture": "styled", + "fixtureDir": "styled-app", + "beforeRoute": "#A103/before", + "afterRoute": "#A103/after", + "intent": "The toggle button is too boxy. Round the corners to a full pill (border-radius: 9999px).", + "sourceFile": "src/components/A103-toggle-border-radius.tsx", + "sourceLine": 14, + "targetTestId": "A103", + "expectedComputedStyles": { "border-radius": "9999px" }, + "tags": ["border-radius", "css-in-js"] + }, + { + "id": "A104", + "fixture": "styled", + "fixtureDir": "styled-app", + "beforeRoute": "#A104/before", + "afterRoute": "#A104/after", + "intent": "The callout text is unreadable. Bump font-size to 16px and line-height to 1.5 for comfortable reading.", + "sourceFile": "src/components/A104-callout-typography.tsx", + "sourceLine": 15, + "targetTestId": "A104", + "expectedComputedStyles": { + "font-size": "16px", + "line-height": "24px" + }, + "tags": ["typography", "css-in-js"] + }, + { + "id": "A105", + "fixture": "styled", + "fixtureDir": "styled-app", + "beforeRoute": "#A105/before", + "afterRoute": "#A105/after", + "intent": "Cards in the stack are touching. Add gap: 12px so they have consistent spacing.", + "sourceFile": "src/components/A105-stack-gap.tsx", + "sourceLine": 15, + "targetTestId": "A105", + "expectedComputedStyles": { "gap": "12px" }, + "tags": ["layout", "flex-gap", "css-in-js"] + } + ] +} diff --git a/packages/domscribe-test-fixtures/styling/baselines/styled/A101.png b/packages/domscribe-test-fixtures/styling/baselines/styled/A101.png new file mode 100644 index 0000000000000000000000000000000000000000..e4c3fb7bc9c28e46b1ca8ba5195a898299382bc4 GIT binary patch literal 3629 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

xrK6ZvFdOIf)?nq zmup6IT~#M7ulcKXSNsoy)tqCaE5#o0L4LNBKuy*3hVp!aoHef-nf;VxDXTL}NAO_A zFw?`eAwMyuINGGEtjkLc2l%Rw^-{--wsLD1xB5J``iNG38{PeSzZHc7>)x2Te#_2P zJ)IgI%zlcX!3O~e@_UdhdrP+3n6QD`}1 zX*7a4{6M`3UwHRS+C=`IBQ%)!M3?H~mdYfg;5cSuORRcC&Aox)kF)3Ug@Iu_;V~p` z`UbUw#9~0;^4H0vnm!+MSz*Y=>iM8gh8 z2Pr_6FA7wEd`wR?CCik!$BAYqQ6AQN@#yO!=dvTVy@|v6WgPbLxFah3+upKr57EzZ zYROq&|EVt!bv1f)K0CM-Zobh7)u}@T&I@p@0YzzQh~|mh+{XZ3GU^IA%3tJ5y&=(v znQi13rSL+bbgsLve3VsEOEc68?UpC>=RUj;5+iyBYuer zKc?YHr5tDDF5^WPQgl@C)0O$bNKpj2#1SPnFh6jYdW^sFwY@7EI_4$ZHw~+D(fyTO z7lA!PPdO(J6P{PqMN@UcR4%37^^{ph+P4>bAeOWCZ2zzD#x~&68V#)h;eVTn4X=}S z0|02(H!om~wEv^o0Nnrpdi~`a0}-qK$%b#uS_dc!VTw2Mv|FnY;LItQALS(K=Dz`; C0m|F} literal 0 HcmV?d00001 diff --git a/packages/domscribe-test-fixtures/styling/baselines/styled/A104.png b/packages/domscribe-test-fixtures/styling/baselines/styled/A104.png new file mode 100644 index 0000000000000000000000000000000000000000..3c641e0eb78ef53af8eeaef008132f1579b8ce93 GIT binary patch literal 10735 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 AL;wH) literal 0 HcmV?d00001 diff --git a/packages/domscribe-test-fixtures/styling/baselines/styled/A105.png b/packages/domscribe-test-fixtures/styling/baselines/styled/A105.png new file mode 100644 index 0000000000000000000000000000000000000000..9884858a09b121c04eac391666d3c99a1c534cfd GIT binary patch literal 11188 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 g?ctwreSj~j;BdEWPVf6JdYfR2X` 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 Tk&2@?5x~WBo^IqH!|wez3IBX4 literal 0 HcmV?d00001 diff --git a/packages/domscribe-test-fixtures/styling/baselines/tailwind/A003.png b/packages/domscribe-test-fixtures/styling/baselines/tailwind/A003.png new file mode 100644 index 0000000000000000000000000000000000000000..acafcda30bc08622b5ee139fe7ff5e88cb8bd829 GIT binary patch literal 4877 zcmeI0`&ZI=8ppq4Xn9F@yr(Pe&Xi4>R8E>Yd6~4Pbnq@1DHeGp@J6CIg657cnK@0V zXWPoBlcdfz!iVOi?pe%><*iE;EgOYx(@ zejeK0hacJX+dbKin-_<}b};;y?lVn-Y;yJ*iCOT#fWeLO%ki+ChP<6Cel?Xf=jo9t zRH;{g?2zAg&EN=|7b1XU)vmjoc+0Az<75f0Q|ni9Pql)2?Dm7AOPUZ z=B3Rq*rZ@{5dM#M0jzt#=+acj=x|nJ*1SsW7a}ODeFl#$B;57QxyeG+iI|L3@tA&Py^_`y+FQ{^8tC z5UfvjovUtr*_0=dgsNv$R3m|Fpi42>rtTDAGsUb=I+8 zbn3zH2~xvTNgwrc;M*Lch41mJBJKmd-Y0nht=l0NdcgZPUS4wc;Nw_{^ZtiMdFb(j z6z~fZgx7f296^)P>T%ef^V4_N5S<4=`X5yz&9e;W!|t3D4acj^?s?TfvvidA;oGBF ztd8}?kqSjP)Mjrntc0y-sTt}YR-gP^)47;QdnpEujFpWd+XrWC=l0!q))Q66>FDq2 zF)GvDeRK(4NF}U&=zEL1$NZXwtVviOb{R$5T?)hK+OlX(xP+7*VvzjFks^$_oLpTk zIWHU(dPKB?<@rr6H^9lgPI+L{SF*yYv5pUto+D-EX!i;QPHJ18_5SJ92EF%@LNKRLKTKQ4?N~sMv=d8x39&R4?mfm1qFlsb+ zgGobQY41}$Pr{EM8FW1ehx>&OcxFXr<1=F#_t+T`TIb@AtU<*1poaA!k-Ds`Shk00 zL#IcaF0Yb#%0;bg8qTJjj3u+w`ds)p7yTiiM`~n){O1_FZF;4PUAWR+K$Sw749mSl z(F@Lo=Qt)->xw_x#jO*Kc(jDy%fiB|Y-9MWiz%JZDSqHEeop?uZ4AP5v5hPzvP4TjS*gqZ56Y(+ve>hJnC<}nNWgeU`?AJ zB50ak|2o_+F-?YEXPHFd31Y> zbEjomw2iyyXX5LVv|oQ?$|%NrO7B)gKU6WZo(;#eu$c@;CTC@S7tNZC&sOJOmHcvl z$TPyAw@RPTSgUo-oxRTWKS^ zqZ}UFv2=qK-e!m#D~DTgPj4ekW(0#{kIv3l_jfVd>(uW%WEt#(sS&gJekuj-(-O8y z`{h|8JnMW6y8D9s%Tc#$ep2fNBoe-za zt6Ofu)3;x?z&IwIM;y@jFD1wEJ4PT;?(^m4Z)z77e=m`bG8puVcy+Ifo9-7^Eo}Tn zvwqQ9JgRsQn|0Lb`(`ovW^8)#fM?6Y^lp~&HPJQm(OAcwwuHF2xiQoJs|zcf;xg%R zX-yj~7IP$V&G(mi{X(l=#`Ek z;iG2uJIP7SjkjO7lA1H$^ORVOb@5o}EELnx$5NF0N1CPOMF3OnCnH!QaPNi5g6Hn* zv9&wjHD%?8J2+9mQ*Pu0EnnzX#|Y{+5V5y)z+dSk{>bl^KK5wwUjq>!nExb@Y2*WNPywAwl(> z-{yi}Sld#6TVFd@+FB zKw8~(#h~sCKBmPb9^9E?b1c^+)V{=%b3tVHs&(YTNusuV6$SbDr^XOcb%kEE5YqI$ zH>Fdr$2-@07b!I~^?Fg!u^%F}m>W#Hv}D3nxpv4%T?!=dQ`zyg-zvLPiM1JZyWe`zEo~*^)-!8_Fpp0i66l&gR?cJS zfnX({eNS5c?o9{umo$Naz|Xt>$1SR*0B*Gr3_Sdt1#D9Gm!xd7E&shOYRE>GrO`aQ i_QT4?LFY5U+CW8L=&^dzmza$TaKabn!}u=x;(q}DZ=x6g literal 0 HcmV?d00001 diff --git a/packages/domscribe-test-fixtures/styling/baselines/tailwind/A004.png b/packages/domscribe-test-fixtures/styling/baselines/tailwind/A004.png new file mode 100644 index 0000000000000000000000000000000000000000..4142b7faa350f905c122669cf726eced3edf2e6e GIT binary patch literal 6616 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/.png`. Expected pass rate is 100% — this is + * a smoke test of the mechanism (server stable, viewport stable, + * pixel-diff stable). If the self-test reports <100%, the harness + * itself is broken; nothing else the falsifier reports can be trusted. + * + * --mode=record + * One-time baseline capture. Writes `baselines//.png` from + * the live `afterRoute`. Run this when adding/updating annotations. + * + * --mode=measure --agent-output=

+ * Production grading. Each annotation's screenshot is read from + * `/.png` and compared to the baseline. This is what the + * post-sprint agent-integration harness will call. + * + * Output + * ------ + * Emits one JSON line to stdout with shape: + * { + * "mode": "self-test|record|measure", + * "total": number, + * "passes": number, + * "fails": number, + * "oneShotRate": number, // passes / total in [0,1]; null when total=0 + * "annotations": [ + * { "id": string, "fixture": string, "passed": boolean, + * "pixelDiffRatio": number, "diffPixels": number, "reason"?: string } + * ] + * } + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { spawn, type ChildProcess } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { chromium, type Browser, type Page } from 'playwright'; +import pixelmatch from 'pixelmatch'; +import { PNG } from 'pngjs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const STYLING_ROOT = path.resolve(__dirname, '..'); +const BASELINES_ROOT = path.join(STYLING_ROOT, 'baselines'); +const ANNOTATIONS_FILE = path.join(STYLING_ROOT, 'annotations.json'); + +/** + * Pixel-diff tolerance. + * + * PER_PIXEL_THRESHOLD — pixelmatch's `threshold` (color distance per + * pixel below which two pixels are considered equal). 0.1 is the + * library's recommended starting point; we keep it modest so the + * harness catches real visual deltas but tolerates AA jitter. + * + * MAX_DIFF_RATIO — the fraction of total pixels that may differ before + * we call the annotation a fail. The canonical-after path diffs at 0, + * so this is a defensive floor for CI worker AA jitter on text glyphs. + * 0.1% (0.001) is tight enough that two images that happen to share + * a mostly-white background (a real false-positive risk we observed in + * sanity testing) cannot slip through, while still absorbing a few + * pixels of subpixel font rendering noise. + */ +const PER_PIXEL_THRESHOLD = 0.1; +const MAX_DIFF_RATIO = 0.001; + +const VIEWPORT = { width: 800, height: 600 }; + +interface Annotation { + id: string; + fixture: string; + fixtureDir: string; + beforeRoute: string; + afterRoute: string; + intent: string; + sourceFile: string; + sourceLine: number; + targetTestId: string; + expectedComputedStyles?: Record; + expectedCustomProperties?: string[]; + tags?: string[]; +} + +interface AnnotationResult { + id: string; + fixture: string; + passed: boolean; + pixelDiffRatio: number; + diffPixels: number; + reason?: string; +} + +interface FixtureServer { + fixture: string; + fixtureDir: string; + port: number; + baseUrl: string; + process: ChildProcess; +} + +type Mode = 'self-test' | 'record' | 'measure'; + +function parseArgs(argv: string[]): { mode: Mode; agentOutputDir?: string } { + let mode: Mode = 'self-test'; + let agentOutputDir: string | undefined; + for (const arg of argv.slice(2)) { + if (arg.startsWith('--mode=')) { + const v = arg.slice('--mode='.length); + if (v !== 'self-test' && v !== 'record' && v !== 'measure') { + throw new Error(`Unknown mode: ${v}`); + } + mode = v; + } else if (arg.startsWith('--agent-output=')) { + agentOutputDir = path.resolve(arg.slice('--agent-output='.length)); + } + } + if (mode === 'measure' && !agentOutputDir) { + throw new Error('--mode=measure requires --agent-output='); + } + return { mode, agentOutputDir }; +} + +function readAnnotations(): Annotation[] { + const raw = JSON.parse(fs.readFileSync(ANNOTATIONS_FILE, 'utf-8')); + return raw.annotations as Annotation[]; +} + +/** + * Build a fixture app via `vite build`. Done once per harness run; we do + * not stream output to stdout because the harness must keep stdout reserved + * for the JSON report. + */ +async function buildFixture(fixtureDir: string): Promise { + await new Promise((resolve, reject) => { + const cwd = path.join(STYLING_ROOT, fixtureDir); + const proc = spawn('npx', ['vite', 'build'], { + cwd, + stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env, FORCE_COLOR: '0' }, + }); + let stderr = ''; + proc.stderr?.on('data', (chunk) => (stderr += chunk.toString())); + proc.on('close', (code) => { + if (code !== 0) { + reject( + new Error( + `vite build failed for ${fixtureDir} (exit ${code}):\n${stderr}`, + ), + ); + } else { + resolve(); + } + }); + proc.on('error', reject); + }); +} + +/** + * Start a `vite preview` for a built fixture and wait for the port to be + * ready. Returns the running child + base URL so the harness can later + * tear it down. If a server is *already* listening on this port and serves + * 200 — typical when a previous harness run leaked a process — we treat it + * as a successful start and skip spawning, since a stale preview is + * sufficient for read-only screenshotting and spawning a second one would + * just bind-fail noisily. + */ +async function startPreview( + fixtureDir: string, + port: number, + fixture: string, +): Promise { + if (await isPortServing(port)) { + return { + fixture, + fixtureDir, + port, + baseUrl: `http://localhost:${port}/`, + // Sentinel: we do not own this process and must not kill it. + process: { killed: true, kill: () => true } as unknown as ChildProcess, + }; + } + + const cwd = path.join(STYLING_ROOT, fixtureDir); + const proc = spawn( + 'npx', + ['vite', 'preview', '--port', String(port), '--strictPort'], + { + cwd, + stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env, FORCE_COLOR: '0' }, + }, + ); + + // Surface preview errors to stderr — never stdout (reserved for JSON). + proc.stderr?.on('data', (chunk) => { + process.stderr.write(`[${fixture}] ${chunk}`); + }); + + await waitForPort(port, 15_000); + + return { + fixture, + fixtureDir, + port, + baseUrl: `http://localhost:${port}/`, + process: proc, + }; +} + +async function isPortServing(port: number): Promise { + try { + const res = await fetch(`http://localhost:${port}/`); + return res.ok || res.status === 304; + } catch { + return false; + } +} + +async function waitForPort(port: number, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + try { + const res = await fetch(`http://localhost:${port}/`); + if (res.ok || res.status === 304) return; + } catch { + // not ready yet + } + await new Promise((r) => setTimeout(r, 200)); + } + throw new Error(`Preview server did not become ready on port ${port}`); +} + +async function stopServer(server: FixtureServer): Promise { + if (server.process.killed) return; + await new Promise((resolve) => { + server.process.once('close', () => resolve()); + server.process.kill('SIGTERM'); + setTimeout(() => { + if (!server.process.killed) server.process.kill('SIGKILL'); + }, 2000); + }); +} + +async function screenshotRoute( + page: Page, + baseUrl: string, + route: string, +): Promise { + await page.goto(`${baseUrl}${route}`, { waitUntil: 'networkidle' }); + // Wait for the test-id to materialize so we are not racing a render. + const testid = /testid=[^"]+/i.exec(route)?.[0]; // unused fallback + void testid; + // 50 ms settle for paint completion. Animations are killed via CSS in + // index.html; this guards against `networkidle` firing one frame early. + await page.waitForTimeout(50); + return page.screenshot({ + type: 'png', + fullPage: false, + animations: 'disabled', + caret: 'hide', + }); +} + +function loadPng(buf: Buffer): PNG { + return PNG.sync.read(buf); +} + +function diff(a: PNG, b: PNG): { diffPixels: number; ratio: number } { + if (a.width !== b.width || a.height !== b.height) { + return { + diffPixels: a.width * a.height, + ratio: 1, + }; + } + const out = new PNG({ width: a.width, height: a.height }); + const diffPixels = pixelmatch(a.data, b.data, out.data, a.width, a.height, { + threshold: PER_PIXEL_THRESHOLD, + }); + return { + diffPixels, + ratio: diffPixels / (a.width * a.height), + }; +} + +interface BrowserContext { + browser: Browser; + page: Page; +} + +async function openBrowser(): Promise { + const browser = await chromium.launch(); + const page = await browser.newPage({ + viewport: VIEWPORT, + deviceScaleFactor: 1, + // Lock locale so font metrics are stable across runners. + locale: 'en-US', + timezoneId: 'UTC', + reducedMotion: 'reduce', + }); + return { browser, page }; +} + +function ensureDir(p: string): void { + fs.mkdirSync(p, { recursive: true }); +} + +function baselinePath(fixture: string, id: string): string { + return path.join(BASELINES_ROOT, fixture, `${id}.png`); +} + +async function runRecord( + servers: Map, + annotations: Annotation[], +): Promise { + const { browser, page } = await openBrowser(); + const results: AnnotationResult[] = []; + try { + for (const ann of annotations) { + const server = servers.get(ann.fixture); + if (!server) { + results.push({ + id: ann.id, + fixture: ann.fixture, + passed: false, + pixelDiffRatio: 1, + diffPixels: -1, + reason: `No preview server for fixture "${ann.fixture}"`, + }); + continue; + } + const png = await screenshotRoute(page, server.baseUrl, ann.afterRoute); + ensureDir(path.join(BASELINES_ROOT, ann.fixture)); + fs.writeFileSync(baselinePath(ann.fixture, ann.id), png); + results.push({ + id: ann.id, + fixture: ann.fixture, + passed: true, + pixelDiffRatio: 0, + diffPixels: 0, + reason: 'baseline recorded', + }); + } + } finally { + await browser.close(); + } + return results; +} + +async function runSelfTest( + servers: Map, + annotations: Annotation[], +): Promise { + return runAgainstSource(servers, annotations, async (page, server, ann) => + screenshotRoute(page, server.baseUrl, ann.afterRoute), + ); +} + +async function runMeasure( + servers: Map, + annotations: Annotation[], + agentOutputDir: string, +): Promise { + return runAgainstSource(servers, annotations, async (_page, _server, ann) => { + const p = path.join(agentOutputDir, `${ann.id}.png`); + if (!fs.existsSync(p)) { + throw new Error(`Agent output missing: ${p}`); + } + return fs.readFileSync(p); + }); +} + +async function runAgainstSource( + servers: Map, + annotations: Annotation[], + source: ( + page: Page, + server: FixtureServer, + ann: Annotation, + ) => Promise, +): Promise { + const { browser, page } = await openBrowser(); + const results: AnnotationResult[] = []; + try { + for (const ann of annotations) { + const baseline = baselinePath(ann.fixture, ann.id); + if (!fs.existsSync(baseline)) { + results.push({ + id: ann.id, + fixture: ann.fixture, + passed: false, + pixelDiffRatio: 1, + diffPixels: -1, + reason: `No baseline at ${path.relative(STYLING_ROOT, baseline)} — run with --mode=record first`, + }); + continue; + } + const server = servers.get(ann.fixture); + if (!server) { + results.push({ + id: ann.id, + fixture: ann.fixture, + passed: false, + pixelDiffRatio: 1, + diffPixels: -1, + reason: `No preview server for fixture "${ann.fixture}"`, + }); + continue; + } + try { + const actualBuf = await source(page, server, ann); + const baselineBuf = fs.readFileSync(baseline); + const { diffPixels, ratio } = diff( + loadPng(actualBuf), + loadPng(baselineBuf), + ); + const passed = ratio <= MAX_DIFF_RATIO; + results.push({ + id: ann.id, + fixture: ann.fixture, + passed, + pixelDiffRatio: ratio, + diffPixels, + reason: passed + ? undefined + : `Pixel diff ${(ratio * 100).toFixed(3)}% exceeds tolerance ${(MAX_DIFF_RATIO * 100).toFixed(3)}%`, + }); + } catch (err) { + results.push({ + id: ann.id, + fixture: ann.fixture, + passed: false, + pixelDiffRatio: 1, + diffPixels: -1, + reason: err instanceof Error ? err.message : String(err), + }); + } + } + } finally { + await browser.close(); + } + return results; +} + +async function main() { + const { mode, agentOutputDir } = parseArgs(process.argv); + const annotations = readAnnotations(); + + // Build both fixtures. We do this every run so a stale dist can never + // cause a false positive in self-test. + const fixtureDirs = Array.from(new Set(annotations.map((a) => a.fixtureDir))); + for (const dir of fixtureDirs) { + await buildFixture(dir); + } + + // Launch a preview per fixture on a deterministic port. Different + // fixtures use different ports so the harness can hold both up at once. + const fixturePort: Record = { + tailwind: 4801, + styled: 4802, + }; + + const servers = new Map(); + try { + for (const ann of annotations) { + if (servers.has(ann.fixture)) continue; + const port = fixturePort[ann.fixture]; + if (port == null) { + throw new Error( + `No port configured for fixture "${ann.fixture}". Add it to fixturePort.`, + ); + } + servers.set( + ann.fixture, + await startPreview(ann.fixtureDir, port, ann.fixture), + ); + } + + let results: AnnotationResult[]; + if (mode === 'record') { + results = await runRecord(servers, annotations); + } else if (mode === 'measure') { + results = await runMeasure(servers, annotations, agentOutputDir!); + } else { + results = await runSelfTest(servers, annotations); + } + + const passes = results.filter((r) => r.passed).length; + const total = results.length; + const fails = total - passes; + const oneShotRate = total === 0 ? null : passes / total; + + process.stdout.write( + JSON.stringify( + { + mode, + total, + passes, + fails, + oneShotRate, + annotations: results, + }, + null, + 2, + ) + '\n', + ); + + // Non-zero exit when the run failed so CI catches regressions. Record + // mode always exits 0 (it cannot "fail" — it's writing baselines). + if (mode !== 'record' && fails > 0) { + process.exitCode = 1; + } + } finally { + for (const server of servers.values()) { + await stopServer(server); + } + } +} + +main().catch((err) => { + process.stderr.write( + `[falsifier] fatal: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n`, + ); + process.exit(2); +}); diff --git a/packages/domscribe-test-fixtures/styling/styled-app/index.html b/packages/domscribe-test-fixtures/styling/styled-app/index.html new file mode 100644 index 0000000..3ab074d --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/styled-app/index.html @@ -0,0 +1,35 @@ + + + + + + Domscribe styling fixture — styled-components + + + +
+ + + diff --git a/packages/domscribe-test-fixtures/styling/styled-app/package.json b/packages/domscribe-test-fixtures/styling/styled-app/package.json new file mode 100644 index 0000000..06c035d --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/styled-app/package.json @@ -0,0 +1,22 @@ +{ + "name": "@domscribe/styling-fixture-styled", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "preview": "vite preview --port 4802 --strictPort" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "styled-components": "^6.1.13" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.6.3", + "vite": "^5.4.11" + } +} diff --git a/packages/domscribe-test-fixtures/styling/styled-app/src/App.tsx b/packages/domscribe-test-fixtures/styling/styled-app/src/App.tsx new file mode 100644 index 0000000..d789076 --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/styled-app/src/App.tsx @@ -0,0 +1,82 @@ +/** + * styled-components fixture — entry point. + * + * Mirrors the Tailwind app's hash-route structure. Each annotation has + * a /before and /after that render the original and canonical-fixed version + * of the same component. CSS-in-JS classes are runtime-generated hashes, + * so the falsifier cannot rely on className for source attribution — the + * MCP tool's build-time `styleSource` (Task A's domain) is the only path + * back to the `styled.X` source block for those. + */ +import { useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { BadgeA101, BadgeA101Fixed } from './components/A101-badge-padding'; +import { HeroA102, HeroA102Fixed } from './components/A102-hero-background'; +import { + ToggleA103, + ToggleA103Fixed, +} from './components/A103-toggle-border-radius'; +import { + CalloutA104, + CalloutA104Fixed, +} from './components/A104-callout-typography'; +import { StackA105, StackA105Fixed } from './components/A105-stack-gap'; + +const Centered = styled.div` + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: white; +`; + +interface Route { + id: string; + Component: () => JSX.Element; +} + +const routes: Record = { + 'A101/before': { id: 'A101-before', Component: BadgeA101 }, + 'A101/after': { id: 'A101-after', Component: BadgeA101Fixed }, + 'A102/before': { id: 'A102-before', Component: HeroA102 }, + 'A102/after': { id: 'A102-after', Component: HeroA102Fixed }, + 'A103/before': { id: 'A103-before', Component: ToggleA103 }, + 'A103/after': { id: 'A103-after', Component: ToggleA103Fixed }, + 'A104/before': { id: 'A104-before', Component: CalloutA104 }, + 'A104/after': { id: 'A104-after', Component: CalloutA104Fixed }, + 'A105/before': { id: 'A105-before', Component: StackA105 }, + 'A105/after': { id: 'A105-after', Component: StackA105Fixed }, +}; + +export function App() { + const [hash, setHash] = useState(() => window.location.hash.slice(1)); + + useEffect(() => { + const onChange = () => setHash(window.location.hash.slice(1)); + window.addEventListener('hashchange', onChange); + return () => window.removeEventListener('hashchange', onChange); + }, []); + + const route = routes[hash]; + + if (!route) { + return ( +
+

styled-components styling fixture

+
    + {Object.keys(routes).map((path) => ( +
  • + #{path} +
  • + ))} +
+
+ ); + } + + return ( + + + + ); +} diff --git a/packages/domscribe-test-fixtures/styling/styled-app/src/components/A101-badge-padding.tsx b/packages/domscribe-test-fixtures/styling/styled-app/src/components/A101-badge-padding.tsx new file mode 100644 index 0000000..5834313 --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/styled-app/src/components/A101-badge-padding.tsx @@ -0,0 +1,37 @@ +/** + * A101 — styled-components badge padding. + * + * Intent: "the New badge is too cramped — bump the padding so it has + * more room around the label". + * + * Demonstrates the CSS-in-JS case where the runtime className is a + * generated hash (`sc-1a2b3c`) — the only path back to the `styled.span` + * source block is via the build-time styleSource on the manifest entry. + */ +import styled from 'styled-components'; + +const BadgeTight = styled.span` + background: var(--color-accent); + color: white; + padding: 2px 6px; + font-size: 12px; + font-weight: 600; + border-radius: 4px; +`; + +const BadgeRoomy = styled.span` + background: var(--color-accent); + color: white; + padding: 8px 16px; + font-size: 12px; + font-weight: 600; + border-radius: 4px; +`; + +export function BadgeA101() { + return NEW; +} + +export function BadgeA101Fixed() { + return NEW; +} diff --git a/packages/domscribe-test-fixtures/styling/styled-app/src/components/A102-hero-background.tsx b/packages/domscribe-test-fixtures/styling/styled-app/src/components/A102-hero-background.tsx new file mode 100644 index 0000000..dda7efc --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/styled-app/src/components/A102-hero-background.tsx @@ -0,0 +1,44 @@ +/** + * A102 — styled-components hero background swap to token. + * + * Intent: "the hero is using a hardcoded color — switch to the surface + * token (var(--color-surface)) so it tracks the theme". + * + * captureStyles runtime snapshot reports both the resolved background-color + * AND the --color-surface var, letting the agent confirm the token is in + * scope at this element. + */ +import styled from 'styled-components'; + +const HeroHardcoded = styled.section` + background: #e0f2fe; + color: var(--color-fg); + padding: 24px 32px; + border-radius: 8px; + width: 320px; +`; + +const HeroTokenized = styled.section` + background: var(--color-surface); + color: var(--color-fg); + padding: 24px 32px; + border-radius: 8px; + width: 320px; +`; + +const HERO_CONTENT = ( + <> +

Welcome back

+

+ Pick up where you left off. +

+ +); + +export function HeroA102() { + return {HERO_CONTENT}; +} + +export function HeroA102Fixed() { + return {HERO_CONTENT}; +} diff --git a/packages/domscribe-test-fixtures/styling/styled-app/src/components/A103-toggle-border-radius.tsx b/packages/domscribe-test-fixtures/styling/styled-app/src/components/A103-toggle-border-radius.tsx new file mode 100644 index 0000000..6e05f9f --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/styled-app/src/components/A103-toggle-border-radius.tsx @@ -0,0 +1,43 @@ +/** + * A103 — styled-components pill-style border-radius. + * + * Intent: "the toggle is too boxy — round the corners to a full pill + * (border-radius: 9999px)". + */ +import styled from 'styled-components'; + +const ToggleBoxy = styled.button` + background: var(--color-accent); + color: white; + padding: 8px 20px; + font-weight: 600; + border: none; + border-radius: 4px; + cursor: pointer; +`; + +const TogglePill = styled.button` + background: var(--color-accent); + color: white; + padding: 8px 20px; + font-weight: 600; + border: none; + border-radius: 9999px; + cursor: pointer; +`; + +export function ToggleA103() { + return ( + + Enabled + + ); +} + +export function ToggleA103Fixed() { + return ( + + Enabled + + ); +} diff --git a/packages/domscribe-test-fixtures/styling/styled-app/src/components/A104-callout-typography.tsx b/packages/domscribe-test-fixtures/styling/styled-app/src/components/A104-callout-typography.tsx new file mode 100644 index 0000000..7a5a610 --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/styled-app/src/components/A104-callout-typography.tsx @@ -0,0 +1,51 @@ +/** + * A104 — styled-components typography fix (font-size + line-height). + * + * Intent: "the callout text is unreadable — make it bigger (16px) with + * comfortable line-height (1.5)". + * + * Demonstrates a multi-property typography change. The runtime + * captureStyles snapshot reports the resolved font-size and line-height, + * which is what the agent needs to confirm the after-state lines up. + */ +import styled from 'styled-components'; + +const CalloutSmall = styled.p` + font-size: 11px; + line-height: 1; + color: var(--color-fg); + background: #fefce8; + padding: 12px 16px; + border-radius: 6px; + max-width: 320px; + margin: 0; +`; + +const CalloutReadable = styled.p` + font-size: 16px; + line-height: 1.5; + color: var(--color-fg); + background: #fefce8; + padding: 12px 16px; + border-radius: 6px; + max-width: 320px; + margin: 0; +`; + +export function CalloutA104() { + return ( + + Pro tip — you can press ? at any time to see all keyboard + shortcuts. + + ); +} + +export function CalloutA104Fixed() { + return ( + + Pro tip — you can press ? at any time to see all keyboard + shortcuts. + + ); +} diff --git a/packages/domscribe-test-fixtures/styling/styled-app/src/components/A105-stack-gap.tsx b/packages/domscribe-test-fixtures/styling/styled-app/src/components/A105-stack-gap.tsx new file mode 100644 index 0000000..29f9bbd --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/styled-app/src/components/A105-stack-gap.tsx @@ -0,0 +1,44 @@ +/** + * A105 — styled-components flex-gap annotation. + * + * Intent: "the cards in the stack are touching — add gap: 12px so they + * have consistent spacing". + */ +import styled from 'styled-components'; + +const StackTight = styled.div` + display: flex; + flex-direction: column; + width: 240px; +`; + +const StackSpaced = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + width: 240px; +`; + +const Card = styled.div` + background: var(--color-surface); + border: 1px solid #e2e8f0; + border-radius: 6px; + padding: 12px 16px; + color: var(--color-fg); +`; + +const CARDS = ( + <> + Q3 metrics + Onboarding queue + Retention dashboard + +); + +export function StackA105() { + return {CARDS}; +} + +export function StackA105Fixed() { + return {CARDS}; +} diff --git a/packages/domscribe-test-fixtures/styling/styled-app/src/main.tsx b/packages/domscribe-test-fixtures/styling/styled-app/src/main.tsx new file mode 100644 index 0000000..de05965 --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/styled-app/src/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/packages/domscribe-test-fixtures/styling/styled-app/tsconfig.json b/packages/domscribe-test-fixtures/styling/styled-app/tsconfig.json new file mode 100644 index 0000000..a075817 --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/styled-app/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "jsx": "react-jsx", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "allowImportingTsExtensions": true, + "noEmit": true + }, + "include": ["src", "vite.config.ts"] +} diff --git a/packages/domscribe-test-fixtures/styling/styled-app/vite.config.ts b/packages/domscribe-test-fixtures/styling/styled-app/vite.config.ts new file mode 100644 index 0000000..05a8dfe --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/styled-app/vite.config.ts @@ -0,0 +1,18 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [react()], + base: './', + build: { + outDir: 'dist', + sourcemap: false, + rollupOptions: { + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name][extname]', + }, + }, + }, +}); diff --git a/packages/domscribe-test-fixtures/styling/tailwind-app/index.html b/packages/domscribe-test-fixtures/styling/tailwind-app/index.html new file mode 100644 index 0000000..4f29b87 --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/tailwind-app/index.html @@ -0,0 +1,31 @@ + + + + + + Domscribe styling fixture — Tailwind + + + +
+ + + diff --git a/packages/domscribe-test-fixtures/styling/tailwind-app/package.json b/packages/domscribe-test-fixtures/styling/tailwind-app/package.json new file mode 100644 index 0000000..900ba9b --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/tailwind-app/package.json @@ -0,0 +1,24 @@ +{ + "name": "@domscribe/styling-fixture-tailwind", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "preview": "vite preview --port 4801 --strictPort" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.6.3", + "vite": "^5.4.11" + } +} diff --git a/packages/domscribe-test-fixtures/styling/tailwind-app/postcss.config.js b/packages/domscribe-test-fixtures/styling/tailwind-app/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/tailwind-app/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/packages/domscribe-test-fixtures/styling/tailwind-app/src/App.tsx b/packages/domscribe-test-fixtures/styling/tailwind-app/src/App.tsx new file mode 100644 index 0000000..1634484 --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/tailwind-app/src/App.tsx @@ -0,0 +1,74 @@ +/** + * Tailwind styling fixture — entry point. + * + * Each annotation has a deterministic route under /A###/before and /A###/after. + * The "before" route renders the component as initially shipped; "after" + * renders the canonical correct fix. The falsifier diffs an + * agent-edit-applied screenshot against the /after route's baseline. + * + * Routing is hash-based and self-contained — no react-router dependency + * keeps the fixture surface small (pixel-diff stability is inversely + * proportional to surface area). + */ +import { useEffect, useState } from 'react'; +import { CardA001, CardA001Fixed } from './components/A001-padding'; +import { AlertA002, AlertA002Fixed } from './components/A002-color-token'; +import { ButtonA003, ButtonA003Fixed } from './components/A003-rounded'; +import { HeadingA004, HeadingA004Fixed } from './components/A004-font-weight'; +import { PanelA005, PanelA005Fixed } from './components/A005-gap'; + +interface Route { + id: string; + Component: () => JSX.Element; +} + +const routes: Record = { + 'A001/before': { id: 'A001-before', Component: CardA001 }, + 'A001/after': { id: 'A001-after', Component: CardA001Fixed }, + 'A002/before': { id: 'A002-before', Component: AlertA002 }, + 'A002/after': { id: 'A002-after', Component: AlertA002Fixed }, + 'A003/before': { id: 'A003-before', Component: ButtonA003 }, + 'A003/after': { id: 'A003-after', Component: ButtonA003Fixed }, + 'A004/before': { id: 'A004-before', Component: HeadingA004 }, + 'A004/after': { id: 'A004-after', Component: HeadingA004Fixed }, + 'A005/before': { id: 'A005-before', Component: PanelA005 }, + 'A005/after': { id: 'A005-after', Component: PanelA005Fixed }, +}; + +export function App() { + const [hash, setHash] = useState(() => window.location.hash.slice(1)); + + useEffect(() => { + const onChange = () => setHash(window.location.hash.slice(1)); + window.addEventListener('hashchange', onChange); + return () => window.removeEventListener('hashchange', onChange); + }, []); + + const route = routes[hash]; + + if (!route) { + return ( +
+

Tailwind styling fixture

+
    + {Object.keys(routes).map((path) => ( +
  • + + #{path} + +
  • + ))} +
+
+ ); + } + + return ( +
+ +
+ ); +} diff --git a/packages/domscribe-test-fixtures/styling/tailwind-app/src/components/A001-padding.tsx b/packages/domscribe-test-fixtures/styling/tailwind-app/src/components/A001-padding.tsx new file mode 100644 index 0000000..c6ee37a --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/tailwind-app/src/components/A001-padding.tsx @@ -0,0 +1,53 @@ +/** + * A001 — Tailwind padding annotation. + * + * Intent: "the card padding is too tight, make it match the design (32px / p-8)". + * + * The pair shows the classic Tailwind utility-class fix: bump `p-2` to `p-8`. + * The runtime captureStyles snapshot will record `padding: 8px` vs `32px` — + * the ground truth that lets an agent confirm the change took without + * having to re-render mentally. + */ +import { type ReactNode } from 'react'; + +function CardShell({ + children, + padding, + testid, +}: { + children: ReactNode; + padding: string; + testid: string; +}) { + return ( +
+
+ {children} +
+
+ ); +} + +const CARD_BODY = ( + <> +

Order #1042

+

Two items · ships Friday

+ +); + +export function CardA001() { + // BEFORE: p-2 — visibly cramped against the slate background. + return ( + + {CARD_BODY} + + ); +} + +export function CardA001Fixed() { + return ( + + {CARD_BODY} + + ); +} diff --git a/packages/domscribe-test-fixtures/styling/tailwind-app/src/components/A002-color-token.tsx b/packages/domscribe-test-fixtures/styling/tailwind-app/src/components/A002-color-token.tsx new file mode 100644 index 0000000..433a3e3 --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/tailwind-app/src/components/A002-color-token.tsx @@ -0,0 +1,48 @@ +/** + * A002 — Tailwind color-token annotation. + * + * Intent: "the alert is too generic — use the brand color from the design + * system instead of `text-red-500`". + * + * The brand color is exposed both as a Tailwind theme extension + * (`text-brand-accent`) AND as a `--color-accent` CSS var on `:root`. The + * runtime captureStyles snapshot exposes both — agent should prefer the + * token name (`brand.accent`) over the hex. + */ +import { type ReactNode } from 'react'; + +function AlertShell({ + children, + color, +}: { + children: ReactNode; + color: string; +}) { + return ( +
+ + {children} +
+ ); +} + +export function AlertA002() { + // BEFORE: text-red-500 — generic Tailwind palette, not the design token. + return ( + + Your trial expires in 3 days. + + ); +} + +export function AlertA002Fixed() { + // AFTER: text-brand-accent — resolves to var(--color-accent). + return ( + + Your trial expires in 3 days. + + ); +} diff --git a/packages/domscribe-test-fixtures/styling/tailwind-app/src/components/A003-rounded.tsx b/packages/domscribe-test-fixtures/styling/tailwind-app/src/components/A003-rounded.tsx new file mode 100644 index 0000000..e7ee581 --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/tailwind-app/src/components/A003-rounded.tsx @@ -0,0 +1,29 @@ +/** + * A003 — Tailwind border-radius annotation. + * + * Intent: "the button corners look square — make them rounded-full like the + * rest of our action buttons". + * + * Demonstrates a conditional-class transformation: the `rounded` utility + * needs to be swapped, not added. Runtime captureStyles will report + * `border-radius: 4px` vs `border-radius: 9999px`. + */ +function ButtonShell({ rounded }: { rounded: string }) { + return ( + + ); +} + +export function ButtonA003() { + return ; +} + +export function ButtonA003Fixed() { + return ; +} diff --git a/packages/domscribe-test-fixtures/styling/tailwind-app/src/components/A004-font-weight.tsx b/packages/domscribe-test-fixtures/styling/tailwind-app/src/components/A004-font-weight.tsx new file mode 100644 index 0000000..9cf52be --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/tailwind-app/src/components/A004-font-weight.tsx @@ -0,0 +1,21 @@ +/** + * A004 — Tailwind typography (font-weight) annotation. + * + * Intent: "the section heading is too light — bump it to font-bold to + * match the page hierarchy". + */ +function HeadingShell({ weight }: { weight: string }) { + return ( +

+ Recent activity +

+ ); +} + +export function HeadingA004() { + return ; +} + +export function HeadingA004Fixed() { + return ; +} diff --git a/packages/domscribe-test-fixtures/styling/tailwind-app/src/components/A005-gap.tsx b/packages/domscribe-test-fixtures/styling/tailwind-app/src/components/A005-gap.tsx new file mode 100644 index 0000000..944a594 --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/tailwind-app/src/components/A005-gap.tsx @@ -0,0 +1,29 @@ +/** + * A005 — Tailwind layout-gap annotation. + * + * Intent: "the items in the panel are touching — add gap-4 so they + * breathe". + * + * Tests layout-spacing fixes (flexbox gap). Runtime captureStyles will + * record `gap: normal` vs `gap: 16px`. + */ +function PanelShell({ gap }: { gap: string }) { + return ( +
+ New + Beta + Pro +
+ ); +} + +export function PanelA005() { + return ; +} + +export function PanelA005Fixed() { + return ; +} diff --git a/packages/domscribe-test-fixtures/styling/tailwind-app/src/index.css b/packages/domscribe-test-fixtures/styling/tailwind-app/src/index.css new file mode 100644 index 0000000..c69b7c2 --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/tailwind-app/src/index.css @@ -0,0 +1,10 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --color-fg: rgb(15, 23, 42); + --color-accent: rgb(59, 130, 246); + --space-sm: 0.5rem; + --space-md: 1rem; +} diff --git a/packages/domscribe-test-fixtures/styling/tailwind-app/src/main.tsx b/packages/domscribe-test-fixtures/styling/tailwind-app/src/main.tsx new file mode 100644 index 0000000..b31332f --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/tailwind-app/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import { App } from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/packages/domscribe-test-fixtures/styling/tailwind-app/tailwind.config.js b/packages/domscribe-test-fixtures/styling/tailwind-app/tailwind.config.js new file mode 100644 index 0000000..0fb735d --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/tailwind-app/tailwind.config.js @@ -0,0 +1,15 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{ts,tsx}'], + theme: { + extend: { + colors: { + brand: { + DEFAULT: 'rgb(15, 23, 42)', + accent: 'rgb(59, 130, 246)', + }, + }, + }, + }, + plugins: [], +}; diff --git a/packages/domscribe-test-fixtures/styling/tailwind-app/tsconfig.json b/packages/domscribe-test-fixtures/styling/tailwind-app/tsconfig.json new file mode 100644 index 0000000..a075817 --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/tailwind-app/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "jsx": "react-jsx", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "allowImportingTsExtensions": true, + "noEmit": true + }, + "include": ["src", "vite.config.ts"] +} diff --git a/packages/domscribe-test-fixtures/styling/tailwind-app/vite.config.ts b/packages/domscribe-test-fixtures/styling/tailwind-app/vite.config.ts new file mode 100644 index 0000000..5a1a7d5 --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/tailwind-app/vite.config.ts @@ -0,0 +1,20 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [react()], + base: './', + build: { + outDir: 'dist', + sourcemap: false, + rollupOptions: { + output: { + // Deterministic asset names so the falsifier's static-serve URLs + // don't shift between builds — pixel-diff stability depends on this. + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name][extname]', + }, + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80a0f05..c4b9387 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -299,6 +299,86 @@ importers: '@playwright/test': specifier: ^1.49.0 version: 1.58.2 + '@types/pixelmatch': + specifier: ^5.2.6 + version: 5.2.6 + '@types/pngjs': + specifier: ^6.0.5 + version: 6.0.5 + pixelmatch: + specifier: ^7.1.0 + version: 7.2.0 + playwright: + specifier: ^1.49.0 + version: 1.58.2 + pngjs: + specifier: ^7.0.0 + version: 7.0.0 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + + packages/domscribe-test-fixtures/styling/styled-app: + dependencies: + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + styled-components: + specifier: ^6.1.13 + version: 6.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.3.0 + version: 18.3.26 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.26) + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@5.4.21(@types/node@25.3.3)(terser@5.44.0)) + typescript: + specifier: ^5.6.3 + version: 5.9.3 + vite: + specifier: ^5.4.11 + version: 5.4.21(@types/node@25.3.3)(terser@5.44.0) + + packages/domscribe-test-fixtures/styling/tailwind-app: + dependencies: + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.3.0 + version: 18.3.26 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.26) + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@5.4.21(@types/node@25.3.3)(terser@5.44.0)) + autoprefixer: + specifier: ^10.4.20 + version: 10.4.27(postcss@8.5.14) + postcss: + specifier: ^8.4.49 + version: 8.5.14 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.19(tsx@4.21.0)(yaml@2.9.0) + typescript: + specifier: ^5.6.3 + version: 5.9.3 + vite: + specifier: ^5.4.11 + version: 5.4.21(@types/node@25.3.3)(terser@5.44.0) packages/domscribe-transform: dependencies: @@ -385,6 +465,10 @@ packages: '@acemir/cssom@0.9.31': resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + '@asamuzakjp/css-color@5.0.1': resolution: {integrity: sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -462,6 +546,10 @@ packages: resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + '@babel/helper-remap-async-to-generator@7.27.1': resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} engines: {node: '>=6.9.0'} @@ -825,6 +913,18 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-regenerator@7.29.0': resolution: {integrity: sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==} engines: {node: '>=6.9.0'} @@ -1028,102 +1128,210 @@ packages: '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emotion/is-prop-valid@1.4.0': + resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.27.3': resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.27.3': resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.27.3': resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.27.3': resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.27.3': resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.27.3': resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.27.3': resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.27.3': resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.27.3': resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.27.3': resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.27.3': resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.27.3': resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.27.3': resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.27.3': resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.27.3': resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.27.3': resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.27.3': resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} engines: {node: '>=18'} @@ -1136,6 +1344,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.27.3': resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} engines: {node: '>=18'} @@ -1148,6 +1362,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.27.3': resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} engines: {node: '>=18'} @@ -1160,24 +1380,48 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.27.3': resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.27.3': resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.27.3': resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.27.3': resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} engines: {node: '>=18'} @@ -2314,6 +2558,9 @@ packages: '@poppinss/exception@1.2.3': resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rolldown/pluginutils@1.0.0-rc.2': resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} @@ -2678,6 +2925,18 @@ packages: '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -2714,9 +2973,20 @@ packages: '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + '@types/pixelmatch@5.2.6': + resolution: {integrity: sha512-wC83uexE5KGuUODn6zkm9gMzTwdY5L0chiK+VrKcDfEjzxh1uadlWTvOmAbCpnM9zx/Ww3f8uKlYQVnO/TrqVg==} + + '@types/pngjs@6.0.5': + resolution: {integrity: sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==} + '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + '@types/react@18.3.26': resolution: {integrity: sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==} @@ -2886,6 +3156,12 @@ packages: resolution: {integrity: sha512-EgyazlL0VejvZqWZ6KL3ig27Yl8RXcwhz1hayuqeAIxaqbsnmSmogL2zKXgGnm9y/A6QkPfZH1BcQoUh1STvtQ==} engines: {node: '>=18'} + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitejs/plugin-vue-jsx@5.1.4': resolution: {integrity: sha512-70LmoVk9riR7qc4W2CpjsbNMWTPnuZb9dpFKX1emru0yP57nsc9k8nhLA6U93ngQapv5VDIUq2JatNfLbBIkrA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3281,6 +3557,9 @@ packages: resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} engines: {node: '>= 14'} + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -3426,6 +3705,10 @@ packages: bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -3532,6 +3815,10 @@ packages: camel-case@4.1.2: resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} @@ -3549,6 +3836,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -3654,6 +3945,10 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + commander@8.3.0: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} @@ -3838,9 +4133,6 @@ packages: resolution: {integrity: sha512-Ml4fP2UT2K3CUBQnVlbdV/8aFDdlY69E+YnwJM+3VUWl08S3J8c8aRuJqCkD9Py8DHZ7zNNvsfKl8psocHZEFg==} engines: {node: '>=20'} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -3982,10 +4274,16 @@ packages: devalue@5.6.3: resolution: {integrity: sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==} + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + diff@8.0.3: resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} engines: {node: '>=0.3.1'} + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dom-converter@0.2.0: resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} @@ -4139,6 +4437,11 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.27.3: resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} @@ -4763,6 +5066,10 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} @@ -4900,6 +5207,10 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -5496,6 +5807,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -5699,6 +6014,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + pify@3.0.0: resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} engines: {node: '>=4'} @@ -5727,6 +6046,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pixelmatch@7.2.0: + resolution: {integrity: sha512-xhcb4yHu9sM/G7foGzoLtXYcC0zHEaOXXjRKhGup0fw78Nf2Tkiapv4EQyMzrbcmQPsllAI7DbFY2UT7PlI9Pg==} + hasBin: true + pkce-challenge@5.0.1: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} @@ -5747,6 +6070,10 @@ packages: engines: {node: '>=18'} hasBin: true + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + portfinder@1.0.38: resolution: {integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==} engines: {node: '>= 10.12'} @@ -5793,6 +6120,36 @@ packages: peerDependencies: postcss: ^8.4.32 + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + postcss-merge-longhand@7.0.5: resolution: {integrity: sha512-Kpu5v4Ys6QI59FxmxtNB/iHUVDn9Y9sYw66D6+SZoIk4QTz1prC4aYkhIESu+ieG1iylod1f8MILMs1Em3mmIw==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} @@ -5829,6 +6186,12 @@ packages: peerDependencies: postcss: ^8.4.32 + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + postcss-normalize-charset@7.0.1: resolution: {integrity: sha512-sn413ofhSQHlZFae//m9FTOfkmiZ+YQXsbosqOWRiVQncU2BA3daX3n0VF3cG6rGLSFVc5Di/yns0dFfh8NFgQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} @@ -5901,6 +6264,10 @@ packages: peerDependencies: postcss: ^8.4.32 + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + postcss-selector-parser@7.1.1: resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} engines: {node: '>=4'} @@ -6037,6 +6404,11 @@ packages: rc9@3.0.0: resolution: {integrity: sha512-MGOue0VqscKWQ104udASX/3GYDcKyPI4j4F8gu/jHHzglpmy9a/anZK3PNe8ug6aZFl+9GxLtdhe3kVZuMaQbA==} + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: @@ -6045,6 +6417,10 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -6053,6 +6429,9 @@ packages: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -6067,6 +6446,10 @@ packages: readdir-glob@1.1.3: resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -6225,6 +6608,9 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -6473,6 +6859,22 @@ packages: structured-clone-es@1.0.0: resolution: {integrity: sha512-FL8EeKFFyNQv5cMnXI31CIMCsFarSVI2bF0U0ImeNE3g/F1IvJQyqzOXxPBRXiwQfyBTlbNe88jh1jFW0O/jiQ==} + styled-components@6.4.2: + resolution: {integrity: sha512-xZBhBJsMtGqb+aKcwKgaT+BtuFums9VynX2JRvXJGTx5UfZzN12rk5r4nVdhXYvRw+hE7yiYxVrOqJZaK2+Txg==} + engines: {node: '>= 16'} + peerDependencies: + css-to-react-native: '>= 3.2.0' + react: '>= 16.8.0' + react-dom: '>= 16.8.0' + react-native: '>= 0.68.0' + peerDependenciesMeta: + css-to-react-native: + optional: true + react-dom: + optional: true + react-native: + optional: true + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -6492,6 +6894,14 @@ packages: peerDependencies: postcss: ^8.4.32 + stylis@4.3.6: + resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} @@ -6524,6 +6934,11 @@ packages: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + tapable@2.2.3: resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==} engines: {node: '>=6'} @@ -6660,6 +7075,9 @@ packages: peerDependencies: typescript: '>=4.8.4' + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + tsconfig-paths@4.2.0: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} @@ -7029,6 +7447,37 @@ packages: vite: ^6.0.0 || ^7.0.0 vue: ^3.5.0 + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + vite@7.3.1: resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -7325,6 +7774,8 @@ snapshots: '@acemir/cssom@0.9.31': {} + '@alloc/quick-lru@5.2.0': {} + '@asamuzakjp/css-color@5.0.1': dependencies: '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) @@ -7453,6 +7904,8 @@ snapshots: '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-plugin-utils@7.29.7': {} + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -7854,6 +8307,16 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -8158,81 +8621,156 @@ snapshots: dependencies: tslib: 2.8.1 + '@emotion/is-prop-valid@1.4.0': + dependencies: + '@emotion/memoize': 0.9.0 + + '@emotion/memoize@0.9.0': {} + + '@esbuild/aix-ppc64@0.21.5': + optional: true + '@esbuild/aix-ppc64@0.27.3': optional: true + '@esbuild/android-arm64@0.21.5': + optional: true + '@esbuild/android-arm64@0.27.3': optional: true + '@esbuild/android-arm@0.21.5': + optional: true + '@esbuild/android-arm@0.27.3': optional: true + '@esbuild/android-x64@0.21.5': + optional: true + '@esbuild/android-x64@0.27.3': optional: true + '@esbuild/darwin-arm64@0.21.5': + optional: true + '@esbuild/darwin-arm64@0.27.3': optional: true + '@esbuild/darwin-x64@0.21.5': + optional: true + '@esbuild/darwin-x64@0.27.3': optional: true + '@esbuild/freebsd-arm64@0.21.5': + optional: true + '@esbuild/freebsd-arm64@0.27.3': optional: true + '@esbuild/freebsd-x64@0.21.5': + optional: true + '@esbuild/freebsd-x64@0.27.3': optional: true + '@esbuild/linux-arm64@0.21.5': + optional: true + '@esbuild/linux-arm64@0.27.3': optional: true + '@esbuild/linux-arm@0.21.5': + optional: true + '@esbuild/linux-arm@0.27.3': optional: true + '@esbuild/linux-ia32@0.21.5': + optional: true + '@esbuild/linux-ia32@0.27.3': optional: true + '@esbuild/linux-loong64@0.21.5': + optional: true + '@esbuild/linux-loong64@0.27.3': optional: true + '@esbuild/linux-mips64el@0.21.5': + optional: true + '@esbuild/linux-mips64el@0.27.3': optional: true + '@esbuild/linux-ppc64@0.21.5': + optional: true + '@esbuild/linux-ppc64@0.27.3': optional: true + '@esbuild/linux-riscv64@0.21.5': + optional: true + '@esbuild/linux-riscv64@0.27.3': optional: true + '@esbuild/linux-s390x@0.21.5': + optional: true + '@esbuild/linux-s390x@0.27.3': optional: true + '@esbuild/linux-x64@0.21.5': + optional: true + '@esbuild/linux-x64@0.27.3': optional: true '@esbuild/netbsd-arm64@0.27.3': optional: true + '@esbuild/netbsd-x64@0.21.5': + optional: true + '@esbuild/netbsd-x64@0.27.3': optional: true '@esbuild/openbsd-arm64@0.27.3': optional: true + '@esbuild/openbsd-x64@0.21.5': + optional: true + '@esbuild/openbsd-x64@0.27.3': optional: true '@esbuild/openharmony-arm64@0.27.3': optional: true + '@esbuild/sunos-x64@0.21.5': + optional: true + '@esbuild/sunos-x64@0.27.3': optional: true + '@esbuild/win32-arm64@0.21.5': + optional: true + '@esbuild/win32-arm64@0.27.3': optional: true + '@esbuild/win32-ia32@0.21.5': + optional: true + '@esbuild/win32-ia32@0.27.3': optional: true + '@esbuild/win32-x64@0.21.5': + optional: true + '@esbuild/win32-x64@0.27.3': optional: true @@ -9425,6 +9963,8 @@ snapshots: '@poppinss/exception@1.2.3': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rolldown/pluginutils@1.0.0-rc.2': {} '@rolldown/pluginutils@1.0.0-rc.7': {} @@ -9719,6 +10259,27 @@ snapshots: '@types/argparse@1.0.38': {} + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -9758,18 +10319,30 @@ snapshots: '@types/parse-json@4.0.2': {} + '@types/pixelmatch@5.2.6': + dependencies: + '@types/node': 25.3.3 + + '@types/pngjs@6.0.5': + dependencies: + '@types/node': 25.3.3 + '@types/prop-types@15.7.15': {} + '@types/react-dom@18.3.7(@types/react@18.3.26)': + dependencies: + '@types/react': 18.3.26 + '@types/react@18.3.26': dependencies: '@types/prop-types': 15.7.15 - csstype: 3.1.3 + csstype: 3.2.3 '@types/resolve@1.20.2': {} '@types/responselike@1.0.0': dependencies: - '@types/node': 20.19.19 + '@types/node': 25.3.3 '@types/semver@7.7.1': {} @@ -10063,6 +10636,18 @@ snapshots: lodash: 4.17.21 minimatch: 7.4.6 + '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@25.3.3)(terser@5.44.0))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.21(@types/node@25.3.3)(terser@5.44.0) + transitivePeerDependencies: + - supports-color + '@vitejs/plugin-vue-jsx@5.1.4(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.29(typescript@5.9.3))': dependencies: '@babel/core': 7.29.0 @@ -10324,7 +10909,7 @@ snapshots: '@vue/reactivity': 3.5.25 '@vue/runtime-core': 3.5.25 '@vue/shared': 3.5.25 - csstype: 3.1.3 + csstype: 3.2.3 '@vue/runtime-dom@3.5.29': dependencies: @@ -10587,6 +11172,8 @@ snapshots: - bare-abort-controller - react-native-b4a + arg@5.0.2: {} + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -10733,6 +11320,8 @@ snapshots: dependencies: require-from-string: 2.0.2 + binary-extensions@2.3.0: {} + bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -10882,6 +11471,8 @@ snapshots: pascal-case: 3.1.2 tslib: 2.8.1 + camelcase-css@2.0.1: {} + caniuse-api@3.0.0: dependencies: browserslist: 4.28.1 @@ -10900,6 +11491,18 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -10996,6 +11599,8 @@ snapshots: commander@2.20.3: {} + commander@4.1.1: {} + commander@8.3.0: {} commondir@1.0.1: {} @@ -11202,8 +11807,6 @@ snapshots: css-tree: 3.1.0 lru-cache: 11.2.6 - csstype@3.1.3: {} - csstype@3.2.3: {} dashdash@1.14.1: @@ -11287,8 +11890,12 @@ snapshots: devalue@5.6.3: {} + didyoumean@1.2.2: {} + diff@8.0.3: {} + dlv@1.1.3: {} + dom-converter@0.2.0: dependencies: utila: 0.4.0 @@ -11443,6 +12050,32 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + esbuild@0.27.3: optionalDependencies: '@esbuild/aix-ppc64': 0.27.3 @@ -12236,6 +12869,10 @@ snapshots: is-arrayish@0.2.1: {} + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + is-core-module@2.16.1: dependencies: hasown: 2.0.2 @@ -12345,10 +12982,12 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 20.19.19 + '@types/node': 25.3.3 merge-stream: 2.0.0 supports-color: 8.1.1 + jiti@1.21.7: {} + jiti@2.6.1: {} jju@1.4.0: {} @@ -13161,6 +13800,8 @@ snapshots: object-assign@4.1.1: {} + object-hash@3.0.0: {} + object-inspect@1.13.4: {} obliterator@2.0.5: {} @@ -13432,6 +14073,8 @@ snapshots: picomatch@4.0.3: {} + pify@2.3.0: {} + pify@3.0.0: {} pino-abstract-transport@1.2.0: @@ -13479,6 +14122,10 @@ snapshots: pirates@4.0.7: {} + pixelmatch@7.2.0: + dependencies: + pngjs: 7.0.0 + pkce-challenge@5.0.1: {} pkg-types@1.3.1: @@ -13501,6 +14148,8 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + pngjs@7.0.0: {} + portfinder@1.0.38: dependencies: async: 3.2.6 @@ -13545,6 +14194,27 @@ snapshots: dependencies: postcss: 8.5.14 + postcss-import@15.1.0(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-js@4.1.0(postcss@8.5.14): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.14 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.14)(tsx@4.21.0)(yaml@2.9.0): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.14 + tsx: 4.21.0 + yaml: 2.9.0 + postcss-merge-longhand@7.0.5(postcss@8.5.14): dependencies: postcss: 8.5.14 @@ -13584,6 +14254,11 @@ snapshots: postcss: 8.5.14 postcss-selector-parser: 7.1.1 + postcss-nested@6.2.0(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + postcss-selector-parser: 6.1.2 + postcss-normalize-charset@7.0.1(postcss@8.5.14): dependencies: postcss: 8.5.14 @@ -13646,6 +14321,11 @@ snapshots: postcss: 8.5.14 postcss-value-parser: 4.2.0 + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss-selector-parser@7.1.1: dependencies: cssesc: 3.0.0 @@ -13786,6 +14466,12 @@ snapshots: defu: 6.1.4 destr: 2.0.5 + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + react-dom@19.2.4(react@19.2.4): dependencies: react: 19.2.4 @@ -13793,12 +14479,18 @@ snapshots: react-is@18.3.1: {} + react-refresh@0.17.0: {} + react@18.3.1: dependencies: loose-envify: 1.4.0 react@19.2.4: {} + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -13827,6 +14519,10 @@ snapshots: dependencies: minimatch: 5.1.9 + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + readdirp@4.1.2: {} readdirp@5.0.0: {} @@ -13985,6 +14681,10 @@ snapshots: dependencies: xmlchars: 2.2.0 + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + scheduler@0.27.0: {} schema-utils@4.3.2: @@ -14283,6 +14983,15 @@ snapshots: structured-clone-es@1.0.0: {} + styled-components@6.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@emotion/is-prop-valid': 1.4.0 + csstype: 3.2.3 + react: 18.3.1 + stylis: 4.3.6 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.4): dependencies: client-only: 0.0.1 @@ -14296,6 +15005,18 @@ snapshots: postcss: 8.5.14 postcss-selector-parser: 7.1.1 + stylis@4.3.6: {} + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + supports-color@10.2.2: {} supports-color@7.2.0: @@ -14324,6 +15045,34 @@ snapshots: tagged-tag@1.0.0: {} + tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.9.0): + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.14 + postcss-import: 15.1.0(postcss@8.5.14) + postcss-js: 4.1.0(postcss@8.5.14) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.14)(tsx@4.21.0)(yaml@2.9.0) + postcss-nested: 6.2.0(postcss@8.5.14) + postcss-selector-parser: 6.1.2 + resolve: 1.22.11 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + tapable@2.2.3: {} tar-stream@2.2.0: @@ -14455,6 +15204,8 @@ snapshots: dependencies: typescript: 5.9.3 + ts-interface-checker@0.1.13: {} + tsconfig-paths@4.2.0: dependencies: json5: 2.2.3 @@ -14860,6 +15611,16 @@ snapshots: vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.9.0) vue: 3.5.29(typescript@5.9.3) + vite@5.4.21(@types/node@25.3.3)(terser@5.44.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.14 + rollup: 4.59.0 + optionalDependencies: + '@types/node': 25.3.3 + fsevents: 2.3.3 + terser: 5.44.0 + vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: esbuild: 0.27.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c54c67c..d0ebb1b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,5 @@ packages: - packages/* - - "!packages/domscribe-test-fixtures/fixtures/**" + - '!packages/domscribe-test-fixtures/fixtures/**' + - packages/domscribe-test-fixtures/styling/tailwind-app + - packages/domscribe-test-fixtures/styling/styled-app From 0998a1fe1b426a4bb3137981eaf42dd6b2fb0b74 Mon Sep 17 00:00:00 2001 From: Domscribe Staff SWE Date: Sun, 7 Jun 2026 07:13:58 -0700 Subject: [PATCH 3/3] fix(test-fixtures): override typecheck on styling fixture apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Nx @nx/js/typescript plugin auto-generates a typecheck target that runs `tsc --build --emitDeclarationOnly`. The styling fixture apps (`@domscribe/styling-fixture-tailwind`, `@domscribe/styling-fixture-styled`) use a Vite app tsconfig with `noEmit: true` and neither `declaration` nor `composite`, so the auto-generated command fails with TS5069. The parent `domscribe-test-fixtures` already opts out of typecheck via `nx:noop`; these nested apps are separate Nx projects and didn't inherit that. Adding a minimal `project.json` for each that overrides typecheck to `tsc --noEmit` (which the PR author already verified is clean) restores the CI green light without weakening type safety. Verified locally: npx nx run-many -t lint test build typecheck --exclude domscribe-test-fixtures → Successfully ran targets lint, test, build, typecheck for 14 projects Co-Authored-By: Claude Opus 4.7 --- .../styling/styled-app/project.json | 22 +++++++++++++++++++ .../styling/tailwind-app/project.json | 22 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 packages/domscribe-test-fixtures/styling/styled-app/project.json create mode 100644 packages/domscribe-test-fixtures/styling/tailwind-app/project.json diff --git a/packages/domscribe-test-fixtures/styling/styled-app/project.json b/packages/domscribe-test-fixtures/styling/styled-app/project.json new file mode 100644 index 0000000..bbf4467 --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/styled-app/project.json @@ -0,0 +1,22 @@ +{ + "name": "@domscribe/styling-fixture-styled", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/domscribe-test-fixtures/styling/styled-app/src", + "tags": ["scope:test", "type:test", "npm:private"], + "targets": { + "typecheck": { + "cache": true, + "executor": "nx:run-commands", + "inputs": [ + "production", + "^production", + { "externalDependencies": ["typescript"] } + ], + "options": { + "command": "tsc --noEmit", + "cwd": "{projectRoot}" + } + } + } +} diff --git a/packages/domscribe-test-fixtures/styling/tailwind-app/project.json b/packages/domscribe-test-fixtures/styling/tailwind-app/project.json new file mode 100644 index 0000000..76cb880 --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/tailwind-app/project.json @@ -0,0 +1,22 @@ +{ + "name": "@domscribe/styling-fixture-tailwind", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/domscribe-test-fixtures/styling/tailwind-app/src", + "tags": ["scope:test", "type:test", "npm:private"], + "targets": { + "typecheck": { + "cache": true, + "executor": "nx:run-commands", + "inputs": [ + "production", + "^production", + { "externalDependencies": ["typescript"] } + ], + "options": { + "command": "tsc --noEmit", + "cwd": "{projectRoot}" + } + } + } +}