@1agh/maude 0.41.0 → 0.43.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/.ai/cache/_stats.json +7 -0
- package/apps/studio/acp/bootstrap-brief.ts +12 -2
- package/apps/studio/annotations-layer.tsx +1055 -278
- package/apps/studio/annotations-model.ts +58 -0
- package/apps/studio/api.ts +173 -63
- package/apps/studio/bin/_html-playwright.mjs +26 -4
- package/apps/studio/bin/_ingest-footage.mjs +386 -0
- package/apps/studio/bin/_pdf-playwright.mjs +13 -2
- package/apps/studio/bin/_png-playwright.mjs +15 -2
- package/apps/studio/bin/_pptx-playwright.mjs +17 -4
- package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
- package/apps/studio/bin/_screenshot-playwright.mjs +17 -2
- package/apps/studio/bin/_svg-playwright.mjs +26 -4
- package/apps/studio/bin/_video-playwright.mjs +141 -71
- package/apps/studio/bin/ingest-footage.sh +27 -0
- package/apps/studio/bin/photo-adjust.sh +163 -0
- package/apps/studio/bin/photo-bg-remove.sh +160 -0
- package/apps/studio/bin/probe-footage.sh +28 -0
- package/apps/studio/bin/read-annotations.mjs +125 -2
- package/apps/studio/bin/screenshot.sh +53 -4
- package/apps/studio/canvas-edit.ts +459 -10
- package/apps/studio/canvas-lib.tsx +523 -5
- package/apps/studio/canvas-pipeline.ts +65 -7
- package/apps/studio/canvas-shell.tsx +156 -16
- package/apps/studio/client/app.jsx +476 -88
- package/apps/studio/client/export-center.jsx +460 -0
- package/apps/studio/client/github.js +4 -0
- package/apps/studio/client/panels/GitPanel.jsx +8 -0
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
- package/apps/studio/client/panels/StickerPicker.jsx +160 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
- package/apps/studio/client/photo-knobs.jsx +437 -0
- package/apps/studio/client/styles/3-shell-maude.css +59 -0
- package/apps/studio/client/styles/4-components.css +150 -0
- package/apps/studio/commands/edit-source-command.ts +31 -4
- package/apps/studio/config.schema.json +2 -2
- package/apps/studio/context-menu.tsx +31 -6
- package/apps/studio/dist/client.bundle.js +5561 -19
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
- package/apps/studio/dist/runtime/pixi-js.js +519 -519
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +25 -0
- package/apps/studio/export-dialog.tsx +18 -25
- package/apps/studio/exporters/_runtime.ts +104 -0
- package/apps/studio/exporters/html.ts +12 -20
- package/apps/studio/exporters/index.ts +14 -2
- package/apps/studio/exporters/jobs.ts +376 -0
- package/apps/studio/exporters/pdf.ts +16 -20
- package/apps/studio/exporters/png.ts +12 -20
- package/apps/studio/exporters/pptx.ts +22 -23
- package/apps/studio/exporters/scope.ts +1 -0
- package/apps/studio/exporters/svg.ts +14 -22
- package/apps/studio/exporters/video.ts +37 -19
- package/apps/studio/footage/schema.test.ts +195 -0
- package/apps/studio/footage/schema.ts +484 -0
- package/apps/studio/footage-store.ts +235 -0
- package/apps/studio/fs-watch.ts +10 -0
- package/apps/studio/git/endpoints.ts +68 -7
- package/apps/studio/git/service.ts +198 -20
- package/apps/studio/github/service.ts +67 -0
- package/apps/studio/handoff.ts +3 -2
- package/apps/studio/http.ts +318 -64
- package/apps/studio/input-router.tsx +10 -0
- package/apps/studio/inspect.ts +53 -1
- package/apps/studio/paths.ts +10 -0
- package/apps/studio/photo/filters.ts +285 -0
- package/apps/studio/photo/pipeline.ts +607 -0
- package/apps/studio/photo/schema.ts +367 -0
- package/apps/studio/photo-store.ts +154 -0
- package/apps/studio/runtime-bundle.ts +29 -6
- package/apps/studio/server.ts +8 -2
- package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
- package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
- package/apps/studio/stickers/life-style/best.png +0 -0
- package/apps/studio/stickers/life-style/come-on.png +0 -0
- package/apps/studio/stickers/life-style/cut.png +0 -0
- package/apps/studio/stickers/life-style/detox.png +0 -0
- package/apps/studio/stickers/life-style/just.png +0 -0
- package/apps/studio/stickers/life-style/manifest.json +68 -0
- package/apps/studio/stickers/life-style/mirror.png +0 -0
- package/apps/studio/stickers/life-style/nice.png +0 -0
- package/apps/studio/stickers/life-style/objects.png +0 -0
- package/apps/studio/stickers/life-style/ok.png +0 -0
- package/apps/studio/stickers/life-style/queen.png +0 -0
- package/apps/studio/stickers/life-style/star.png +0 -0
- package/apps/studio/stickers/life-style/sun.png +0 -0
- package/apps/studio/stickers/life-style/water.png +0 -0
- package/apps/studio/stickers/life-style/yeah.png +0 -0
- package/apps/studio/stickers/life-style/you-can.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
- package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
- package/apps/studio/stickers/project-status/group-100.png +0 -0
- package/apps/studio/stickers/project-status/group-101.png +0 -0
- package/apps/studio/stickers/project-status/group-102.png +0 -0
- package/apps/studio/stickers/project-status/group-103.png +0 -0
- package/apps/studio/stickers/project-status/group-104.png +0 -0
- package/apps/studio/stickers/project-status/group-105.png +0 -0
- package/apps/studio/stickers/project-status/group-106.png +0 -0
- package/apps/studio/stickers/project-status/group-107.png +0 -0
- package/apps/studio/stickers/project-status/group-108.png +0 -0
- package/apps/studio/stickers/project-status/group-109.png +0 -0
- package/apps/studio/stickers/project-status/group-110.png +0 -0
- package/apps/studio/stickers/project-status/group-111.png +0 -0
- package/apps/studio/stickers/project-status/group-112.png +0 -0
- package/apps/studio/stickers/project-status/group-113.png +0 -0
- package/apps/studio/stickers/project-status/group-114.png +0 -0
- package/apps/studio/stickers/project-status/group-115.png +0 -0
- package/apps/studio/stickers/project-status/group-117.png +0 -0
- package/apps/studio/stickers/project-status/group-118.png +0 -0
- package/apps/studio/stickers/project-status/group-119.png +0 -0
- package/apps/studio/stickers/project-status/group-120.png +0 -0
- package/apps/studio/stickers/project-status/group-121.png +0 -0
- package/apps/studio/stickers/project-status/group-122.png +0 -0
- package/apps/studio/stickers/project-status/group-41.png +0 -0
- package/apps/studio/stickers/project-status/group-42.png +0 -0
- package/apps/studio/stickers/project-status/group-43.png +0 -0
- package/apps/studio/stickers/project-status/group-44.png +0 -0
- package/apps/studio/stickers/project-status/group-45.png +0 -0
- package/apps/studio/stickers/project-status/group-46.png +0 -0
- package/apps/studio/stickers/project-status/group-47.png +0 -0
- package/apps/studio/stickers/project-status/group-48.png +0 -0
- package/apps/studio/stickers/project-status/group-49.png +0 -0
- package/apps/studio/stickers/project-status/group-50.png +0 -0
- package/apps/studio/stickers/project-status/group-51.png +0 -0
- package/apps/studio/stickers/project-status/group-52.png +0 -0
- package/apps/studio/stickers/project-status/group-53.png +0 -0
- package/apps/studio/stickers/project-status/group-54.png +0 -0
- package/apps/studio/stickers/project-status/group-55.png +0 -0
- package/apps/studio/stickers/project-status/group-56.png +0 -0
- package/apps/studio/stickers/project-status/group-57.png +0 -0
- package/apps/studio/stickers/project-status/group-58.png +0 -0
- package/apps/studio/stickers/project-status/group-59.png +0 -0
- package/apps/studio/stickers/project-status/group-60.png +0 -0
- package/apps/studio/stickers/project-status/group-61.png +0 -0
- package/apps/studio/stickers/project-status/group-62.png +0 -0
- package/apps/studio/stickers/project-status/group-63.png +0 -0
- package/apps/studio/stickers/project-status/group-64.png +0 -0
- package/apps/studio/stickers/project-status/group-65.png +0 -0
- package/apps/studio/stickers/project-status/group-66.png +0 -0
- package/apps/studio/stickers/project-status/group-67.png +0 -0
- package/apps/studio/stickers/project-status/group-68.png +0 -0
- package/apps/studio/stickers/project-status/group-69.png +0 -0
- package/apps/studio/stickers/project-status/group-70.png +0 -0
- package/apps/studio/stickers/project-status/group-71.png +0 -0
- package/apps/studio/stickers/project-status/group-72.png +0 -0
- package/apps/studio/stickers/project-status/group-73.png +0 -0
- package/apps/studio/stickers/project-status/group-74.png +0 -0
- package/apps/studio/stickers/project-status/group-75.png +0 -0
- package/apps/studio/stickers/project-status/group-76.png +0 -0
- package/apps/studio/stickers/project-status/group-77.png +0 -0
- package/apps/studio/stickers/project-status/group-78.png +0 -0
- package/apps/studio/stickers/project-status/group-79.png +0 -0
- package/apps/studio/stickers/project-status/group-80.png +0 -0
- package/apps/studio/stickers/project-status/group-81.png +0 -0
- package/apps/studio/stickers/project-status/group-82.png +0 -0
- package/apps/studio/stickers/project-status/group-83.png +0 -0
- package/apps/studio/stickers/project-status/group-84.png +0 -0
- package/apps/studio/stickers/project-status/group-85.png +0 -0
- package/apps/studio/stickers/project-status/group-86.png +0 -0
- package/apps/studio/stickers/project-status/group-87.png +0 -0
- package/apps/studio/stickers/project-status/group-88.png +0 -0
- package/apps/studio/stickers/project-status/group-89.png +0 -0
- package/apps/studio/stickers/project-status/group-90.png +0 -0
- package/apps/studio/stickers/project-status/group-91.png +0 -0
- package/apps/studio/stickers/project-status/group-92.png +0 -0
- package/apps/studio/stickers/project-status/group-93.png +0 -0
- package/apps/studio/stickers/project-status/group-94.png +0 -0
- package/apps/studio/stickers/project-status/group-96.png +0 -0
- package/apps/studio/stickers/project-status/group-97.png +0 -0
- package/apps/studio/stickers/project-status/group-98.png +0 -0
- package/apps/studio/stickers/project-status/group-99.png +0 -0
- package/apps/studio/stickers/project-status/manifest.json +328 -0
- package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
- package/apps/studio/test/acp-commands.test.ts +2 -1
- package/apps/studio/test/asset-api.test.ts +3 -3
- package/apps/studio/test/canvas-edit.test.ts +17 -6
- package/apps/studio/test/canvas-origin-gate.test.ts +19 -0
- package/apps/studio/test/dns-rebinding-guard.test.ts +27 -0
- package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
- package/apps/studio/test/edit-source-command.test.ts +23 -0
- package/apps/studio/test/element-structural-edit.test.ts +46 -0
- package/apps/studio/test/export-center.test.tsx +287 -0
- package/apps/studio/test/export-shim-multi-capture.test.ts +83 -0
- package/apps/studio/test/exporters/history.test.ts +32 -3
- package/apps/studio/test/exporters/jobs.test.ts +263 -0
- package/apps/studio/test/footage-store.test.ts +100 -0
- package/apps/studio/test/git-branches.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +56 -1
- package/apps/studio/test/input-router.test.ts +33 -0
- package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
- package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
- package/apps/studio/test/photo-edit-api.test.ts +201 -0
- package/apps/studio/test/photo-filters.test.ts +149 -0
- package/apps/studio/test/photo-pipeline.test.ts +106 -0
- package/apps/studio/test/photo-taxonomy.test.ts +96 -0
- package/apps/studio/test/read-annotations.test.ts +164 -0
- package/apps/studio/test/shell-importmap.test.ts +49 -0
- package/apps/studio/test/text-editability-stamp.test.ts +131 -0
- package/apps/studio/test/timeline-parse.test.ts +57 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
- package/apps/studio/test/undo-stack.test.ts +33 -0
- package/apps/studio/test/video-asset.test.ts +5 -5
- package/apps/studio/test/video-render-bridge.test.ts +23 -1
- package/apps/studio/text-caret.ts +239 -0
- package/apps/studio/tool-palette.tsx +43 -20
- package/apps/studio/undo-stack.ts +94 -2
- package/apps/studio/use-annotation-resize.tsx +4 -4
- package/apps/studio/use-canvas-media-drop.tsx +38 -1
- package/apps/studio/use-selection-set.tsx +21 -0
- package/apps/studio/video-comp.tsx +48 -7
- package/apps/studio/whats-new.json +70 -0
- package/apps/studio/ws.ts +6 -0
- package/cli/commands/design.mjs +34 -3
- package/cli/lib/gitignore-block.mjs +2 -0
- package/package.json +10 -10
- package/plugins/design/templates/_shell.html +1 -0
|
@@ -74,11 +74,21 @@ export interface ArrowBind {
|
|
|
74
74
|
* element). Absent / empty = ungrouped. Serialized as `data-group-ids`.
|
|
75
75
|
* - `author` — provenance. `'ai'` when created through the
|
|
76
76
|
* `maude design annotate` write surface. Absent = human-drawn.
|
|
77
|
+
* - `authorName` / `authorId` — Phase 3 (whiteboard-improvements): who drew
|
|
78
|
+
* a human-authored stroke, stamped from presence identity (`useCollab`'s
|
|
79
|
+
* `myName`/`myConnId` — git `user.name`, else an `anonymous-*` fallback).
|
|
80
|
+
* Absent on AI-authored strokes and on any stroke drawn before this
|
|
81
|
+
* field existed (back-compat — no author badge, not an error). Never
|
|
82
|
+
* trust a wire/stored COLOR for this identity — badges re-derive their
|
|
83
|
+
* color from the name via `colorForName` so they match the author's live
|
|
84
|
+
* presence hue.
|
|
77
85
|
*/
|
|
78
86
|
interface StrokeBase {
|
|
79
87
|
id: string;
|
|
80
88
|
groupIds?: string[];
|
|
81
89
|
author?: 'ai';
|
|
90
|
+
authorName?: string;
|
|
91
|
+
authorId?: string;
|
|
82
92
|
/**
|
|
83
93
|
* FigJam v3 — rotation in degrees (clockwise) around the stroke's bbox
|
|
84
94
|
* center. Absent / 0 = axis-aligned (back-compat). Honoured by the box-
|
|
@@ -553,6 +563,11 @@ export const IMAGE_MIN_SIZE = 16;
|
|
|
553
563
|
export const IMAGE_MAX_DROP_SIDE = 480;
|
|
554
564
|
export const LINK_DEFAULT_W = 260;
|
|
555
565
|
export const LINK_DEFAULT_H = 76;
|
|
566
|
+
// Phase 4 (whiteboard-improvements) — a sticker dropped from the picker has no
|
|
567
|
+
// natural size to probe (unlike createImageFromFile's Image.onload path — the
|
|
568
|
+
// shell already resolved it to a project asset by the time the canvas hears
|
|
569
|
+
// about it) — one fixed, friendly on-board size for every sticker.
|
|
570
|
+
export const STICKER_DROP_SIZE = 160;
|
|
556
571
|
// DDR-150 P4 — media-reference chip (dropped video/audio → assets/ pointer).
|
|
557
572
|
export const MEDIAREF_DEFAULT_W = 280;
|
|
558
573
|
export const MEDIAREF_DEFAULT_H = 76;
|
|
@@ -926,6 +941,12 @@ function rootExtraAttrs(s: Stroke): string {
|
|
|
926
941
|
extra += ` data-group-ids="${esc(s.groupIds.join(' '))}"`;
|
|
927
942
|
}
|
|
928
943
|
if (s.author === 'ai') extra += ' data-author="ai"';
|
|
944
|
+
// Phase 3 (whiteboard-improvements) — human author identity, independent of
|
|
945
|
+
// `data-author="ai"` (never both: a stroke is either agent- or human-drawn).
|
|
946
|
+
// escAttr (not esc) — these are attribute values, not element content; see
|
|
947
|
+
// the Phase 23 escAttr() docblock above for why a bare `>` here is unsafe.
|
|
948
|
+
if (s.authorName) extra += ` data-author-name="${escAttr(s.authorName)}"`;
|
|
949
|
+
if (s.authorId) extra += ` data-author-id="${escAttr(s.authorId)}"`;
|
|
929
950
|
// FigJam v3 — rotation: `data-rot` is the round-trip source of truth; the
|
|
930
951
|
// transform is presentational so the persisted SVG renders rotated in any
|
|
931
952
|
// viewer. The pivot derives from stored geometry → idempotent re-serialize.
|
|
@@ -1302,6 +1323,33 @@ function parseBindAttr(raw: string | null): ArrowBind | null {
|
|
|
1302
1323
|
return bind;
|
|
1303
1324
|
}
|
|
1304
1325
|
|
|
1326
|
+
const AUTHOR_NAME_MAX_LEN = 64;
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* Phase 3 (whiteboard-improvements) — an `authorName` parsed off disk may
|
|
1330
|
+
* have come from a peer's synced `.annotations.svg` (untrusted foreign
|
|
1331
|
+
* input, same class of risk `use-collab.tsx`'s `sanitizeForeignState` /
|
|
1332
|
+
* `sanitizeName` already guard presence names against). Strips control
|
|
1333
|
+
* chars, bidi overrides/isolates, zero-width chars, and the BOM; caps
|
|
1334
|
+
* length. Mirrors that function's code-point ranges rather than importing
|
|
1335
|
+
* it — this module stays dependency-free of the stateful collab layer.
|
|
1336
|
+
*/
|
|
1337
|
+
function sanitizeAuthorName(raw: string): string {
|
|
1338
|
+
let cleaned = '';
|
|
1339
|
+
for (const ch of raw) {
|
|
1340
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
1341
|
+
const unsafe =
|
|
1342
|
+
cp <= 0x1f ||
|
|
1343
|
+
(cp >= 0x7f && cp <= 0x9f) ||
|
|
1344
|
+
(cp >= 0x200b && cp <= 0x200f) ||
|
|
1345
|
+
(cp >= 0x202a && cp <= 0x202e) ||
|
|
1346
|
+
(cp >= 0x2066 && cp <= 0x2069) ||
|
|
1347
|
+
cp === 0xfeff;
|
|
1348
|
+
if (!unsafe) cleaned += ch;
|
|
1349
|
+
}
|
|
1350
|
+
return cleaned.trim().slice(0, AUTHOR_NAME_MAX_LEN);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1305
1353
|
/** FigJam v3 — read the cross-tool root attrs back onto a parsed stroke. */
|
|
1306
1354
|
function readSharedAttrs(el: Element, s: Stroke): void {
|
|
1307
1355
|
const g = el.getAttribute('data-group-ids');
|
|
@@ -1310,6 +1358,16 @@ function readSharedAttrs(el: Element, s: Stroke): void {
|
|
|
1310
1358
|
if (ids.length) s.groupIds = ids;
|
|
1311
1359
|
}
|
|
1312
1360
|
if (el.getAttribute('data-author') === 'ai') s.author = 'ai';
|
|
1361
|
+
const authorName = el.getAttribute('data-author-name');
|
|
1362
|
+
if (authorName) {
|
|
1363
|
+
const cleaned = sanitizeAuthorName(authorName);
|
|
1364
|
+
if (cleaned) s.authorName = cleaned;
|
|
1365
|
+
}
|
|
1366
|
+
const authorId = el.getAttribute('data-author-id');
|
|
1367
|
+
if (authorId) {
|
|
1368
|
+
const cleanedId = sanitizeAuthorName(authorId);
|
|
1369
|
+
if (cleanedId) s.authorId = cleanedId;
|
|
1370
|
+
}
|
|
1313
1371
|
// FigJam v3 — rotation. `data-rot` is authoritative (the transform attr is
|
|
1314
1372
|
// presentational); malformed / out-of-range values drop to axis-aligned.
|
|
1315
1373
|
const rotRaw = el.getAttribute('data-rot');
|
package/apps/studio/api.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
insertArtboard,
|
|
24
24
|
insertClip,
|
|
25
25
|
insertElement,
|
|
26
|
+
insertElementIntoArtboard,
|
|
26
27
|
type MovePosition,
|
|
27
28
|
moveElement,
|
|
28
29
|
removeAttribute,
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
} from './canvas-edit.ts';
|
|
38
39
|
import type { Context } from './context.ts';
|
|
39
40
|
import { createHistory } from './history.ts';
|
|
41
|
+
import { STICKERS_DIR } from './paths.ts';
|
|
40
42
|
|
|
41
43
|
// Directories that never hold user-facing canvases. Exported so the
|
|
42
44
|
// external-canvas watcher (`canvas-list-watch.ts`) shares one source instead of
|
|
@@ -180,16 +182,6 @@ export interface GitCommitter {
|
|
|
180
182
|
commits: number;
|
|
181
183
|
}
|
|
182
184
|
|
|
183
|
-
// Phase 6.5 T10 — export history. Five-deep ring buffer of recent exports
|
|
184
|
-
// surfaced by the dialog's "Recent" tab + ⌘⇧E re-run.
|
|
185
|
-
export interface ExportHistoryEntry {
|
|
186
|
-
format: string;
|
|
187
|
-
scope: string;
|
|
188
|
-
options?: Record<string, unknown>;
|
|
189
|
-
filename: string;
|
|
190
|
-
at: string;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
185
|
export type CreateCanvasResult =
|
|
194
186
|
| { ok: true; file: string; rel: string; slug: string }
|
|
195
187
|
| { ok: false; status: number; error: string };
|
|
@@ -265,6 +257,9 @@ export interface Api {
|
|
|
265
257
|
saveAsset(bytes: Uint8Array): Promise<SaveAssetResult>;
|
|
266
258
|
/** Stage F1 — list content-addressed image/video assets for the AssetPicker. */
|
|
267
259
|
listAssets(): Promise<{ ok: true; assets: AssetListing[] }>;
|
|
260
|
+
/** Phase 4 (feature-whiteboard-annotation-improvements) — the bundled sticker
|
|
261
|
+
* catalogue (MAUDE's own, not the served project's) for the StickerPicker. */
|
|
262
|
+
listStickers(): Promise<{ ok: true; packs: StickerPack[] }>;
|
|
268
263
|
/** DDR-148 — streaming variant for the HTTP route (100 MB video without a
|
|
269
264
|
* full in-RAM buffer). Sniffs + caps + content-addresses like saveAsset. */
|
|
270
265
|
saveAssetFromStream(stream: ReadableStream<Uint8Array>): Promise<SaveAssetResult>;
|
|
@@ -298,7 +293,13 @@ export interface Api {
|
|
|
298
293
|
idIndex?: unknown;
|
|
299
294
|
}): Promise<EditOpResult>;
|
|
300
295
|
// Phase 12 (DDR-103) — inline text-content edit (POST /_api/edit-text). Main-origin only.
|
|
301
|
-
editText(input: {
|
|
296
|
+
editText(input: {
|
|
297
|
+
canvas?: unknown;
|
|
298
|
+
id?: unknown;
|
|
299
|
+
text?: unknown;
|
|
300
|
+
occurrence?: unknown;
|
|
301
|
+
before?: unknown;
|
|
302
|
+
}): Promise<EditOpResult>;
|
|
302
303
|
// Phase 12.2 (DDR-104) — custom HTML attribute edit (POST /_api/edit-attr). Main-origin
|
|
303
304
|
// only. The CSS panel's "custom HTML attribute" escape hatch (data-*, aria-*, role, …);
|
|
304
305
|
// writes a plain JSX attribute via editAttribute's non-`style.` path.
|
|
@@ -406,10 +407,16 @@ export interface Api {
|
|
|
406
407
|
}): Promise<
|
|
407
408
|
{ ok: true; deletedId: string; seq?: number } | { ok: false; status: number; error: string }
|
|
408
409
|
>;
|
|
409
|
-
/**
|
|
410
|
+
/**
|
|
411
|
+
* Insert a synthesized div/text/image relative to a reference element, OR —
|
|
412
|
+
* when the artboard has no element to anchor on yet — as a direct child of
|
|
413
|
+
* `artboardId` (the tool-palette "+ Element" empty-artboard fallback).
|
|
414
|
+
* Exactly one of `refId` / `artboardId` must be provided.
|
|
415
|
+
*/
|
|
410
416
|
insertElementOp(input: {
|
|
411
417
|
canvas?: unknown;
|
|
412
418
|
refId?: unknown;
|
|
419
|
+
artboardId?: unknown;
|
|
413
420
|
position?: unknown;
|
|
414
421
|
kind?: unknown;
|
|
415
422
|
src?: unknown;
|
|
@@ -465,9 +472,6 @@ export interface Api {
|
|
|
465
472
|
// Aggregate data
|
|
466
473
|
buildIndexData(): Promise<unknown>;
|
|
467
474
|
buildSystemData(dsName?: string | null): Promise<unknown>;
|
|
468
|
-
// Export history (Phase 6.5 T10)
|
|
469
|
-
loadExportHistory(): Promise<ExportHistoryEntry[]>;
|
|
470
|
-
appendExportHistory(entry: ExportHistoryEntry): Promise<void>;
|
|
471
475
|
}
|
|
472
476
|
|
|
473
477
|
export interface ApiHooks {
|
|
@@ -485,8 +489,19 @@ export { ASSET_IMAGE_HREF_RE, sanitizeAnnotationSvg } from './annotations-model.
|
|
|
485
489
|
|
|
486
490
|
import { sanitizeAnnotationSvg } from './annotations-model.ts';
|
|
487
491
|
|
|
488
|
-
/**
|
|
489
|
-
|
|
492
|
+
/**
|
|
493
|
+
* Phase 23 — per-file ceiling for a still image. Raised 10 MB → 50 MB (still
|
|
494
|
+
* well under the video cap / MAX_REQUEST_BODY headroom) after a real drone
|
|
495
|
+
* photo tripped the old ceiling. Overridable via `MAUDE_ASSET_MAX_IMAGE_BYTES`
|
|
496
|
+
* (bytes), mirroring {@link ASSET_MAX_VIDEO_BYTES}'s override. The route lives
|
|
497
|
+
* on the (untrusted) canvas origin (DDR-088), and images stream through the
|
|
498
|
+
* same category-capped writer as video/audio (`saveAssetFromStream`), so
|
|
499
|
+
* raising this doesn't reintroduce the memory-amplification risk DDR-088 capped.
|
|
500
|
+
*/
|
|
501
|
+
export const ASSET_MAX_BYTES = (() => {
|
|
502
|
+
const env = Number(process.env.MAUDE_ASSET_MAX_IMAGE_BYTES);
|
|
503
|
+
return Number.isFinite(env) && env > 0 ? env : 50 * 1024 * 1024;
|
|
504
|
+
})();
|
|
490
505
|
|
|
491
506
|
/**
|
|
492
507
|
* Phase 23 security review (DDR-088 follow-up) — aggregate per-server-instance
|
|
@@ -578,6 +593,24 @@ export interface AssetListing {
|
|
|
578
593
|
mtimeMs: number;
|
|
579
594
|
}
|
|
580
595
|
|
|
596
|
+
/** Phase 4 (feature-whiteboard-annotation-improvements) — one bundled sticker. */
|
|
597
|
+
export interface StickerItem {
|
|
598
|
+
file: string;
|
|
599
|
+
keywords: string[];
|
|
600
|
+
/** Servable path — `/_stickers/<pack>/<file>` (main-origin static route). */
|
|
601
|
+
url: string;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** A bundled sticker pack — one `apps/studio/stickers/<slug>/manifest.json`. */
|
|
605
|
+
export interface StickerPack {
|
|
606
|
+
slug: string;
|
|
607
|
+
name: string;
|
|
608
|
+
author: string;
|
|
609
|
+
attributionUrl: string;
|
|
610
|
+
license: string;
|
|
611
|
+
stickers: StickerItem[];
|
|
612
|
+
}
|
|
613
|
+
|
|
581
614
|
/** DDR-148 — media category, decides which per-file cap applies. */
|
|
582
615
|
export type AssetCategory = 'image' | 'video' | 'audio';
|
|
583
616
|
|
|
@@ -589,7 +622,7 @@ export interface AssetTypeInfo {
|
|
|
589
622
|
|
|
590
623
|
/**
|
|
591
624
|
* DDR-148 — per-file ceiling for time-based media (video + audio). Images keep
|
|
592
|
-
* the tighter {@link ASSET_MAX_BYTES}
|
|
625
|
+
* the tighter {@link ASSET_MAX_BYTES} cap. Overridable via
|
|
593
626
|
* `MAUDE_ASSET_MAX_VIDEO_BYTES` (bytes) for power users. The route lives on the
|
|
594
627
|
* (untrusted) canvas origin, so this cap + the session budget + the streamed
|
|
595
628
|
* write are the trust mitigation, exactly like the image caps (DDR-088).
|
|
@@ -608,7 +641,9 @@ const UNSUPPORTED_ASSET_MSG =
|
|
|
608
641
|
'unsupported media type — png/jpeg/gif/webp images or mp4/mov/webm/mp3/wav/m4a media only (SVG/script rejected)';
|
|
609
642
|
|
|
610
643
|
function capError(category?: AssetCategory): string {
|
|
611
|
-
if (category === 'image')
|
|
644
|
+
if (category === 'image') {
|
|
645
|
+
return `image exceeds the ${Math.round(ASSET_MAX_BYTES / (1024 * 1024))} MB cap`;
|
|
646
|
+
}
|
|
612
647
|
const mb = Math.round(ASSET_MAX_VIDEO_BYTES / (1024 * 1024));
|
|
613
648
|
return `media exceeds the ${mb} MB cap`;
|
|
614
649
|
}
|
|
@@ -1329,8 +1364,9 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1329
1364
|
// optional (DDR Task 9):
|
|
1330
1365
|
// • magic-byte sniff → true type ∈ {png,jpg,gif,webp}; a header lie or an
|
|
1331
1366
|
// SVG (script-bearing vector) is rejected — bytes decide, name is ignored.
|
|
1332
|
-
// •
|
|
1333
|
-
// SVG-text gate in
|
|
1367
|
+
// • per-category ceiling ({@link ASSET_MAX_BYTES} for images; assets get
|
|
1368
|
+
// their OWN cap; never routed through the 1 MB SVG-text gate in
|
|
1369
|
+
// saveAnnotations).
|
|
1334
1370
|
// • content-addressed name `assets/<sha8-of-bytes>.<ext>` → identical drops
|
|
1335
1371
|
// dedupe → a malicious canvas can't fill the disk with N copies of one
|
|
1336
1372
|
// image, and orphan-on-delete is safe (shared content survives).
|
|
@@ -1344,8 +1380,8 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1344
1380
|
* request body chunk-by-chunk so a 100 MB video never lands as a single
|
|
1345
1381
|
* ArrayBuffer in RAM (the memory-amplification vector DDR-088 §review flagged
|
|
1346
1382
|
* for the untrusted canvas origin — worse now that the per-file cap is 100 MB,
|
|
1347
|
-
* not
|
|
1348
|
-
* streaming (image
|
|
1383
|
+
* not a few MB). Sniffs the type from the head, applies the CATEGORY cap while
|
|
1384
|
+
* streaming (image {@link ASSET_MAX_BYTES} · video/audio {@link ASSET_MAX_VIDEO_BYTES}), writes
|
|
1349
1385
|
* to a temp file, then content-addresses (sha8) → dedupe-rename. Nothing is
|
|
1350
1386
|
* ever parsed — only magic bytes are read. `saveAsset(bytes)` wraps this so
|
|
1351
1387
|
* both the route and any programmatic caller share ONE tested path.
|
|
@@ -1525,6 +1561,59 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1525
1561
|
return { ok: true, assets: out.slice(0, 500) };
|
|
1526
1562
|
}
|
|
1527
1563
|
|
|
1564
|
+
// Phase 4 (feature-whiteboard-annotation-improvements) — the bundled sticker
|
|
1565
|
+
// catalogue for the StickerPicker. Reads from MAUDE's OWN `STICKERS_DIR`
|
|
1566
|
+
// (paths.ts, DDR-045) — never the served project's designRoot, unlike
|
|
1567
|
+
// listAssets above. A pack directory missing or with an invalid
|
|
1568
|
+
// manifest.json is skipped, not fatal — one bad pack shouldn't blank the
|
|
1569
|
+
// whole picker. MAIN-ORIGIN ONLY at the route layer (the picker is shell UI,
|
|
1570
|
+
// same posture as listAssets/AssetPicker).
|
|
1571
|
+
async function listStickers(): Promise<{ ok: true; packs: StickerPack[] }> {
|
|
1572
|
+
let packDirs: string[] = [];
|
|
1573
|
+
try {
|
|
1574
|
+
const entries = await readdir(STICKERS_DIR, { withFileTypes: true });
|
|
1575
|
+
packDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1576
|
+
} catch {
|
|
1577
|
+
return { ok: true, packs: [] }; // no bundled stickers in this layout
|
|
1578
|
+
}
|
|
1579
|
+
const packs: StickerPack[] = [];
|
|
1580
|
+
for (const slug of packDirs) {
|
|
1581
|
+
try {
|
|
1582
|
+
const manifestPath = path.join(STICKERS_DIR, slug, 'manifest.json');
|
|
1583
|
+
const raw = await readFile(manifestPath, 'utf8');
|
|
1584
|
+
const manifest = JSON.parse(raw) as {
|
|
1585
|
+
name?: string;
|
|
1586
|
+
author?: string;
|
|
1587
|
+
attributionUrl?: string;
|
|
1588
|
+
license?: string;
|
|
1589
|
+
stickers?: Array<{ file?: string; keywords?: string[] }>;
|
|
1590
|
+
};
|
|
1591
|
+
const stickers: StickerItem[] = (manifest.stickers ?? [])
|
|
1592
|
+
.filter(
|
|
1593
|
+
(s): s is { file: string; keywords?: string[] } =>
|
|
1594
|
+
typeof s.file === 'string' && !!s.file
|
|
1595
|
+
)
|
|
1596
|
+
.map((s) => ({
|
|
1597
|
+
file: s.file,
|
|
1598
|
+
keywords: Array.isArray(s.keywords)
|
|
1599
|
+
? s.keywords.filter((k) => typeof k === 'string')
|
|
1600
|
+
: [],
|
|
1601
|
+
url: `/_stickers/${slug}/${encodeURIComponent(s.file)}`,
|
|
1602
|
+
}));
|
|
1603
|
+
packs.push({
|
|
1604
|
+
slug,
|
|
1605
|
+
name: manifest.name ?? slug,
|
|
1606
|
+
author: manifest.author ?? '',
|
|
1607
|
+
attributionUrl: manifest.attributionUrl ?? '',
|
|
1608
|
+
license: manifest.license ?? '',
|
|
1609
|
+
stickers,
|
|
1610
|
+
});
|
|
1611
|
+
} catch {}
|
|
1612
|
+
}
|
|
1613
|
+
packs.sort((a, b) => a.name.localeCompare(b.name));
|
|
1614
|
+
return { ok: true, packs };
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1528
1617
|
// Phase 31 follow-up — persist an image pasted straight from the clipboard into
|
|
1529
1618
|
// the ACP composer (a screenshot has no path yet), so the chip can point Claude
|
|
1530
1619
|
// at a real file to Read. Sibling of saveAsset, but:
|
|
@@ -1532,13 +1621,17 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1532
1621
|
// the versioned `assets/`; pasted screenshots are ephemeral, not canvas media.
|
|
1533
1622
|
// • returns an ABSOLUTE path — Claude runs with its own cwd, so a project-
|
|
1534
1623
|
// relative string could miss; an absolute path always resolves.
|
|
1535
|
-
// Same load-bearing caps as saveAsset (magic-byte sniff → no SVG/script,
|
|
1536
|
-
// content-addressed name, shared session write budget).
|
|
1537
|
-
// route layer (the untrusted canvas can't reach it).
|
|
1624
|
+
// Same load-bearing caps as saveAsset (magic-byte sniff → no SVG/script,
|
|
1625
|
+
// ASSET_MAX_BYTES, content-addressed name, shared session write budget).
|
|
1626
|
+
// MAIN-ORIGIN ONLY at the route layer (the untrusted canvas can't reach it).
|
|
1538
1627
|
async function saveChatAttachment(bytes: Uint8Array): Promise<SaveAssetResult> {
|
|
1539
1628
|
if (!bytes || bytes.length === 0) return { ok: false, status: 400, error: 'empty body' };
|
|
1540
1629
|
if (bytes.length > ASSET_MAX_BYTES) {
|
|
1541
|
-
return {
|
|
1630
|
+
return {
|
|
1631
|
+
ok: false,
|
|
1632
|
+
status: 413,
|
|
1633
|
+
error: `attachment exceeds the ${Math.round(ASSET_MAX_BYTES / (1024 * 1024))} MB cap`,
|
|
1634
|
+
};
|
|
1542
1635
|
}
|
|
1543
1636
|
const kind = sniffImageType(bytes);
|
|
1544
1637
|
if (!kind) {
|
|
@@ -2022,6 +2115,8 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2022
2115
|
canvas?: unknown;
|
|
2023
2116
|
id?: unknown;
|
|
2024
2117
|
text?: unknown;
|
|
2118
|
+
occurrence?: unknown;
|
|
2119
|
+
before?: unknown;
|
|
2025
2120
|
}): Promise<EditOpResult> {
|
|
2026
2121
|
const r = resolveCanvasAbs(input.canvas);
|
|
2027
2122
|
if (!r.ok) return r;
|
|
@@ -2030,7 +2125,23 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2030
2125
|
if (typeof input.text !== 'string') return { ok: false, status: 400, error: 'text required' };
|
|
2031
2126
|
if (input.text.length > 5000) return { ok: false, status: 413, error: 'text too long' };
|
|
2032
2127
|
const text = input.text;
|
|
2033
|
-
|
|
2128
|
+
// Optional context for editing `{variable}` text (unified-text-editing
|
|
2129
|
+
// follow-up): which rendered instance, and its pre-edit text — both used
|
|
2130
|
+
// only to target the right source string; a bad/absent value just makes an
|
|
2131
|
+
// unresolvable expression edit fail loud (route to /design:edit).
|
|
2132
|
+
const occurrence =
|
|
2133
|
+
typeof input.occurrence === 'number' &&
|
|
2134
|
+
Number.isInteger(input.occurrence) &&
|
|
2135
|
+
input.occurrence >= 0
|
|
2136
|
+
? input.occurrence
|
|
2137
|
+
: undefined;
|
|
2138
|
+
const before =
|
|
2139
|
+
typeof input.before === 'string' && input.before.length <= 5000 ? input.before : undefined;
|
|
2140
|
+
return suppressedEdit(
|
|
2141
|
+
r.abs,
|
|
2142
|
+
() => runEditText(r.abs, id, text, { occurrence, before }),
|
|
2143
|
+
'edit failed'
|
|
2144
|
+
);
|
|
2034
2145
|
}
|
|
2035
2146
|
|
|
2036
2147
|
async function editAttr(input: {
|
|
@@ -2360,10 +2471,12 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2360
2471
|
}
|
|
2361
2472
|
}
|
|
2362
2473
|
|
|
2363
|
-
/** Insert a synthesized element (div/text/image) relative to `refId
|
|
2474
|
+
/** Insert a synthesized element (div/text/image) relative to `refId`, or —
|
|
2475
|
+
* empty-artboard fallback — as a direct child of `artboardId`. */
|
|
2364
2476
|
async function insertElementOp(input: {
|
|
2365
2477
|
canvas?: unknown;
|
|
2366
2478
|
refId?: unknown;
|
|
2479
|
+
artboardId?: unknown;
|
|
2367
2480
|
position?: unknown;
|
|
2368
2481
|
kind?: unknown;
|
|
2369
2482
|
src?: unknown;
|
|
@@ -2374,14 +2487,30 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2374
2487
|
const r = resolveCanvasAbs(input.canvas);
|
|
2375
2488
|
if (!r.ok) return r;
|
|
2376
2489
|
if (!takeStructuralToken()) return RATE_LIMITED;
|
|
2377
|
-
const
|
|
2378
|
-
|
|
2490
|
+
const hasRefId = typeof input.refId === 'string' && input.refId.trim() !== '';
|
|
2491
|
+
const hasArtboardId = typeof input.artboardId === 'string' && input.artboardId.trim() !== '';
|
|
2492
|
+
if (hasRefId === hasArtboardId) {
|
|
2493
|
+
return { ok: false, status: 400, error: 'provide exactly one of refId / artboardId' };
|
|
2494
|
+
}
|
|
2495
|
+
const refId = hasRefId ? (input.refId as string).trim() : '';
|
|
2496
|
+
if (hasRefId && !CD_ID_RE.test(refId)) {
|
|
2379
2497
|
return { ok: false, status: 400, error: 'invalid reference data-cd-id' };
|
|
2380
2498
|
}
|
|
2499
|
+
const artboardId = hasArtboardId ? (input.artboardId as string).trim() : '';
|
|
2500
|
+
if (hasArtboardId && !/^[A-Za-z][\w-]{0,63}$/.test(artboardId)) {
|
|
2501
|
+
return { ok: false, status: 400, error: 'invalid artboard id' };
|
|
2502
|
+
}
|
|
2381
2503
|
const position = input.position;
|
|
2382
2504
|
if (typeof position !== 'string' || !MOVE_POSITIONS.has(position)) {
|
|
2383
2505
|
return { ok: false, status: 400, error: 'invalid position' };
|
|
2384
2506
|
}
|
|
2507
|
+
if (hasArtboardId && position !== 'inside-start' && position !== 'inside-end') {
|
|
2508
|
+
return {
|
|
2509
|
+
ok: false,
|
|
2510
|
+
status: 400,
|
|
2511
|
+
error: 'artboardId insert requires inside-start/inside-end',
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2385
2514
|
const kind = input.kind;
|
|
2386
2515
|
if (kind !== 'div' && kind !== 'text' && kind !== 'image') {
|
|
2387
2516
|
return { ok: false, status: 400, error: 'invalid kind (div|text|image)' };
|
|
@@ -2396,10 +2525,18 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2396
2525
|
ctx.bus.emit('activity:unsuppress', rel);
|
|
2397
2526
|
return { ok: false, status: 413, error: 'canvas source too large to grow' };
|
|
2398
2527
|
}
|
|
2399
|
-
const res =
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2528
|
+
const res = hasArtboardId
|
|
2529
|
+
? await insertElementIntoArtboard(
|
|
2530
|
+
r.abs,
|
|
2531
|
+
artboardId,
|
|
2532
|
+
position as 'inside-start' | 'inside-end',
|
|
2533
|
+
kind as InsertKind,
|
|
2534
|
+
{ src }
|
|
2535
|
+
)
|
|
2536
|
+
: await insertElement(r.abs, refId, position as MovePosition, kind as InsertKind, {
|
|
2537
|
+
src,
|
|
2538
|
+
occurrence: refIndex,
|
|
2539
|
+
});
|
|
2403
2540
|
const after = await Bun.file(r.abs).text();
|
|
2404
2541
|
if (after === before) {
|
|
2405
2542
|
ctx.bus.emit('activity:unsuppress', rel);
|
|
@@ -3006,32 +3143,6 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
3006
3143
|
};
|
|
3007
3144
|
}
|
|
3008
3145
|
|
|
3009
|
-
// ---------- Export history (Phase 6.5 T10) ----------
|
|
3010
|
-
//
|
|
3011
|
-
// 5-deep ring buffer persisted at `<designRoot>/_export-history.json`.
|
|
3012
|
-
// Reads tolerate missing / malformed files (returns []). Writes truncate
|
|
3013
|
-
// to most-recent-first.
|
|
3014
|
-
|
|
3015
|
-
const HISTORY_PATH = path.join(paths.designRoot, '_export-history.json');
|
|
3016
|
-
const HISTORY_DEPTH = 5;
|
|
3017
|
-
|
|
3018
|
-
async function loadExportHistory(): Promise<ExportHistoryEntry[]> {
|
|
3019
|
-
try {
|
|
3020
|
-
const raw = await Bun.file(HISTORY_PATH).text();
|
|
3021
|
-
const arr = JSON.parse(raw);
|
|
3022
|
-
if (!Array.isArray(arr)) return [];
|
|
3023
|
-
return arr.slice(0, HISTORY_DEPTH);
|
|
3024
|
-
} catch {
|
|
3025
|
-
return [];
|
|
3026
|
-
}
|
|
3027
|
-
}
|
|
3028
|
-
|
|
3029
|
-
async function appendExportHistory(entry: ExportHistoryEntry): Promise<void> {
|
|
3030
|
-
const prev = await loadExportHistory();
|
|
3031
|
-
const next = [entry, ...prev].slice(0, HISTORY_DEPTH);
|
|
3032
|
-
await Bun.write(HISTORY_PATH, JSON.stringify(next, null, 2));
|
|
3033
|
-
}
|
|
3034
|
-
|
|
3035
3146
|
function tokenKind(name: string, value: string): string {
|
|
3036
3147
|
const n = name.toLowerCase();
|
|
3037
3148
|
const v = String(value).trim();
|
|
@@ -3247,6 +3358,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
3247
3358
|
saveAnnotations,
|
|
3248
3359
|
saveAsset,
|
|
3249
3360
|
listAssets,
|
|
3361
|
+
listStickers,
|
|
3250
3362
|
saveAssetFromStream,
|
|
3251
3363
|
saveChatAttachment,
|
|
3252
3364
|
resolveChatAttachment,
|
|
@@ -3273,7 +3385,5 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
3273
3385
|
reorderRevert,
|
|
3274
3386
|
buildIndexData,
|
|
3275
3387
|
buildSystemData,
|
|
3276
|
-
loadExportHistory,
|
|
3277
|
-
appendExportHistory,
|
|
3278
3388
|
};
|
|
3279
3389
|
}
|
|
@@ -62,14 +62,31 @@ try {
|
|
|
62
62
|
world.style.zoom = '1';
|
|
63
63
|
world.style.transform = 'none';
|
|
64
64
|
}
|
|
65
|
-
for (const el of document.querySelectorAll('[data-dc-screen]')) {
|
|
66
|
-
el.style.left = '0px';
|
|
67
|
-
el.style.top = '0px';
|
|
68
|
-
}
|
|
69
65
|
});
|
|
70
66
|
|
|
71
67
|
const written = [];
|
|
72
68
|
|
|
69
|
+
// Pin one artboard to (0,0) for its capture, returning a restore function.
|
|
70
|
+
// Per-target rather than a one-shot reset of every `[data-dc-screen]` — a
|
|
71
|
+
// multi-target loop must not leave an already-captured artboard sitting at
|
|
72
|
+
// the origin, overlapping the next target's geometry (the scatter bug).
|
|
73
|
+
const pinArtboard = async (handle) => {
|
|
74
|
+
const saved = await handle.evaluate((el) => {
|
|
75
|
+
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
76
|
+
const prev = { left: ab.style.left, top: ab.style.top };
|
|
77
|
+
ab.style.left = '0px';
|
|
78
|
+
ab.style.top = '0px';
|
|
79
|
+
return prev;
|
|
80
|
+
});
|
|
81
|
+
return async () => {
|
|
82
|
+
await handle.evaluate((el, prev) => {
|
|
83
|
+
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
84
|
+
ab.style.left = prev.left;
|
|
85
|
+
ab.style.top = prev.top;
|
|
86
|
+
}, saved);
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
73
90
|
if (multi) {
|
|
74
91
|
if (!outDir) {
|
|
75
92
|
console.error('_html-playwright: --multi requires --out-dir');
|
|
@@ -80,10 +97,13 @@ try {
|
|
|
80
97
|
for (let i = 0; i < screens.length; i += 1) {
|
|
81
98
|
const handle = screens[i];
|
|
82
99
|
const id = (await handle.getAttribute('data-dc-screen')) ?? `artboard-${i + 1}`;
|
|
100
|
+
const restore = await pinArtboard(handle);
|
|
83
101
|
const html = await serializeOne(handle, false);
|
|
102
|
+
await restore();
|
|
84
103
|
const target = join(outDir, `${id}.html`);
|
|
85
104
|
writeFileSync(target, html, 'utf8');
|
|
86
105
|
written.push(target);
|
|
106
|
+
console.log(`MAUDE_PROGRESS {"current":${i + 1},"total":${screens.length}}`);
|
|
87
107
|
}
|
|
88
108
|
} else {
|
|
89
109
|
if (!out) {
|
|
@@ -93,7 +113,9 @@ try {
|
|
|
93
113
|
mkdirSync(dirname(out), { recursive: true });
|
|
94
114
|
const handle = page.locator(selector ?? '[data-dc-screen]:first-of-type').first();
|
|
95
115
|
await handle.waitFor({ state: 'visible', timeout: timeoutMs });
|
|
116
|
+
const restore = await pinArtboard(handle);
|
|
96
117
|
const html = await serializeOne(handle, widen);
|
|
118
|
+
await restore();
|
|
97
119
|
writeFileSync(out, html, 'utf8');
|
|
98
120
|
written.push(out);
|
|
99
121
|
}
|