@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
@@ -155,7 +155,7 @@ export interface Api {
155
155
  saveAnnotations(file: string, svg: string): Promise<boolean>;
156
156
  // Aggregate data
157
157
  buildIndexData(): Promise<unknown>;
158
- buildSystemData(): Promise<unknown>;
158
+ buildSystemData(dsName?: string | null): Promise<unknown>;
159
159
  // Export history (Phase 6.5 T10)
160
160
  loadExportHistory(): Promise<ExportHistoryEntry[]>;
161
161
  appendExportHistory(entry: ExportHistoryEntry): Promise<void>;
@@ -765,9 +765,34 @@ export function createApi(ctx: Context, onCommentsChanged: (file: string) => voi
765
765
  return tokens;
766
766
  }
767
767
 
768
- async function buildSystemData() {
769
- const sysAbs = path.join(paths.designRoot, paths.systemDirRel);
770
- const sysRel = path.posix.join(paths.designRel, paths.systemDirRel);
768
+ /**
769
+ * Build the System view payload.
770
+ *
771
+ * - When `dsName` is null/undefined, scope to the top-level system dir
772
+ * (`paths.systemDirRel`) and read tokens from `cfg.tokensCssRel`. This is
773
+ * the legacy single-DS shape every pre-DDR-048 caller sees.
774
+ * - When `dsName` is provided, scope to the matching `designSystems[]` entry:
775
+ * `sysAbs`/`sysRel` point at the DS folder, `tokensAbs` reads from the
776
+ * per-DS `tokensCssRel` (auto-resolved by `normalizeDesignSystems`), and
777
+ * the previews/ui_kits galleries are restricted to that DS subtree.
778
+ *
779
+ * Returns `null` when `dsName` is set but not found in `cfg.designSystems` so
780
+ * the HTTP handler can 404 instead of silently falling back.
781
+ *
782
+ * DDR-048 — system view renders user tokens only; the per-DS scope is what
783
+ * keeps multi-DS projects from blending the wrong tokens into the wrong
784
+ * preview gallery.
785
+ */
786
+ async function buildSystemData(dsName?: string | null) {
787
+ let dsEntry: NonNullable<typeof cfg.designSystems>[number] | null = null;
788
+ if (dsName) {
789
+ dsEntry = cfg.designSystems?.find((d) => d.name === dsName) ?? null;
790
+ if (!dsEntry) return null;
791
+ }
792
+
793
+ const scopedSystemRel = dsEntry ? dsEntry.path : paths.systemDirRel;
794
+ const sysAbs = path.join(paths.designRoot, scopedSystemRel);
795
+ const sysRel = path.posix.join(paths.designRel, scopedSystemRel);
771
796
 
772
797
  let readme: string | null = null;
773
798
  let readmePath: string | null = null;
@@ -794,8 +819,12 @@ export function createApi(ctx: Context, onCommentsChanged: (file: string) => voi
794
819
 
795
820
  let tokens: ReturnType<typeof parseTokens> = [];
796
821
  let tokensPath: string | null = null;
822
+ // Per-DS tokens path (auto-resolved by normalizeDesignSystems) wins; the
823
+ // top-level cfg.tokensCssRel is a project-wide fallback for legacy
824
+ // single-DS configs that don't declare `designSystems[]`. DDR-048.
825
+ const tokensCssRel = dsEntry?.tokensCssRel ?? cfg.tokensCssRel;
797
826
  try {
798
- const tokensAbs = path.join(paths.designRoot, cfg.tokensCssRel);
827
+ const tokensAbs = path.join(paths.designRoot, tokensCssRel);
799
828
  const css = await readFile(tokensAbs, 'utf8');
800
829
  tokens = parseTokens(css);
801
830
  tokensPath = path.relative(paths.repoRoot, tokensAbs);
@@ -812,16 +841,21 @@ export function createApi(ctx: Context, onCommentsChanged: (file: string) => voi
812
841
  async function galleryFor(folderName: string) {
813
842
  const matches: { abs: string; rel: string }[] = [];
814
843
  try {
815
- const subs = await readdir(sysAbs, { withFileTypes: true });
816
- for (const s of subs) {
817
- if (!s.isDirectory()) continue;
818
- const candidate = path.join(sysAbs, s.name, folderName);
819
- try {
820
- const st = await statp(candidate);
821
- if (st.isDirectory())
822
- matches.push({ abs: candidate, rel: path.posix.join(sysRel, s.name, folderName) });
823
- } catch {
824
- /* ignore */
844
+ // When scoped to a single DS (sysAbs IS the DS folder), only check
845
+ // the DS-relative `<folderName>` subdir — scanning sub-dirs here
846
+ // would treat the DS's own `preview/` as a sibling DS root.
847
+ if (!dsEntry) {
848
+ const subs = await readdir(sysAbs, { withFileTypes: true });
849
+ for (const s of subs) {
850
+ if (!s.isDirectory()) continue;
851
+ const candidate = path.join(sysAbs, s.name, folderName);
852
+ try {
853
+ const st = await statp(candidate);
854
+ if (st.isDirectory())
855
+ matches.push({ abs: candidate, rel: path.posix.join(sysRel, s.name, folderName) });
856
+ } catch {
857
+ /* ignore */
858
+ }
825
859
  }
826
860
  }
827
861
  try {
@@ -839,12 +873,12 @@ export function createApi(ctx: Context, onCommentsChanged: (file: string) => voi
839
873
  }
840
874
  const items: { label: string; path: string; group: string }[] = [];
841
875
  for (const m of matches) {
842
- const files = await findFiles(m.abs, m.rel, ['.html']);
876
+ const files = await findFiles(m.abs, m.rel, ['.tsx', '.html']);
843
877
  for (const f of files) {
844
878
  const fname = f
845
879
  .split('/')
846
880
  .pop()
847
- ?.replace(/\.html$/i, '');
881
+ ?.replace(/\.(tsx|html)$/i, '');
848
882
  const group = f.split('/').slice(-2, -1)[0] || folderName;
849
883
  const label = fname.toLowerCase() === 'index' ? group : fname;
850
884
  items.push({ label, path: f, group });
@@ -856,10 +890,29 @@ export function createApi(ctx: Context, onCommentsChanged: (file: string) => voi
856
890
  const previewGallery = await galleryFor('preview');
857
891
  const uiKitsGallery = await galleryFor('ui_kits');
858
892
 
893
+ // Always advertise the available DSes so the client can render the picker
894
+ // even when the initial fetch was unscoped — avoids a second roundtrip.
895
+ const availableDesignSystems = (cfg.designSystems ?? []).map((d) => ({
896
+ name: d.name,
897
+ path: d.path,
898
+ description: d.description ?? null,
899
+ }));
900
+
859
901
  return {
860
902
  project: cfg.name,
861
903
  designRoot: paths.designRel,
862
904
  systemDir: sysRel,
905
+ ds: dsEntry
906
+ ? {
907
+ name: dsEntry.name,
908
+ path: dsEntry.path,
909
+ description: dsEntry.description ?? null,
910
+ rootClass: dsEntry.rootClass ?? null,
911
+ themeDefault: dsEntry.themeDefault ?? null,
912
+ }
913
+ : null,
914
+ availableDesignSystems,
915
+ defaultDesignSystem: cfg.defaultDesignSystem ?? availableDesignSystems[0]?.name ?? null,
863
916
  readme,
864
917
  readmePath,
865
918
  tokens,
@@ -867,8 +920,8 @@ export function createApi(ctx: Context, onCommentsChanged: (file: string) => voi
867
920
  tokensPath,
868
921
  previewGallery,
869
922
  uiKitsGallery,
870
- rootClass: cfg.rootClass,
871
- themeDefault: cfg.themeDefault,
923
+ rootClass: dsEntry?.rootClass ?? cfg.rootClass,
924
+ themeDefault: dsEntry?.themeDefault ?? cfg.themeDefault,
872
925
  teamAccentDefault: cfg.teamAccentDefault,
873
926
  };
874
927
  }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * @file artboard-marquee.tsx — Task 24 sub-item (Wave 2 follow-up, G8)
3
+ * @scope plugins/design/dev-server/artboard-marquee.tsx
4
+ * @purpose Drag-to-lasso multi-select for artboards. Bare-button pointerdown
5
+ * on empty world (NOT inside any artboard, NOT on floating chrome)
6
+ * starts a marquee. 4 px drag threshold suppresses click jitter.
7
+ * pointerup intersects the marquee AABB with every artboard's
8
+ * screen-coord bbox; hits become Selection entries with
9
+ * `artboardId` set. Modifier semantics — bare = replace, Shift
10
+ * = add (Alt-subtract / Shift+Alt-intersect are deferred).
11
+ *
12
+ * The marquee rect itself follows DDR-046 rev 2: 1 px solid
13
+ * accent border + 8 % accent fill. SOLID, not dashed — solid +
14
+ * tinted fill = "active gesture" idiom, dashed = persistent
15
+ * group bbox.
16
+ */
17
+
18
+ import { useEffect, useRef } from 'react';
19
+
20
+ import { useArtboardsContext } from './canvas-lib.tsx';
21
+ import { type Selection, useSelectionSet } from './use-selection-set.tsx';
22
+ import { useToolMode } from './use-tool-mode.tsx';
23
+
24
+ const DRAG_THRESHOLD_PX = 4;
25
+
26
+ const MARQUEE_CSS = `
27
+ .dc-cv-marquee {
28
+ position: fixed;
29
+ pointer-events: none;
30
+ z-index: 5;
31
+ border: 1px solid var(--accent, #0d99ff);
32
+ background: color-mix(in oklab, var(--accent, #0d99ff) 8%, transparent);
33
+ display: none;
34
+ }
35
+ `.trim();
36
+
37
+ function ensureMarqueeStyles(): void {
38
+ if (typeof document === 'undefined') return;
39
+ if (document.getElementById('dc-cv-marquee-css')) return;
40
+ const s = document.createElement('style');
41
+ s.id = 'dc-cv-marquee-css';
42
+ s.textContent = MARQUEE_CSS;
43
+ document.head.appendChild(s);
44
+ }
45
+
46
+ function shouldIgnoreTarget(t: EventTarget | null): boolean {
47
+ if (!t || !(t as Element).closest) return true;
48
+ const el = t as Element;
49
+ // Skip if the pointerdown landed inside any artboard — that's a select-
50
+ // or-drag gesture, not a marquee. Same hard ceiling as resolveHoverTarget.
51
+ if (el.closest('[data-dc-screen]')) return true;
52
+ // Skip floating chrome and existing overlays (their click handlers run).
53
+ if (
54
+ el.closest(
55
+ '.dc-mm, .dc-zoom-tb, .dc-tool-palette, .dc-context-menu, .dc-cv-halo, .dc-cv-group-bbox, .cm-composer, .cm-thread, .cm-mention-popup, .cm-pin, .dc-annot-svg, .dc-annot-ctx, .dc-tp-popover, .dc-annot-resize-handle'
56
+ )
57
+ ) {
58
+ return true;
59
+ }
60
+ return false;
61
+ }
62
+
63
+ export function ArtboardMarqueeOverlay() {
64
+ ensureMarqueeStyles();
65
+ const selSet = useSelectionSet();
66
+ const artboardsCtx = useArtboardsContext();
67
+ const { tool } = useToolMode();
68
+ const overlayRef = useRef<HTMLDivElement | null>(null);
69
+ const stateRef = useRef<{
70
+ pointerId: number;
71
+ startX: number;
72
+ startY: number;
73
+ crossed: boolean;
74
+ shift: boolean;
75
+ } | null>(null);
76
+
77
+ useEffect(() => {
78
+ if (tool !== 'move') return;
79
+ if (typeof document === 'undefined') return;
80
+
81
+ const onDown = (e: PointerEvent) => {
82
+ if (e.button !== 0) return;
83
+ // Bare or Shift only — Cmd/Ctrl/Alt clicks belong to other gestures
84
+ // (Cmd-select, Alt-copy-drag) and shouldn't start a marquee.
85
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
86
+ if (shouldIgnoreTarget(e.target)) return;
87
+ stateRef.current = {
88
+ pointerId: e.pointerId,
89
+ startX: e.clientX,
90
+ startY: e.clientY,
91
+ crossed: false,
92
+ shift: e.shiftKey,
93
+ };
94
+ };
95
+
96
+ const onMove = (e: PointerEvent) => {
97
+ const s = stateRef.current;
98
+ if (!s || e.pointerId !== s.pointerId) return;
99
+ const dx = e.clientX - s.startX;
100
+ const dy = e.clientY - s.startY;
101
+ if (!s.crossed) {
102
+ if (Math.hypot(dx, dy) < DRAG_THRESHOLD_PX) return;
103
+ s.crossed = true;
104
+ // Per post-Wave-2 feedback, the marquee never pre-emptively clears
105
+ // the existing selection. The set is only updated on pointerup if
106
+ // the marquee actually captured something — empty-marquee + bare-
107
+ // click preserve the previous selection. Esc is the only deselect.
108
+ }
109
+ const div = overlayRef.current;
110
+ if (!div) return;
111
+ const left = Math.min(s.startX, e.clientX);
112
+ const top = Math.min(s.startY, e.clientY);
113
+ const w = Math.abs(dx);
114
+ const h = Math.abs(dy);
115
+ div.style.display = 'block';
116
+ div.style.left = `${left}px`;
117
+ div.style.top = `${top}px`;
118
+ div.style.width = `${w}px`;
119
+ div.style.height = `${h}px`;
120
+ };
121
+
122
+ const finish = (e: PointerEvent) => {
123
+ const s = stateRef.current;
124
+ if (!s || e.pointerId !== s.pointerId) return;
125
+ stateRef.current = null;
126
+ const div = overlayRef.current;
127
+ if (div) div.style.display = 'none';
128
+ if (!s.crossed) return;
129
+ if (!artboardsCtx) return;
130
+
131
+ // Intersect the marquee AABB with each artboard's screen-coord bbox.
132
+ // getBoundingClientRect on the article reads the post-zoom, post-pan
133
+ // screen position — exactly what we want, no manual world↔screen math.
134
+ const left = Math.min(s.startX, e.clientX);
135
+ const top = Math.min(s.startY, e.clientY);
136
+ const right = Math.max(s.startX, e.clientX);
137
+ const bottom = Math.max(s.startY, e.clientY);
138
+ const hits: Selection[] = [];
139
+ for (const r of artboardsCtx.artboards) {
140
+ const el = document.querySelector(`[data-dc-screen="${r.id}"]`);
141
+ if (!el) continue;
142
+ const b = (el as HTMLElement).getBoundingClientRect();
143
+ if (b.width === 0 && b.height === 0) continue;
144
+ // AABB intersection test — any overlap counts as a hit.
145
+ if (b.right < left || b.left > right) continue;
146
+ if (b.bottom < top || b.top > bottom) continue;
147
+ hits.push({
148
+ selector: `[data-dc-screen="${r.id}"]`,
149
+ artboardId: r.id,
150
+ });
151
+ }
152
+ if (hits.length === 0) return;
153
+ if (s.shift) selSet.add(hits);
154
+ else selSet.replace(hits);
155
+ };
156
+
157
+ document.addEventListener('pointerdown', onDown, true);
158
+ document.addEventListener('pointermove', onMove);
159
+ document.addEventListener('pointerup', finish);
160
+ document.addEventListener('pointercancel', finish);
161
+ return () => {
162
+ document.removeEventListener('pointerdown', onDown, true);
163
+ document.removeEventListener('pointermove', onMove);
164
+ document.removeEventListener('pointerup', finish);
165
+ document.removeEventListener('pointercancel', finish);
166
+ };
167
+ }, [tool, selSet, artboardsCtx]);
168
+
169
+ return <div ref={overlayRef} className="dc-cv-marquee" aria-hidden="true" />;
170
+ }
@@ -27,15 +27,24 @@ import path from 'node:path';
27
27
 
28
28
  import type { BunPlugin } from 'bun';
29
29
 
30
+ import { DEV_SERVER_ROOT } from './paths.ts';
31
+
30
32
  export const CANVAS_LIB_SPECIFIER = '@maude/canvas-lib';
31
33
 
32
34
  /**
33
35
  * Returns the dev-server-internal canvas-lib path. The `_designRoot` parameter
34
36
  * is retained for one minor (back-compat with callers we don't control) but
35
37
  * ignored — canvas-lib now ships with the dev-server install (DDR-025).
38
+ *
39
+ * Uses DEV_SERVER_ROOT (paths.ts) instead of `import.meta.dir` per DDR-045 —
40
+ * inside `bun --compile` standalone binaries `import.meta.dir` resolves to the
41
+ * virtual `/$bunfs/root` and any subsequent `existsSync` / `fs.watch` against
42
+ * that path silently fails. Same bug class as v0.18.0/0.18.1; this one surfaces
43
+ * as `[canvas-lib] failed to watch ... '/$bunfs/root/canvas-lib.tsx'` at boot
44
+ * and disables canvas-lib HMR.
36
45
  */
37
46
  export function canvasLibPath(_designRoot?: string): string {
38
- return path.join(import.meta.dir, 'canvas-lib.tsx');
47
+ return path.join(DEV_SERVER_ROOT, 'canvas-lib.tsx');
39
48
  }
40
49
 
41
50
  export interface CanvasLibResolverOptions {