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'; 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 0000000..e4c3fb7 Binary files /dev/null and b/packages/domscribe-test-fixtures/styling/baselines/styled/A101.png differ diff --git a/packages/domscribe-test-fixtures/styling/baselines/styled/A102.png b/packages/domscribe-test-fixtures/styling/baselines/styled/A102.png new file mode 100644 index 0000000..efdfaa2 Binary files /dev/null and b/packages/domscribe-test-fixtures/styling/baselines/styled/A102.png differ diff --git a/packages/domscribe-test-fixtures/styling/baselines/styled/A103.png b/packages/domscribe-test-fixtures/styling/baselines/styled/A103.png new file mode 100644 index 0000000..065945a Binary files /dev/null and b/packages/domscribe-test-fixtures/styling/baselines/styled/A103.png differ 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 0000000..3c641e0 Binary files /dev/null and b/packages/domscribe-test-fixtures/styling/baselines/styled/A104.png differ 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 0000000..9884858 Binary files /dev/null and b/packages/domscribe-test-fixtures/styling/baselines/styled/A105.png differ diff --git a/packages/domscribe-test-fixtures/styling/baselines/tailwind/A001.png b/packages/domscribe-test-fixtures/styling/baselines/tailwind/A001.png new file mode 100644 index 0000000..c18cbc7 Binary files /dev/null and b/packages/domscribe-test-fixtures/styling/baselines/tailwind/A001.png differ diff --git a/packages/domscribe-test-fixtures/styling/baselines/tailwind/A002.png b/packages/domscribe-test-fixtures/styling/baselines/tailwind/A002.png new file mode 100644 index 0000000..0177404 Binary files /dev/null and b/packages/domscribe-test-fixtures/styling/baselines/tailwind/A002.png differ 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 0000000..acafcda Binary files /dev/null and b/packages/domscribe-test-fixtures/styling/baselines/tailwind/A003.png differ 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 0000000..4142b7f Binary files /dev/null and b/packages/domscribe-test-fixtures/styling/baselines/tailwind/A004.png differ diff --git a/packages/domscribe-test-fixtures/styling/baselines/tailwind/A005.png b/packages/domscribe-test-fixtures/styling/baselines/tailwind/A005.png new file mode 100644 index 0000000..7f28fd9 Binary files /dev/null and b/packages/domscribe-test-fixtures/styling/baselines/tailwind/A005.png differ diff --git a/packages/domscribe-test-fixtures/styling/scripts/falsifier.ts b/packages/domscribe-test-fixtures/styling/scripts/falsifier.ts new file mode 100644 index 0000000..1ef0d39 --- /dev/null +++ b/packages/domscribe-test-fixtures/styling/scripts/falsifier.ts @@ -0,0 +1,533 @@ +/** + * RFC 0001 falsifier harness. + * + * This is the measurement instrument for the sprint-2734 thesis: "≥70% agent + * one-shot styling completion by sprint 2734+6". It does NOT itself invoke + * an agent — it grades a directory of agent outputs (one screenshot per + * annotation) against the canonical-after baselines. + * + * Modes + * ----- + * --mode=self-test (default) + * Builds and previews both fixtures, navigates to each annotation's + * `afterRoute`, snapshots, and compares to the baseline at + * `baselines//.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/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/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/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}" + } + } + } +} 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