@1agh/maude 0.45.1 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/apps/studio/acp/bridge.ts +628 -74
  2. package/apps/studio/acp/index.ts +172 -23
  3. package/apps/studio/acp/probe.ts +31 -7
  4. package/apps/studio/acp/transcript.ts +91 -5
  5. package/apps/studio/annotations-snap.ts +50 -10
  6. package/apps/studio/api.ts +344 -14
  7. package/apps/studio/artboard-guides-overlay.tsx +270 -0
  8. package/apps/studio/bin/_import-asset.mjs +35 -5
  9. package/apps/studio/bin/_pdf-playwright.mjs +35 -4
  10. package/apps/studio/bin/_png-playwright.mjs +38 -4
  11. package/apps/studio/bin/_pw-launch.mjs +52 -0
  12. package/apps/studio/bin/_pw-launch.test.mjs +90 -0
  13. package/apps/studio/bin/_smart-frames.mjs +419 -0
  14. package/apps/studio/bin/_smart-frames.test.mjs +140 -0
  15. package/apps/studio/bin/smart-frames.sh +30 -0
  16. package/apps/studio/canvas-edit.ts +302 -3
  17. package/apps/studio/canvas-lib.tsx +203 -11
  18. package/apps/studio/canvas-shell.tsx +167 -28
  19. package/apps/studio/client/app.jsx +678 -176
  20. package/apps/studio/client/panels/CapabilityBar.jsx +101 -0
  21. package/apps/studio/client/panels/ChatPanel.jsx +846 -133
  22. package/apps/studio/client/panels/ElicitationPrompt.jsx +429 -0
  23. package/apps/studio/client/panels/PermissionPrompt.jsx +119 -0
  24. package/apps/studio/client/panels/ReadinessList.jsx +17 -3
  25. package/apps/studio/client/panels/SettingsPanel.jsx +211 -0
  26. package/apps/studio/client/panels/ToolGroup.jsx +79 -0
  27. package/apps/studio/client/panels/acp-capabilities.js +82 -0
  28. package/apps/studio/client/panels/acp-elicitation.js +194 -0
  29. package/apps/studio/client/panels/acp-runtime.js +248 -9
  30. package/apps/studio/client/panels/acp-usage.js +65 -0
  31. package/apps/studio/client/panels/transcript-view.js +36 -0
  32. package/apps/studio/client/styles/6-acp-chat.css +689 -11
  33. package/apps/studio/dist/client.bundle.js +1738 -1736
  34. package/apps/studio/dist/comment-mount.js +2 -2
  35. package/apps/studio/dist/styles.css +1 -1
  36. package/apps/studio/dom-selection.ts +20 -0
  37. package/apps/studio/export-dialog.tsx +138 -18
  38. package/apps/studio/exporters/pdf.ts +332 -18
  39. package/apps/studio/exporters/png.ts +45 -3
  40. package/apps/studio/footage/schema.ts +17 -1
  41. package/apps/studio/generation/gemma-models.ts +224 -0
  42. package/apps/studio/generation/prefs.ts +43 -0
  43. package/apps/studio/generation/whisper-models.test.ts +28 -1
  44. package/apps/studio/generation/whisper-models.ts +22 -7
  45. package/apps/studio/http.ts +246 -9
  46. package/apps/studio/print/marks.ts +113 -0
  47. package/apps/studio/print/units.ts +269 -0
  48. package/apps/studio/print-overlay-content.tsx +132 -0
  49. package/apps/studio/test/acp-bridge.test.ts +11 -6
  50. package/apps/studio/test/acp-capabilities.test.ts +123 -0
  51. package/apps/studio/test/acp-caps-bridge.test.ts +274 -0
  52. package/apps/studio/test/acp-elicitation-bridge.test.ts +475 -0
  53. package/apps/studio/test/acp-elicitation.test.ts +251 -0
  54. package/apps/studio/test/acp-mode-banner.test.ts +45 -0
  55. package/apps/studio/test/acp-permission-prompt.test.ts +77 -0
  56. package/apps/studio/test/acp-permission.test.ts +262 -0
  57. package/apps/studio/test/acp-session-allowed-tools.test.ts +91 -0
  58. package/apps/studio/test/acp-toolgroup.test.ts +76 -0
  59. package/apps/studio/test/acp-transcript-view.test.ts +71 -0
  60. package/apps/studio/test/acp-transcript.test.ts +75 -2
  61. package/apps/studio/test/acp-usage-bridge.test.ts +136 -0
  62. package/apps/studio/test/acp-usage.test.ts +143 -0
  63. package/apps/studio/test/annotations-snap.test.ts +56 -0
  64. package/apps/studio/test/artboard-guides-overlay.test.tsx +152 -0
  65. package/apps/studio/test/artboard-kinds.test.tsx +83 -0
  66. package/apps/studio/test/artboard-selection-attrs.test.ts +69 -0
  67. package/apps/studio/test/canvas-meta-api.test.ts +167 -0
  68. package/apps/studio/test/element-structural-edit.test.ts +250 -0
  69. package/apps/studio/test/exporters/png.test.ts +49 -1
  70. package/apps/studio/test/fixtures/mock-acp-agent-caps.mjs +158 -0
  71. package/apps/studio/test/fixtures/mock-acp-agent-elicit-flood.mjs +46 -0
  72. package/apps/studio/test/fixtures/mock-acp-agent-elicit-url.mjs +42 -0
  73. package/apps/studio/test/fixtures/mock-acp-agent-elicit.mjs +63 -0
  74. package/apps/studio/test/fixtures/mock-acp-agent-permission-flood.mjs +51 -0
  75. package/apps/studio/test/fixtures/mock-acp-agent-permission.mjs +50 -0
  76. package/apps/studio/test/fixtures/mock-acp-agent-usage.mjs +56 -0
  77. package/apps/studio/test/import-asset.test.ts +31 -3
  78. package/apps/studio/test/pdf-print-boxes.test.ts +272 -0
  79. package/apps/studio/test/print-marks.test.ts +113 -0
  80. package/apps/studio/test/print-units.test.ts +173 -0
  81. package/apps/studio/test/use-snap-guides.test.ts +81 -0
  82. package/apps/studio/use-chrome-visibility.tsx +19 -0
  83. package/apps/studio/use-element-resize.tsx +21 -2
  84. package/apps/studio/use-snap-guides.tsx +73 -5
  85. package/apps/studio/use-spacing-handles.tsx +9 -5
  86. package/apps/studio/whats-new.json +62 -0
  87. package/cli/commands/design.mjs +116 -3
  88. package/cli/lib/pkg-root.mjs +42 -10
  89. package/cli/lib/pkg-root.test.mjs +33 -1
  90. package/package.json +11 -9
  91. package/plugins/design/dependencies.json +35 -0
@@ -101,7 +101,20 @@ import {
101
101
  } from 'react';
102
102
 
103
103
  import { ArtboardActivityOverlay } from './artboard-activity-overlay.tsx';
104
+ import {
105
+ ArtboardGuidesOverlay,
106
+ type GuideDefinitions,
107
+ type OverlayVisibility,
108
+ } from './artboard-guides-overlay.tsx';
104
109
  import { CanvasShell } from './canvas-shell.tsx';
110
+ import type { ArtboardPrintProp } from './print/units.ts';
111
+ // feature-2-print-artboards T3 — side-effect import: registers the print
112
+ // kind's overlay content (bleed/trim/margin) into artboard-guides-overlay's
113
+ // registry at module load, per that file's own "call registerKindOverlay at
114
+ // module load" contract. Must live in the same module graph every canvas
115
+ // bundle already pulls in (canvas-lib IS that shared graph — DDR-025), so a
116
+ // canvas never renders without the registration having run first.
117
+ import './print-overlay-content.tsx';
105
118
  import {
106
119
  buildMoveArtboardsRecord,
107
120
  diffLayoutPositions,
@@ -299,6 +312,21 @@ const ENGINE_CSS = `
299
312
  color: var(--fg-1, #4a3f30);
300
313
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
301
314
  }
315
+ /* T3 — per-kind chrome. Subtle tint on the header + a small glyph before the
316
+ label text, for any non-digital kind. digital (the default, and every
317
+ unmigrated canvas) renders no chip and no tint — byte-identical chrome. */
318
+ .dc-canvas .dc-artboard[data-dc-kind]:not([data-dc-kind='digital']) .dc-artboard-label {
319
+ background: color-mix(in oklab, var(--accent, var(--fg-2, #4a3f30)) 10%, var(--bg-2, #e8e3d8));
320
+ }
321
+ .dc-canvas .dc-artboard-kind-chip {
322
+ display: inline-flex;
323
+ align-items: center;
324
+ justify-content: center;
325
+ margin-right: 6px;
326
+ color: var(--accent, var(--fg-1, #4a3f30));
327
+ opacity: 0.85;
328
+ vertical-align: -1px;
329
+ }
302
330
  .dc-canvas .dc-artboard-body {
303
331
  flex: 1;
304
332
  position: relative;
@@ -379,12 +407,25 @@ function ensureEngineStyles(): void {
379
407
  // world-coord positioning) and by DCSection (which collapses inside the
380
408
  // canvas) and by future T3 components (MiniMap, ZoomToolbar).
381
409
 
410
+ /**
411
+ * What an artboard IS — digital screen / print page / web flow / video comp.
412
+ * Absent JSX prop resolves to `digital` (DDR-027-consistent: JSX is truth,
413
+ * no meta PATCH lane). Drives chrome (T3), the guides overlay content
414
+ * registry (T4), and generation/editing rules downstream (feature-2-print,
415
+ * feature-3-web).
416
+ */
417
+ export type ArtboardKind = 'digital' | 'print' | 'web' | 'video';
418
+
382
419
  export interface ArtboardRect {
383
420
  id: string;
384
421
  x: number;
385
422
  y: number;
386
423
  w: number;
387
424
  h: number;
425
+ /** Resolved kind (explicit `kind` prop, or `video` fallback via
426
+ * `subtreeHasVideoComp`, or `digital`) — JSX-authoritative, recomputed
427
+ * from `harvestArtboards` every render; never persisted to meta. */
428
+ kind: ArtboardKind;
388
429
  }
389
430
 
390
431
  export interface ViewportState {
@@ -445,8 +486,11 @@ interface ArtboardSeed {
445
486
  id: string;
446
487
  w: number;
447
488
  h: number;
489
+ kind: ArtboardKind;
448
490
  }
449
491
 
492
+ const ARTBOARD_KINDS: ReadonlySet<string> = new Set(['digital', 'print', 'web', 'video']);
493
+
450
494
  /**
451
495
  * Walk a children subtree harvesting DCArtboard descriptors in render order.
452
496
  * DCSection (and any other wrapper) is traversed but doesn't appear in the
@@ -469,11 +513,22 @@ function harvestArtboards(children: ReactNode): ArtboardSeed[] {
469
513
  (typeof type === 'function' &&
470
514
  (type as { displayName?: string }).displayName === 'DCArtboard');
471
515
  if (isArtboard) {
472
- const props = node.props as { id?: string; width?: number; height?: number };
516
+ const props = node.props as {
517
+ id?: string;
518
+ width?: number;
519
+ height?: number;
520
+ kind?: string;
521
+ children?: ReactNode;
522
+ };
523
+ const explicitKind =
524
+ typeof props.kind === 'string' && ARTBOARD_KINDS.has(props.kind)
525
+ ? (props.kind as ArtboardKind)
526
+ : undefined;
473
527
  out.push({
474
528
  id: typeof props.id === 'string' && props.id.length > 0 ? props.id : `__ab_${auto}`,
475
529
  w: typeof props.width === 'number' ? props.width : VP_GRID.w,
476
530
  h: typeof props.height === 'number' ? props.height : VP_GRID.h,
531
+ kind: explicitKind ?? (subtreeHasVideoComp(props.children) ? 'video' : 'digital'),
477
532
  });
478
533
  auto++;
479
534
  return;
@@ -529,6 +584,7 @@ function synthDefaultGrid(seeds: ArtboardSeed[]): ArtboardRect[] {
529
584
  y: row * (cellH + VP_GRID.gutter),
530
585
  w: seed.w,
531
586
  h: seed.h,
587
+ kind: seed.kind,
532
588
  };
533
589
  });
534
590
  }
@@ -565,6 +621,9 @@ function readCanvasMeta():
565
621
  | {
566
622
  layout?: { artboards?: ArtboardRect[] };
567
623
  viewport?: ViewportState;
624
+ /** T6 — per-user overlay-visibility bag, GET-merged in from view.json
625
+ * server-side (never present in the versioned `.meta.json` itself). */
626
+ overlays?: OverlayVisibility;
568
627
  }
569
628
  | undefined {
570
629
  if (typeof window === 'undefined') return undefined;
@@ -572,6 +631,7 @@ function readCanvasMeta():
572
631
  __canvas_meta__?: {
573
632
  layout?: { artboards?: ArtboardRect[] };
574
633
  viewport?: ViewportState;
634
+ overlays?: OverlayVisibility;
575
635
  };
576
636
  };
577
637
  return w.__canvas_meta__;
@@ -1487,6 +1547,21 @@ function DesignCanvasInner({ children, controls }: DesignCanvasProps) {
1487
1547
  const hostRef = useRef<HTMLDivElement | null>(null);
1488
1548
  const worldRef = useRef<HTMLDivElement | null>(null);
1489
1549
 
1550
+ // T6 — self-seed the (per-canvas, persisted) guides visibility into the
1551
+ // shared ChromeVisibilityContext once at mount, from the SAME
1552
+ // window.__canvas_meta__ snapshot the viewport/layout reads above use.
1553
+ // Unlike minimap/zoom/present (global shell prefs pushed in via postMessage
1554
+ // shortly after load), `guides` has no global default to race — it's
1555
+ // read directly out of this canvas's own meta, so there's nothing to wait
1556
+ // for. The shell's View-menu toggle then live-updates it through the same
1557
+ // `dgn:'view-chrome'` channel as the other three flags.
1558
+ // T6 — the per-canvas overlay-visibility self-seed used to live here, but
1559
+ // this component renders CanvasShell as its CHILD and the
1560
+ // ChromeVisibilityProvider mounts INSIDE CanvasShell — so this component's
1561
+ // own useChromeVisibility() was always null and the seed silently never
1562
+ // ran (dead code since the foundation feature). The working seed now lives
1563
+ // in canvas-shell.tsx's CanvasRouter, which sits inside the provider.
1564
+
1490
1565
  const seeds = useMemo(() => harvestArtboards(children), [children]);
1491
1566
 
1492
1567
  // Merge JSX-derived defaults with meta-persisted positions. Per DDR-027,
@@ -1511,6 +1586,10 @@ function DesignCanvasInner({ children, controls }: DesignCanvasProps) {
1511
1586
  y: Number.isFinite(m.y) ? m.y : d.y,
1512
1587
  w: typeof m.w === 'number' && m.w > 0 ? m.w : d.w,
1513
1588
  h: typeof m.h === 'number' && m.h > 0 ? m.h : d.h,
1589
+ // kind is JSX-authoritative like w/h (DDR-027) but, unlike w/h, was
1590
+ // never part of the legacy persisted layout shape — always the fresh
1591
+ // seed value, no meta fallback to tolerate.
1592
+ kind: d.kind,
1514
1593
  };
1515
1594
  });
1516
1595
  }, [seeds]);
@@ -1803,6 +1882,57 @@ export function DCSection({
1803
1882
  }
1804
1883
  DCSection.displayName = 'DCSection';
1805
1884
 
1885
+ /**
1886
+ * Small monochrome glyph shown in the SKU-strip header for a non-`digital`
1887
+ * artboard (T3). Mirrors the corner video-badge's stroke style (viewBox 16,
1888
+ * `currentColor`, ~1.2px stroke) for chrome consistency. `video` reuses that
1889
+ * exact badge glyph.
1890
+ */
1891
+ function ArtboardKindIcon({ kind }: { kind: ArtboardKind }) {
1892
+ if (kind === 'print') {
1893
+ return (
1894
+ <svg viewBox="0 0 16 16" width="10" height="10" fill="none" aria-hidden="true">
1895
+ <rect
1896
+ x="3"
1897
+ y="1.5"
1898
+ width="10"
1899
+ height="5"
1900
+ rx="0.5"
1901
+ stroke="currentColor"
1902
+ strokeWidth="1.2"
1903
+ />
1904
+ <rect x="1.5" y="6" width="13" height="6" rx="1" stroke="currentColor" strokeWidth="1.2" />
1905
+ <rect x="4.5" y="10.5" width="7" height="4" stroke="currentColor" strokeWidth="1.1" />
1906
+ </svg>
1907
+ );
1908
+ }
1909
+ if (kind === 'web') {
1910
+ return (
1911
+ <svg viewBox="0 0 16 16" width="10" height="10" fill="none" aria-hidden="true">
1912
+ <circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.2" />
1913
+ <path
1914
+ d="M2 8h12M8 2c2 2 2 10 0 12M8 2c-2 2-2 10 0 12"
1915
+ stroke="currentColor"
1916
+ strokeWidth="1.1"
1917
+ />
1918
+ </svg>
1919
+ );
1920
+ }
1921
+ // video — same glyph as the corner Timeline badge.
1922
+ return (
1923
+ <svg viewBox="0 0 16 16" width="10" height="10" fill="none" aria-hidden="true">
1924
+ <rect x="1.5" y="4.5" width="8" height="7" rx="1.2" stroke="currentColor" strokeWidth="1.3" />
1925
+ <path
1926
+ d="M9.5 7l4-2.3v6.6l-4-2.3"
1927
+ stroke="currentColor"
1928
+ strokeWidth="1.3"
1929
+ strokeLinejoin="round"
1930
+ />
1931
+ </svg>
1932
+ );
1933
+ }
1934
+ ArtboardKindIcon.displayName = 'ArtboardKindIcon';
1935
+
1806
1936
  /**
1807
1937
  * Bordered artboard with a SKU-strip header. Inside DesignCanvas its world
1808
1938
  * position comes from meta.layout (or the synthesized default grid); the
@@ -1820,6 +1950,9 @@ export function DCArtboard({
1820
1950
  padding,
1821
1951
  layout,
1822
1952
  gap,
1953
+ kind,
1954
+ guides,
1955
+ print,
1823
1956
  children,
1824
1957
  }: {
1825
1958
  id: string;
@@ -1833,17 +1966,33 @@ export function DCArtboard({
1833
1966
  fixed?: boolean;
1834
1967
  /** `.dc-artboard-body` background (CSS color/token, e.g. "var(--bg-1)"). */
1835
1968
  background?: string;
1836
- /** `.dc-artboard-body` padding, px, all sides. */
1837
- padding?: number;
1969
+ /** `.dc-artboard-body` padding — px number, or a `var(--token)` string
1970
+ * (space-token binding from the Inspector's artboard panel). */
1971
+ padding?: number | string;
1838
1972
  /** `.dc-artboard-body` layout mode. Default = the engine's plain block flow. */
1839
1973
  layout?: 'block' | 'flex-col' | 'flex-row' | 'grid';
1840
- /** `.dc-artboard-body` gap, px — only visible under flex-col/flex-row/grid. */
1841
- gap?: number;
1974
+ /** `.dc-artboard-body` gap — px number or `var(--token)` string; only
1975
+ * visible under flex-col/flex-row/grid. */
1976
+ gap?: number | string;
1977
+ /** What this artboard IS — digital screen / print page / web flow / video
1978
+ * comp. Absent ⇒ `digital` (or `video` when the subtree contains a
1979
+ * `<VideoComp>`, for unmigrated canvases — see `subtreeHasVideoComp`).
1980
+ * An explicit prop always supersedes that structural fallback. */
1981
+ kind?: ArtboardKind;
1982
+ /** Generic layout guides (T5) — Figma-vocabulary columns/rows/grid,
1983
+ * versioned design intent. Kind-agnostic: any artboard can carry these
1984
+ * regardless of `kind`. Visibility is per-user (T6), not this prop. */
1985
+ guides?: GuideDefinitions;
1986
+ /** feature-2-print-artboards T2 — paper/orientation/bleed/margins intent.
1987
+ * Only meaningful when `kind="print"`; the overlay (T3) reads it to draw
1988
+ * bleed/trim/margin guides against THIS artboard's own resolved px size. */
1989
+ print?: ArtboardPrintProp;
1842
1990
  children: ReactNode;
1843
1991
  }) {
1844
1992
  const ctx = useWorldContext();
1845
1993
  const _controller = useViewportControllerContext();
1846
1994
  const toolMode = useToolModeOptional();
1995
+ const chrome = useChromeVisibility();
1847
1996
  const selSet = useSelectionSetOptional();
1848
1997
  const dragBus = useDragStateContext();
1849
1998
  // Phase 13 / DDR-029 — live "agent works here" overlay. Inert (present=false)
@@ -1853,10 +2002,15 @@ export function DCArtboard({
1853
2002
  // its presence color + show its funny name instead of the generic file label.
1854
2003
  const agent = useAgentPresence();
1855
2004
  const rect = ctx ? ctx.rectFor(id) : null;
2005
+ // T1/T2 — explicit `kind` wins; `subtreeHasVideoComp` is the fallback that
2006
+ // keeps existing unmigrated video canvases badged identically without
2007
+ // requiring a JSX edit (Design Decision 1).
2008
+ const hasVideoContent = useMemo(() => subtreeHasVideoComp(children), [children]);
2009
+ const resolvedKind: ArtboardKind = kind ?? (hasVideoContent ? 'video' : 'digital');
1856
2010
  // DDR-148 — badge video artboards in the header; clicking the badge opens
1857
2011
  // the timeline panel via the same postMessage the context-menu "Open
1858
2012
  // Timeline" entry already sends (canvas-shell.tsx artboardHasVideo/handler).
1859
- const hasVideo = useMemo(() => subtreeHasVideoComp(children), [children]);
2013
+ const hasVideo = resolvedKind === 'video';
1860
2014
  const openTimeline = useCallback(
1861
2015
  (e: ReactMouseEvent<HTMLButtonElement>) => {
1862
2016
  e.stopPropagation();
@@ -1921,8 +2075,9 @@ export function DCArtboard({
1921
2075
  const bodyStyle = useMemo<CSSProperties>(() => {
1922
2076
  const st: CSSProperties = {};
1923
2077
  if (background) st.background = background;
1924
- if (typeof padding === 'number') st.padding = padding;
1925
- if (typeof gap === 'number') st.gap = gap;
2078
+ // number → px; string → a var(--token) binding (validated server-side).
2079
+ if (typeof padding === 'number' || typeof padding === 'string') st.padding = padding;
2080
+ if (typeof gap === 'number' || typeof gap === 'string') st.gap = gap;
1926
2081
  if (layout === 'flex-col') {
1927
2082
  st.display = 'flex';
1928
2083
  st.flexDirection = 'column';
@@ -1943,9 +2098,20 @@ export function DCArtboard({
1943
2098
  const readBackAttrs = {
1944
2099
  'data-dc-fixed': fixed ? 'true' : undefined,
1945
2100
  'data-dc-bg': background,
1946
- 'data-dc-padding': typeof padding === 'number' ? String(padding) : undefined,
2101
+ 'data-dc-padding': padding != null ? String(padding) : undefined,
1947
2102
  'data-dc-layout': layout,
1948
- 'data-dc-gap': typeof gap === 'number' ? String(gap) : undefined,
2103
+ 'data-dc-gap': gap != null ? String(gap) : undefined,
2104
+ // Always present (unlike the optional-override props above) — this is a
2105
+ // resolved classification, not a "no override" style knob. Chrome, the
2106
+ // Inspector kind picker (T8), and canvas-shell's artboardHasVideo gate
2107
+ // (T2) all key off this attribute.
2108
+ 'data-dc-kind': resolvedKind,
2109
+ // feature-2-print-artboards T2 — read-back for the Inspector's print
2110
+ // picker (paper/orientation/bleed pre-fill), same "stamp the resolved
2111
+ // prop as JSON, parse it back" shape `guides` would need if it grew an
2112
+ // Inspector editor. Small object (≤ ~150 bytes), so a plain JSON attr is
2113
+ // simpler than adding parallel data-dc-print-* scalar attrs per field.
2114
+ 'data-dc-print': print ? JSON.stringify(print) : undefined,
1949
2115
  };
1950
2116
 
1951
2117
  if (!ctx || !rect) {
@@ -2037,7 +2203,22 @@ export function DCArtboard({
2037
2203
  }}
2038
2204
  {...handleProps}
2039
2205
  >
2040
- <button type="button" className="dc-artboard-label sku" aria-label={`Artboard ${label}`}>
2206
+ <button
2207
+ type="button"
2208
+ className="dc-artboard-label sku"
2209
+ // a11y-auditor (T3 review) — the kind chip is aria-hidden (decorative,
2210
+ // redundant with the Inspector's Kind picker), so a non-digital kind
2211
+ // must still reach the artboard's own accessible name or it's
2212
+ // invisible to AT users entirely.
2213
+ aria-label={
2214
+ resolvedKind !== 'digital' ? `Artboard ${label}, ${resolvedKind}` : `Artboard ${label}`
2215
+ }
2216
+ >
2217
+ {resolvedKind !== 'digital' ? (
2218
+ <span className="dc-artboard-kind-chip" aria-hidden="true">
2219
+ <ArtboardKindIcon kind={resolvedKind} />
2220
+ </span>
2221
+ ) : null}
2041
2222
  {label}
2042
2223
  </button>
2043
2224
  {hasVideo ? (
@@ -2079,6 +2260,17 @@ export function DCArtboard({
2079
2260
  color={agent?.color}
2080
2261
  />
2081
2262
  ) : null}
2263
+ {/* T4 — world-coord sibling, same reasoning as ArtboardActivityOverlay
2264
+ above: must stay OUTSIDE `.dc-artboard`'s contain:paint/
2265
+ content-visibility subtree so guides are never culled/frozen with
2266
+ exported content (they're never exported at all). */}
2267
+ <ArtboardGuidesOverlay
2268
+ rect={{ x: liveX, y: liveY, w: rect.w, h: rect.h }}
2269
+ kind={resolvedKind}
2270
+ guides={guides}
2271
+ print={print}
2272
+ visibility={{ guides: chrome?.guides ?? false, print: chrome?.print ?? false }}
2273
+ />
2082
2274
  </>
2083
2275
  );
2084
2276
  }
@@ -570,10 +570,17 @@ function getLastLayersArtboardId(): string | null {
570
570
  // DDR-148 — an artboard hosting a <VideoComp> stamps a `[data-comp-id]`
571
571
  // element inside it (see video-comp.tsx). Gates the artboard-chrome context
572
572
  // menu's "Open Timeline" entry to artboards that actually have a sequence.
573
+ // T2 (feature-1-artboard-kinds-foundation) — merged with the resolved `kind`:
574
+ // an explicit `kind="video"` (DCArtboard's `data-dc-kind` readback) also
575
+ // qualifies, even before any `<VideoComp>` content exists yet; the structural
576
+ // `[data-comp-id]` check stays as the fallback for unmigrated canvases.
573
577
  function artboardHasVideo(artboardId: string | null | undefined): boolean {
574
578
  if (typeof document === 'undefined' || !artboardId) return false;
575
579
  try {
576
- return !!document.querySelector(`[data-dc-screen="${CSS.escape(artboardId)}"] [data-comp-id]`);
580
+ const root = document.querySelector(`[data-dc-screen="${CSS.escape(artboardId)}"]`);
581
+ if (!root) return false;
582
+ if (root.getAttribute('data-dc-kind') === 'video') return true;
583
+ return !!root.querySelector('[data-comp-id]');
577
584
  } catch {
578
585
  return false;
579
586
  }
@@ -1421,6 +1428,56 @@ function buildRegistry(deps: {
1421
1428
  ],
1422
1429
  };
1423
1430
 
1431
+ // feature-1-artboard-kinds-foundation, T8 — kind-switch submenu, same
1432
+ // shape as themeItem above (a parent-only MenuItem whose onSelect never
1433
+ // fires). The write is main-origin-only (DDR-054): post to the parent
1434
+ // shell (dgn:'set-artboard-kind-request'), which calls
1435
+ // api.setArtboardKindOp and records undo — mirrors delete-artboard-request.
1436
+ const postArtboardKindRequest = (artboardId: string, kind: string | null): void => {
1437
+ try {
1438
+ window.parent.postMessage({ dgn: 'set-artboard-kind-request', artboardId, kind }, '*');
1439
+ } catch {
1440
+ /* detached / cross-origin */
1441
+ }
1442
+ };
1443
+ const kindItem: MenuItem = {
1444
+ id: 'kind',
1445
+ label: 'Artboard kind',
1446
+ onSelect: () => {
1447
+ /* parent of a submenu — never invoked directly */
1448
+ },
1449
+ submenu: [
1450
+ {
1451
+ id: 'kind-digital',
1452
+ label: 'Digital (default)',
1453
+ onSelect: (target) => {
1454
+ if (target.artboardId) postArtboardKindRequest(target.artboardId, null);
1455
+ },
1456
+ },
1457
+ {
1458
+ id: 'kind-print',
1459
+ label: 'Print',
1460
+ onSelect: (target) => {
1461
+ if (target.artboardId) postArtboardKindRequest(target.artboardId, 'print');
1462
+ },
1463
+ },
1464
+ {
1465
+ id: 'kind-web',
1466
+ label: 'Web',
1467
+ onSelect: (target) => {
1468
+ if (target.artboardId) postArtboardKindRequest(target.artboardId, 'web');
1469
+ },
1470
+ },
1471
+ {
1472
+ id: 'kind-video',
1473
+ label: 'Video',
1474
+ onSelect: (target) => {
1475
+ if (target.artboardId) postArtboardKindRequest(target.artboardId, 'video');
1476
+ },
1477
+ },
1478
+ ],
1479
+ };
1480
+
1424
1481
  return {
1425
1482
  element: [
1426
1483
  [
@@ -1771,7 +1828,7 @@ function buildRegistry(deps: {
1771
1828
  onSelect: () => distributeArtboards('y'),
1772
1829
  },
1773
1830
  ],
1774
- [themeItem],
1831
+ [themeItem, kindItem],
1775
1832
  [
1776
1833
  {
1777
1834
  id: 'delete-artboard',
@@ -1938,6 +1995,34 @@ function CanvasRouter({
1938
1995
  // consumes.
1939
1996
  const chromeCtx = useChromeVisibility();
1940
1997
 
1998
+ // Dogfood follow-up (print artboards) — self-seed the per-canvas persisted
1999
+ // overlay visibility (view.json `overlays.guides`/`overlays.print`, GET-
2000
+ // merged into `window.__canvas_meta__` by _shell.html before mount) into
2001
+ // the chrome store, once per mount. This seed originally lived in
2002
+ // DesignCanvasInner (canvas-lib.tsx) — but that component renders
2003
+ // CanvasShell as its CHILD, and ChromeVisibilityProvider mounts INSIDE
2004
+ // CanvasShell, so DesignCanvasInner's own useChromeVisibility() was always
2005
+ // null and the seed silently never ran (dead since the foundation feature;
2006
+ // the generic `guides` toggle had no UI yet, so nothing surfaced it —
2007
+ // "Show print guides" is its first real consumer). CanvasRouter sits
2008
+ // inside the provider (it's the view-chrome bridge), so the seed works
2009
+ // here. Absent keys stay HIDDEN (the T6 gotcha — an old view.json must
2010
+ // not suddenly paint guides nobody asked for).
2011
+ const overlaysSeededRef = useRef(false);
2012
+ useEffect(() => {
2013
+ if (overlaysSeededRef.current || !chromeCtx) return;
2014
+ overlaysSeededRef.current = true;
2015
+ const w = window as unknown as {
2016
+ __canvas_meta__?: { overlays?: Record<string, boolean> };
2017
+ };
2018
+ const overlays = w.__canvas_meta__?.overlays;
2019
+ if (!overlays) return;
2020
+ const patch: { guides?: boolean; print?: boolean } = {};
2021
+ if (overlays.guides === true) patch.guides = true;
2022
+ if (overlays.print === true) patch.print = true;
2023
+ if (Object.keys(patch).length > 0) chromeCtx.setChrome(patch);
2024
+ }, [chromeCtx]);
2025
+
1941
2026
  // Task A2 (feature-element-editing-robustness) — host-scroll-0 invariant.
1942
2027
  // `.dc-canvas` (overflow:hidden) and each `.dc-artboard-body` are the engine's
1943
2028
  // clip layers; the camera lives entirely in the `.dc-world` transform, and
@@ -2037,29 +2122,39 @@ function CanvasRouter({
2037
2122
  // request-layers re-walks + posts the tree for an artboard.
2038
2123
  if (m.dgn === 'select-by-id') {
2039
2124
  const mm = m as { id?: string; artboardId?: string | null; index?: number };
2040
- if (mm.id) {
2041
- const target = resolveSelectionEl(document, {
2042
- id: mm.id,
2043
- artboardId: mm.artboardId,
2044
- index: mm.index,
2045
- });
2046
- if (target) {
2047
- selSet.replace(
2048
- hoverTargetToSelection({
2049
- el: target,
2050
- cdId: mm.id,
2051
- artboardId: mm.artboardId ?? null,
2052
- } as HoverTarget)
2053
- );
2054
- // Task A1 — reveal THROUGH the camera, not host scroll. The old
2055
- // `scrollIntoView` scrolled the overflow:hidden `.dc-canvas` /
2056
- // `.dc-artboard-body` clip layers, desyncing the `.dc-world`
2057
- // transform (Bugs A + B). `revealElementViaCamera` pans the world
2058
- // the minimal amount only when the element is off-screen.
2059
- const host = hostRef.current;
2060
- if (host && zoomController) {
2061
- revealElementViaCamera(host, target as HTMLElement, zoomController);
2062
- }
2125
+ // Dogfood follow-up — a bare ARTBOARD selection (no data-cd-id, the
2126
+ // ArtboardKnobs panel's own case) has no `mm.id` to resolve by, so
2127
+ // the element branch below never ran for it: after a structural
2128
+ // artboard write (kind/print/style/hug/resize) the Inspector kept
2129
+ // showing the PRE-edit attrs snapshot (e.g. "Digital" right after
2130
+ // switching to "Print") until the user manually re-clicked the
2131
+ // artboard. Resolve by the SAME `[data-dc-screen="<id>"]` selector
2132
+ // ArtboardKnobs itself reads its selection from (dom-selection.ts
2133
+ // `hoverTargetToSelection` "2. data-dc-screen" resolution order) when
2134
+ // there's no cdId to anchor on.
2135
+ const target = mm.id
2136
+ ? resolveSelectionEl(document, { id: mm.id, artboardId: mm.artboardId, index: mm.index })
2137
+ : mm.artboardId
2138
+ ? resolveSelectionEl(document, {
2139
+ selector: `[data-dc-screen="${CSS.escape(mm.artboardId)}"]`,
2140
+ })
2141
+ : null;
2142
+ if (target) {
2143
+ selSet.replace(
2144
+ hoverTargetToSelection({
2145
+ el: target,
2146
+ cdId: mm.id,
2147
+ artboardId: mm.artboardId ?? null,
2148
+ } as HoverTarget)
2149
+ );
2150
+ // Task A1 — reveal THROUGH the camera, not host scroll. The old
2151
+ // `scrollIntoView` scrolled the overflow:hidden `.dc-canvas` /
2152
+ // `.dc-artboard-body` clip layers, desyncing the `.dc-world`
2153
+ // transform (Bugs A + B). `revealElementViaCamera` pans the world
2154
+ // the minimal amount only when the element is off-screen.
2155
+ const host = hostRef.current;
2156
+ if (host && zoomController) {
2157
+ revealElementViaCamera(host, target as HTMLElement, zoomController);
2063
2158
  }
2064
2159
  }
2065
2160
  return;
@@ -2188,13 +2283,57 @@ function CanvasRouter({
2188
2283
  }
2189
2284
  // Shell View-menu chrome toggles + Presentation Mode. Only the fields the
2190
2285
  // shell sends are merged, so a `{ present: true }` enter keeps the user's
2191
- // individual minimap/zoom toggles for the eventual exit.
2286
+ // individual minimap/zoom toggles for the eventual exit. `guides` (T6) is
2287
+ // the odd one out — per-CANVAS persisted state, not a global pref — but
2288
+ // rides the same live-broadcast channel; the shell PATCHes view.json
2289
+ // separately (see setGuidesVisible in app.jsx).
2192
2290
  if (m.dgn === 'view-chrome' && chromeCtx) {
2193
- const mm = m as { minimap?: boolean; zoom?: boolean; present?: boolean };
2194
- const patch: { minimap?: boolean; zoom?: boolean; present?: boolean } = {};
2291
+ const mm = m as {
2292
+ minimap?: boolean;
2293
+ zoom?: boolean;
2294
+ present?: boolean;
2295
+ guides?: boolean;
2296
+ print?: boolean;
2297
+ };
2298
+ const patch: {
2299
+ minimap?: boolean;
2300
+ zoom?: boolean;
2301
+ present?: boolean;
2302
+ guides?: boolean;
2303
+ print?: boolean;
2304
+ } = {};
2195
2305
  if (typeof mm.minimap === 'boolean') patch.minimap = mm.minimap;
2196
2306
  if (typeof mm.zoom === 'boolean') patch.zoom = mm.zoom;
2197
2307
  if (typeof mm.present === 'boolean') patch.present = mm.present;
2308
+ if (typeof mm.guides === 'boolean') patch.guides = mm.guides;
2309
+ // feature-2-print-artboards T3 — "Show print guides" (bleed/trim/
2310
+ // margin overlay). Same per-canvas-persisted shape as `guides`.
2311
+ if (typeof mm.print === 'boolean') patch.print = mm.print;
2312
+ // Dogfood follow-up — mirror `guides`/`print` into the in-iframe
2313
+ // `window.__canvas_meta__.overlays` snapshot, exactly like the
2314
+ // existing viewport/layout mirror in canvas-lib.tsx's onSettle (DDR-
2315
+ // 138 comment: "populated ONCE at page load — so after a settle, a
2316
+ // soft HMR remount (any module edit: text, reorder, agent edit)
2317
+ // would re-read the STALE page-load [value]"). Without this, a
2318
+ // structural edit AFTER toggling guides on (e.g. a kind switch,
2319
+ // which is exactly such an edit) remounts DesignCanvasInner, its
2320
+ // `guidesSeededRef` self-seed effect re-fires, and reads the stale
2321
+ // page-load overlays snapshot — silently reverting a live-toggled
2322
+ // guide back to hidden even though the View-menu checkbox still
2323
+ // shows it checked. Same class of bug DDR-138 fixed for the camera;
2324
+ // this is its `overlays` counterpart.
2325
+ if (patch.guides !== undefined || patch.print !== undefined) {
2326
+ const w = window as unknown as {
2327
+ __canvas_meta__?: { overlays?: Record<string, boolean> };
2328
+ };
2329
+ if (w.__canvas_meta__) {
2330
+ w.__canvas_meta__.overlays = {
2331
+ ...w.__canvas_meta__.overlays,
2332
+ ...(patch.guides !== undefined ? { guides: patch.guides } : {}),
2333
+ ...(patch.print !== undefined ? { print: patch.print } : {}),
2334
+ };
2335
+ }
2336
+ }
2198
2337
  chromeCtx.setChrome(patch);
2199
2338
  return;
2200
2339
  }