@agent-native/core 0.80.10 → 0.80.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +108 -15
- package/corpus/core/src/client/AssistantChat.tsx +123 -26
- package/corpus/core/src/client/sse-event-processor.ts +9 -0
- package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
- package/corpus/templates/assets/AGENTS.md +4 -0
- package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
- package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
- package/corpus/templates/assets/actions/generate-image.ts +118 -67
- package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
- package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
- package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
- package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
- package/corpus/templates/assets/app/i18n-data.ts +61 -0
- package/corpus/templates/assets/app/routes/_index.tsx +4 -0
- package/corpus/templates/assets/app/routes/library.tsx +145 -38
- package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
- package/corpus/templates/assets/server/lib/generation.ts +135 -11
- package/corpus/templates/assets/shared/api.ts +4 -0
- package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
- package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +13 -5
- package/corpus/templates/design/actions/apply-design-state.ts +17 -4
- package/corpus/templates/design/actions/apply-motion-edit.ts +9 -46
- package/corpus/templates/design/actions/delete-design.ts +20 -0
- package/corpus/templates/design/actions/get-component-details.ts +16 -7
- package/corpus/templates/design/actions/index-design-tokens.ts +8 -4
- package/corpus/templates/design/actions/list-design-states.ts +19 -1
- package/corpus/templates/design/actions/open-component-source.ts +7 -2
- package/corpus/templates/design/actions/preview-component-prop-edit.ts +32 -26
- package/corpus/templates/design/actions/preview-shader-fill.ts +9 -11
- package/corpus/templates/design/actions/run-design-audit.ts +21 -6
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +139 -14
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +20 -3
- package/corpus/templates/design/app/components/design/EditPanel.tsx +428 -31
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +110 -30
- package/corpus/templates/design/app/components/design/MotionDock.tsx +9 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1 -1
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +12 -5
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +4 -2
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +13 -8
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/i18n-data.ts +11 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +684 -179
- package/corpus/templates/design/changelog/2026-06-30-design-token-edits-now-stay-visible-in-previews-and-token-li.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +94 -2
- package/corpus/templates/design/shared/component-model.ts +35 -0
- package/corpus/templates/design/shared/motion-compiler.ts +79 -8
- package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +94 -10
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +14 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +115 -22
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +10 -0
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Clips AI request bridge
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Polls the `list-ai-requests` action — a single access-scoped call that returns
|
|
5
|
+
* every pending `clips-ai-request-*` entry for recordings the user can access.
|
|
6
|
+
* The bridge sends queued recording work to the agent chat exactly once per
|
|
6
7
|
* (recordingId, kind, requestedAt).
|
|
7
8
|
*
|
|
8
9
|
* Once handled we DELETE the request entry so the next page load / tab switch
|
|
@@ -21,6 +22,7 @@ import { useRecordings, type RecordingSummary } from "./use-library";
|
|
|
21
22
|
|
|
22
23
|
const DEFAULT_TITLE = "Untitled recording";
|
|
23
24
|
const POLL_INTERVAL_MS = 3000;
|
|
25
|
+
const TWO_MINUTES_MS = 2 * 60 * 1000;
|
|
24
26
|
|
|
25
27
|
/** True when `title` is blank or equal to the server-seeded default. */
|
|
26
28
|
export function isDefaultTitle(title: string | null | undefined): boolean {
|
|
@@ -62,22 +64,21 @@ const DISPATCHABLE_REQUESTS = new Set([
|
|
|
62
64
|
"generate-workflow",
|
|
63
65
|
]);
|
|
64
66
|
|
|
65
|
-
async function
|
|
66
|
-
const url = agentNativePath(
|
|
67
|
-
`/_agent-native/application-state/${encodeURIComponent(
|
|
68
|
-
`clips-ai-request-${recordingId}`,
|
|
69
|
-
)}`,
|
|
70
|
-
);
|
|
67
|
+
async function listRequests(): Promise<Map<string, AiRequest>> {
|
|
71
68
|
try {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
69
|
+
const result = (await callAction("list-ai-requests", {} as any, {
|
|
70
|
+
method: "GET",
|
|
71
|
+
})) as { requests?: AiRequest[] } | null | undefined;
|
|
72
|
+
return new Map(
|
|
73
|
+
(result?.requests ?? [])
|
|
74
|
+
.filter(
|
|
75
|
+
(r): r is AiRequest & { recordingId: string } => !!r?.recordingId,
|
|
76
|
+
)
|
|
77
|
+
.map((r) => [r.recordingId, r]),
|
|
78
|
+
);
|
|
79
79
|
} catch {
|
|
80
|
-
|
|
80
|
+
// Swallow — the next tick retries.
|
|
81
|
+
return new Map();
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -120,16 +121,15 @@ export function useAutoTitleBridge(): void {
|
|
|
120
121
|
if (cancelled || inflight.current) return;
|
|
121
122
|
inflight.current = true;
|
|
122
123
|
try {
|
|
124
|
+
const requestsById = await listRequests();
|
|
125
|
+
if (cancelled) return;
|
|
126
|
+
|
|
123
127
|
for (const rec of readyRecordings) {
|
|
124
128
|
if (cancelled) return;
|
|
125
129
|
|
|
126
|
-
const request =
|
|
130
|
+
const request = requestsById.get(rec.id) ?? null;
|
|
127
131
|
|
|
128
|
-
if (
|
|
129
|
-
request?.kind &&
|
|
130
|
-
DISPATCHABLE_REQUESTS.has(request.kind) &&
|
|
131
|
-
request.recordingId === rec.id
|
|
132
|
-
) {
|
|
132
|
+
if (request?.kind && DISPATCHABLE_REQUESTS.has(request.kind)) {
|
|
133
133
|
// Server queued a delegation — use the full context it provided.
|
|
134
134
|
// Key includes requestedAt so each distinct server request fires
|
|
135
135
|
// exactly once, independent of any prior fallback dispatch.
|
|
@@ -164,9 +164,8 @@ export function useAutoTitleBridge(): void {
|
|
|
164
164
|
continue;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (ageMs < TWO_MINUTES_MS) continue;
|
|
167
|
+
if (Date.now() - new Date(rec.createdAt).getTime() < TWO_MINUTES_MS)
|
|
168
|
+
continue;
|
|
170
169
|
|
|
171
170
|
// Use a dedicated key so a later server-queued request (e.g. from
|
|
172
171
|
// a long transcription that finishes after the 2-min window) is
|
|
@@ -49,7 +49,10 @@ import type {
|
|
|
49
49
|
ClassEditIntent,
|
|
50
50
|
StyleEditIntent,
|
|
51
51
|
} from "../shared/code-layer.js";
|
|
52
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
componentNameFor,
|
|
54
|
+
componentNodeIdMatches,
|
|
55
|
+
} from "../shared/component-model.js";
|
|
53
56
|
import { hasCapability } from "../shared/design-source-capabilities.js";
|
|
54
57
|
import { normalizeDesignSourceType } from "../shared/source-mode.js";
|
|
55
58
|
|
|
@@ -296,7 +299,9 @@ export default defineAction({
|
|
|
296
299
|
source: codeLayerSource,
|
|
297
300
|
});
|
|
298
301
|
|
|
299
|
-
const node = projection.nodes.find((n) =>
|
|
302
|
+
const node = projection.nodes.find((n) =>
|
|
303
|
+
componentNodeIdMatches(n, nodeId),
|
|
304
|
+
);
|
|
300
305
|
if (!node) {
|
|
301
306
|
throw new Error(
|
|
302
307
|
`Node "${nodeId}" not found. Run get-code-layer-projection to list current ids.`,
|
|
@@ -398,7 +403,9 @@ export default defineAction({
|
|
|
398
403
|
}
|
|
399
404
|
|
|
400
405
|
// ── Persist ──────────────────────────────────────────────────────────────
|
|
401
|
-
|
|
406
|
+
const shouldPersist = changed || patchedContent !== (file.content ?? "");
|
|
407
|
+
|
|
408
|
+
if (shouldPersist) {
|
|
402
409
|
await persistEdit({
|
|
403
410
|
id: file.id,
|
|
404
411
|
designId: file.designId,
|
|
@@ -419,13 +426,14 @@ export default defineAction({
|
|
|
419
426
|
componentName,
|
|
420
427
|
sourceType,
|
|
421
428
|
editKind: edit.kind,
|
|
422
|
-
persisted:
|
|
429
|
+
persisted: shouldPersist,
|
|
423
430
|
ctaRequired: false,
|
|
424
431
|
fileId: file.id,
|
|
425
432
|
filename: file.filename,
|
|
433
|
+
content: patchedContent,
|
|
426
434
|
bytesBefore: html.length,
|
|
427
435
|
bytesAfter: patchedContent.length,
|
|
428
|
-
note:
|
|
436
|
+
note: shouldPersist
|
|
429
437
|
? "Edit applied and persisted via the deterministic HTML-patch path."
|
|
430
438
|
: "No change applied — the edit produced the same result as the current content.",
|
|
431
439
|
};
|
|
@@ -152,10 +152,23 @@ export default defineAction({
|
|
|
152
152
|
if (route !== undefined) patch.route = route;
|
|
153
153
|
if (fixtureData !== undefined) {
|
|
154
154
|
// Sanitise replayed markup (stored-XSS guard) before persisting.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
155
|
+
if (fixtureData !== null) {
|
|
156
|
+
const fixtureDataJson = JSON.stringify(
|
|
157
|
+
sanitizeStatePayload(fixtureData),
|
|
158
|
+
);
|
|
159
|
+
if (
|
|
160
|
+
Buffer.byteLength(fixtureDataJson, "utf8") > CAPTURE_DATA_MAX_BYTES
|
|
161
|
+
) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`fixtureData exceeds the ${Math.round(
|
|
164
|
+
CAPTURE_DATA_MAX_BYTES / 1024,
|
|
165
|
+
)}KB limit. Trim the payload before updating this fixture.`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
patch.fixtureData = fixtureDataJson;
|
|
169
|
+
} else {
|
|
170
|
+
patch.fixtureData = null;
|
|
171
|
+
}
|
|
159
172
|
}
|
|
160
173
|
if (captureData !== undefined) {
|
|
161
174
|
if (captureData !== null) {
|
|
@@ -36,7 +36,11 @@ import { z } from "zod";
|
|
|
36
36
|
|
|
37
37
|
import { getDb, schema } from "../server/db/index.js";
|
|
38
38
|
import "../server/db/index.js"; // ensure registerShareableResource runs
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
assertSafeMotionCssProperty,
|
|
41
|
+
assertSafeMotionCssToken,
|
|
42
|
+
compile,
|
|
43
|
+
} from "../shared/motion-compiler.js";
|
|
40
44
|
import type { MotionTrack } from "../shared/motion-timeline.js";
|
|
41
45
|
|
|
42
46
|
// ─── Zod schemas ─────────────────────────────────────────────────────────────
|
|
@@ -85,47 +89,6 @@ const MOTION_STYLE_CLOSE = "</style>";
|
|
|
85
89
|
*/
|
|
86
90
|
const STYLE_CLOSE_RE = /<\s*\/\s*style\b[^>]*>/i;
|
|
87
91
|
|
|
88
|
-
/**
|
|
89
|
-
* Reject `<`, `>`, and any `</style` sequence in a CSS token before it is
|
|
90
|
-
* compiled into the managed `<style>` block. Keyframe `value` / `ease` come
|
|
91
|
-
* straight from the caller and are interpolated raw into the CSS, so an
|
|
92
|
-
* unescaped `</style>` (or angle bracket) would break out of the style block
|
|
93
|
-
* and inject arbitrary markup. Returns the original string when safe; throws on
|
|
94
|
-
* a breakout attempt so the whole atomic write fails loudly rather than
|
|
95
|
-
* persisting a poisoned block.
|
|
96
|
-
*/
|
|
97
|
-
function assertSafeCssToken(value: string, field: string): string {
|
|
98
|
-
if (/[<>]/.test(value) || /<\s*\/\s*style/i.test(value)) {
|
|
99
|
-
throw new Error(
|
|
100
|
-
`Invalid ${field}: "<", ">", and "</style" are not allowed in motion CSS values.`,
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
return value;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Validate that a CSS property name is a safe CSS identifier.
|
|
108
|
-
*
|
|
109
|
-
* Accepts standard and vendor-prefixed property names (e.g. "opacity",
|
|
110
|
-
* "transform", "-webkit-transform") and nothing else. Rejects any string
|
|
111
|
-
* containing `:`, `;`, `{`, `}`, `<`, `/`, whitespace, or other characters
|
|
112
|
-
* that could break out of a CSS declaration or `<style>` block context when
|
|
113
|
-
* the property is interpolated as `${property}: ${value};` inside
|
|
114
|
-
* `@keyframes`.
|
|
115
|
-
*
|
|
116
|
-
* Uses the strict CSS ident regex `^-?[a-zA-Z][a-zA-Z0-9-]*$` which covers
|
|
117
|
-
* every real animatable CSS property while blocking injection payloads.
|
|
118
|
-
*/
|
|
119
|
-
function assertSafeCssProperty(property: string, field: string): string {
|
|
120
|
-
if (!/^-?[a-zA-Z][a-zA-Z0-9-]*$/.test(property)) {
|
|
121
|
-
throw new Error(
|
|
122
|
-
`Invalid ${field}: "${property}" is not a valid CSS property identifier. ` +
|
|
123
|
-
"Only ASCII letters, digits, hyphens, and an optional leading hyphen are allowed.",
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
return property;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
92
|
/**
|
|
130
93
|
* Inject or replace the managed `<style data-agent-native-motion>` block in
|
|
131
94
|
* the HTML content. Inserts before `</head>` when not already present.
|
|
@@ -328,15 +291,15 @@ export default defineAction({
|
|
|
328
291
|
// keyframe values, and easing strings before they are compiled into the
|
|
329
292
|
// managed <style> block.
|
|
330
293
|
for (const track of typedTracks) {
|
|
331
|
-
|
|
294
|
+
assertSafeMotionCssProperty(track.property, "track.property");
|
|
332
295
|
for (const kf of track.keyframes) {
|
|
333
|
-
|
|
296
|
+
assertSafeMotionCssToken(kf.value, "keyframe value");
|
|
334
297
|
if (kf.ease !== undefined) {
|
|
335
|
-
|
|
298
|
+
assertSafeMotionCssToken(kf.ease, "keyframe ease");
|
|
336
299
|
}
|
|
337
300
|
}
|
|
338
301
|
}
|
|
339
|
-
|
|
302
|
+
assertSafeMotionCssToken(defaultEase, "defaultEase");
|
|
340
303
|
|
|
341
304
|
const { css, hash } = compile({
|
|
342
305
|
id: timelineId ?? "",
|
|
@@ -16,6 +16,26 @@ export default defineAction({
|
|
|
16
16
|
|
|
17
17
|
const db = getDb();
|
|
18
18
|
|
|
19
|
+
await db
|
|
20
|
+
.delete(schema.designShares)
|
|
21
|
+
.where(eq(schema.designShares.resourceId, id));
|
|
22
|
+
|
|
23
|
+
await db
|
|
24
|
+
.delete(schema.componentIndex)
|
|
25
|
+
.where(eq(schema.componentIndex.designId, id));
|
|
26
|
+
|
|
27
|
+
await db
|
|
28
|
+
.delete(schema.motionTimeline)
|
|
29
|
+
.where(eq(schema.motionTimeline.designId, id));
|
|
30
|
+
|
|
31
|
+
await db
|
|
32
|
+
.delete(schema.designState)
|
|
33
|
+
.where(eq(schema.designState.designId, id));
|
|
34
|
+
|
|
35
|
+
await db
|
|
36
|
+
.delete(schema.designReviewSnapshot)
|
|
37
|
+
.where(eq(schema.designReviewSnapshot.designId, id));
|
|
38
|
+
|
|
19
39
|
// Delete associated files first
|
|
20
40
|
await db
|
|
21
41
|
.delete(schema.designFiles)
|
|
@@ -27,6 +27,7 @@ import { buildCodeLayerProjection } from "../shared/code-layer.js";
|
|
|
27
27
|
import type { CodeLayerSource } from "../shared/code-layer.js";
|
|
28
28
|
import {
|
|
29
29
|
componentNameFor,
|
|
30
|
+
componentNodeIdMatches,
|
|
30
31
|
extractProps,
|
|
31
32
|
type ComponentInstance,
|
|
32
33
|
} from "../shared/component-model.js";
|
|
@@ -112,6 +113,14 @@ export default defineAction({
|
|
|
112
113
|
const caps = resolveSourceCapabilities(sourceType);
|
|
113
114
|
const canResolveToFile = hasCapability(caps, "resolveNodeToFile");
|
|
114
115
|
const hasFullIndex = hasCapability(caps, "indexComponents");
|
|
116
|
+
const canEditProps =
|
|
117
|
+
sourceType === "inline" || hasCapability(caps, "applyEdit");
|
|
118
|
+
const ctaRequired = !hasFullIndex || !canEditProps;
|
|
119
|
+
const ctaMessage = !hasFullIndex
|
|
120
|
+
? "Full prop controls (TypeScript prop types, cva variants, Storybook stories) require a connected Builder app. Connect Builder to unlock."
|
|
121
|
+
: !canEditProps
|
|
122
|
+
? "Prop write-back requires the bridge applyEdit capability. Preview controls remain available until source write hardening is enabled."
|
|
123
|
+
: undefined;
|
|
115
124
|
|
|
116
125
|
// ── Fetch design file ────────────────────────────────────────────────────
|
|
117
126
|
const conditions = [
|
|
@@ -153,7 +162,9 @@ export default defineAction({
|
|
|
153
162
|
source: codeLayerSource,
|
|
154
163
|
});
|
|
155
164
|
|
|
156
|
-
const node = projection.nodes.find((n) =>
|
|
165
|
+
const node = projection.nodes.find((n) =>
|
|
166
|
+
componentNodeIdMatches(n, nodeId),
|
|
167
|
+
);
|
|
157
168
|
if (!node) {
|
|
158
169
|
throw new Error(
|
|
159
170
|
`Node "${nodeId}" not found in projection. Run get-code-layer-projection to list current node ids.`,
|
|
@@ -180,7 +191,7 @@ export default defineAction({
|
|
|
180
191
|
props: observedProps,
|
|
181
192
|
alpineData,
|
|
182
193
|
selector: node.selector,
|
|
183
|
-
nodeId
|
|
194
|
+
nodeId,
|
|
184
195
|
};
|
|
185
196
|
|
|
186
197
|
// ── Lookup persisted component_index row ─────────────────────────────────
|
|
@@ -231,11 +242,9 @@ export default defineAction({
|
|
|
231
242
|
capabilities: {
|
|
232
243
|
canResolveToFile,
|
|
233
244
|
hasFullIndex,
|
|
234
|
-
canEditProps
|
|
235
|
-
ctaRequired
|
|
236
|
-
ctaMessage
|
|
237
|
-
? "Full prop controls (TypeScript prop types, cva variants, Storybook stories) require a connected Builder app. Connect Builder to unlock."
|
|
238
|
-
: undefined,
|
|
245
|
+
canEditProps,
|
|
246
|
+
ctaRequired,
|
|
247
|
+
ctaMessage,
|
|
239
248
|
},
|
|
240
249
|
};
|
|
241
250
|
},
|
|
@@ -7,7 +7,10 @@ import { z } from "zod";
|
|
|
7
7
|
import { getDb, schema } from "../server/db/index.js";
|
|
8
8
|
import "../server/db/index.js"; // ensure registerShareableResource runs
|
|
9
9
|
import type { DesignSystemData } from "../shared/api.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
isDirectCssVarSelectionKey,
|
|
12
|
+
resolveTweaksToCssVars,
|
|
13
|
+
} from "../shared/resolve-tweaks.js";
|
|
11
14
|
|
|
12
15
|
// ---------------------------------------------------------------------------
|
|
13
16
|
// Token type classification
|
|
@@ -224,9 +227,10 @@ export default defineAction({
|
|
|
224
227
|
tweaks as TweakDef[],
|
|
225
228
|
tweakSelections,
|
|
226
229
|
);
|
|
227
|
-
const tweakCssVars = new Set(
|
|
228
|
-
tweaks.map((t) => t.cssVar).filter(Boolean) as string[],
|
|
229
|
-
|
|
230
|
+
const tweakCssVars = new Set([
|
|
231
|
+
...(tweaks.map((t) => t.cssVar).filter(Boolean) as string[]),
|
|
232
|
+
...Object.keys(tweakSelections).filter(isDirectCssVarSelectionKey),
|
|
233
|
+
]);
|
|
230
234
|
for (const [cssVar, value] of Object.entries(resolvedOverrides)) {
|
|
231
235
|
rawTokens.set(cssVar, { value, source: "Tweaks" });
|
|
232
236
|
}
|
|
@@ -6,6 +6,18 @@ import { z } from "zod";
|
|
|
6
6
|
import { getDb, schema } from "../server/db/index.js";
|
|
7
7
|
import "../server/db/index.js"; // ensure registerShareableResource runs
|
|
8
8
|
|
|
9
|
+
function parseJsonRecord(raw: string | null): Record<string, unknown> | null {
|
|
10
|
+
if (!raw) return null;
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
13
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
14
|
+
? (parsed as Record<string, unknown>)
|
|
15
|
+
: null;
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
9
21
|
export default defineAction({
|
|
10
22
|
description:
|
|
11
23
|
"List all design states, fixtures, and captures for a design. " +
|
|
@@ -52,6 +64,8 @@ export default defineAction({
|
|
|
52
64
|
kind: schema.designState.kind,
|
|
53
65
|
breakpoint: schema.designState.breakpoint,
|
|
54
66
|
route: schema.designState.route,
|
|
67
|
+
fixtureData: schema.designState.fixtureData,
|
|
68
|
+
captureData: schema.designState.captureData,
|
|
55
69
|
previewRef: schema.designState.previewRef,
|
|
56
70
|
createdAt: schema.designState.createdAt,
|
|
57
71
|
updatedAt: schema.designState.updatedAt,
|
|
@@ -66,7 +80,11 @@ export default defineAction({
|
|
|
66
80
|
|
|
67
81
|
return {
|
|
68
82
|
count: rows.length,
|
|
69
|
-
states: rows
|
|
83
|
+
states: rows.map((row) => ({
|
|
84
|
+
...row,
|
|
85
|
+
fixtureData: parseJsonRecord(row.fixtureData),
|
|
86
|
+
captureData: parseJsonRecord(row.captureData),
|
|
87
|
+
})),
|
|
70
88
|
};
|
|
71
89
|
},
|
|
72
90
|
});
|
|
@@ -36,7 +36,10 @@ import "../server/db/index.js"; // ensure registerShareableResource runs
|
|
|
36
36
|
import { resolveSourceCapabilities } from "../shared/capability-resolver.js";
|
|
37
37
|
import { buildCodeLayerProjection } from "../shared/code-layer.js";
|
|
38
38
|
import type { CodeLayerSource } from "../shared/code-layer.js";
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
componentNameFor,
|
|
41
|
+
componentNodeIdMatches,
|
|
42
|
+
} from "../shared/component-model.js";
|
|
40
43
|
import { hasCapability } from "../shared/design-source-capabilities.js";
|
|
41
44
|
import { normalizeDesignSourceType } from "../shared/source-mode.js";
|
|
42
45
|
|
|
@@ -148,7 +151,9 @@ export default defineAction({
|
|
|
148
151
|
source: codeLayerSource,
|
|
149
152
|
});
|
|
150
153
|
|
|
151
|
-
const node = projection.nodes.find((n) =>
|
|
154
|
+
const node = projection.nodes.find((n) =>
|
|
155
|
+
componentNodeIdMatches(n, nodeId),
|
|
156
|
+
);
|
|
152
157
|
if (!node) {
|
|
153
158
|
throw new Error(
|
|
154
159
|
`Node "${nodeId}" not found. Run get-code-layer-projection to list current ids.`,
|
|
@@ -32,7 +32,11 @@ import { getDb, schema } from "../server/db/index.js";
|
|
|
32
32
|
import "../server/db/index.js"; // ensure registerShareableResource runs
|
|
33
33
|
import { buildCodeLayerProjection } from "../shared/code-layer.js";
|
|
34
34
|
import type { CodeLayerSource } from "../shared/code-layer.js";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
componentNameFor,
|
|
37
|
+
componentNodeIdMatches,
|
|
38
|
+
extractProps,
|
|
39
|
+
} from "../shared/component-model.js";
|
|
36
40
|
import { normalizeDesignSourceType } from "../shared/source-mode.js";
|
|
37
41
|
|
|
38
42
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -62,7 +66,15 @@ async function liveContent(
|
|
|
62
66
|
export interface ComponentPropPreviewMessage {
|
|
63
67
|
/** Matches the existing parent→iframe postMessage type vocabulary. */
|
|
64
68
|
type: "style-change" | "replace-document-content" | "select-element";
|
|
65
|
-
|
|
69
|
+
selector?: string;
|
|
70
|
+
nodeId?: string;
|
|
71
|
+
attributeOverrides?: Record<string, string>;
|
|
72
|
+
classEdit?: {
|
|
73
|
+
kind: "class";
|
|
74
|
+
operation: "replace";
|
|
75
|
+
from: string;
|
|
76
|
+
to: string;
|
|
77
|
+
};
|
|
66
78
|
}
|
|
67
79
|
|
|
68
80
|
// ─── Action ───────────────────────────────────────────────────────────────────
|
|
@@ -156,7 +168,9 @@ export default defineAction({
|
|
|
156
168
|
source: codeLayerSource,
|
|
157
169
|
});
|
|
158
170
|
|
|
159
|
-
const node = projection.nodes.find((n) =>
|
|
171
|
+
const node = projection.nodes.find((n) =>
|
|
172
|
+
componentNodeIdMatches(n, nodeId),
|
|
173
|
+
);
|
|
160
174
|
if (!node) {
|
|
161
175
|
throw new Error(
|
|
162
176
|
`Node "${nodeId}" not found. Run get-code-layer-projection to list current ids.`,
|
|
@@ -182,35 +196,29 @@ export default defineAction({
|
|
|
182
196
|
// attribute without a full HTML replace.
|
|
183
197
|
messages.push({
|
|
184
198
|
type: "style-change",
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
attributeOverrides: { "x-data": edit.value },
|
|
189
|
-
},
|
|
199
|
+
selector: node.selector,
|
|
200
|
+
nodeId,
|
|
201
|
+
attributeOverrides: { "x-data": edit.value },
|
|
190
202
|
});
|
|
191
203
|
} else if (edit.kind === "attribute") {
|
|
192
204
|
messages.push({
|
|
193
205
|
type: "style-change",
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
attributeOverrides: { [edit.attribute]: edit.value },
|
|
198
|
-
},
|
|
206
|
+
selector: node.selector,
|
|
207
|
+
nodeId,
|
|
208
|
+
attributeOverrides: { [edit.attribute]: edit.value },
|
|
199
209
|
});
|
|
200
210
|
} else if (edit.kind === "classReplace") {
|
|
201
211
|
// Communicate as a responsive-class intent so the bridge applies it via
|
|
202
212
|
// the existing deterministic class-patch path.
|
|
203
213
|
messages.push({
|
|
204
214
|
type: "style-change",
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
to: edit.to,
|
|
213
|
-
},
|
|
215
|
+
selector: node.selector,
|
|
216
|
+
nodeId,
|
|
217
|
+
classEdit: {
|
|
218
|
+
kind: "class",
|
|
219
|
+
operation: "replace",
|
|
220
|
+
from: edit.from,
|
|
221
|
+
to: edit.to,
|
|
214
222
|
},
|
|
215
223
|
});
|
|
216
224
|
}
|
|
@@ -218,10 +226,8 @@ export default defineAction({
|
|
|
218
226
|
// Also emit a select-element message so the canvas highlights the node.
|
|
219
227
|
messages.push({
|
|
220
228
|
type: "select-element",
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
nodeId,
|
|
224
|
-
},
|
|
229
|
+
selector: node.selector,
|
|
230
|
+
nodeId,
|
|
225
231
|
});
|
|
226
232
|
|
|
227
233
|
const sourceType =
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* preview-shader-fill — preview-only action.
|
|
3
3
|
*
|
|
4
4
|
* Returns the CSS mesh-gradient approximation for a shader fill so the caller
|
|
5
|
-
* can apply it to the selected node via the
|
|
6
|
-
*
|
|
5
|
+
* can apply it to the selected node via the `shader-fill-preview` bridge
|
|
6
|
+
* message **without persisting anything**.
|
|
7
7
|
*
|
|
8
8
|
* Deliberately lightweight:
|
|
9
9
|
* - No DB access.
|
|
@@ -84,11 +84,11 @@ export default defineAction({
|
|
|
84
84
|
Preview a CSS mesh-gradient shader fill on the selected design node without persisting anything.
|
|
85
85
|
|
|
86
86
|
Returns:
|
|
87
|
-
- previewCss — a CSS \`background\` value for the live preview (inject via
|
|
87
|
+
- previewCss — a CSS \`background\` value for the live preview (inject via the shader-fill-preview bridge message).
|
|
88
88
|
- fallbackCss — a simpler static CSS \`background\` for export / PDF / SSR contexts.
|
|
89
89
|
- fallbackBlock — a complete CSS rule block (selector + fallback background) ready to embed.
|
|
90
90
|
- descriptor — the resolved and validated ShaderDescriptor.
|
|
91
|
-
- bridgeMessage — a ready-to-use JSON bridge payload for the iframe
|
|
91
|
+
- bridgeMessage — a ready-to-use JSON bridge payload for the iframe shader-fill-preview message.
|
|
92
92
|
|
|
93
93
|
Call this before apply-shader-fill. The preview is purely CSS — no WebGL, no canvas, no writes.
|
|
94
94
|
|
|
@@ -139,14 +139,12 @@ support; it has no effect on the static CSS preview output today.
|
|
|
139
139
|
const fallbackBlock = buildShaderFillFallbackBlock(selector, descriptor);
|
|
140
140
|
|
|
141
141
|
// Build a ready-to-post bridge payload for the iframe.
|
|
142
|
-
// The `
|
|
142
|
+
// The `shader-fill-preview` message type is handled by DesignCanvas.tsx.
|
|
143
143
|
const bridgeMessage = {
|
|
144
|
-
type: "
|
|
145
|
-
nodeId: target?.nodeId ?? null,
|
|
144
|
+
type: "shader-fill-preview",
|
|
146
145
|
selector: target?.selector ?? null,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
},
|
|
146
|
+
nodeId: target?.nodeId ?? null,
|
|
147
|
+
css: previewCss,
|
|
150
148
|
// Metadata so the client can attach a "preview only" badge.
|
|
151
149
|
_preview: true,
|
|
152
150
|
_source: "shader-fill",
|
|
@@ -167,7 +165,7 @@ support; it has no effect on the static CSS preview output today.
|
|
|
167
165
|
JSON.stringify(bridgeMessage, null, 2),
|
|
168
166
|
``,
|
|
169
167
|
`Apply steps:`,
|
|
170
|
-
`1. Inject the previewCss via a bridge "
|
|
168
|
+
`1. Inject the previewCss via a bridge "shader-fill-preview" message (no write, no persist).`,
|
|
171
169
|
`2. If the user approves, call apply-shader-fill — but note that action is currently GATED`,
|
|
172
170
|
` and will return a clear not-yet-available result until runtime rendering +`,
|
|
173
171
|
` source-write + diff proof are all in place.`,
|
|
@@ -170,7 +170,7 @@ function checkTapTargets(html: string): A11yFinding[] {
|
|
|
170
170
|
nodeId: extractNodeId(tag),
|
|
171
171
|
selector: extractSelector(tag, "button"),
|
|
172
172
|
wcag: "2.5.5",
|
|
173
|
-
fixAvailable:
|
|
173
|
+
fixAvailable: true,
|
|
174
174
|
});
|
|
175
175
|
}
|
|
176
176
|
idx++;
|
|
@@ -272,7 +272,7 @@ function checkFocusVisibility(html: string): A11yFinding[] {
|
|
|
272
272
|
nodeId: extractNodeId(tag),
|
|
273
273
|
selector: extractSelector(tag, tag.match(/^<([a-z]+)/i)?.[1] ?? "*"),
|
|
274
274
|
wcag: "2.4.7",
|
|
275
|
-
fixAvailable:
|
|
275
|
+
fixAvailable: true,
|
|
276
276
|
});
|
|
277
277
|
}
|
|
278
278
|
inlineIdx++;
|
|
@@ -286,10 +286,23 @@ function checkContrastHint(html: string): A11yFinding[] {
|
|
|
286
286
|
// Static analysis cannot compute real contrast ratios without a DOM/CSS
|
|
287
287
|
// cascade resolver. We flag the presence of explicit low-opacity text colors
|
|
288
288
|
// as a human-review hint — the UI will show these as "info" prompts.
|
|
289
|
-
const
|
|
290
|
-
|
|
289
|
+
const tagPattern = /<([a-z][a-z0-9:-]*)\b[^>]*>/gi;
|
|
290
|
+
const textColorPattern = /\btext-(?:white|black|gray-\d+)\b/i;
|
|
291
|
+
const lowOpacityPattern = /\b(?:opacity-[0-3]\d|text-opacity-[0-3]\d)\b/i;
|
|
291
292
|
let idx = 0;
|
|
292
|
-
for (const m of html.matchAll(
|
|
293
|
+
for (const m of html.matchAll(tagPattern)) {
|
|
294
|
+
const tag = m[0];
|
|
295
|
+
const classMatch = tag.match(/\bclass\s*=\s*(?:"([^"]*?)"|'([^']*?)')/i);
|
|
296
|
+
const className = classMatch?.[1] ?? classMatch?.[2] ?? "";
|
|
297
|
+
if (
|
|
298
|
+
!textColorPattern.test(className) ||
|
|
299
|
+
!lowOpacityPattern.test(className)
|
|
300
|
+
) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const tagName = m[1] ?? "*";
|
|
304
|
+
const nodeId = extractNodeId(tag);
|
|
305
|
+
const selector = extractSelector(tag, tagName);
|
|
293
306
|
findings.push({
|
|
294
307
|
id: `contrast:low-opacity-${idx}`,
|
|
295
308
|
severity: "info" as A11ySeverity,
|
|
@@ -298,6 +311,8 @@ function checkContrastHint(html: string): A11yFinding[] {
|
|
|
298
311
|
"Text element has a low-opacity modifier — verify contrast ratio meets 4.5:1 minimum.",
|
|
299
312
|
detail:
|
|
300
313
|
"Low-opacity text can fail WCAG 1.4.3. Run a live contrast check in the browser.",
|
|
314
|
+
nodeId,
|
|
315
|
+
selector,
|
|
301
316
|
wcag: "1.4.3",
|
|
302
317
|
fixAvailable: false,
|
|
303
318
|
});
|
|
@@ -353,7 +368,7 @@ export default defineAction({
|
|
|
353
368
|
),
|
|
354
369
|
}),
|
|
355
370
|
readOnly: true,
|
|
356
|
-
http: { method: "
|
|
371
|
+
http: { method: "POST" },
|
|
357
372
|
run: async ({ designId, fileId, filename }) => {
|
|
358
373
|
const db = getDb();
|
|
359
374
|
|