@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.
Files changed (260) hide show
  1. package/apps/studio/.ai/cache/_stats.json +7 -0
  2. package/apps/studio/acp/bootstrap-brief.ts +12 -2
  3. package/apps/studio/annotations-layer.tsx +1055 -278
  4. package/apps/studio/annotations-model.ts +58 -0
  5. package/apps/studio/api.ts +173 -63
  6. package/apps/studio/bin/_html-playwright.mjs +26 -4
  7. package/apps/studio/bin/_ingest-footage.mjs +386 -0
  8. package/apps/studio/bin/_pdf-playwright.mjs +13 -2
  9. package/apps/studio/bin/_png-playwright.mjs +15 -2
  10. package/apps/studio/bin/_pptx-playwright.mjs +17 -4
  11. package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
  12. package/apps/studio/bin/_screenshot-playwright.mjs +17 -2
  13. package/apps/studio/bin/_svg-playwright.mjs +26 -4
  14. package/apps/studio/bin/_video-playwright.mjs +141 -71
  15. package/apps/studio/bin/ingest-footage.sh +27 -0
  16. package/apps/studio/bin/photo-adjust.sh +163 -0
  17. package/apps/studio/bin/photo-bg-remove.sh +160 -0
  18. package/apps/studio/bin/probe-footage.sh +28 -0
  19. package/apps/studio/bin/read-annotations.mjs +125 -2
  20. package/apps/studio/bin/screenshot.sh +53 -4
  21. package/apps/studio/canvas-edit.ts +459 -10
  22. package/apps/studio/canvas-lib.tsx +523 -5
  23. package/apps/studio/canvas-pipeline.ts +65 -7
  24. package/apps/studio/canvas-shell.tsx +156 -16
  25. package/apps/studio/client/app.jsx +476 -88
  26. package/apps/studio/client/export-center.jsx +460 -0
  27. package/apps/studio/client/github.js +4 -0
  28. package/apps/studio/client/panels/GitPanel.jsx +8 -0
  29. package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
  30. package/apps/studio/client/panels/StickerPicker.jsx +160 -0
  31. package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
  32. package/apps/studio/client/photo-knobs.jsx +437 -0
  33. package/apps/studio/client/styles/3-shell-maude.css +59 -0
  34. package/apps/studio/client/styles/4-components.css +150 -0
  35. package/apps/studio/commands/edit-source-command.ts +31 -4
  36. package/apps/studio/config.schema.json +2 -2
  37. package/apps/studio/context-menu.tsx +31 -6
  38. package/apps/studio/dist/client.bundle.js +5561 -19
  39. package/apps/studio/dist/comment-mount.js +2 -2
  40. package/apps/studio/dist/runtime/.min-sizes.json +1 -0
  41. package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
  42. package/apps/studio/dist/runtime/pixi-js.js +519 -519
  43. package/apps/studio/dist/styles.css +1 -1
  44. package/apps/studio/dom-selection.ts +25 -0
  45. package/apps/studio/export-dialog.tsx +18 -25
  46. package/apps/studio/exporters/_runtime.ts +104 -0
  47. package/apps/studio/exporters/html.ts +12 -20
  48. package/apps/studio/exporters/index.ts +14 -2
  49. package/apps/studio/exporters/jobs.ts +376 -0
  50. package/apps/studio/exporters/pdf.ts +16 -20
  51. package/apps/studio/exporters/png.ts +12 -20
  52. package/apps/studio/exporters/pptx.ts +22 -23
  53. package/apps/studio/exporters/scope.ts +1 -0
  54. package/apps/studio/exporters/svg.ts +14 -22
  55. package/apps/studio/exporters/video.ts +37 -19
  56. package/apps/studio/footage/schema.test.ts +195 -0
  57. package/apps/studio/footage/schema.ts +484 -0
  58. package/apps/studio/footage-store.ts +235 -0
  59. package/apps/studio/fs-watch.ts +10 -0
  60. package/apps/studio/git/endpoints.ts +68 -7
  61. package/apps/studio/git/service.ts +198 -20
  62. package/apps/studio/github/service.ts +67 -0
  63. package/apps/studio/handoff.ts +3 -2
  64. package/apps/studio/http.ts +318 -64
  65. package/apps/studio/input-router.tsx +10 -0
  66. package/apps/studio/inspect.ts +53 -1
  67. package/apps/studio/paths.ts +10 -0
  68. package/apps/studio/photo/filters.ts +285 -0
  69. package/apps/studio/photo/pipeline.ts +607 -0
  70. package/apps/studio/photo/schema.ts +367 -0
  71. package/apps/studio/photo-store.ts +154 -0
  72. package/apps/studio/runtime-bundle.ts +29 -6
  73. package/apps/studio/server.ts +8 -2
  74. package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
  75. package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
  76. package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
  77. package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
  78. package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
  79. package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
  80. package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
  81. package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
  82. package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
  83. package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
  84. package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
  85. package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
  86. package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
  87. package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
  88. package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
  89. package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
  90. package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
  91. package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
  92. package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
  93. package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
  94. package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
  95. package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
  96. package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
  97. package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
  98. package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
  99. package/apps/studio/stickers/life-style/best.png +0 -0
  100. package/apps/studio/stickers/life-style/come-on.png +0 -0
  101. package/apps/studio/stickers/life-style/cut.png +0 -0
  102. package/apps/studio/stickers/life-style/detox.png +0 -0
  103. package/apps/studio/stickers/life-style/just.png +0 -0
  104. package/apps/studio/stickers/life-style/manifest.json +68 -0
  105. package/apps/studio/stickers/life-style/mirror.png +0 -0
  106. package/apps/studio/stickers/life-style/nice.png +0 -0
  107. package/apps/studio/stickers/life-style/objects.png +0 -0
  108. package/apps/studio/stickers/life-style/ok.png +0 -0
  109. package/apps/studio/stickers/life-style/queen.png +0 -0
  110. package/apps/studio/stickers/life-style/star.png +0 -0
  111. package/apps/studio/stickers/life-style/sun.png +0 -0
  112. package/apps/studio/stickers/life-style/water.png +0 -0
  113. package/apps/studio/stickers/life-style/yeah.png +0 -0
  114. package/apps/studio/stickers/life-style/you-can.png +0 -0
  115. package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
  116. package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
  117. package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
  118. package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
  119. package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
  120. package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
  121. package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
  122. package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
  123. package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
  124. package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
  125. package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
  126. package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
  127. package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
  128. package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
  129. package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
  130. package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
  131. package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
  132. package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
  133. package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
  134. package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
  135. package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
  136. package/apps/studio/stickers/project-status/group-100.png +0 -0
  137. package/apps/studio/stickers/project-status/group-101.png +0 -0
  138. package/apps/studio/stickers/project-status/group-102.png +0 -0
  139. package/apps/studio/stickers/project-status/group-103.png +0 -0
  140. package/apps/studio/stickers/project-status/group-104.png +0 -0
  141. package/apps/studio/stickers/project-status/group-105.png +0 -0
  142. package/apps/studio/stickers/project-status/group-106.png +0 -0
  143. package/apps/studio/stickers/project-status/group-107.png +0 -0
  144. package/apps/studio/stickers/project-status/group-108.png +0 -0
  145. package/apps/studio/stickers/project-status/group-109.png +0 -0
  146. package/apps/studio/stickers/project-status/group-110.png +0 -0
  147. package/apps/studio/stickers/project-status/group-111.png +0 -0
  148. package/apps/studio/stickers/project-status/group-112.png +0 -0
  149. package/apps/studio/stickers/project-status/group-113.png +0 -0
  150. package/apps/studio/stickers/project-status/group-114.png +0 -0
  151. package/apps/studio/stickers/project-status/group-115.png +0 -0
  152. package/apps/studio/stickers/project-status/group-117.png +0 -0
  153. package/apps/studio/stickers/project-status/group-118.png +0 -0
  154. package/apps/studio/stickers/project-status/group-119.png +0 -0
  155. package/apps/studio/stickers/project-status/group-120.png +0 -0
  156. package/apps/studio/stickers/project-status/group-121.png +0 -0
  157. package/apps/studio/stickers/project-status/group-122.png +0 -0
  158. package/apps/studio/stickers/project-status/group-41.png +0 -0
  159. package/apps/studio/stickers/project-status/group-42.png +0 -0
  160. package/apps/studio/stickers/project-status/group-43.png +0 -0
  161. package/apps/studio/stickers/project-status/group-44.png +0 -0
  162. package/apps/studio/stickers/project-status/group-45.png +0 -0
  163. package/apps/studio/stickers/project-status/group-46.png +0 -0
  164. package/apps/studio/stickers/project-status/group-47.png +0 -0
  165. package/apps/studio/stickers/project-status/group-48.png +0 -0
  166. package/apps/studio/stickers/project-status/group-49.png +0 -0
  167. package/apps/studio/stickers/project-status/group-50.png +0 -0
  168. package/apps/studio/stickers/project-status/group-51.png +0 -0
  169. package/apps/studio/stickers/project-status/group-52.png +0 -0
  170. package/apps/studio/stickers/project-status/group-53.png +0 -0
  171. package/apps/studio/stickers/project-status/group-54.png +0 -0
  172. package/apps/studio/stickers/project-status/group-55.png +0 -0
  173. package/apps/studio/stickers/project-status/group-56.png +0 -0
  174. package/apps/studio/stickers/project-status/group-57.png +0 -0
  175. package/apps/studio/stickers/project-status/group-58.png +0 -0
  176. package/apps/studio/stickers/project-status/group-59.png +0 -0
  177. package/apps/studio/stickers/project-status/group-60.png +0 -0
  178. package/apps/studio/stickers/project-status/group-61.png +0 -0
  179. package/apps/studio/stickers/project-status/group-62.png +0 -0
  180. package/apps/studio/stickers/project-status/group-63.png +0 -0
  181. package/apps/studio/stickers/project-status/group-64.png +0 -0
  182. package/apps/studio/stickers/project-status/group-65.png +0 -0
  183. package/apps/studio/stickers/project-status/group-66.png +0 -0
  184. package/apps/studio/stickers/project-status/group-67.png +0 -0
  185. package/apps/studio/stickers/project-status/group-68.png +0 -0
  186. package/apps/studio/stickers/project-status/group-69.png +0 -0
  187. package/apps/studio/stickers/project-status/group-70.png +0 -0
  188. package/apps/studio/stickers/project-status/group-71.png +0 -0
  189. package/apps/studio/stickers/project-status/group-72.png +0 -0
  190. package/apps/studio/stickers/project-status/group-73.png +0 -0
  191. package/apps/studio/stickers/project-status/group-74.png +0 -0
  192. package/apps/studio/stickers/project-status/group-75.png +0 -0
  193. package/apps/studio/stickers/project-status/group-76.png +0 -0
  194. package/apps/studio/stickers/project-status/group-77.png +0 -0
  195. package/apps/studio/stickers/project-status/group-78.png +0 -0
  196. package/apps/studio/stickers/project-status/group-79.png +0 -0
  197. package/apps/studio/stickers/project-status/group-80.png +0 -0
  198. package/apps/studio/stickers/project-status/group-81.png +0 -0
  199. package/apps/studio/stickers/project-status/group-82.png +0 -0
  200. package/apps/studio/stickers/project-status/group-83.png +0 -0
  201. package/apps/studio/stickers/project-status/group-84.png +0 -0
  202. package/apps/studio/stickers/project-status/group-85.png +0 -0
  203. package/apps/studio/stickers/project-status/group-86.png +0 -0
  204. package/apps/studio/stickers/project-status/group-87.png +0 -0
  205. package/apps/studio/stickers/project-status/group-88.png +0 -0
  206. package/apps/studio/stickers/project-status/group-89.png +0 -0
  207. package/apps/studio/stickers/project-status/group-90.png +0 -0
  208. package/apps/studio/stickers/project-status/group-91.png +0 -0
  209. package/apps/studio/stickers/project-status/group-92.png +0 -0
  210. package/apps/studio/stickers/project-status/group-93.png +0 -0
  211. package/apps/studio/stickers/project-status/group-94.png +0 -0
  212. package/apps/studio/stickers/project-status/group-96.png +0 -0
  213. package/apps/studio/stickers/project-status/group-97.png +0 -0
  214. package/apps/studio/stickers/project-status/group-98.png +0 -0
  215. package/apps/studio/stickers/project-status/group-99.png +0 -0
  216. package/apps/studio/stickers/project-status/manifest.json +328 -0
  217. package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
  218. package/apps/studio/test/acp-commands.test.ts +2 -1
  219. package/apps/studio/test/asset-api.test.ts +3 -3
  220. package/apps/studio/test/canvas-edit.test.ts +17 -6
  221. package/apps/studio/test/canvas-origin-gate.test.ts +19 -0
  222. package/apps/studio/test/dns-rebinding-guard.test.ts +27 -0
  223. package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
  224. package/apps/studio/test/edit-source-command.test.ts +23 -0
  225. package/apps/studio/test/element-structural-edit.test.ts +46 -0
  226. package/apps/studio/test/export-center.test.tsx +287 -0
  227. package/apps/studio/test/export-shim-multi-capture.test.ts +83 -0
  228. package/apps/studio/test/exporters/history.test.ts +32 -3
  229. package/apps/studio/test/exporters/jobs.test.ts +263 -0
  230. package/apps/studio/test/footage-store.test.ts +100 -0
  231. package/apps/studio/test/git-branches.test.ts +97 -0
  232. package/apps/studio/test/github-api.test.ts +56 -1
  233. package/apps/studio/test/input-router.test.ts +33 -0
  234. package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
  235. package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
  236. package/apps/studio/test/photo-edit-api.test.ts +201 -0
  237. package/apps/studio/test/photo-filters.test.ts +149 -0
  238. package/apps/studio/test/photo-pipeline.test.ts +106 -0
  239. package/apps/studio/test/photo-taxonomy.test.ts +96 -0
  240. package/apps/studio/test/read-annotations.test.ts +164 -0
  241. package/apps/studio/test/shell-importmap.test.ts +49 -0
  242. package/apps/studio/test/text-editability-stamp.test.ts +131 -0
  243. package/apps/studio/test/timeline-parse.test.ts +57 -0
  244. package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
  245. package/apps/studio/test/undo-stack.test.ts +33 -0
  246. package/apps/studio/test/video-asset.test.ts +5 -5
  247. package/apps/studio/test/video-render-bridge.test.ts +23 -1
  248. package/apps/studio/text-caret.ts +239 -0
  249. package/apps/studio/tool-palette.tsx +43 -20
  250. package/apps/studio/undo-stack.ts +94 -2
  251. package/apps/studio/use-annotation-resize.tsx +4 -4
  252. package/apps/studio/use-canvas-media-drop.tsx +38 -1
  253. package/apps/studio/use-selection-set.tsx +21 -0
  254. package/apps/studio/video-comp.tsx +48 -7
  255. package/apps/studio/whats-new.json +70 -0
  256. package/apps/studio/ws.ts +6 -0
  257. package/cli/commands/design.mjs +34 -3
  258. package/cli/lib/gitignore-block.mjs +2 -0
  259. package/package.json +10 -10
  260. package/plugins/design/templates/_shell.html +1 -0
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env bash
2
+ # photo-adjust.sh — feature-photo-editor (Stage G, Task 19). Headless PARAMETRIC
3
+ # photo edit: assemble a PhotoEdit from flags and PUT it to the running dev
4
+ # server's /_api/photo-edit route so an agent (or a human) can tune a photo
5
+ # without opening a browser or clicking a slider.
6
+ #
7
+ # Deliberately the THIN, NON-BROWSER sibling of photo-bg-remove.sh. Background
8
+ # removal needs a browser round-trip because @imgly runs the ML model client-
9
+ # side; parametric edits are pure JSON, so a direct curl to the already-validated
10
+ # route (the photo-store cap stack — validatePhotoEdit + sha8 + containment +
11
+ # size cap — is the gate) is the whole job. DO NOT "fix" this asymmetry by making
12
+ # this verb heavier: there is no inference step here, so a harness canvas +
13
+ # agent-browser would be pure overhead.
14
+ #
15
+ # Reached via `maude design photo-adjust` (never a raw bin path — DDR-062).
16
+ #
17
+ # Usage:
18
+ # photo-adjust.sh --asset <assets/<sha8>.<ext> | <sha8>> [--root <repo>]
19
+ # [--brightness N] [--contrast N] [--saturation N] [--exposure N]
20
+ # [--hue N] [--sepia N] [--grayscale N] [--invert N]
21
+ # [--duotone "#aabbcc,#ddeeff[,intensity]"]
22
+ # [--grain "amount[,size]"]
23
+ # [--pattern "type[,scale,opacity,blend]"]
24
+ # [--mask "preset[,strength]"]
25
+ # [--replace] (overwrite the sidecar instead of merging onto it)
26
+ # [--reset] (clear the sidecar to neutral / unedited)
27
+ #
28
+ # Adjustment ranges (normalized, 0 = neutral): brightness/contrast/saturation/
29
+ # exposure ∈ −1..1, hue ∈ −180..180 (deg), sepia/grayscale/invert ∈ 0..1.
30
+ # Merges onto the existing sidecar by default (successive calls accumulate).
31
+ #
32
+ # Requires a running dev server (caller runs `maude design server-up` first);
33
+ # reads the port from <designRoot>/_server.json.
34
+ #
35
+ # Stdout (last line): the sidecar path `assets/<sha8>.photo.json` (for $(...)).
36
+ # Stderr: progress / diagnostics.
37
+ # Exit: 0 ok / 1 server or asset problem / 2 bad args / 3 server rejected edit.
38
+
39
+ set -euo pipefail
40
+
41
+ ASSET="" REPO="" REPLACE=0 RESET=0
42
+ BRIGHTNESS="" CONTRAST="" SATURATION="" EXPOSURE="" HUE="" SEPIA="" GRAYSCALE="" INVERT=""
43
+ DUOTONE="" GRAIN="" PATTERN="" MASK=""
44
+
45
+ while [ $# -gt 0 ]; do
46
+ case "$1" in
47
+ --asset) ASSET="$2"; shift 2 ;;
48
+ --root) REPO="$2"; shift 2 ;;
49
+ --brightness) BRIGHTNESS="$2"; shift 2 ;;
50
+ --contrast) CONTRAST="$2"; shift 2 ;;
51
+ --saturation) SATURATION="$2"; shift 2 ;;
52
+ --exposure) EXPOSURE="$2"; shift 2 ;;
53
+ --hue) HUE="$2"; shift 2 ;;
54
+ --sepia) SEPIA="$2"; shift 2 ;;
55
+ --grayscale) GRAYSCALE="$2"; shift 2 ;;
56
+ --invert) INVERT="$2"; shift 2 ;;
57
+ --duotone) DUOTONE="$2"; shift 2 ;;
58
+ --grain) GRAIN="$2"; shift 2 ;;
59
+ --pattern) PATTERN="$2"; shift 2 ;;
60
+ --mask) MASK="$2"; shift 2 ;;
61
+ --replace) REPLACE=1; shift ;;
62
+ --reset) RESET=1; shift ;;
63
+ --help|-h) sed -n '2,39p' "$0" | sed 's/^# \?//'; exit 0 ;;
64
+ *) echo "photo-adjust.sh: unknown arg '$1'" >&2; exit 2 ;;
65
+ esac
66
+ done
67
+
68
+ [ -n "$ASSET" ] || { echo "photo-adjust.sh: --asset is required" >&2; exit 2; }
69
+ command -v jq >/dev/null 2>&1 || { echo "photo-adjust.sh: jq is required" >&2; exit 1; }
70
+
71
+ if [ -z "$REPO" ]; then
72
+ REPO="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
73
+ fi
74
+ DESIGN_ROOT="$REPO/.design"
75
+ [ -d "$DESIGN_ROOT" ] || { echo "photo-adjust.sh: no .design/ under $REPO" >&2; exit 1; }
76
+
77
+ STATE="$DESIGN_ROOT/_server.json"
78
+ [ -f "$STATE" ] || { echo "photo-adjust.sh: no _server.json (start the dev server first)" >&2; exit 1; }
79
+ PORT=$(jq -r '.port // empty' "$STATE" 2>/dev/null)
80
+ [ -n "$PORT" ] || { echo "photo-adjust.sh: could not read port from $STATE" >&2; exit 1; }
81
+
82
+ BASE_URL="http://127.0.0.1:$PORT/_api/photo-edit"
83
+ # URL-encode the asset param minimally (slashes are safe as a query value here).
84
+ ENC_ASSET=$(printf '%s' "$ASSET" | jq -sRr @uri)
85
+ URL="$BASE_URL?asset=$ENC_ASSET"
86
+
87
+ # --reset short-circuits: write a neutral sidecar.
88
+ if [ "$RESET" -eq 1 ]; then
89
+ RESP=$(curl -s -X PUT -H 'Content-Type: application/json' -d '{}' "$URL")
90
+ OK=$(printf '%s' "$RESP" | jq -r '.ok // false' 2>/dev/null || echo false)
91
+ [ "$OK" = "true" ] || { echo "photo-adjust.sh: reset rejected: $RESP" >&2; exit 3; }
92
+ printf '%s\n' "$(printf '%s' "$RESP" | jq -r '.path')"
93
+ exit 0
94
+ fi
95
+
96
+ # Assemble the patch from set flags (unset flags are omitted entirely).
97
+ PATCH=$(jq -n \
98
+ --arg brightness "$BRIGHTNESS" --arg contrast "$CONTRAST" --arg saturation "$SATURATION" \
99
+ --arg exposure "$EXPOSURE" --arg hue "$HUE" --arg sepia "$SEPIA" \
100
+ --arg grayscale "$GRAYSCALE" --arg invert "$INVERT" \
101
+ --arg duotone "$DUOTONE" --arg grain "$GRAIN" --arg pattern "$PATTERN" --arg mask "$MASK" '
102
+ def n($s): if $s == "" then null else ($s | tonumber) end;
103
+ def adj:
104
+ { brightness: n($brightness), contrast: n($contrast), saturation: n($saturation),
105
+ exposure: n($exposure), hue: n($hue), sepia: n($sepia),
106
+ grayscale: n($grayscale), invert: n($invert) }
107
+ | with_entries(select(.value != null));
108
+ def duo:
109
+ if $duotone == "" then null
110
+ else ($duotone | split(",")) as $p
111
+ | { enabled: true, colorA: $p[0], colorB: $p[1] }
112
+ + (if ($p | length) > 2 then { intensity: ($p[2] | tonumber) } else {} end)
113
+ end;
114
+ def gr:
115
+ if $grain == "" then null
116
+ else ($grain | split(",")) as $p
117
+ | { enabled: true, amount: ($p[0] | tonumber) }
118
+ + (if ($p | length) > 1 then { size: ($p[1] | tonumber) } else {} end)
119
+ end;
120
+ def pat:
121
+ if $pattern == "" then null
122
+ else ($pattern | split(",")) as $p
123
+ | { enabled: true, type: $p[0] }
124
+ + (if ($p | length) > 1 then { scale: ($p[1] | tonumber) } else {} end)
125
+ + (if ($p | length) > 2 then { opacity: ($p[2] | tonumber) } else {} end)
126
+ + (if ($p | length) > 3 then { blend: $p[3] } else {} end)
127
+ end;
128
+ def msk:
129
+ if $mask == "" then null
130
+ else ($mask | split(",")) as $p
131
+ | { preset: $p[0] }
132
+ + (if ($p | length) > 1 then { strength: ($p[1] | tonumber) } else {} end)
133
+ end;
134
+ ({}
135
+ + (adj as $a | if ($a | length) > 0 then { adjustments: $a } else {} end)
136
+ + (duo as $d | if $d then { duotone: $d } else {} end)
137
+ + (gr as $g | if $g then { grain: $g } else {} end)
138
+ + (pat as $p | if $p then { pattern: $p } else {} end)
139
+ + (msk as $m | if $m then { mask: $m } else {} end))
140
+ ')
141
+
142
+ if [ "$(printf '%s' "$PATCH" | jq -r 'keys | length')" = "0" ]; then
143
+ echo "photo-adjust.sh: no edit flags given (nothing to do)" >&2
144
+ exit 2
145
+ fi
146
+
147
+ if [ "$REPLACE" -eq 1 ]; then
148
+ BODY="$PATCH"
149
+ else
150
+ # Merge onto the current sidecar (deep object merge via jq's `*`), dropping the
151
+ # server-stamped `version` (it's re-stamped on write).
152
+ BASE=$(curl -s "$URL" 2>/dev/null || echo '{}')
153
+ printf '%s' "$BASE" | jq -e . >/dev/null 2>&1 || BASE='{}'
154
+ BODY=$(jq -n --argjson base "$BASE" --argjson patch "$PATCH" '($base | del(.version)) * $patch')
155
+ fi
156
+
157
+ RESP=$(curl -s -X PUT -H 'Content-Type: application/json' -d "$BODY" "$URL")
158
+ OK=$(printf '%s' "$RESP" | jq -r '.ok // false' 2>/dev/null || echo false)
159
+ if [ "$OK" != "true" ]; then
160
+ echo "photo-adjust.sh: server rejected the edit: $RESP" >&2
161
+ exit 3
162
+ fi
163
+ printf '%s\n' "$(printf '%s' "$RESP" | jq -r '.path')"
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env bash
2
+ # photo-bg-remove.sh — feature-photo-editor (Stage G, Task 18). Headless
3
+ # background removal: mirrors draw-proof.sh's structure (throwaway harness
4
+ # canvas + agent-browser drive) but the "proof" here isn't a screenshot ladder
5
+ # — it's the client-side @imgly ML pass actually running and reporting back
6
+ # through DOM attributes agent-browser polls, since there's no other channel
7
+ # to cross the process boundary out of a headless browser.
8
+ #
9
+ # Design note (the decision this task was originally deferred on): the harness
10
+ # canvas runs inside the split-origin (DDR-054) canvas iframe, but BOTH
11
+ # `/_api/asset` and `/_api/photo-edit` are already canvas-safe routes (see
12
+ # their CANVAS_SAFE_API comments in http.ts) — so the harness component
13
+ # (`PhotoBgRemoveHarness`, canvas-lib.tsx) posts the matte and the updated
14
+ # PhotoEdit sidecar directly from the canvas origin. No cross-origin relay,
15
+ # no base64-through-a-DOM-attribute round-trip needed.
16
+ #
17
+ # Reached via `maude design photo-bg-remove` (never a raw bin path — DDR-062).
18
+ #
19
+ # Usage:
20
+ # photo-bg-remove.sh --asset <assets/<sha8>.<ext>> [--root <repo>]
21
+ # [--slug <name>] [--timeout <secs>]
22
+ #
23
+ # Requires a running dev server (caller runs `maude design server-up` first);
24
+ # reads the port from <designRoot>/_server.json. Requires agent-browser (the
25
+ # ML pass needs a real WASM/WebGPU-capable browser driving real DOM state —
26
+ # there is no non-browser path, unlike photo-adjust.sh's plain curl).
27
+ #
28
+ # Stdout (last line): the new matte asset path `assets/<sha8>.png` (for $(...)).
29
+ # Stderr: progress / diagnostics.
30
+ # Exit: 0 success / 1 missing input, server, or agent-browser / 2 bad args
31
+ # / 3 drive/navigation failed / 4 the harness itself reported an error
32
+ # / 5 timed out waiting for the ML pass to finish.
33
+
34
+ set -uo pipefail
35
+
36
+ ASSET="" SLUG="" REPO="" TIMEOUT=150
37
+
38
+ while [ $# -gt 0 ]; do
39
+ case "$1" in
40
+ --asset) ASSET="$2"; shift 2 ;;
41
+ --slug) SLUG="$2"; shift 2 ;;
42
+ --root) REPO="$2"; shift 2 ;;
43
+ --timeout) TIMEOUT="$2"; shift 2 ;;
44
+ --help|-h)
45
+ sed -n '2,29p' "$0" | sed 's/^# \?//'
46
+ exit 0
47
+ ;;
48
+ *)
49
+ echo "photo-bg-remove.sh: unknown arg '$1' (try --help)" >&2
50
+ exit 2
51
+ ;;
52
+ esac
53
+ done
54
+
55
+ [ -n "$ASSET" ] || { echo "photo-bg-remove.sh: --asset <assets/<sha8>.<ext>> required" >&2; exit 2; }
56
+ # Strip a leading designRoot-relative slash/prefix if the caller passed one.
57
+ ASSET="${ASSET#/}"
58
+ case "$ASSET" in
59
+ assets/*) : ;;
60
+ *) echo "photo-bg-remove.sh: --asset must look like assets/<sha8>.<ext>, got '$ASSET'" >&2; exit 2 ;;
61
+ esac
62
+
63
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
64
+ [ -n "$SLUG" ] || SLUG=$(basename "$ASSET" | sed -E 's/\.[^.]+$//')
65
+ SLUG=$(bash "$SCRIPT_DIR/slug.sh" "$SLUG" 2>/dev/null || printf '%s' "$SLUG" | tr '[:upper:]' '[:lower:]')
66
+
67
+ # ---------- resolve repo + design root + port ----------
68
+ if [ -z "$REPO" ]; then
69
+ REPO="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
70
+ fi
71
+ DESIGN_ROOT="$REPO/.design"
72
+ [ -d "$DESIGN_ROOT" ] || { echo "photo-bg-remove.sh: no .design/ under $REPO" >&2; exit 1; }
73
+ [ -f "$DESIGN_ROOT/$ASSET" ] || { echo "photo-bg-remove.sh: asset not found: $DESIGN_ROOT/$ASSET" >&2; exit 1; }
74
+
75
+ STATE="$DESIGN_ROOT/_server.json"
76
+ [ -f "$STATE" ] || { echo "photo-bg-remove.sh: $STATE missing — run \`maude design server-up\` first" >&2; exit 1; }
77
+ if command -v jq >/dev/null 2>&1; then
78
+ PORT=$(jq -r .port "$STATE" 2>/dev/null)
79
+ else
80
+ PORT=$(sed -nE 's/.*"port"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/p' "$STATE" | head -n1)
81
+ fi
82
+ [ -n "$PORT" ] || { echo "photo-bg-remove.sh: no port in $STATE" >&2; exit 1; }
83
+
84
+ # ---------- resolve agent-browser (hard dependency — see header) ----------
85
+ # Mirrors screenshot.sh's MAUDE_AGENT_BROWSER override (DDR-144 attacker-F4 — an
86
+ # explicit single-binary pointer, never a PATH prepend a same-user attacker
87
+ # could shadow) and its ensure-browser.sh chrome resolution.
88
+ AB="${MAUDE_AGENT_BROWSER:-agent-browser}"
89
+ command -v "$AB" >/dev/null 2>&1 || {
90
+ echo "photo-bg-remove.sh: agent-browser not found (required — the ML pass needs a real WASM/WebGPU browser, there is no non-browser fallback here)" >&2
91
+ exit 1
92
+ }
93
+ if [ -z "${AGENT_BROWSER_EXECUTABLE_PATH:-}" ]; then
94
+ ES_FLAGS="--quiet"
95
+ [ -z "${MAUDE_DEV_SERVER_ROOT:-}" ] && ES_FLAGS="--quiet --no-download"
96
+ BROWSER_PATH="$(bash "$SCRIPT_DIR/ensure-browser.sh" $ES_FLAGS 2>/dev/null)"
97
+ [ -n "$BROWSER_PATH" ] && export AGENT_BROWSER_EXECUTABLE_PATH="$BROWSER_PATH"
98
+ fi
99
+
100
+ # ---------- generate the harness canvas ----------
101
+ PHOTO_DIR="$DESIGN_ROOT/_photo"
102
+ mkdir -p "$PHOTO_DIR"
103
+ HARNESS_TSX="$PHOTO_DIR/$SLUG.bgremove.tsx"
104
+ {
105
+ echo "import { PhotoBgRemoveHarness } from '@maude/canvas-lib';"
106
+ echo
107
+ echo "// AUTO-GENERATED by \`maude design photo-bg-remove\` — headless bg-removal proof"
108
+ echo "// harness. Lives under _photo/ (gitignored); safe to delete + regenerate."
109
+ echo "export default function PhotoBgRemoveCanvas() {"
110
+ printf ' return <PhotoBgRemoveHarness source=%s />;\n' "\"$ASSET\""
111
+ echo "}"
112
+ } > "$HARNESS_TSX"
113
+ echo "→ harness canvas: ${HARNESS_TSX#$REPO/}" >&2
114
+
115
+ REL="_photo/$SLUG.bgremove.tsx"
116
+ URL="http://localhost:$PORT/_canvas-shell.html?canvas=$REL"
117
+ SESSION="photo-bgremove-$SLUG-$$"
118
+ SEL="[data-photo-bgremove-status]"
119
+
120
+ cleanup() { "$AB" --session "$SESSION" close >/dev/null 2>&1 || true; }
121
+ trap cleanup EXIT
122
+
123
+ echo "→ driving $URL (session $SESSION)" >&2
124
+ if ! "$AB" --session "$SESSION" open "$URL" >&2; then
125
+ echo "photo-bg-remove.sh: agent-browser failed to open the harness canvas" >&2
126
+ exit 3
127
+ fi
128
+
129
+ # ---------- poll the harness's status attribute ----------
130
+ # The ML pass includes a first-use ~40 MB model-weight fetch (Task 11/12) —
131
+ # this can dominate the wait far more than inference itself, hence the
132
+ # generous default timeout.
133
+ ELAPSED=0
134
+ STATUS="pending"
135
+ while [ "$ELAPSED" -lt "$TIMEOUT" ]; do
136
+ STATUS="$("$AB" --session "$SESSION" get attr "$SEL" data-photo-bgremove-status 2>/dev/null | tr -d '"')"
137
+ case "$STATUS" in
138
+ done|error) break ;;
139
+ esac
140
+ sleep 2
141
+ ELAPSED=$((ELAPSED + 2))
142
+ done
143
+
144
+ if [ "$STATUS" = "error" ]; then
145
+ ERR="$("$AB" --session "$SESSION" get attr "$SEL" data-photo-bgremove-error 2>/dev/null | tr -d '"')"
146
+ echo "photo-bg-remove.sh: harness reported an error: ${ERR:-unknown}" >&2
147
+ exit 4
148
+ fi
149
+
150
+ if [ "$STATUS" != "done" ]; then
151
+ echo "photo-bg-remove.sh: timed out after ${TIMEOUT}s waiting for the ML pass (still '$STATUS')" >&2
152
+ exit 5
153
+ fi
154
+
155
+ RESULT="$("$AB" --session "$SESSION" get attr "$SEL" data-photo-bgremove-result 2>/dev/null | tr -d '"')"
156
+ [ -n "$RESULT" ] || { echo "photo-bg-remove.sh: harness reported done but no result asset" >&2; exit 4; }
157
+
158
+ echo "→ background removed → $RESULT" >&2
159
+ # Last stdout line = the new matte asset path, for $(maude design photo-bg-remove ...) capture.
160
+ printf '%s\n' "$RESULT"
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bash
2
+ # probe-footage.sh — thin shim over _probe-footage-playwright.mjs; reached via
3
+ # `maude design probe-footage` (DDR-062), never a raw bin path. Extracts keyframe
4
+ # PNGs from one clip in headless Chromium for the footage-analyst's vision pass
5
+ # (feature-footage-analysis-director, Task 4). See the .mjs for the rationale.
6
+ #
7
+ # Usage:
8
+ # probe-footage.sh <assets/<sha8>.<ext>> --root <repo> [--design-root .design]
9
+ # [--frames N] [--out-dir DIR]
10
+ #
11
+ # stdout on success = a JSON manifest { asset, durationSec, width, height, frames }.
12
+ # Exit: 0 ok · 2 usage · 3 no browser · 4 decode/probe error · 1 other.
13
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
14
+
15
+ case "$1" in
16
+ --help|-h) sed -n '2,14p' "$0" | sed 's/^# \?//'; exit 0 ;;
17
+ esac
18
+
19
+ # The probe imports `playwright` (Chromium). Prefer node (the runtime the other
20
+ # playwright shims run under); fall back to bun in a dev tree.
21
+ if command -v node >/dev/null 2>&1; then
22
+ exec node "$SCRIPT_DIR/_probe-footage-playwright.mjs" "$@"
23
+ elif command -v bun >/dev/null 2>&1; then
24
+ exec bun run "$SCRIPT_DIR/_probe-footage-playwright.mjs" "$@"
25
+ else
26
+ echo "probe-footage.sh: node or bun is required." >&2
27
+ exit 1
28
+ fi
@@ -105,8 +105,15 @@ Args:
105
105
  --json No-op (JSON is the only output form).
106
106
 
107
107
  FigJam v3 additive fields per annotation: z (render order), groupIds (deepest →
108
- shallowest), author ("ai" for annotate-verb strokes), and from/to host ids on
109
- bound arrows.
108
+ shallowest), author ("ai" for annotate-verb strokes), authorName/authorId
109
+ (human-drawn stickies), and from/to host ids on bound arrows.
110
+
111
+ Section annotations additionally carry members: an array of every OTHER
112
+ annotation whose center falls inside the section's rect, in SPATIAL reading
113
+ order (top-to-bottom, left-to-right — not paint order), each as
114
+ { id, tool, order, x, y, w, h, href? }. Answers "what's in this section, and
115
+ in what order" for prompts like "make a video / Instagram carousel from
116
+ this section" without hand-computing containment.
110
117
 
111
118
  Emits JSON to stdout. A missing annotation file emits [] (exit 0).`;
112
119
 
@@ -395,6 +402,31 @@ function readUnknown(tool, attrs, inner) {
395
402
  // provenance, render order (z = document position). Applied uniformly after
396
403
  // the per-tool extraction; arrows additionally surface bound endpoints as
397
404
  // `from`/`to` host ids so a bound diagram reads back as a GRAPH.
405
+ const AUTHOR_NAME_MAX_LEN = 64;
406
+
407
+ // Phase 3 (whiteboard-improvements) — mirrors annotations-model.ts's
408
+ // sanitizeAuthorName (same code-point ranges: control chars, bidi
409
+ // overrides/isolates, zero-width chars, BOM) so a peer-synced
410
+ // `.annotations.svg`'s authorName/authorId reads identically through
411
+ // either surface. Every string this CLI returns is already treated as DATA
412
+ // by the whiteboard skill's trust model — this additionally strips the
413
+ // characters that could visually spoof an author name.
414
+ function sanitizeAuthorName(raw) {
415
+ let cleaned = '';
416
+ for (const ch of raw) {
417
+ const cp = ch.codePointAt(0) ?? 0;
418
+ const unsafe =
419
+ cp <= 0x1f ||
420
+ (cp >= 0x7f && cp <= 0x9f) ||
421
+ (cp >= 0x200b && cp <= 0x200f) ||
422
+ (cp >= 0x202a && cp <= 0x202e) ||
423
+ (cp >= 0x2066 && cp <= 0x2069) ||
424
+ cp === 0xfeff;
425
+ if (!unsafe) cleaned += ch;
426
+ }
427
+ return cleaned.trim().slice(0, AUTHOR_NAME_MAX_LEN);
428
+ }
429
+
398
430
  function withShared(o, attrs, z) {
399
431
  o.z = z;
400
432
  const g = attr(attrs, 'data-group-ids');
@@ -403,6 +435,16 @@ function withShared(o, attrs, z) {
403
435
  if (ids.length) o.groupIds = ids;
404
436
  }
405
437
  if (attr(attrs, 'data-author') === 'ai') o.author = 'ai';
438
+ const authorName = attr(attrs, 'data-author-name');
439
+ if (authorName) {
440
+ const cleaned = sanitizeAuthorName(decodeEntities(authorName));
441
+ if (cleaned) o.authorName = cleaned;
442
+ }
443
+ const authorId = attr(attrs, 'data-author-id');
444
+ if (authorId) {
445
+ const cleanedId = sanitizeAuthorName(decodeEntities(authorId));
446
+ if (cleanedId) o.authorId = cleanedId;
447
+ }
406
448
  if (o.tool === 'arrow') {
407
449
  const sb = parseBind(attr(attrs, 'data-start-bind'));
408
450
  if (sb) o.from = sb.hostId;
@@ -615,6 +657,85 @@ function anchorToElement(ann, elements) {
615
657
  };
616
658
  }
617
659
 
660
+ // ─────────────────────────────────────────────────────────────────────────────
661
+ // Section membership + reading order (Phase 5, feature-whiteboard-
662
+ // annotation-improvements). A common flow: the user drops several media/
663
+ // stickies INTO a section, then asks "make a video from this" / "turn this
664
+ // into an Instagram carousel" — the agent needs to know exactly which
665
+ // annotations live in a given section, and in what visual order, without
666
+ // hand-computing containment or guessing from raw x/y. Mirrors
667
+ // `findElement`'s smallest-containing-rect convention (annotation CENTER
668
+ // inside a rect), applied against SECTION rects instead of DOM element
669
+ // rects.
670
+
671
+ /**
672
+ * Spatial reading order — top-to-bottom, then left-to-right — NOT `z`
673
+ * (paint/document order): a user can drop stickies in any order they like,
674
+ * and assembling a carousel/video in PAINT order would visibly scramble it.
675
+ * Rows are bucketed by a Y-tolerance band (half the smallest member's
676
+ * height, floored at 16 units) so items a few units off from each other
677
+ * still read as "the same row"; `z` only breaks a true tie (same band, same
678
+ * x) between two members.
679
+ */
680
+ function attachSectionMembers(annotations) {
681
+ const sections = annotations.filter((a) => a.tool === 'section' && a.id);
682
+ if (!sections.length) return annotations;
683
+
684
+ const centerOf = (a) => {
685
+ if (a.x == null || a.y == null) return null;
686
+ return { cx: a.x + (a.w || 0) / 2, cy: a.y + (a.h || 0) / 2 };
687
+ };
688
+
689
+ const membersBySection = new Map();
690
+ for (const section of sections) {
691
+ const sx1 = section.x;
692
+ const sy1 = section.y;
693
+ const sx2 = section.x + (section.w || 0);
694
+ const sy2 = section.y + (section.h || 0);
695
+ const candidates = [];
696
+ for (const a of annotations) {
697
+ if (a.tool === 'section' || !a.id) continue;
698
+ const c = centerOf(a);
699
+ if (!c) continue;
700
+ if (c.cx < sx1 || c.cx > sx2 || c.cy < sy1 || c.cy > sy2) continue;
701
+ candidates.push({ ann: a, c });
702
+ }
703
+ if (!candidates.length) {
704
+ membersBySection.set(section.id, []);
705
+ continue;
706
+ }
707
+ const heights = candidates.map((m) => m.ann.h || 0).filter((h) => h > 0);
708
+ const minHeight = heights.length ? Math.min(...heights) : 40;
709
+ const band = Math.max(16, minHeight / 2);
710
+ candidates.sort((m1, m2) => {
711
+ const dy = m1.c.cy - m2.c.cy;
712
+ if (Math.abs(dy) > band) return dy;
713
+ const dx = m1.c.cx - m2.c.cx;
714
+ if (dx !== 0) return dx;
715
+ return (m1.ann.z ?? 0) - (m2.ann.z ?? 0);
716
+ });
717
+ membersBySection.set(
718
+ section.id,
719
+ candidates.map((m, i) => ({
720
+ id: m.ann.id,
721
+ tool: m.ann.tool,
722
+ order: i,
723
+ x: m.ann.x,
724
+ y: m.ann.y,
725
+ w: m.ann.w,
726
+ h: m.ann.h,
727
+ ...(m.ann.href ? { href: m.ann.href } : null),
728
+ }))
729
+ );
730
+ }
731
+
732
+ return annotations.map((a) =>
733
+ a.tool === 'section' && membersBySection.has(a.id)
734
+ ? { ...a, members: membersBySection.get(a.id) }
735
+ : a
736
+ );
737
+ }
738
+
618
739
  // ─────────────────────────────────────────────────────────────────────────────
619
740
  // Main
620
741
 
@@ -674,6 +795,7 @@ function main() {
674
795
  if (artboards.length) {
675
796
  annotations = annotations.map((a) => anchorToArtboard(a, artboards));
676
797
  }
798
+ annotations = attachSectionMembers(annotations);
677
799
 
678
800
  if (args.graph) {
679
801
  process.stdout.write(`${JSON.stringify({ annotations, graph: buildGraph(annotations) })}\n`);
@@ -689,6 +811,7 @@ function main() {
689
811
  // canvas-rects manifest without a second implementation.
690
812
  export {
691
813
  anchorToElement,
814
+ attachSectionMembers,
692
815
  buildGraph,
693
816
  fileSlug,
694
817
  findElement,
@@ -9,12 +9,18 @@
9
9
  # [--screen <id> | --element <id> | --selector <css> | --full]
10
10
  # [--all-screens] [--out <path>] [--out-dir <dir>]
11
11
  # [--timeout 8] [--engine auto|agent-browser|playwright]
12
- # [--root <repo>]
12
+ # [--theme <name>] [--root <repo>]
13
13
  #
14
14
  # Notes:
15
15
  # - Exactly one of --screen / --element / --selector / --full is required
16
16
  # (or --all-screens which loops over every [data-dc-screen]/[data-dc-slot]).
17
17
  # - --out required for single-shot modes; --out-dir required for --all-screens.
18
+ # - --theme <name> forces every `[data-theme]` element (DS artboard wrappers)
19
+ # to that value BEFORE capture, via a DOM eval — does not touch the actual
20
+ # canvas file or the running server's state. Used to capture a DS's
21
+ # alternate theme deterministically for the dual-theme reality check
22
+ # (/design:new step 9) instead of relying on whatever the canvas is pinned
23
+ # to. No-op if the canvas has no `[data-theme]` elements.
18
24
  # - URL resolution: --url > --port + _active.json > _server.json + _active.json.
19
25
  # - Stdout: written PNG paths, one per line (composable in for-loops).
20
26
  # - Stderr: diagnostic, engine choice, timing.
@@ -30,6 +36,7 @@ TIMEOUT=8
30
36
  ENGINE="auto"
31
37
  ALL_SCREENS=0
32
38
  ROOT=""
39
+ THEME=""
33
40
 
34
41
  while [ $# -gt 0 ]; do
35
42
  case "$1" in
@@ -44,9 +51,10 @@ while [ $# -gt 0 ]; do
44
51
  --out-dir) OUT_DIR="$2"; shift 2 ;;
45
52
  --timeout) TIMEOUT="$2"; shift 2 ;;
46
53
  --engine) ENGINE="$2"; shift 2 ;;
54
+ --theme) THEME="$2"; shift 2 ;;
47
55
  --root) ROOT="$2"; shift 2 ;;
48
56
  --help|-h)
49
- sed -n '2,22p' "$0" | sed 's/^# \?//'
57
+ sed -n '2,27p' "$0" | sed 's/^# \?//'
50
58
  exit 0
51
59
  ;;
52
60
  *)
@@ -57,6 +65,20 @@ while [ $# -gt 0 ]; do
57
65
  done
58
66
 
59
67
  # ---------- arg validation ----------
68
+ # --theme ultimately traces back to config.json's designSystems[].themes[]
69
+ # (untrusted, same posture as prep.sh's MOODBOARD_VARIANTS). Callers (e.g.
70
+ # new.md step 9) build an --out-dir path from this value — reject anything
71
+ # outside a safe slug charset here too, at the shared sink, rather than
72
+ # trusting every caller to re-derive this guard.
73
+ if [ -n "$THEME" ]; then
74
+ case "$THEME" in
75
+ *[!A-Za-z0-9._-]*|"")
76
+ echo "screenshot.sh: --theme '$THEME' contains unsafe characters (allowed: A-Za-z0-9._-)" >&2
77
+ exit 2
78
+ ;;
79
+ esac
80
+ fi
81
+
60
82
  if [ $ALL_SCREENS -eq 1 ]; then
61
83
  [ -z "$OUT_DIR" ] && { echo "screenshot.sh: --all-screens needs --out-dir" >&2; exit 2; }
62
84
  else
@@ -189,10 +211,12 @@ pw_screenshot() {
189
211
  return 1
190
212
  fi
191
213
  echo "→ playwright engine; first invocation may install chromium (~150MB, one-off)" >&2
214
+ local theme_args=()
215
+ [ -n "$THEME" ] && theme_args=(--theme "$THEME")
192
216
  if [ -n "$css" ]; then
193
- npm exec --yes --package=playwright -- node "$pw_script" --url "$URL" --selector "$css" --out "$out" --timeout "$TIMEOUT" >&2 || return 1
217
+ npm exec --yes --package=playwright -- node "$pw_script" --url "$URL" --selector "$css" --out "$out" --timeout "$TIMEOUT" "${theme_args[@]}" >&2 || return 1
194
218
  else
195
- npm exec --yes --package=playwright -- node "$pw_script" --url "$URL" --out "$out" --timeout "$TIMEOUT" >&2 || return 1
219
+ npm exec --yes --package=playwright -- node "$pw_script" --url "$URL" --out "$out" --timeout "$TIMEOUT" "${theme_args[@]}" >&2 || return 1
196
220
  fi
197
221
  [ -s "$out" ] || { echo "✗ playwright wrote no file: $out" >&2; return 1; }
198
222
  return 0
@@ -224,9 +248,34 @@ navigate_once() {
224
248
  echo "→ no DC mount detected after ${TIMEOUT}s — proceeding (may be a non-canvas page)" >&2
225
249
  sleep 1
226
250
  fi
251
+ apply_theme_override
227
252
  fi
228
253
  }
229
254
 
255
+ # ---------- --theme override (dual-theme reality check) ----------
256
+ # Forces every element carrying a `data-theme` attribute (DS artboard theme
257
+ # wrappers) to $THEME, so the caller can deterministically capture a DS's
258
+ # alternate theme instead of whatever the canvas happens to be pinned to.
259
+ # agent-browser only — the playwright path applies it inside
260
+ # _screenshot-playwright.mjs (passed via --theme) since that shim owns its own
261
+ # page.evaluate. No-op when --theme wasn't passed or no [data-theme] elements
262
+ # exist (e.g. a single-theme DS or a non-canvas page).
263
+ apply_theme_override() {
264
+ [ -z "$THEME" ] && return 0
265
+ [ "$ENGINE" = "agent-browser" ] || return 0
266
+ # THEME ultimately traces back to config.json's designSystems[].themes[]
267
+ # array — treated as untrusted input elsewhere in this codebase (see
268
+ # prep.sh's MOODBOARD_VARIANTS clamp). JSON-encode it via jq so it lands as
269
+ # a properly-escaped JS string literal in the eval'd expression below,
270
+ # instead of naively splicing the raw value into a JS string (which a
271
+ # crafted config value could break out of).
272
+ local theme_json
273
+ theme_json=$(jq -Rn --arg t "$THEME" '$t' 2>/dev/null) || return 0
274
+ local n
275
+ n=$("$AB" eval "(function(t){var els=document.querySelectorAll('[data-theme]');els.forEach(function(el){el.setAttribute('data-theme',t)});return els.length})($theme_json)" 2>/dev/null)
276
+ echo "→ theme override: forced data-theme=\"$THEME\" on $(printf '%s' "$n" | tr -d '[:space:]') element(s)" >&2
277
+ }
278
+
230
279
  capture() {
231
280
  local css="$1"
232
281
  local out="$2"