@1agh/maude 0.45.1 → 0.46.0
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/apps/studio/acp/bridge.ts +628 -74
- package/apps/studio/acp/index.ts +172 -23
- package/apps/studio/acp/probe.ts +31 -7
- package/apps/studio/acp/transcript.ts +91 -5
- package/apps/studio/annotations-snap.ts +50 -10
- package/apps/studio/api.ts +344 -14
- package/apps/studio/artboard-guides-overlay.tsx +270 -0
- package/apps/studio/bin/_import-asset.mjs +35 -5
- package/apps/studio/bin/_pdf-playwright.mjs +35 -4
- package/apps/studio/bin/_png-playwright.mjs +38 -4
- package/apps/studio/bin/_pw-launch.mjs +52 -0
- package/apps/studio/bin/_pw-launch.test.mjs +90 -0
- package/apps/studio/bin/_smart-frames.mjs +419 -0
- package/apps/studio/bin/_smart-frames.test.mjs +140 -0
- package/apps/studio/bin/smart-frames.sh +30 -0
- package/apps/studio/canvas-edit.ts +302 -3
- package/apps/studio/canvas-lib.tsx +203 -11
- package/apps/studio/canvas-shell.tsx +167 -28
- package/apps/studio/client/app.jsx +678 -176
- package/apps/studio/client/panels/CapabilityBar.jsx +101 -0
- package/apps/studio/client/panels/ChatPanel.jsx +846 -133
- package/apps/studio/client/panels/ElicitationPrompt.jsx +429 -0
- package/apps/studio/client/panels/PermissionPrompt.jsx +119 -0
- package/apps/studio/client/panels/ReadinessList.jsx +17 -3
- package/apps/studio/client/panels/SettingsPanel.jsx +211 -0
- package/apps/studio/client/panels/ToolGroup.jsx +79 -0
- package/apps/studio/client/panels/acp-capabilities.js +82 -0
- package/apps/studio/client/panels/acp-elicitation.js +194 -0
- package/apps/studio/client/panels/acp-runtime.js +248 -9
- package/apps/studio/client/panels/acp-usage.js +65 -0
- package/apps/studio/client/panels/transcript-view.js +36 -0
- package/apps/studio/client/styles/6-acp-chat.css +689 -11
- package/apps/studio/dist/client.bundle.js +1738 -1736
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +20 -0
- package/apps/studio/export-dialog.tsx +138 -18
- package/apps/studio/exporters/pdf.ts +332 -18
- package/apps/studio/exporters/png.ts +45 -3
- package/apps/studio/footage/schema.ts +17 -1
- package/apps/studio/generation/gemma-models.ts +224 -0
- package/apps/studio/generation/prefs.ts +43 -0
- package/apps/studio/generation/whisper-models.test.ts +28 -1
- package/apps/studio/generation/whisper-models.ts +22 -7
- package/apps/studio/http.ts +246 -9
- package/apps/studio/print/marks.ts +113 -0
- package/apps/studio/print/units.ts +269 -0
- package/apps/studio/print-overlay-content.tsx +132 -0
- package/apps/studio/test/acp-bridge.test.ts +11 -6
- package/apps/studio/test/acp-capabilities.test.ts +123 -0
- package/apps/studio/test/acp-caps-bridge.test.ts +274 -0
- package/apps/studio/test/acp-elicitation-bridge.test.ts +475 -0
- package/apps/studio/test/acp-elicitation.test.ts +251 -0
- package/apps/studio/test/acp-mode-banner.test.ts +45 -0
- package/apps/studio/test/acp-permission-prompt.test.ts +77 -0
- package/apps/studio/test/acp-permission.test.ts +262 -0
- package/apps/studio/test/acp-session-allowed-tools.test.ts +91 -0
- package/apps/studio/test/acp-toolgroup.test.ts +76 -0
- package/apps/studio/test/acp-transcript-view.test.ts +71 -0
- package/apps/studio/test/acp-transcript.test.ts +75 -2
- package/apps/studio/test/acp-usage-bridge.test.ts +136 -0
- package/apps/studio/test/acp-usage.test.ts +143 -0
- package/apps/studio/test/annotations-snap.test.ts +56 -0
- package/apps/studio/test/artboard-guides-overlay.test.tsx +152 -0
- package/apps/studio/test/artboard-kinds.test.tsx +83 -0
- package/apps/studio/test/artboard-selection-attrs.test.ts +69 -0
- package/apps/studio/test/canvas-meta-api.test.ts +167 -0
- package/apps/studio/test/element-structural-edit.test.ts +250 -0
- package/apps/studio/test/exporters/png.test.ts +49 -1
- package/apps/studio/test/fixtures/mock-acp-agent-caps.mjs +158 -0
- package/apps/studio/test/fixtures/mock-acp-agent-elicit-flood.mjs +46 -0
- package/apps/studio/test/fixtures/mock-acp-agent-elicit-url.mjs +42 -0
- package/apps/studio/test/fixtures/mock-acp-agent-elicit.mjs +63 -0
- package/apps/studio/test/fixtures/mock-acp-agent-permission-flood.mjs +51 -0
- package/apps/studio/test/fixtures/mock-acp-agent-permission.mjs +50 -0
- package/apps/studio/test/fixtures/mock-acp-agent-usage.mjs +56 -0
- package/apps/studio/test/import-asset.test.ts +31 -3
- package/apps/studio/test/pdf-print-boxes.test.ts +272 -0
- package/apps/studio/test/print-marks.test.ts +113 -0
- package/apps/studio/test/print-units.test.ts +173 -0
- package/apps/studio/test/use-snap-guides.test.ts +81 -0
- package/apps/studio/use-chrome-visibility.tsx +19 -0
- package/apps/studio/use-element-resize.tsx +21 -2
- package/apps/studio/use-snap-guides.tsx +73 -5
- package/apps/studio/use-spacing-handles.tsx +9 -5
- package/apps/studio/whats-new.json +62 -0
- package/cli/commands/design.mjs +116 -3
- package/cli/lib/pkg-root.mjs +42 -10
- package/cli/lib/pkg-root.test.mjs +33 -1
- package/package.json +11 -9
- package/plugins/design/dependencies.json +35 -0
package/apps/studio/api.ts
CHANGED
|
@@ -34,7 +34,10 @@ import {
|
|
|
34
34
|
retimeSequence,
|
|
35
35
|
retimeSequenceByClip,
|
|
36
36
|
editText as runEditText,
|
|
37
|
+
setArtboardGuides,
|
|
37
38
|
setArtboardHug,
|
|
39
|
+
setArtboardKind,
|
|
40
|
+
setArtboardPrint,
|
|
38
41
|
setArtboardStyle,
|
|
39
42
|
toggleClipHidden,
|
|
40
43
|
} from './canvas-edit.ts';
|
|
@@ -47,6 +50,7 @@ import {
|
|
|
47
50
|
} from './generation/audio-library.ts';
|
|
48
51
|
import { createHistory } from './history.ts';
|
|
49
52
|
import { STICKERS_DIR } from './paths.ts';
|
|
53
|
+
import { getPaperPreset, MAX_PRINT_MM } from './print/units.ts';
|
|
50
54
|
|
|
51
55
|
// Directories that never hold user-facing canvases. Exported so the
|
|
52
56
|
// external-canvas watcher (`canvas-list-watch.ts`) shares one source instead of
|
|
@@ -488,6 +492,24 @@ export interface Api {
|
|
|
488
492
|
layout?: unknown;
|
|
489
493
|
gap?: unknown;
|
|
490
494
|
}): Promise<{ ok: true; seq?: number } | { ok: false; status: number; error: string }>;
|
|
495
|
+
/** Kind-switch surfaces (T8) — context menu + Inspector picker. */
|
|
496
|
+
setArtboardKindOp(input: {
|
|
497
|
+
canvas?: unknown;
|
|
498
|
+
artboardId?: unknown;
|
|
499
|
+
kind?: unknown;
|
|
500
|
+
}): Promise<{ ok: true; seq?: number } | { ok: false; status: number; error: string }>;
|
|
501
|
+
/** Generic layout guides (T5) — replace-whole-prop write. */
|
|
502
|
+
setArtboardGuidesOp(input: {
|
|
503
|
+
canvas?: unknown;
|
|
504
|
+
artboardId?: unknown;
|
|
505
|
+
guides?: unknown;
|
|
506
|
+
}): Promise<{ ok: true; seq?: number } | { ok: false; status: number; error: string }>;
|
|
507
|
+
/** feature-2-print-artboards T2 — paper/orientation/bleed/margins, replace-whole-prop write. */
|
|
508
|
+
setArtboardPrintOp(input: {
|
|
509
|
+
canvas?: unknown;
|
|
510
|
+
artboardId?: unknown;
|
|
511
|
+
print?: unknown;
|
|
512
|
+
}): Promise<{ ok: true; seq?: number } | { ok: false; status: number; error: string }>;
|
|
491
513
|
/** Duplicate an element (Cmd+D) — a copy as the next sibling, whole-file undo. */
|
|
492
514
|
duplicateElementOp(input: {
|
|
493
515
|
canvas?: unknown;
|
|
@@ -1214,16 +1236,66 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1214
1236
|
return path.join(paths.canvasStateDir, `${fileSlug(file)}.view.json`);
|
|
1215
1237
|
}
|
|
1216
1238
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1239
|
+
const MAX_OVERLAY_KEYS = 32;
|
|
1240
|
+
const MAX_OVERLAY_KEY_LEN = 64;
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* Validate a candidate overlay-visibility bag — feature-1-artboard-kinds-
|
|
1244
|
+
* foundation T6. A flat string→boolean map (`{ guides: true }`); downstream
|
|
1245
|
+
* plans (print bleed, web breakpoints) add their own keys without a schema
|
|
1246
|
+
* change here. Reachable from the untrusted canvas origin (DDR-054) via the
|
|
1247
|
+
* same `/_api/canvas-meta` PATCH the viewport lane already uses, so it's
|
|
1248
|
+
* capped the same way `set-artboard-style` caps its patch shape — bounded
|
|
1249
|
+
* key COUNT and key LENGTH, not just type. Empty object is valid (an
|
|
1250
|
+
* explicit "nothing on" state); malformed input is null (silent no-op).
|
|
1251
|
+
*/
|
|
1252
|
+
function normalizeOverlays(v: unknown): Record<string, boolean> | null {
|
|
1253
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) return null;
|
|
1254
|
+
const entries = Object.entries(v as Record<string, unknown>);
|
|
1255
|
+
if (entries.length > MAX_OVERLAY_KEYS) return null;
|
|
1256
|
+
const out: Record<string, boolean> = {};
|
|
1257
|
+
for (const [k, val] of entries) {
|
|
1258
|
+
if (k.length === 0 || k.length > MAX_OVERLAY_KEY_LEN) return null;
|
|
1259
|
+
if (typeof val !== 'boolean') return null;
|
|
1260
|
+
out[k] = val;
|
|
1261
|
+
}
|
|
1262
|
+
return out;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/** Raw view-file contents, tolerant of a missing/corrupt file (→ `{}`) — the
|
|
1266
|
+
* read half of the read-modify-write both `saveCanvasView` and
|
|
1267
|
+
* `saveCanvasOverlays` need so writing one lane never clobbers the other. */
|
|
1268
|
+
async function readCanvasViewRaw(file: string): Promise<Record<string, unknown>> {
|
|
1269
|
+
const viewAbs = canvasViewPath(file);
|
|
1270
|
+
if (!viewAbs) return {};
|
|
1271
|
+
try {
|
|
1272
|
+
const obj = JSON.parse(await Bun.file(viewAbs).text());
|
|
1273
|
+
return obj && typeof obj === 'object' && !Array.isArray(obj)
|
|
1274
|
+
? (obj as Record<string, unknown>)
|
|
1275
|
+
: {};
|
|
1276
|
+
} catch {
|
|
1277
|
+
return {};
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
async function loadCanvasView(file: string): Promise<{
|
|
1282
|
+
viewport?: { x: number; y: number; zoom: number };
|
|
1283
|
+
overlays?: Record<string, boolean>;
|
|
1284
|
+
} | null> {
|
|
1220
1285
|
const viewAbs = canvasViewPath(file);
|
|
1221
1286
|
if (!viewAbs) return null;
|
|
1222
1287
|
try {
|
|
1223
1288
|
const obj = JSON.parse(await Bun.file(viewAbs).text());
|
|
1224
1289
|
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
1225
1290
|
const vp = normalizeViewport((obj as { viewport?: unknown }).viewport);
|
|
1226
|
-
|
|
1291
|
+
const ov = normalizeOverlays((obj as { overlays?: unknown }).overlays);
|
|
1292
|
+
const result: {
|
|
1293
|
+
viewport?: { x: number; y: number; zoom: number };
|
|
1294
|
+
overlays?: Record<string, boolean>;
|
|
1295
|
+
} = {};
|
|
1296
|
+
if (vp) result.viewport = vp;
|
|
1297
|
+
if (ov && Object.keys(ov).length > 0) result.overlays = ov;
|
|
1298
|
+
return result;
|
|
1227
1299
|
}
|
|
1228
1300
|
return null;
|
|
1229
1301
|
} catch {
|
|
@@ -1233,7 +1305,8 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1233
1305
|
|
|
1234
1306
|
/** Persist the per-user camera. Validates + clamps; best-effort (mkdir the
|
|
1235
1307
|
* bucket if absent). Returns the normalized viewport on write, null when the
|
|
1236
|
-
* path is rejected or the viewport is invalid (no write).
|
|
1308
|
+
* path is rejected or the viewport is invalid (no write). Read-modify-write
|
|
1309
|
+
* so an existing `overlays` key in the same view file survives. */
|
|
1237
1310
|
async function saveCanvasView(
|
|
1238
1311
|
file: string,
|
|
1239
1312
|
viewport: unknown
|
|
@@ -1244,13 +1317,44 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1244
1317
|
if (!vp) return null;
|
|
1245
1318
|
try {
|
|
1246
1319
|
await mkdir(paths.canvasStateDir, { recursive: true });
|
|
1247
|
-
|
|
1320
|
+
const current = await readCanvasViewRaw(file);
|
|
1321
|
+
await Bun.write(viewAbs, `${JSON.stringify({ ...current, viewport: vp }, null, 2)}\n`);
|
|
1248
1322
|
return vp;
|
|
1249
1323
|
} catch {
|
|
1250
1324
|
return null;
|
|
1251
1325
|
}
|
|
1252
1326
|
}
|
|
1253
1327
|
|
|
1328
|
+
/**
|
|
1329
|
+
* Persist the per-user overlay-visibility bag (T6). Shallow-merges into
|
|
1330
|
+
* whatever `overlays` the view file already has — a `{ guides: true }` patch
|
|
1331
|
+
* doesn't erase a `bleed` key a downstream print-plan toggle already set.
|
|
1332
|
+
* Never touches the versioned `.meta.json`. Read-modify-write so an existing
|
|
1333
|
+
* `viewport` key survives.
|
|
1334
|
+
*/
|
|
1335
|
+
async function saveCanvasOverlays(
|
|
1336
|
+
file: string,
|
|
1337
|
+
overlays: unknown
|
|
1338
|
+
): Promise<Record<string, boolean> | null> {
|
|
1339
|
+
const viewAbs = canvasViewPath(file);
|
|
1340
|
+
if (!viewAbs) return null;
|
|
1341
|
+
const ov = normalizeOverlays(overlays);
|
|
1342
|
+
if (ov === null) return null;
|
|
1343
|
+
try {
|
|
1344
|
+
await mkdir(paths.canvasStateDir, { recursive: true });
|
|
1345
|
+
const current = await readCanvasViewRaw(file);
|
|
1346
|
+
const prevOverlays =
|
|
1347
|
+
current.overlays && typeof current.overlays === 'object' && !Array.isArray(current.overlays)
|
|
1348
|
+
? (current.overlays as Record<string, unknown>)
|
|
1349
|
+
: {};
|
|
1350
|
+
const merged = normalizeOverlays({ ...prevOverlays, ...ov }) ?? ov;
|
|
1351
|
+
await Bun.write(viewAbs, `${JSON.stringify({ ...current, overlays: merged }, null, 2)}\n`);
|
|
1352
|
+
return merged;
|
|
1353
|
+
} catch {
|
|
1354
|
+
return null;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1254
1358
|
async function loadCanvasMeta(file: string): Promise<Record<string, unknown> | null> {
|
|
1255
1359
|
const metaAbs = canvasMetaPath(file);
|
|
1256
1360
|
if (!metaAbs) return null;
|
|
@@ -1274,11 +1378,17 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1274
1378
|
// codebase convention + biome's noDelete.)
|
|
1275
1379
|
obj.viewport = undefined;
|
|
1276
1380
|
obj.last_modified = undefined;
|
|
1381
|
+
// T6 — `overlays` (per-user guide/mark visibility) is runtime state same as
|
|
1382
|
+
// `viewport`; a versioned `.meta.json` should never carry one (sanitizer),
|
|
1383
|
+
// but strip defensively before overlaying the real per-user value below.
|
|
1384
|
+
obj.overlays = undefined;
|
|
1277
1385
|
const view = await loadCanvasView(file);
|
|
1278
1386
|
if (view?.viewport) obj.viewport = view.viewport;
|
|
1279
|
-
|
|
1280
|
-
//
|
|
1281
|
-
|
|
1387
|
+
if (view?.overlays) obj.overlays = view.overlays;
|
|
1388
|
+
// Preserve the historic contract: no meta AND no camera/overlays → null
|
|
1389
|
+
// (GET → {}, PATCH-on-rejected-path → 404). A view-only canvas still
|
|
1390
|
+
// returns its camera/overlays.
|
|
1391
|
+
if (!hadMeta && !view?.viewport && !view?.overlays) return null;
|
|
1282
1392
|
return obj;
|
|
1283
1393
|
}
|
|
1284
1394
|
|
|
@@ -1331,6 +1441,27 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1331
1441
|
}
|
|
1332
1442
|
}
|
|
1333
1443
|
|
|
1444
|
+
// --- Per-user overlay-visibility lane (T6): overlays → view file, never
|
|
1445
|
+
// the versioned meta. Same shape as the viewport lane above — `null`
|
|
1446
|
+
// clears, an invalid bag is a silent no-op (normalizeOverlays → null).
|
|
1447
|
+
if (patch.overlays !== undefined) {
|
|
1448
|
+
if (patch.overlays === null) {
|
|
1449
|
+
const viewAbs = canvasViewPath(file);
|
|
1450
|
+
if (viewAbs) {
|
|
1451
|
+
try {
|
|
1452
|
+
const current = await readCanvasViewRaw(file);
|
|
1453
|
+
const { overlays: _drop, ...rest } = current;
|
|
1454
|
+
await mkdir(paths.canvasStateDir, { recursive: true });
|
|
1455
|
+
await Bun.write(viewAbs, `${JSON.stringify(rest, null, 2)}\n`);
|
|
1456
|
+
} catch {
|
|
1457
|
+
/* best-effort clear */
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
} else {
|
|
1461
|
+
await saveCanvasOverlays(file, patch.overlays);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1334
1465
|
// --- Shared document lane: layout → versioned meta, stamps last_modified. ---
|
|
1335
1466
|
if (patch.layout !== undefined) {
|
|
1336
1467
|
let current: Record<string, unknown> = {};
|
|
@@ -2952,6 +3083,193 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2952
3083
|
}
|
|
2953
3084
|
}
|
|
2954
3085
|
|
|
3086
|
+
const ARTBOARD_KIND_VALUES = new Set(['digital', 'print', 'web', 'video']);
|
|
3087
|
+
|
|
3088
|
+
/** Kind-switch surfaces (T8): context menu + Inspector picker. */
|
|
3089
|
+
async function setArtboardKindOp(input: {
|
|
3090
|
+
canvas?: unknown;
|
|
3091
|
+
artboardId?: unknown;
|
|
3092
|
+
kind?: unknown;
|
|
3093
|
+
}): Promise<{ ok: true; seq?: number } | { ok: false; status: number; error: string }> {
|
|
3094
|
+
const r = resolveCanvasAbs(input.canvas);
|
|
3095
|
+
if (!r.ok) return r;
|
|
3096
|
+
if (!takeStructuralToken()) return RATE_LIMITED;
|
|
3097
|
+
const artboardId = typeof input.artboardId === 'string' ? input.artboardId.trim() : '';
|
|
3098
|
+
if (!/^[A-Za-z][\w-]{0,63}$/.test(artboardId)) {
|
|
3099
|
+
return { ok: false, status: 400, error: 'invalid artboard id' };
|
|
3100
|
+
}
|
|
3101
|
+
let kind: string | null;
|
|
3102
|
+
if (input.kind === null) {
|
|
3103
|
+
kind = null;
|
|
3104
|
+
} else if (typeof input.kind === 'string' && ARTBOARD_KIND_VALUES.has(input.kind)) {
|
|
3105
|
+
kind = input.kind;
|
|
3106
|
+
} else {
|
|
3107
|
+
return { ok: false, status: 400, error: 'invalid kind' };
|
|
3108
|
+
}
|
|
3109
|
+
const rel = path.relative(paths.designRoot, r.abs);
|
|
3110
|
+
ctx.bus.emit('activity:suppress', rel);
|
|
3111
|
+
try {
|
|
3112
|
+
const before = await Bun.file(r.abs).text();
|
|
3113
|
+
await setArtboardKind(r.abs, artboardId, kind);
|
|
3114
|
+
const after = await Bun.file(r.abs).text();
|
|
3115
|
+
if (after === before) {
|
|
3116
|
+
ctx.bus.emit('activity:unsuppress', rel);
|
|
3117
|
+
return { ok: true };
|
|
3118
|
+
}
|
|
3119
|
+
try {
|
|
3120
|
+
await history.writeSnapshot(rel, before, 'pre-set-artboard-kind');
|
|
3121
|
+
} catch {
|
|
3122
|
+
/* snapshot best-effort */
|
|
3123
|
+
}
|
|
3124
|
+
return { ok: true, seq: logUndo(r.abs, before, after) };
|
|
3125
|
+
} catch (err) {
|
|
3126
|
+
ctx.bus.emit('activity:unsuppress', rel);
|
|
3127
|
+
return {
|
|
3128
|
+
ok: false,
|
|
3129
|
+
status: err instanceof CanvasEditError ? 422 : 500,
|
|
3130
|
+
error: err instanceof Error ? err.message : 'set-artboard-kind failed',
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
const MAX_GUIDES_JSON_BYTES = 4096;
|
|
3136
|
+
|
|
3137
|
+
/** Generic layout guides (T5) — Inspector/skill writer. Replace-whole-prop,
|
|
3138
|
+
* never a deep merge (see applySetArtboardGuides's own doc comment). */
|
|
3139
|
+
async function setArtboardGuidesOp(input: {
|
|
3140
|
+
canvas?: unknown;
|
|
3141
|
+
artboardId?: unknown;
|
|
3142
|
+
guides?: unknown;
|
|
3143
|
+
}): Promise<{ ok: true; seq?: number } | { ok: false; status: number; error: string }> {
|
|
3144
|
+
const r = resolveCanvasAbs(input.canvas);
|
|
3145
|
+
if (!r.ok) return r;
|
|
3146
|
+
if (!takeStructuralToken()) return RATE_LIMITED;
|
|
3147
|
+
const artboardId = typeof input.artboardId === 'string' ? input.artboardId.trim() : '';
|
|
3148
|
+
if (!/^[A-Za-z][\w-]{0,63}$/.test(artboardId)) {
|
|
3149
|
+
return { ok: false, status: 400, error: 'invalid artboard id' };
|
|
3150
|
+
}
|
|
3151
|
+
let guides: Record<string, unknown> | null;
|
|
3152
|
+
if (input.guides === null) {
|
|
3153
|
+
guides = null;
|
|
3154
|
+
} else if (
|
|
3155
|
+
input.guides &&
|
|
3156
|
+
typeof input.guides === 'object' &&
|
|
3157
|
+
!Array.isArray(input.guides) &&
|
|
3158
|
+
JSON.stringify(input.guides).length <= MAX_GUIDES_JSON_BYTES
|
|
3159
|
+
) {
|
|
3160
|
+
guides = input.guides as Record<string, unknown>;
|
|
3161
|
+
} else {
|
|
3162
|
+
return { ok: false, status: 400, error: 'invalid guides' };
|
|
3163
|
+
}
|
|
3164
|
+
const rel = path.relative(paths.designRoot, r.abs);
|
|
3165
|
+
ctx.bus.emit('activity:suppress', rel);
|
|
3166
|
+
try {
|
|
3167
|
+
const before = await Bun.file(r.abs).text();
|
|
3168
|
+
await setArtboardGuides(r.abs, artboardId, guides);
|
|
3169
|
+
const after = await Bun.file(r.abs).text();
|
|
3170
|
+
if (after === before) {
|
|
3171
|
+
ctx.bus.emit('activity:unsuppress', rel);
|
|
3172
|
+
return { ok: true };
|
|
3173
|
+
}
|
|
3174
|
+
try {
|
|
3175
|
+
await history.writeSnapshot(rel, before, 'pre-set-artboard-guides');
|
|
3176
|
+
} catch {
|
|
3177
|
+
/* snapshot best-effort */
|
|
3178
|
+
}
|
|
3179
|
+
return { ok: true, seq: logUndo(r.abs, before, after) };
|
|
3180
|
+
} catch (err) {
|
|
3181
|
+
ctx.bus.emit('activity:unsuppress', rel);
|
|
3182
|
+
return {
|
|
3183
|
+
ok: false,
|
|
3184
|
+
status: err instanceof CanvasEditError ? 422 : 500,
|
|
3185
|
+
error: err instanceof Error ? err.message : 'set-artboard-guides failed',
|
|
3186
|
+
};
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
const PRINT_ORIENTATION_VALUES = new Set(['portrait', 'landscape']);
|
|
3191
|
+
|
|
3192
|
+
function isFiniteMmOrUndefined(v: unknown): v is number | undefined {
|
|
3193
|
+
return (
|
|
3194
|
+
v === undefined ||
|
|
3195
|
+
(typeof v === 'number' && Number.isFinite(v) && v >= 0 && v <= MAX_PRINT_MM)
|
|
3196
|
+
);
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
/**
|
|
3200
|
+
* feature-2-print-artboards T2 — paper/orientation/bleed/margins. Replace-
|
|
3201
|
+
* whole-prop (see applySetArtboardPrint's own doc comment) — the caller
|
|
3202
|
+
* sends the full merged object, not a delta. `print: null` clears the
|
|
3203
|
+
* prop. Shape validation lives HERE (canvas-edit.ts stays generic
|
|
3204
|
+
* Record<string, unknown> AST surgery, same split as setArtboardGuidesOp).
|
|
3205
|
+
*/
|
|
3206
|
+
async function setArtboardPrintOp(input: {
|
|
3207
|
+
canvas?: unknown;
|
|
3208
|
+
artboardId?: unknown;
|
|
3209
|
+
print?: unknown;
|
|
3210
|
+
}): Promise<{ ok: true; seq?: number } | { ok: false; status: number; error: string }> {
|
|
3211
|
+
const r = resolveCanvasAbs(input.canvas);
|
|
3212
|
+
if (!r.ok) return r;
|
|
3213
|
+
if (!takeStructuralToken()) return RATE_LIMITED;
|
|
3214
|
+
const artboardId = typeof input.artboardId === 'string' ? input.artboardId.trim() : '';
|
|
3215
|
+
if (!/^[A-Za-z][\w-]{0,63}$/.test(artboardId)) {
|
|
3216
|
+
return { ok: false, status: 400, error: 'invalid artboard id' };
|
|
3217
|
+
}
|
|
3218
|
+
let print: Record<string, unknown> | null;
|
|
3219
|
+
if (input.print === null) {
|
|
3220
|
+
print = null;
|
|
3221
|
+
} else if (input.print && typeof input.print === 'object' && !Array.isArray(input.print)) {
|
|
3222
|
+
const p = input.print as Record<string, unknown>;
|
|
3223
|
+
if (typeof p.paper !== 'string' || !getPaperPreset(p.paper)) {
|
|
3224
|
+
return { ok: false, status: 400, error: 'invalid or unknown paper preset' };
|
|
3225
|
+
}
|
|
3226
|
+
if (p.orientation !== undefined && !PRINT_ORIENTATION_VALUES.has(p.orientation as string)) {
|
|
3227
|
+
return { ok: false, status: 400, error: 'invalid orientation' };
|
|
3228
|
+
}
|
|
3229
|
+
if (!isFiniteMmOrUndefined(p.bleedMm)) {
|
|
3230
|
+
return { ok: false, status: 400, error: 'invalid bleedMm' };
|
|
3231
|
+
}
|
|
3232
|
+
if (p.marginsMm !== undefined) {
|
|
3233
|
+
if (typeof p.marginsMm !== 'object' || p.marginsMm === null || Array.isArray(p.marginsMm)) {
|
|
3234
|
+
return { ok: false, status: 400, error: 'invalid marginsMm' };
|
|
3235
|
+
}
|
|
3236
|
+
const m = p.marginsMm as Record<string, unknown>;
|
|
3237
|
+
for (const side of ['top', 'right', 'bottom', 'left']) {
|
|
3238
|
+
if (!isFiniteMmOrUndefined(m[side])) {
|
|
3239
|
+
return { ok: false, status: 400, error: `invalid marginsMm.${side}` };
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
print = p;
|
|
3244
|
+
} else {
|
|
3245
|
+
return { ok: false, status: 400, error: 'invalid print' };
|
|
3246
|
+
}
|
|
3247
|
+
const rel = path.relative(paths.designRoot, r.abs);
|
|
3248
|
+
ctx.bus.emit('activity:suppress', rel);
|
|
3249
|
+
try {
|
|
3250
|
+
const before = await Bun.file(r.abs).text();
|
|
3251
|
+
await setArtboardPrint(r.abs, artboardId, print);
|
|
3252
|
+
const after = await Bun.file(r.abs).text();
|
|
3253
|
+
if (after === before) {
|
|
3254
|
+
ctx.bus.emit('activity:unsuppress', rel);
|
|
3255
|
+
return { ok: true };
|
|
3256
|
+
}
|
|
3257
|
+
try {
|
|
3258
|
+
await history.writeSnapshot(rel, before, 'pre-set-artboard-print');
|
|
3259
|
+
} catch {
|
|
3260
|
+
/* snapshot best-effort */
|
|
3261
|
+
}
|
|
3262
|
+
return { ok: true, seq: logUndo(r.abs, before, after) };
|
|
3263
|
+
} catch (err) {
|
|
3264
|
+
ctx.bus.emit('activity:unsuppress', rel);
|
|
3265
|
+
return {
|
|
3266
|
+
ok: false,
|
|
3267
|
+
status: err instanceof CanvasEditError ? 422 : 500,
|
|
3268
|
+
error: err instanceof Error ? err.message : 'set-artboard-print failed',
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
|
|
2955
3273
|
const ARTBOARD_LAYOUT_VALUES = new Set(['block', 'flex-col', 'flex-row', 'grid']);
|
|
2956
3274
|
|
|
2957
3275
|
/** Set artboard "more settings" — background / padding / layout / gap. */
|
|
@@ -2972,9 +3290,9 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2972
3290
|
}
|
|
2973
3291
|
const patch: {
|
|
2974
3292
|
background?: string | null;
|
|
2975
|
-
padding?: number | null;
|
|
3293
|
+
padding?: number | string | null;
|
|
2976
3294
|
layout?: string | null;
|
|
2977
|
-
gap?: number | null;
|
|
3295
|
+
gap?: number | string | null;
|
|
2978
3296
|
} = {};
|
|
2979
3297
|
if ('background' in input) {
|
|
2980
3298
|
if (input.background === null) patch.background = null;
|
|
@@ -2992,19 +3310,28 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2992
3310
|
return { ok: false, status: 400, error: 'invalid layout' };
|
|
2993
3311
|
}
|
|
2994
3312
|
}
|
|
2995
|
-
|
|
3313
|
+
// Dogfood (artboard panel ↔ shared inspector controls) — padding/gap
|
|
3314
|
+
// accept a design-token binding alongside raw px. STRICT single-var shape
|
|
3315
|
+
// (not a free-form CSS string like `background`): the value lands in a
|
|
3316
|
+
// JSX prop AND in `.dc-artboard-body`'s inline style, so anything beyond
|
|
3317
|
+
// one `var(--…)` reference is rejected rather than round-tripped.
|
|
3318
|
+
const BOX_TOKEN_RE = /^var\(--[a-zA-Z0-9_-]{1,64}\)$/;
|
|
3319
|
+
const clampBoxDim = (v: unknown): number | string | null | undefined => {
|
|
2996
3320
|
if (v === null) return null;
|
|
2997
3321
|
if (v === undefined) return undefined;
|
|
3322
|
+
if (typeof v === 'string' && BOX_TOKEN_RE.test(v)) return v;
|
|
2998
3323
|
return Number.isFinite(Number(v)) ? Math.max(0, Math.min(512, Math.round(Number(v)))) : NaN;
|
|
2999
3324
|
};
|
|
3000
3325
|
if ('padding' in input) {
|
|
3001
3326
|
const p = clampBoxDim(input.padding);
|
|
3002
|
-
if (Number.isNaN(p))
|
|
3327
|
+
if (typeof p === 'number' && Number.isNaN(p))
|
|
3328
|
+
return { ok: false, status: 400, error: 'invalid padding' };
|
|
3003
3329
|
patch.padding = p;
|
|
3004
3330
|
}
|
|
3005
3331
|
if ('gap' in input) {
|
|
3006
3332
|
const g = clampBoxDim(input.gap);
|
|
3007
|
-
if (Number.isNaN(g))
|
|
3333
|
+
if (typeof g === 'number' && Number.isNaN(g))
|
|
3334
|
+
return { ok: false, status: 400, error: 'invalid gap' };
|
|
3008
3335
|
patch.gap = g;
|
|
3009
3336
|
}
|
|
3010
3337
|
if (Object.keys(patch).length === 0) {
|
|
@@ -3772,6 +4099,9 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
3772
4099
|
resizeArtboardOp,
|
|
3773
4100
|
setArtboardHugOp,
|
|
3774
4101
|
setArtboardStyleOp,
|
|
4102
|
+
setArtboardKindOp,
|
|
4103
|
+
setArtboardGuidesOp,
|
|
4104
|
+
setArtboardPrintOp,
|
|
3775
4105
|
deleteArtboardOp,
|
|
3776
4106
|
duplicateElementOp,
|
|
3777
4107
|
editScopeOp,
|