@1agh/maude 0.42.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 -22
- package/apps/studio/bin/_ingest-footage.mjs +386 -0
- package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
- 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/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 +451 -34
- package/apps/studio/client/export-center.jsx +35 -1
- 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 +42 -0
- package/apps/studio/commands/edit-source-command.ts +31 -4
- 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/exporters/jobs.ts +47 -5
- package/apps/studio/exporters/video.ts +24 -4
- 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 +196 -20
- package/apps/studio/github/service.ts +67 -0
- package/apps/studio/handoff.ts +3 -2
- package/apps/studio/http.ts +173 -14
- 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 +5 -1
- 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 +13 -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/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 +53 -0
- package/cli/commands/design.mjs +34 -3
- package/cli/lib/gitignore-block.mjs +1 -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
|
|
@@ -255,6 +257,9 @@ export interface Api {
|
|
|
255
257
|
saveAsset(bytes: Uint8Array): Promise<SaveAssetResult>;
|
|
256
258
|
/** Stage F1 — list content-addressed image/video assets for the AssetPicker. */
|
|
257
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[] }>;
|
|
258
263
|
/** DDR-148 — streaming variant for the HTTP route (100 MB video without a
|
|
259
264
|
* full in-RAM buffer). Sniffs + caps + content-addresses like saveAsset. */
|
|
260
265
|
saveAssetFromStream(stream: ReadableStream<Uint8Array>): Promise<SaveAssetResult>;
|
|
@@ -288,7 +293,13 @@ export interface Api {
|
|
|
288
293
|
idIndex?: unknown;
|
|
289
294
|
}): Promise<EditOpResult>;
|
|
290
295
|
// Phase 12 (DDR-103) — inline text-content edit (POST /_api/edit-text). Main-origin only.
|
|
291
|
-
editText(input: {
|
|
296
|
+
editText(input: {
|
|
297
|
+
canvas?: unknown;
|
|
298
|
+
id?: unknown;
|
|
299
|
+
text?: unknown;
|
|
300
|
+
occurrence?: unknown;
|
|
301
|
+
before?: unknown;
|
|
302
|
+
}): Promise<EditOpResult>;
|
|
292
303
|
// Phase 12.2 (DDR-104) — custom HTML attribute edit (POST /_api/edit-attr). Main-origin
|
|
293
304
|
// only. The CSS panel's "custom HTML attribute" escape hatch (data-*, aria-*, role, …);
|
|
294
305
|
// writes a plain JSX attribute via editAttribute's non-`style.` path.
|
|
@@ -396,10 +407,16 @@ export interface Api {
|
|
|
396
407
|
}): Promise<
|
|
397
408
|
{ ok: true; deletedId: string; seq?: number } | { ok: false; status: number; error: string }
|
|
398
409
|
>;
|
|
399
|
-
/**
|
|
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
|
+
*/
|
|
400
416
|
insertElementOp(input: {
|
|
401
417
|
canvas?: unknown;
|
|
402
418
|
refId?: unknown;
|
|
419
|
+
artboardId?: unknown;
|
|
403
420
|
position?: unknown;
|
|
404
421
|
kind?: unknown;
|
|
405
422
|
src?: unknown;
|
|
@@ -472,8 +489,19 @@ export { ASSET_IMAGE_HREF_RE, sanitizeAnnotationSvg } from './annotations-model.
|
|
|
472
489
|
|
|
473
490
|
import { sanitizeAnnotationSvg } from './annotations-model.ts';
|
|
474
491
|
|
|
475
|
-
/**
|
|
476
|
-
|
|
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
|
+
})();
|
|
477
505
|
|
|
478
506
|
/**
|
|
479
507
|
* Phase 23 security review (DDR-088 follow-up) — aggregate per-server-instance
|
|
@@ -565,6 +593,24 @@ export interface AssetListing {
|
|
|
565
593
|
mtimeMs: number;
|
|
566
594
|
}
|
|
567
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
|
+
|
|
568
614
|
/** DDR-148 — media category, decides which per-file cap applies. */
|
|
569
615
|
export type AssetCategory = 'image' | 'video' | 'audio';
|
|
570
616
|
|
|
@@ -576,7 +622,7 @@ export interface AssetTypeInfo {
|
|
|
576
622
|
|
|
577
623
|
/**
|
|
578
624
|
* DDR-148 — per-file ceiling for time-based media (video + audio). Images keep
|
|
579
|
-
* the tighter {@link ASSET_MAX_BYTES}
|
|
625
|
+
* the tighter {@link ASSET_MAX_BYTES} cap. Overridable via
|
|
580
626
|
* `MAUDE_ASSET_MAX_VIDEO_BYTES` (bytes) for power users. The route lives on the
|
|
581
627
|
* (untrusted) canvas origin, so this cap + the session budget + the streamed
|
|
582
628
|
* write are the trust mitigation, exactly like the image caps (DDR-088).
|
|
@@ -595,7 +641,9 @@ const UNSUPPORTED_ASSET_MSG =
|
|
|
595
641
|
'unsupported media type — png/jpeg/gif/webp images or mp4/mov/webm/mp3/wav/m4a media only (SVG/script rejected)';
|
|
596
642
|
|
|
597
643
|
function capError(category?: AssetCategory): string {
|
|
598
|
-
if (category === 'image')
|
|
644
|
+
if (category === 'image') {
|
|
645
|
+
return `image exceeds the ${Math.round(ASSET_MAX_BYTES / (1024 * 1024))} MB cap`;
|
|
646
|
+
}
|
|
599
647
|
const mb = Math.round(ASSET_MAX_VIDEO_BYTES / (1024 * 1024));
|
|
600
648
|
return `media exceeds the ${mb} MB cap`;
|
|
601
649
|
}
|
|
@@ -1316,8 +1364,9 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1316
1364
|
// optional (DDR Task 9):
|
|
1317
1365
|
// • magic-byte sniff → true type ∈ {png,jpg,gif,webp}; a header lie or an
|
|
1318
1366
|
// SVG (script-bearing vector) is rejected — bytes decide, name is ignored.
|
|
1319
|
-
// •
|
|
1320
|
-
// 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).
|
|
1321
1370
|
// • content-addressed name `assets/<sha8-of-bytes>.<ext>` → identical drops
|
|
1322
1371
|
// dedupe → a malicious canvas can't fill the disk with N copies of one
|
|
1323
1372
|
// image, and orphan-on-delete is safe (shared content survives).
|
|
@@ -1331,8 +1380,8 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1331
1380
|
* request body chunk-by-chunk so a 100 MB video never lands as a single
|
|
1332
1381
|
* ArrayBuffer in RAM (the memory-amplification vector DDR-088 §review flagged
|
|
1333
1382
|
* for the untrusted canvas origin — worse now that the per-file cap is 100 MB,
|
|
1334
|
-
* not
|
|
1335
|
-
* 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
|
|
1336
1385
|
* to a temp file, then content-addresses (sha8) → dedupe-rename. Nothing is
|
|
1337
1386
|
* ever parsed — only magic bytes are read. `saveAsset(bytes)` wraps this so
|
|
1338
1387
|
* both the route and any programmatic caller share ONE tested path.
|
|
@@ -1512,6 +1561,59 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1512
1561
|
return { ok: true, assets: out.slice(0, 500) };
|
|
1513
1562
|
}
|
|
1514
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
|
+
|
|
1515
1617
|
// Phase 31 follow-up — persist an image pasted straight from the clipboard into
|
|
1516
1618
|
// the ACP composer (a screenshot has no path yet), so the chip can point Claude
|
|
1517
1619
|
// at a real file to Read. Sibling of saveAsset, but:
|
|
@@ -1519,13 +1621,17 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1519
1621
|
// the versioned `assets/`; pasted screenshots are ephemeral, not canvas media.
|
|
1520
1622
|
// • returns an ABSOLUTE path — Claude runs with its own cwd, so a project-
|
|
1521
1623
|
// relative string could miss; an absolute path always resolves.
|
|
1522
|
-
// Same load-bearing caps as saveAsset (magic-byte sniff → no SVG/script,
|
|
1523
|
-
// content-addressed name, shared session write budget).
|
|
1524
|
-
// 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).
|
|
1525
1627
|
async function saveChatAttachment(bytes: Uint8Array): Promise<SaveAssetResult> {
|
|
1526
1628
|
if (!bytes || bytes.length === 0) return { ok: false, status: 400, error: 'empty body' };
|
|
1527
1629
|
if (bytes.length > ASSET_MAX_BYTES) {
|
|
1528
|
-
return {
|
|
1630
|
+
return {
|
|
1631
|
+
ok: false,
|
|
1632
|
+
status: 413,
|
|
1633
|
+
error: `attachment exceeds the ${Math.round(ASSET_MAX_BYTES / (1024 * 1024))} MB cap`,
|
|
1634
|
+
};
|
|
1529
1635
|
}
|
|
1530
1636
|
const kind = sniffImageType(bytes);
|
|
1531
1637
|
if (!kind) {
|
|
@@ -2009,6 +2115,8 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2009
2115
|
canvas?: unknown;
|
|
2010
2116
|
id?: unknown;
|
|
2011
2117
|
text?: unknown;
|
|
2118
|
+
occurrence?: unknown;
|
|
2119
|
+
before?: unknown;
|
|
2012
2120
|
}): Promise<EditOpResult> {
|
|
2013
2121
|
const r = resolveCanvasAbs(input.canvas);
|
|
2014
2122
|
if (!r.ok) return r;
|
|
@@ -2017,7 +2125,23 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2017
2125
|
if (typeof input.text !== 'string') return { ok: false, status: 400, error: 'text required' };
|
|
2018
2126
|
if (input.text.length > 5000) return { ok: false, status: 413, error: 'text too long' };
|
|
2019
2127
|
const text = input.text;
|
|
2020
|
-
|
|
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
|
+
);
|
|
2021
2145
|
}
|
|
2022
2146
|
|
|
2023
2147
|
async function editAttr(input: {
|
|
@@ -2347,10 +2471,12 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2347
2471
|
}
|
|
2348
2472
|
}
|
|
2349
2473
|
|
|
2350
|
-
/** 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`. */
|
|
2351
2476
|
async function insertElementOp(input: {
|
|
2352
2477
|
canvas?: unknown;
|
|
2353
2478
|
refId?: unknown;
|
|
2479
|
+
artboardId?: unknown;
|
|
2354
2480
|
position?: unknown;
|
|
2355
2481
|
kind?: unknown;
|
|
2356
2482
|
src?: unknown;
|
|
@@ -2361,14 +2487,30 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2361
2487
|
const r = resolveCanvasAbs(input.canvas);
|
|
2362
2488
|
if (!r.ok) return r;
|
|
2363
2489
|
if (!takeStructuralToken()) return RATE_LIMITED;
|
|
2364
|
-
const
|
|
2365
|
-
|
|
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)) {
|
|
2366
2497
|
return { ok: false, status: 400, error: 'invalid reference data-cd-id' };
|
|
2367
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
|
+
}
|
|
2368
2503
|
const position = input.position;
|
|
2369
2504
|
if (typeof position !== 'string' || !MOVE_POSITIONS.has(position)) {
|
|
2370
2505
|
return { ok: false, status: 400, error: 'invalid position' };
|
|
2371
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
|
+
}
|
|
2372
2514
|
const kind = input.kind;
|
|
2373
2515
|
if (kind !== 'div' && kind !== 'text' && kind !== 'image') {
|
|
2374
2516
|
return { ok: false, status: 400, error: 'invalid kind (div|text|image)' };
|
|
@@ -2383,10 +2525,18 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2383
2525
|
ctx.bus.emit('activity:unsuppress', rel);
|
|
2384
2526
|
return { ok: false, status: 413, error: 'canvas source too large to grow' };
|
|
2385
2527
|
}
|
|
2386
|
-
const res =
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
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
|
+
});
|
|
2390
2540
|
const after = await Bun.file(r.abs).text();
|
|
2391
2541
|
if (after === before) {
|
|
2392
2542
|
ctx.bus.emit('activity:unsuppress', rel);
|
|
@@ -3208,6 +3358,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
3208
3358
|
saveAnnotations,
|
|
3209
3359
|
saveAsset,
|
|
3210
3360
|
listAssets,
|
|
3361
|
+
listStickers,
|
|
3211
3362
|
saveAssetFromStream,
|
|
3212
3363
|
saveChatAttachment,
|
|
3213
3364
|
resolveChatAttachment,
|