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 (
+
+ );
+}
+
+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 (
+
+ Continue
+
+ );
+}
+
+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