@1agh/maude 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -9
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +126 -44
- package/plugins/design/dev-server/annotations-layer.tsx +343 -112
- package/plugins/design/dev-server/api.ts +70 -17
- package/plugins/design/dev-server/artboard-marquee.tsx +174 -0
- package/plugins/design/dev-server/bin/asset-sweep.sh +180 -0
- package/plugins/design/dev-server/bin/visual-sanity.sh +221 -0
- package/plugins/design/dev-server/canvas-lib.tsx +691 -119
- package/plugins/design/dev-server/canvas-shell.tsx +818 -42
- package/plugins/design/dev-server/client/app.jsx +173 -52
- package/plugins/design/dev-server/client/hmr.mjs +28 -0
- package/plugins/design/dev-server/client/styles/1-tokens.css +15 -8
- package/plugins/design/dev-server/client/styles/4-components.css +32 -2
- package/plugins/design/dev-server/commands/annotation-strokes-command.ts +127 -0
- package/plugins/design/dev-server/commands/equal-spacing-command.ts +28 -0
- package/plugins/design/dev-server/commands/move-artboards-command.ts +158 -0
- package/plugins/design/dev-server/comments-overlay.tsx +12 -4
- package/plugins/design/dev-server/config.schema.json +31 -5
- package/plugins/design/dev-server/context-menu.tsx +3 -3
- package/plugins/design/dev-server/context.ts +37 -3
- package/plugins/design/dev-server/contextual-toolbar.tsx +241 -0
- package/plugins/design/dev-server/dist/client.bundle.js +212 -103
- package/plugins/design/dev-server/dist/runtime/motion.js +165 -0
- package/plugins/design/dev-server/dist/runtime/motion_react.js +409 -0
- package/plugins/design/dev-server/dist/styles.css +38 -2
- package/plugins/design/dev-server/equal-spacing-detector.ts +98 -0
- package/plugins/design/dev-server/equal-spacing-handles.tsx +289 -0
- package/plugins/design/dev-server/export-dialog.tsx +3 -3
- package/plugins/design/dev-server/handoff.ts +24 -0
- package/plugins/design/dev-server/http.ts +41 -2
- package/plugins/design/dev-server/input-router.tsx +74 -10
- package/plugins/design/dev-server/marquee-overlay.tsx +282 -0
- package/plugins/design/dev-server/runtime-bundle.ts +7 -0
- package/plugins/design/dev-server/server.mjs +84 -18
- package/plugins/design/dev-server/test/annotation-strokes-command.test.ts +149 -0
- package/plugins/design/dev-server/test/annotations-layer.test.ts +44 -0
- package/plugins/design/dev-server/test/canvas-lib-motion.test.ts +131 -0
- package/plugins/design/dev-server/test/context-resolve-tokens.test.ts +97 -0
- package/plugins/design/dev-server/test/equal-spacing-detector.test.ts +93 -0
- package/plugins/design/dev-server/test/input-router.test.ts +87 -1
- package/plugins/design/dev-server/test/marquee-overlay.test.ts +94 -0
- package/plugins/design/dev-server/test/move-artboards-command.test.ts +108 -0
- package/plugins/design/dev-server/test/snap-distance-pill.test.ts +68 -0
- package/plugins/design/dev-server/test/system-endpoint.test.ts +144 -0
- package/plugins/design/dev-server/test/undo-stack.test.ts +211 -0
- package/plugins/design/dev-server/test/use-cursor-modifiers.test.ts +59 -0
- package/plugins/design/dev-server/test/use-keyboard-discipline.test.ts +27 -0
- package/plugins/design/dev-server/test/use-undo-stack.test.tsx +193 -0
- package/plugins/design/dev-server/tool-palette.tsx +71 -15
- package/plugins/design/dev-server/undo-hud.tsx +95 -0
- package/plugins/design/dev-server/undo-stack.ts +240 -0
- package/plugins/design/dev-server/use-annotation-resize.tsx +295 -0
- package/plugins/design/dev-server/use-artboard-drag.tsx +6 -3
- package/plugins/design/dev-server/use-cursor-modifiers.tsx +122 -0
- package/plugins/design/dev-server/use-keyboard-discipline.tsx +125 -0
- package/plugins/design/dev-server/use-snap-guides.tsx +25 -1
- package/plugins/design/dev-server/use-tool-mode.tsx +31 -2
- package/plugins/design/dev-server/use-undo-stack.tsx +355 -0
- package/plugins/design/templates/_shell.html +17 -6
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +245 -0
- package/plugins/design/templates/design-system-inspiration/_MAPPING.md +2 -2
- package/plugins/design/templates/design-system-inspiration/core/preview/_components.css.tpl +129 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +63 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/motion.css.tpl +106 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/motion.tsx.tpl +208 -0
- /package/plugins/design/templates/design-system-inspiration/core/preview/{motion.html → .archive/motion.html} +0 -0
|
@@ -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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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,
|
|
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
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
if (
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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 {
|
|
@@ -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,174 @@
|
|
|
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 { DRAG_THRESHOLD_PX } from './input-router.tsx';
|
|
22
|
+
import { type Selection, useSelectionSet } from './use-selection-set.tsx';
|
|
23
|
+
import { useToolMode } from './use-tool-mode.tsx';
|
|
24
|
+
|
|
25
|
+
const MARQUEE_CSS = `
|
|
26
|
+
.dc-cv-marquee {
|
|
27
|
+
position: fixed;
|
|
28
|
+
pointer-events: none;
|
|
29
|
+
z-index: 5;
|
|
30
|
+
border: 1px solid var(--accent, #0d99ff);
|
|
31
|
+
background: color-mix(in oklab, var(--accent, #0d99ff) 8%, transparent);
|
|
32
|
+
display: none;
|
|
33
|
+
}
|
|
34
|
+
`.trim();
|
|
35
|
+
|
|
36
|
+
function ensureMarqueeStyles(): void {
|
|
37
|
+
if (typeof document === 'undefined') return;
|
|
38
|
+
if (document.getElementById('dc-cv-marquee-css')) return;
|
|
39
|
+
const s = document.createElement('style');
|
|
40
|
+
s.id = 'dc-cv-marquee-css';
|
|
41
|
+
s.textContent = MARQUEE_CSS;
|
|
42
|
+
document.head.appendChild(s);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function shouldIgnoreTarget(t: EventTarget | null): boolean {
|
|
46
|
+
if (!t || !(t as Element).closest) return true;
|
|
47
|
+
const el = t as Element;
|
|
48
|
+
// Skip if the pointerdown landed inside any artboard — that's a select-
|
|
49
|
+
// or-drag gesture, not a marquee. Same hard ceiling as resolveHoverTarget.
|
|
50
|
+
if (el.closest('[data-dc-screen]')) return true;
|
|
51
|
+
// Skip floating chrome and existing overlays (their click handlers run).
|
|
52
|
+
if (
|
|
53
|
+
el.closest(
|
|
54
|
+
'.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, .dc-multi-artboard-tb, .dc-elem-ctx-tb, .dc-cv-eq-spacing-layer'
|
|
55
|
+
)
|
|
56
|
+
) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function ArtboardMarqueeOverlay() {
|
|
63
|
+
ensureMarqueeStyles();
|
|
64
|
+
const selSet = useSelectionSet();
|
|
65
|
+
const artboardsCtx = useArtboardsContext();
|
|
66
|
+
const { tool } = useToolMode();
|
|
67
|
+
const overlayRef = useRef<HTMLDivElement | null>(null);
|
|
68
|
+
const stateRef = useRef<{
|
|
69
|
+
pointerId: number;
|
|
70
|
+
startX: number;
|
|
71
|
+
startY: number;
|
|
72
|
+
crossed: boolean;
|
|
73
|
+
shift: boolean;
|
|
74
|
+
} | null>(null);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (tool !== 'move') return;
|
|
78
|
+
if (typeof document === 'undefined') return;
|
|
79
|
+
|
|
80
|
+
const onDown = (e: PointerEvent) => {
|
|
81
|
+
if (e.button !== 0) return;
|
|
82
|
+
// Bare or Shift only — Cmd/Ctrl/Alt clicks belong to other gestures
|
|
83
|
+
// (Cmd-select, Alt-copy-drag) and shouldn't start a marquee.
|
|
84
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
85
|
+
if (shouldIgnoreTarget(e.target)) return;
|
|
86
|
+
stateRef.current = {
|
|
87
|
+
pointerId: e.pointerId,
|
|
88
|
+
startX: e.clientX,
|
|
89
|
+
startY: e.clientY,
|
|
90
|
+
crossed: false,
|
|
91
|
+
shift: e.shiftKey,
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const onMove = (e: PointerEvent) => {
|
|
96
|
+
const s = stateRef.current;
|
|
97
|
+
if (!s || e.pointerId !== s.pointerId) return;
|
|
98
|
+
const dx = e.clientX - s.startX;
|
|
99
|
+
const dy = e.clientY - s.startY;
|
|
100
|
+
if (!s.crossed) {
|
|
101
|
+
if (Math.hypot(dx, dy) < DRAG_THRESHOLD_PX) return;
|
|
102
|
+
s.crossed = true;
|
|
103
|
+
// Per post-Wave-2 feedback, the marquee never pre-emptively clears
|
|
104
|
+
// the existing selection. The set is only updated on pointerup if
|
|
105
|
+
// the marquee actually captured something — empty-marquee + bare-
|
|
106
|
+
// click preserve the previous selection. Esc is the only deselect.
|
|
107
|
+
}
|
|
108
|
+
const div = overlayRef.current;
|
|
109
|
+
if (!div) return;
|
|
110
|
+
const left = Math.min(s.startX, e.clientX);
|
|
111
|
+
const top = Math.min(s.startY, e.clientY);
|
|
112
|
+
const w = Math.abs(dx);
|
|
113
|
+
const h = Math.abs(dy);
|
|
114
|
+
div.style.display = 'block';
|
|
115
|
+
div.style.left = `${left}px`;
|
|
116
|
+
div.style.top = `${top}px`;
|
|
117
|
+
div.style.width = `${w}px`;
|
|
118
|
+
div.style.height = `${h}px`;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const finish = (e: PointerEvent) => {
|
|
122
|
+
const s = stateRef.current;
|
|
123
|
+
if (!s || e.pointerId !== s.pointerId) return;
|
|
124
|
+
stateRef.current = null;
|
|
125
|
+
const div = overlayRef.current;
|
|
126
|
+
if (div) div.style.display = 'none';
|
|
127
|
+
if (!s.crossed) {
|
|
128
|
+
// Bare click on empty world without drag → clear selection
|
|
129
|
+
// (post-Wave-3 user feedback). Shift-click preserves it.
|
|
130
|
+
if (!s.shift) selSet.clear();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!artboardsCtx) return;
|
|
134
|
+
|
|
135
|
+
// Intersect the marquee AABB with each artboard's screen-coord bbox.
|
|
136
|
+
// getBoundingClientRect on the article reads the post-zoom, post-pan
|
|
137
|
+
// screen position — exactly what we want, no manual world↔screen math.
|
|
138
|
+
const left = Math.min(s.startX, e.clientX);
|
|
139
|
+
const top = Math.min(s.startY, e.clientY);
|
|
140
|
+
const right = Math.max(s.startX, e.clientX);
|
|
141
|
+
const bottom = Math.max(s.startY, e.clientY);
|
|
142
|
+
const hits: Selection[] = [];
|
|
143
|
+
for (const r of artboardsCtx.artboards) {
|
|
144
|
+
const el = document.querySelector(`[data-dc-screen="${r.id}"]`);
|
|
145
|
+
if (!el) continue;
|
|
146
|
+
const b = (el as HTMLElement).getBoundingClientRect();
|
|
147
|
+
if (b.width === 0 && b.height === 0) continue;
|
|
148
|
+
// AABB intersection test — any overlap counts as a hit.
|
|
149
|
+
if (b.right < left || b.left > right) continue;
|
|
150
|
+
if (b.bottom < top || b.top > bottom) continue;
|
|
151
|
+
hits.push({
|
|
152
|
+
selector: `[data-dc-screen="${r.id}"]`,
|
|
153
|
+
artboardId: r.id,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (hits.length === 0) return;
|
|
157
|
+
if (s.shift) selSet.add(hits);
|
|
158
|
+
else selSet.replace(hits);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
document.addEventListener('pointerdown', onDown, true);
|
|
162
|
+
document.addEventListener('pointermove', onMove);
|
|
163
|
+
document.addEventListener('pointerup', finish);
|
|
164
|
+
document.addEventListener('pointercancel', finish);
|
|
165
|
+
return () => {
|
|
166
|
+
document.removeEventListener('pointerdown', onDown, true);
|
|
167
|
+
document.removeEventListener('pointermove', onMove);
|
|
168
|
+
document.removeEventListener('pointerup', finish);
|
|
169
|
+
document.removeEventListener('pointercancel', finish);
|
|
170
|
+
};
|
|
171
|
+
}, [tool, selSet, artboardsCtx]);
|
|
172
|
+
|
|
173
|
+
return <div ref={overlayRef} className="dc-cv-marquee" aria-hidden="true" />;
|
|
174
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# asset-sweep.sh — pre-scaffold real-asset sweep for `/design:setup-ds`.
|
|
3
|
+
# Greps a target repo for production sources of logo/mark/wordmark/mascot/glyph/
|
|
4
|
+
# illustration BEFORE Batch A authors any placeholder. Single hit at a
|
|
5
|
+
# conventional path → copy 1:1. Multiple hits → caller asks user via
|
|
6
|
+
# AskUserQuestion. Zero hits → THEN placeholder authorship is permitted.
|
|
7
|
+
#
|
|
8
|
+
# Closes D-2 in the studyfi imprint-bootstrap retro (placeholder bleed).
|
|
9
|
+
# Spec: `.ai/plans/phase-3.7-setup-ds-hardening-and-motion-subsystem.md` Task 1.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# asset-sweep.sh --root <repo> [--query "logo,mark,wordmark,mascot,glyph,illustration"]
|
|
13
|
+
# [--depth 6] [--ext "svg,png"]
|
|
14
|
+
#
|
|
15
|
+
# Defaults:
|
|
16
|
+
# --query "logo,mark,wordmark,mascot,glyph,illustration"
|
|
17
|
+
# --depth 6 (`find -maxdepth`; capped to keep monorepo sweeps < 2s)
|
|
18
|
+
# --ext "svg" (svg-only by default — production marks are vector)
|
|
19
|
+
#
|
|
20
|
+
# Output: one JSON object on stdout, e.g.
|
|
21
|
+
# { "logo": ["packages/ui/src/components/ui/logo/logo.svg"],
|
|
22
|
+
# "mark": [],
|
|
23
|
+
# "wordmark": [],
|
|
24
|
+
# "mascot": [],
|
|
25
|
+
# "glyph": ["packages/ui/.../glyphs/sparkle.svg",
|
|
26
|
+
# "packages/ui/.../glyphs/check.svg"],
|
|
27
|
+
# "illustration": [] }
|
|
28
|
+
#
|
|
29
|
+
# Paths are relative to --root. Sorted lexicographically. Duplicates removed.
|
|
30
|
+
# Excludes: node_modules/, .git/, .design/_history/, .next/, dist/, build/.
|
|
31
|
+
#
|
|
32
|
+
# Exit: 0 always (zero hits is a valid result, not an error). 2 = bad args.
|
|
33
|
+
|
|
34
|
+
ROOT=""
|
|
35
|
+
QUERY="logo,mark,wordmark,mascot,glyph,illustration"
|
|
36
|
+
DEPTH=6
|
|
37
|
+
EXT="svg"
|
|
38
|
+
|
|
39
|
+
while [ $# -gt 0 ]; do
|
|
40
|
+
case "$1" in
|
|
41
|
+
--root) ROOT="$2"; shift 2 ;;
|
|
42
|
+
--query) QUERY="$2"; shift 2 ;;
|
|
43
|
+
--depth) DEPTH="$2"; shift 2 ;;
|
|
44
|
+
--ext) EXT="$2"; shift 2 ;;
|
|
45
|
+
--help|-h)
|
|
46
|
+
sed -n '2,30p' "$0" | sed 's/^# \?//'
|
|
47
|
+
exit 0
|
|
48
|
+
;;
|
|
49
|
+
*)
|
|
50
|
+
echo "asset-sweep.sh: unknown arg '$1' (try --help)" >&2
|
|
51
|
+
exit 2
|
|
52
|
+
;;
|
|
53
|
+
esac
|
|
54
|
+
done
|
|
55
|
+
|
|
56
|
+
if [ -z "$ROOT" ]; then
|
|
57
|
+
echo "asset-sweep.sh: --root <repo> required" >&2
|
|
58
|
+
exit 2
|
|
59
|
+
fi
|
|
60
|
+
if [ ! -d "$ROOT" ]; then
|
|
61
|
+
echo "asset-sweep.sh: --root '$ROOT' is not a directory" >&2
|
|
62
|
+
exit 2
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Resolve --root to an absolute path so the relative-path strip below is stable
|
|
66
|
+
# regardless of caller cwd.
|
|
67
|
+
ROOT_ABS=$(cd "$ROOT" && pwd)
|
|
68
|
+
|
|
69
|
+
# Build the find -name predicate for the extension list.
|
|
70
|
+
# EXT="svg,png" → \( -name "*.svg" -o -name "*.png" \)
|
|
71
|
+
ext_predicate=()
|
|
72
|
+
IFS=',' read -r -a EXT_ARR <<< "$EXT"
|
|
73
|
+
ext_predicate+=("(")
|
|
74
|
+
first=1
|
|
75
|
+
for e in "${EXT_ARR[@]}"; do
|
|
76
|
+
e_trim=$(echo "$e" | tr -d '[:space:]')
|
|
77
|
+
[ -z "$e_trim" ] && continue
|
|
78
|
+
if [ $first -eq 1 ]; then
|
|
79
|
+
ext_predicate+=("-name" "*.$e_trim")
|
|
80
|
+
first=0
|
|
81
|
+
else
|
|
82
|
+
ext_predicate+=("-o" "-name" "*.$e_trim")
|
|
83
|
+
fi
|
|
84
|
+
done
|
|
85
|
+
ext_predicate+=(")")
|
|
86
|
+
|
|
87
|
+
# Single find pass; bucket by noun in-memory. Earlier per-noun-find walked the
|
|
88
|
+
# tree N times (6× for default query) — ~4s on a 20k-file repo. Single pass
|
|
89
|
+
# pushes us under the 2s/50k budget from the phase plan.
|
|
90
|
+
#
|
|
91
|
+
# Match policy (case-insensitive): a file is bucketed under noun N if N appears
|
|
92
|
+
# as a substring of the basename OR as a path segment in the parent chain
|
|
93
|
+
# (catches `packages/ui/.../logo/logo.svg` AND `assets/glyphs/foo.svg`).
|
|
94
|
+
|
|
95
|
+
# Normalize nouns once.
|
|
96
|
+
IFS=',' read -r -a NOUNS <<< "$QUERY"
|
|
97
|
+
declare -a NOUNS_LC=()
|
|
98
|
+
for n in "${NOUNS[@]}"; do
|
|
99
|
+
n_trim=$(echo "$n" | tr -d '[:space:]')
|
|
100
|
+
[ -z "$n_trim" ] && continue
|
|
101
|
+
NOUNS_LC+=("$(printf '%s' "$n_trim" | tr '[:upper:]' '[:lower:]')")
|
|
102
|
+
done
|
|
103
|
+
|
|
104
|
+
# Buckets keyed by noun. Bash 4+ supports declare -A; macOS ships bash 3.2
|
|
105
|
+
# by default — fall back to a per-noun delimiter-joined string in flat vars.
|
|
106
|
+
declare -a BUCKETS=()
|
|
107
|
+
for _ in "${NOUNS_LC[@]}"; do BUCKETS+=(""); done
|
|
108
|
+
|
|
109
|
+
# Walk once. Prune vendor/build dirs to keep the tree small.
|
|
110
|
+
while IFS= read -r f; do
|
|
111
|
+
[ -z "$f" ] && continue
|
|
112
|
+
rel="${f#"$ROOT_ABS"/}"
|
|
113
|
+
base="${f##*/}"
|
|
114
|
+
base_lc=$(printf '%s' "$base" | tr '[:upper:]' '[:lower:]')
|
|
115
|
+
parent="${f%/*}"
|
|
116
|
+
parent_lc=$(printf '%s' "$parent" | tr '[:upper:]' '[:lower:]')
|
|
117
|
+
i=0
|
|
118
|
+
for noun in "${NOUNS_LC[@]}"; do
|
|
119
|
+
matched=0
|
|
120
|
+
case "$base_lc" in *"$noun"*) matched=1 ;; esac
|
|
121
|
+
if [ $matched -eq 0 ]; then
|
|
122
|
+
case "$parent_lc" in
|
|
123
|
+
*"/$noun"*|*"/$noun"|*"/${noun}s"*|*"/${noun}s") matched=1 ;;
|
|
124
|
+
esac
|
|
125
|
+
fi
|
|
126
|
+
if [ $matched -eq 1 ]; then
|
|
127
|
+
if [ -z "${BUCKETS[$i]}" ]; then
|
|
128
|
+
BUCKETS[$i]="$rel"
|
|
129
|
+
else
|
|
130
|
+
BUCKETS[$i]="${BUCKETS[$i]}"$'\n'"$rel"
|
|
131
|
+
fi
|
|
132
|
+
fi
|
|
133
|
+
i=$((i + 1))
|
|
134
|
+
done
|
|
135
|
+
done < <(
|
|
136
|
+
find "$ROOT_ABS" \
|
|
137
|
+
-maxdepth "$DEPTH" \
|
|
138
|
+
\( \
|
|
139
|
+
-path "*/node_modules" -o \
|
|
140
|
+
-path "*/.git" -o \
|
|
141
|
+
-path "*/.design/_history" -o \
|
|
142
|
+
-path "*/.next" -o \
|
|
143
|
+
-path "*/dist" -o \
|
|
144
|
+
-path "*/build" -o \
|
|
145
|
+
-path "*/coverage" -o \
|
|
146
|
+
-path "*/.turbo" -o \
|
|
147
|
+
-path "*/.cache" \
|
|
148
|
+
\) -prune -o \
|
|
149
|
+
-type f \
|
|
150
|
+
"${ext_predicate[@]}" \
|
|
151
|
+
-print 2>/dev/null
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# JSON emission. Avoid jq as a hard dep (matches server-up.sh's defensive
|
|
155
|
+
# posture) — the JSON shape is small + predictable.
|
|
156
|
+
printf '{'
|
|
157
|
+
first=1
|
|
158
|
+
i=0
|
|
159
|
+
for noun in "${NOUNS_LC[@]}"; do
|
|
160
|
+
if [ $first -eq 0 ]; then printf ','; fi
|
|
161
|
+
first=0
|
|
162
|
+
printf '\n "%s": [' "$noun"
|
|
163
|
+
if [ -n "${BUCKETS[$i]}" ]; then
|
|
164
|
+
# Sort + dedupe, then JSON-emit.
|
|
165
|
+
printf '\n'
|
|
166
|
+
ln_first=1
|
|
167
|
+
while IFS= read -r hit; do
|
|
168
|
+
[ -z "$hit" ] && continue
|
|
169
|
+
esc=$(printf '%s' "$hit" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')
|
|
170
|
+
if [ $ln_first -eq 0 ]; then printf ',\n'; fi
|
|
171
|
+
ln_first=0
|
|
172
|
+
printf ' "%s"' "$esc"
|
|
173
|
+
done < <(printf '%s\n' "${BUCKETS[$i]}" | sort -u)
|
|
174
|
+
printf '\n ]'
|
|
175
|
+
else
|
|
176
|
+
printf ']'
|
|
177
|
+
fi
|
|
178
|
+
i=$((i + 1))
|
|
179
|
+
done
|
|
180
|
+
printf '\n}\n'
|