@1agh/maude 0.18.2 → 0.19.1

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 (31) hide show
  1. package/cli/bin/maude.mjs +16 -0
  2. package/cli/lib/update-check.mjs +145 -0
  3. package/cli/lib/update-check.test.mjs +32 -0
  4. package/package.json +8 -8
  5. package/plugins/design/dev-server/annotations-context-toolbar.tsx +14 -6
  6. package/plugins/design/dev-server/annotations-layer.tsx +144 -22
  7. package/plugins/design/dev-server/api.ts +72 -19
  8. package/plugins/design/dev-server/artboard-marquee.tsx +170 -0
  9. package/plugins/design/dev-server/canvas-lib-resolver.ts +10 -1
  10. package/plugins/design/dev-server/canvas-lib.tsx +190 -94
  11. package/plugins/design/dev-server/canvas-shell.tsx +478 -34
  12. package/plugins/design/dev-server/client/app.jsx +177 -56
  13. package/plugins/design/dev-server/client/styles/1-tokens.css +15 -8
  14. package/plugins/design/dev-server/client/styles/4-components.css +32 -2
  15. package/plugins/design/dev-server/config.schema.json +31 -5
  16. package/plugins/design/dev-server/context-menu.tsx +3 -3
  17. package/plugins/design/dev-server/context.ts +37 -3
  18. package/plugins/design/dev-server/dist/client.bundle.js +219 -107
  19. package/plugins/design/dev-server/dist/styles.css +38 -2
  20. package/plugins/design/dev-server/export-dialog.tsx +3 -3
  21. package/plugins/design/dev-server/http.ts +14 -2
  22. package/plugins/design/dev-server/input-router.tsx +22 -8
  23. package/plugins/design/dev-server/server.mjs +86 -20
  24. package/plugins/design/dev-server/server.ts +76 -30
  25. package/plugins/design/dev-server/test/context-resolve-tokens.test.ts +97 -0
  26. package/plugins/design/dev-server/test/snap-distance-pill.test.ts +68 -0
  27. package/plugins/design/dev-server/test/system-endpoint.test.ts +144 -0
  28. package/plugins/design/dev-server/tool-palette.tsx +71 -15
  29. package/plugins/design/dev-server/use-annotation-resize.tsx +295 -0
  30. package/plugins/design/dev-server/use-snap-guides.tsx +25 -1
  31. package/plugins/design/dev-server/use-tool-mode.tsx +31 -2
@@ -66,6 +66,7 @@
66
66
 
67
67
  import {
68
68
  type CSSProperties,
69
+ Fragment,
69
70
  type ReactNode,
70
71
  type PointerEvent as ReactPointerEvent,
71
72
  type RefObject,
@@ -108,12 +109,63 @@ const ENGINE_CSS = `
108
109
  inset: 0;
109
110
  overflow: hidden;
110
111
  outline: none;
112
+ /* DDR-046 — snap-layer magenta is distinct from --accent so the snap chrome
113
+ never visually melts into the selection halo during a drag-snap gesture.
114
+ OKLCH default approximates FigJam magenta in the project's color space. */
115
+ --guide-magenta: oklch(62% 0.28 350);
111
116
  background-color: var(--bg-1, #f4f1ea);
112
117
  background-image:
113
118
  linear-gradient(var(--border-subtle, rgba(0,0,0,0.08)) 1px, transparent 1px),
114
119
  linear-gradient(90deg, var(--border-subtle, rgba(0,0,0,0.08)) 1px, transparent 1px);
115
120
  background-size: 24px 24px;
116
121
  }
122
+ /* DDR-046 — Snap guides. Sibling kind = confident magenta + glow + distance
123
+ pill. Grid kind = lighter gray fallback, no pill. Width 2 px (up from 1 px)
124
+ so the line stays readable at zoom < 0.8. */
125
+ .dc-snap-guide {
126
+ position: fixed;
127
+ pointer-events: none;
128
+ z-index: 6;
129
+ animation: dc-snap-spawn 80ms cubic-bezier(0.2, 0.8, 0.2, 1);
130
+ transform-origin: center;
131
+ }
132
+ .dc-snap-guide--sibling {
133
+ background: var(--guide-magenta, oklch(62% 0.28 350));
134
+ box-shadow: 0 0 4px color-mix(in oklab, var(--guide-magenta, oklch(62% 0.28 350)) 35%, transparent);
135
+ }
136
+ .dc-snap-guide--grid {
137
+ background: color-mix(in oklab, var(--fg-3, var(--fg-1, #4a3f30)) 40%, transparent);
138
+ }
139
+ .dc-snap-pill {
140
+ position: fixed;
141
+ pointer-events: none;
142
+ z-index: 7;
143
+ font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
144
+ font-size: 11px;
145
+ font-weight: 600;
146
+ line-height: 1;
147
+ color: #fff;
148
+ background: var(--guide-magenta, oklch(62% 0.28 350));
149
+ padding: 3px 6px;
150
+ border-radius: 3px;
151
+ white-space: nowrap;
152
+ font-variant-numeric: tabular-nums;
153
+ animation: dc-snap-pill-spawn 80ms cubic-bezier(0.2, 0.8, 0.2, 1);
154
+ }
155
+ @keyframes dc-snap-spawn {
156
+ from { opacity: 0; transform: scaleY(0.92); }
157
+ to { opacity: 1; transform: scaleY(1); }
158
+ }
159
+ @keyframes dc-snap-pill-spawn {
160
+ from { opacity: 0; transform: translate(-50%, -50%) scale(0.88); }
161
+ to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
162
+ }
163
+ @media (prefers-reduced-motion: reduce) {
164
+ .dc-snap-guide,
165
+ .dc-snap-pill {
166
+ animation: none !important;
167
+ }
168
+ }
117
169
  .dc-canvas:focus { outline: none; }
118
170
  .dc-world {
119
171
  position: absolute;
@@ -129,8 +181,11 @@ const ENGINE_CSS = `
129
181
  .dc-canvas .dc-artboard {
130
182
  background: var(--bg-0, #ffffff);
131
183
  color: var(--fg-0, #2a2520);
132
- border: 1px solid var(--fg-0, #2a2520);
133
- box-shadow: 6px 6px 0 var(--fg-0, #2a2520);
184
+ /* Quiet frame chrome — FigJam Section / Figma Frame canonical. The Memphis
185
+ hard-shadow is the Maude brand on USER CONTENT inside artboards; the frame
186
+ itself stays calm so user content reads as loud. */
187
+ border: 1px solid color-mix(in oklab, var(--fg-0, #2a2520) 22%, transparent);
188
+ border-radius: 2px;
134
189
  display: flex;
135
190
  flex-direction: column;
136
191
  overflow: hidden;
@@ -166,25 +221,15 @@ button.dc-artboard-label:focus-visible { outline: 2px solid var(--accent, #d63b1
166
221
  /* Phase 4.2 — drag chrome. */
167
222
  .dc-canvas[data-active-tool="move"] .dc-artboard-label { cursor: grab; }
168
223
  .dc-canvas[data-active-tool="move"] .dc-artboard-label:active { cursor: grabbing; }
169
- .dc-canvas .dc-artboard.dc-dragging { opacity: 0.3; }
170
- .dc-canvas .dc-artboard-ghost {
171
- position: absolute;
172
- pointer-events: none;
173
- opacity: 0.5;
174
- background: var(--bg-0, #ffffff);
175
- border: 1px solid var(--fg-0, #2a2520);
176
- box-shadow: 6px 6px 0 var(--fg-0, #2a2520);
177
- z-index: 4;
178
- }
179
- .dc-canvas .dc-artboard-ghost-label {
180
- background: var(--bg-2, #e8e3d8);
181
- border-bottom: 1px solid var(--fg-0, #2a2520);
182
- padding: 6px 14px;
183
- font-size: 10px;
184
- letter-spacing: 0.06em;
185
- text-transform: uppercase;
186
- color: var(--fg-1, #4a3f30);
187
- font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
224
+ /* Post-Wave-2 direct artboard drag. The article itself updates its
225
+ inline left/top during drag (no ghost placeholder, no opacity fade on
226
+ the original). Compositor handles the pixel movement; halo + group
227
+ bbox naturally follow via getBoundingClientRect on the article. */
228
+ .dc-canvas .dc-artboard.dc-dragging {
229
+ z-index: 5;
230
+ /* Keep the cursor consistent with the label's grabbing affordance even
231
+ when the pointer drifts off the label strip during the drag. */
232
+ cursor: grabbing;
188
233
  }
189
234
  `.trim();
190
235
 
@@ -237,6 +282,12 @@ function useWorldContext(): WorldContextValue | null {
237
282
  return useContext(WorldContext);
238
283
  }
239
284
 
285
+ /** Read-only access to the artboard list + viewport state. Used by overlays
286
+ * that need to operate on artboards directly (distribute, marquee, etc.). */
287
+ export function useArtboardsContext(): WorldContextValue | null {
288
+ return useContext(WorldContext);
289
+ }
290
+
240
291
  // Phase 5.1: annotations-layer needs the world `<div>` to portal a stroke SVG
241
292
  // inside it (so CSS zoom + translate apply natively, no per-frame projection
242
293
  // math). Expose only the ref — the rest of WorldContextValue stays internal.
@@ -1355,50 +1406,41 @@ export function DCArtboard({
1355
1406
  const isFollower = !!followerOffset;
1356
1407
  const isInDrag = isLeader || isFollower;
1357
1408
 
1358
- // Ghost position (world coords).
1359
- let ghostX = 0;
1360
- let ghostY = 0;
1409
+ // Live drag position (world coords). The article's own `left/top` updates
1410
+ // each frame while the drag is in flight — no ghost placeholder, no faded
1411
+ // original. commitFromState then persists the final position on settle.
1412
+ let liveX = rect.x;
1413
+ let liveY = rect.y;
1361
1414
  if (busDrag?.kind === 'dragging') {
1362
1415
  if (isLeader) {
1363
- ghostX = busDrag.leaderRect.x;
1364
- ghostY = busDrag.leaderRect.y;
1416
+ liveX = busDrag.leaderRect.x;
1417
+ liveY = busDrag.leaderRect.y;
1365
1418
  } else if (isFollower && followerOffset) {
1366
- ghostX = busDrag.leaderRect.x + followerOffset.offsetX;
1367
- ghostY = busDrag.leaderRect.y + followerOffset.offsetY;
1419
+ liveX = busDrag.leaderRect.x + followerOffset.offsetX;
1420
+ liveY = busDrag.leaderRect.y + followerOffset.offsetY;
1368
1421
  }
1369
1422
  }
1370
1423
 
1371
1424
  const handleProps = dragHook.bindHandle();
1372
1425
 
1373
1426
  return (
1374
- <>
1375
- <article
1376
- className={`dc-artboard dc-positioned${isInDrag ? ' dc-dragging' : ''}`}
1377
- data-dc-screen={id}
1378
- aria-current={isActive ? 'true' : undefined}
1379
- style={{ left: rect.x, top: rect.y, width: rect.w, height: rect.h }}
1380
- {...handleProps}
1427
+ <article
1428
+ className={`dc-artboard dc-positioned${isInDrag ? ' dc-dragging' : ''}`}
1429
+ data-dc-screen={id}
1430
+ aria-current={isActive ? 'true' : undefined}
1431
+ style={{ left: liveX, top: liveY, width: rect.w, height: rect.h }}
1432
+ {...handleProps}
1433
+ >
1434
+ <button
1435
+ type="button"
1436
+ className="dc-artboard-label sku"
1437
+ onClick={onFocus}
1438
+ aria-label={`Focus artboard ${label}`}
1381
1439
  >
1382
- <button
1383
- type="button"
1384
- className="dc-artboard-label sku"
1385
- onClick={onFocus}
1386
- aria-label={`Focus artboard ${label}`}
1387
- >
1388
- {label}
1389
- </button>
1390
- <div className="dc-artboard-body">{children}</div>
1391
- </article>
1392
- {isInDrag ? (
1393
- <div
1394
- className="dc-artboard-ghost"
1395
- aria-hidden="true"
1396
- style={{ left: ghostX, top: ghostY, width: rect.w, height: rect.h }}
1397
- >
1398
- <div className="dc-artboard-ghost-label">{label}</div>
1399
- </div>
1400
- ) : null}
1401
- </>
1440
+ {label}
1441
+ </button>
1442
+ <div className="dc-artboard-body">{children}</div>
1443
+ </article>
1402
1444
  );
1403
1445
  }
1404
1446
  DCArtboard.displayName = 'DCArtboard';
@@ -1410,6 +1452,15 @@ DCArtboard.displayName = 'DCArtboard';
1410
1452
  // come from `dragBus.current.snap.guides`; world→screen projection uses the
1411
1453
  // live viewport (`v.x + worldCoord * v.zoom` — same convention as `writeTransform`).
1412
1454
 
1455
+ // DDR-046 — render kind-aware snap guides + distance pills. `kind === 'sibling'`
1456
+ // gets the confident magenta + glow; `kind === 'grid'` gets a lighter gray (the
1457
+ // grid is fallback when no sibling fires). Pre-DDR-046 guides emit no `kind`
1458
+ // field — treat as sibling for back-compat. The `Δ{Math.round(delta)}` pill
1459
+ // renders mid-span when |delta| > 0 and screen-span exceeds 60 px (smaller
1460
+ // spans hide the pill so it never overlaps the line itself).
1461
+ const MIN_PILL_SPAN_PX = 60;
1462
+ const GUIDE_THICKNESS_PX = 2;
1463
+
1413
1464
  export function SnapGuideOverlay() {
1414
1465
  const dragBus = useDragStateContext();
1415
1466
  const world = useWorldContext();
@@ -1421,49 +1472,78 @@ export function SnapGuideOverlay() {
1421
1472
  return (
1422
1473
  <>
1423
1474
  {s.snap.guides.map((g, i) => {
1475
+ const kindClass = g.kind === 'grid' ? 'dc-snap-guide--grid' : 'dc-snap-guide--sibling';
1476
+ const delta = g.delta ?? 0;
1477
+ const showPill = g.kind !== 'grid' && Math.abs(delta) > 0;
1424
1478
  if (g.axis === 'x') {
1425
1479
  const sx = vp.x + g.pos * vp.zoom;
1426
1480
  const sFrom = vp.y + g.from * vp.zoom;
1427
1481
  const sTo = vp.y + g.to * vp.zoom;
1482
+ const screenSpan = sTo - sFrom;
1428
1483
  return (
1429
- <div
1484
+ <Fragment
1430
1485
  // biome-ignore lint/suspicious/noArrayIndexKey: guides are positional
1431
1486
  key={`x-${i}`}
1432
- className="dc-snap-guide"
1433
- style={{
1434
- position: 'fixed',
1435
- pointerEvents: 'none',
1436
- background: 'var(--accent, #d63b1f)',
1437
- left: sx,
1438
- top: sFrom,
1439
- width: 1,
1440
- height: Math.max(1, sTo - sFrom),
1441
- zIndex: 6,
1442
- }}
1443
- aria-hidden="true"
1444
- />
1487
+ >
1488
+ <div
1489
+ className={`dc-snap-guide ${kindClass}`}
1490
+ style={{
1491
+ left: sx - GUIDE_THICKNESS_PX / 2,
1492
+ top: sFrom,
1493
+ width: GUIDE_THICKNESS_PX,
1494
+ height: Math.max(GUIDE_THICKNESS_PX, screenSpan),
1495
+ }}
1496
+ aria-hidden="true"
1497
+ />
1498
+ {showPill && screenSpan >= MIN_PILL_SPAN_PX && (
1499
+ <div
1500
+ className="dc-snap-pill"
1501
+ style={{
1502
+ left: sx,
1503
+ top: sFrom + screenSpan / 2,
1504
+ transform: 'translate(-50%, -50%)',
1505
+ }}
1506
+ aria-hidden="true"
1507
+ >
1508
+ Δ{Math.round(Math.abs(delta))}
1509
+ </div>
1510
+ )}
1511
+ </Fragment>
1445
1512
  );
1446
1513
  }
1447
1514
  const sy = vp.y + g.pos * vp.zoom;
1448
1515
  const sFrom = vp.x + g.from * vp.zoom;
1449
1516
  const sTo = vp.x + g.to * vp.zoom;
1517
+ const screenSpan = sTo - sFrom;
1450
1518
  return (
1451
- <div
1519
+ <Fragment
1452
1520
  // biome-ignore lint/suspicious/noArrayIndexKey: guides are positional
1453
1521
  key={`y-${i}`}
1454
- className="dc-snap-guide"
1455
- style={{
1456
- position: 'fixed',
1457
- pointerEvents: 'none',
1458
- background: 'var(--accent, #d63b1f)',
1459
- left: sFrom,
1460
- top: sy,
1461
- width: Math.max(1, sTo - sFrom),
1462
- height: 1,
1463
- zIndex: 6,
1464
- }}
1465
- aria-hidden="true"
1466
- />
1522
+ >
1523
+ <div
1524
+ className={`dc-snap-guide ${kindClass}`}
1525
+ style={{
1526
+ left: sFrom,
1527
+ top: sy - GUIDE_THICKNESS_PX / 2,
1528
+ width: Math.max(GUIDE_THICKNESS_PX, screenSpan),
1529
+ height: GUIDE_THICKNESS_PX,
1530
+ }}
1531
+ aria-hidden="true"
1532
+ />
1533
+ {showPill && screenSpan >= MIN_PILL_SPAN_PX && (
1534
+ <div
1535
+ className="dc-snap-pill"
1536
+ style={{
1537
+ left: sFrom + screenSpan / 2,
1538
+ top: sy,
1539
+ transform: 'translate(-50%, -50%)',
1540
+ }}
1541
+ aria-hidden="true"
1542
+ >
1543
+ Δ{Math.round(Math.abs(delta))}
1544
+ </div>
1545
+ )}
1546
+ </Fragment>
1467
1547
  );
1468
1548
  })}
1469
1549
  </>
@@ -1484,6 +1564,14 @@ export function DCPostIt({ children }: { children: ReactNode }) {
1484
1564
  // same vocabulary; if a DS wants to restyle, it can target `.dc-mm` /
1485
1565
  // `.dc-zoom-tb` directly.
1486
1566
 
1567
+ // DDR-046 — Floating chrome (mini-map, zoom HUD, tool palette, popovers, comment
1568
+ // composer, export dialog) drops the brutalist 4 × 4 × 0 hard offset shadow in
1569
+ // favor of a soft ambient. The hard offset stays on app-shell chrome only
1570
+ // (menubar, header, tab strip) — that's the project's intentional brutalist
1571
+ // identity. Floating layer = soft. App frame = hard.
1572
+ const FLOATING_SHADOW = '0 6px 24px color-mix(in oklab, var(--fg-0, #1c1917) 10%, transparent)';
1573
+ const FLOATING_RADIUS = '8px';
1574
+
1487
1575
  const OVERLAY_CSS = `
1488
1576
  .dc-mm {
1489
1577
  position: absolute;
@@ -1491,15 +1579,15 @@ const OVERLAY_CSS = `
1491
1579
  bottom: 16px;
1492
1580
  width: 196px;
1493
1581
  height: 132px;
1494
- background: var(--bg-2, var(--bg-1, rgba(255,255,255,0.98)));
1582
+ background: var(--bg-0, #ffffff);
1495
1583
  border: 1px solid var(--fg-0, #1c1917);
1496
- border-radius: 0;
1584
+ border-radius: ${FLOATING_RADIUS};
1497
1585
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
1498
1586
  font-size: 10px;
1499
1587
  color: var(--fg-1, rgba(40,30,20,0.7));
1500
1588
  z-index: 6;
1501
1589
  user-select: none;
1502
- box-shadow: 4px 4px 0 var(--fg-0, #1c1917);
1590
+ box-shadow: ${FLOATING_SHADOW};
1503
1591
  overflow: hidden;
1504
1592
  }
1505
1593
  .dc-mm-hd {
@@ -1508,6 +1596,7 @@ const OVERLAY_CSS = `
1508
1596
  letter-spacing: 0.05em;
1509
1597
  text-transform: uppercase;
1510
1598
  font-size: 9px;
1599
+ background: var(--bg-1, #f4f1ea);
1511
1600
  }
1512
1601
  .dc-mm-body {
1513
1602
  position: relative;
@@ -1515,15 +1604,21 @@ const OVERLAY_CSS = `
1515
1604
  height: calc(100% - 22px);
1516
1605
  overflow: hidden;
1517
1606
  cursor: pointer;
1607
+ background: var(--bg-1, #f4f1ea);
1518
1608
  }
1519
1609
  .dc-mm-rect {
1520
1610
  position: absolute;
1521
- background: rgba(0,0,0,0.06);
1522
- border: 1px solid rgba(0,0,0,0.18);
1611
+ background: color-mix(in oklab, var(--fg-0, #1c1917) 14%, transparent);
1612
+ border: 1px solid color-mix(in oklab, var(--fg-0, #1c1917) 28%, transparent);
1613
+ border-radius: 1px;
1523
1614
  }
1615
+ /* Filled viewport indicator — FigJam / Figma both ship a tinted fill, not
1616
+ outline-only. Reads from a glance as "what slice of the world you're on". */
1524
1617
  .dc-mm-vp {
1525
1618
  position: absolute;
1526
- border: 2px solid #d63b1f;
1619
+ background: color-mix(in oklab, var(--accent, #d63b1f) 12%, transparent);
1620
+ border: 1.5px solid var(--accent, #d63b1f);
1621
+ border-radius: 1px;
1527
1622
  pointer-events: none;
1528
1623
  }
1529
1624
  .dc-zoom-tb {
@@ -1533,15 +1628,15 @@ const OVERLAY_CSS = `
1533
1628
  transform: translateX(-50%);
1534
1629
  display: flex;
1535
1630
  align-items: stretch;
1536
- background: var(--bg-2, rgba(255,255,255,0.94));
1631
+ background: var(--bg-0, #ffffff);
1537
1632
  border: 1px solid var(--fg-0, #1c1917);
1538
- border-radius: 0;
1633
+ border-radius: ${FLOATING_RADIUS};
1539
1634
  overflow: hidden;
1540
1635
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
1541
1636
  font-size: 11px;
1542
1637
  color: var(--fg-1, rgba(40,30,20,0.85));
1543
1638
  z-index: 6;
1544
- box-shadow: 4px 4px 0 var(--fg-0, #1c1917);
1639
+ box-shadow: ${FLOATING_SHADOW};
1545
1640
  }
1546
1641
  .dc-zoom-tb button {
1547
1642
  appearance: none;
@@ -1554,10 +1649,11 @@ const OVERLAY_CSS = `
1554
1649
  cursor: pointer;
1555
1650
  min-width: 36px;
1556
1651
  text-align: center;
1652
+ transition: background 80ms linear;
1557
1653
  }
1558
1654
  .dc-zoom-tb button:last-child { border-right: 0; }
1559
- .dc-zoom-tb button:hover { background: rgba(0,0,0,0.04); }
1560
- .dc-zoom-tb button:focus-visible { outline: 2px solid #d63b1f; outline-offset: -2px; }
1655
+ .dc-zoom-tb button:hover { background: color-mix(in oklab, var(--fg-0, #1c1917) 5%, transparent); }
1656
+ .dc-zoom-tb button:focus-visible { outline: 2px solid var(--accent, #d63b1f); outline-offset: -2px; }
1561
1657
  .dc-zoom-tb-pct { font-variant-numeric: tabular-nums; min-width: 52px; }
1562
1658
  `.trim();
1563
1659