@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.
- package/cli/bin/maude.mjs +16 -0
- package/cli/lib/update-check.mjs +145 -0
- package/cli/lib/update-check.test.mjs +32 -0
- package/package.json +8 -8
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +14 -6
- package/plugins/design/dev-server/annotations-layer.tsx +144 -22
- package/plugins/design/dev-server/api.ts +72 -19
- package/plugins/design/dev-server/artboard-marquee.tsx +170 -0
- package/plugins/design/dev-server/canvas-lib-resolver.ts +10 -1
- package/plugins/design/dev-server/canvas-lib.tsx +190 -94
- package/plugins/design/dev-server/canvas-shell.tsx +478 -34
- package/plugins/design/dev-server/client/app.jsx +177 -56
- 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/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/dist/client.bundle.js +219 -107
- package/plugins/design/dev-server/dist/styles.css +38 -2
- package/plugins/design/dev-server/export-dialog.tsx +3 -3
- package/plugins/design/dev-server/http.ts +14 -2
- package/plugins/design/dev-server/input-router.tsx +22 -8
- package/plugins/design/dev-server/server.mjs +86 -20
- package/plugins/design/dev-server/server.ts +76 -30
- package/plugins/design/dev-server/test/context-resolve-tokens.test.ts +97 -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/tool-palette.tsx +71 -15
- package/plugins/design/dev-server/use-annotation-resize.tsx +295 -0
- package/plugins/design/dev-server/use-snap-guides.tsx +25 -1
- package/plugins/design/dev-server/use-tool-mode.tsx +31 -2
|
@@ -1387,8 +1387,44 @@
|
|
|
1387
1387
|
border-bottom: 2px solid var(--u-fg-0);
|
|
1388
1388
|
padding-bottom: var(--u-s-3);
|
|
1389
1389
|
margin-bottom: var(--u-s-5);
|
|
1390
|
-
|
|
1391
|
-
display:
|
|
1390
|
+
flex-wrap: wrap;
|
|
1391
|
+
display: flex;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
.sv-header .sv-title {
|
|
1395
|
+
flex: auto;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
.sv-ds-picker {
|
|
1399
|
+
align-items: baseline;
|
|
1400
|
+
gap: var(--u-s-2);
|
|
1401
|
+
font-family: var(--u-font-mono);
|
|
1402
|
+
letter-spacing: var(--tracking-sku);
|
|
1403
|
+
text-transform: uppercase;
|
|
1404
|
+
color: var(--u-fg-2);
|
|
1405
|
+
font-size: 10px;
|
|
1406
|
+
display: inline-flex;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
.sv-ds-picker select {
|
|
1410
|
+
font-family: var(--u-font-mono);
|
|
1411
|
+
color: var(--u-fg-0);
|
|
1412
|
+
background: var(--u-bg-1);
|
|
1413
|
+
border: 1px solid var(--u-border);
|
|
1414
|
+
border-radius: var(--u-r-sm);
|
|
1415
|
+
text-transform: none;
|
|
1416
|
+
letter-spacing: 0;
|
|
1417
|
+
padding: 2px 6px;
|
|
1418
|
+
font-size: 11px;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
.sv-ds-description {
|
|
1422
|
+
font-family: var(--u-font-body);
|
|
1423
|
+
font-size: var(--type-sm);
|
|
1424
|
+
color: var(--u-fg-1);
|
|
1425
|
+
margin: 0 0 var(--u-s-5);
|
|
1426
|
+
max-width: 72ch;
|
|
1427
|
+
line-height: 1.5;
|
|
1392
1428
|
}
|
|
1393
1429
|
|
|
1394
1430
|
.sv-sku {
|
|
@@ -115,9 +115,9 @@ const DIALOG_CSS = `
|
|
|
115
115
|
.dc-export-dialog {
|
|
116
116
|
border: 1px solid var(--u-fg-0, #1c1917);
|
|
117
117
|
padding: 0;
|
|
118
|
-
border-radius:
|
|
119
|
-
background: var(--u-bg-
|
|
120
|
-
box-shadow:
|
|
118
|
+
border-radius: 8px;
|
|
119
|
+
background: var(--u-bg-0, var(--bg-0, #fff));
|
|
120
|
+
box-shadow: 0 6px 24px color-mix(in oklab, var(--u-fg-0, #1c1917) 10%, transparent);
|
|
121
121
|
font-family: var(--u-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
122
122
|
color: var(--u-fg-0, var(--fg-0, #1a1a1a));
|
|
123
123
|
width: min(640px, 100vw - 48px);
|
|
@@ -218,8 +218,20 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect): Http {
|
|
|
218
218
|
'/_index-data': async () =>
|
|
219
219
|
Response.json(await api.buildIndexData(), { headers: { 'Cache-Control': 'no-store' } }),
|
|
220
220
|
|
|
221
|
-
'/_system-data': async () =>
|
|
222
|
-
|
|
221
|
+
'/_system-data': async (req: Request) => {
|
|
222
|
+
// DDR-048 — `?ds=<name>` scopes to one design system (per-DS tokens,
|
|
223
|
+
// per-DS preview gallery). Omitted = legacy top-level scan for
|
|
224
|
+
// backwards compat with single-DS projects.
|
|
225
|
+
const dsName = new URL(req.url).searchParams.get('ds');
|
|
226
|
+
const data = await api.buildSystemData(dsName);
|
|
227
|
+
if (data === null) {
|
|
228
|
+
return Response.json(
|
|
229
|
+
{ error: 'unknown design system', ds: dsName },
|
|
230
|
+
{ status: 404, headers: { 'Cache-Control': 'no-store' } }
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return Response.json(data, { headers: { 'Cache-Control': 'no-store' } });
|
|
234
|
+
},
|
|
223
235
|
|
|
224
236
|
'/_comments-all': async () =>
|
|
225
237
|
Response.json(await api.loadAllComments(), { headers: { 'Cache-Control': 'no-store' } }),
|
|
@@ -469,16 +469,30 @@ export function resolveHoverTarget(
|
|
|
469
469
|
const artboardEl = hit.closest?.('[data-dc-screen]') ?? null;
|
|
470
470
|
const artboardId = artboardEl?.getAttribute('data-dc-screen') ?? null;
|
|
471
471
|
|
|
472
|
-
// Hover-target hard ceiling = `.dc-artboard-body`.
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
//
|
|
472
|
+
// Hover-target hard ceiling = `.dc-artboard-body`. Inner DOM content lives
|
|
473
|
+
// there; chrome lives outside (label, header, article root). The two paths
|
|
474
|
+
// diverge from here:
|
|
475
|
+
// * hit ∈ body → resolve to the deepest stamped element (existing
|
|
476
|
+
// deep/top logic below).
|
|
477
|
+
// * hit ∈ chrome (label/header/article-root) → the user wants to select
|
|
478
|
+
// the WHOLE artboard. Return the article element itself with no cdId;
|
|
479
|
+
// consumers (hoverTargetToSelection) fall back to a
|
|
480
|
+
// `[data-dc-screen="…"]` selector that wraps the whole frame. This is
|
|
481
|
+
// what enables Cmd+Shift+Click multi-select of artboards (T24 / G8).
|
|
478
482
|
const bodyEl = hit.closest?.('.dc-artboard-body') ?? null;
|
|
479
|
-
if (!bodyEl)
|
|
483
|
+
if (!bodyEl) {
|
|
484
|
+
if (artboardEl && artboardId) {
|
|
485
|
+
return { el: artboardEl, cdId: null, artboardId };
|
|
486
|
+
}
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
480
489
|
if (hit === bodyEl) {
|
|
481
|
-
// Clicked
|
|
490
|
+
// Clicked the body wrapper itself (empty padding inside an artboard, no
|
|
491
|
+
// user content under the cursor). Promote to "select whole artboard" so
|
|
492
|
+
// the gesture stays consistent with chrome clicks above.
|
|
493
|
+
if (artboardEl && artboardId) {
|
|
494
|
+
return { el: artboardEl, cdId: null, artboardId };
|
|
495
|
+
}
|
|
482
496
|
return null;
|
|
483
497
|
}
|
|
484
498
|
|
|
@@ -78,7 +78,28 @@ function loadConfig() {
|
|
|
78
78
|
console.error(` warn: ${CONFIG_PATH} is not valid JSON: ${e.message}. Using defaults.`);
|
|
79
79
|
return { ...DEFAULT_CONFIG, _source: 'defaults (config invalid)' };
|
|
80
80
|
}
|
|
81
|
-
return { ...DEFAULT_CONFIG, ...parsed, _source: '.design/config.json' };
|
|
81
|
+
return normalizeDesignSystems({ ...DEFAULT_CONFIG, ...parsed, _source: '.design/config.json' });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Mirror of context.ts `normalizeDesignSystems` for the legacy Node entry.
|
|
86
|
+
* Auto-derives per-DS `tokensCssRel = <entry.path>/colors_and_type.css` when
|
|
87
|
+
* missing so the System view can find tokens regardless of multi-DS layout.
|
|
88
|
+
* DDR-048. Keep in lock-step with the .ts version until DDR-009 retires this.
|
|
89
|
+
*/
|
|
90
|
+
function normalizeDesignSystems(cfg) {
|
|
91
|
+
if (!cfg.designSystems?.length) return cfg;
|
|
92
|
+
const designSystems = cfg.designSystems.map((entry) => {
|
|
93
|
+
const p = String(entry.path).replace(/^\/+|\/+$/g, '');
|
|
94
|
+
return {
|
|
95
|
+
...entry,
|
|
96
|
+
path: p,
|
|
97
|
+
tokensCssRel:
|
|
98
|
+
(entry.tokensCssRel ?? '').toString().replace(/^\/+/, '') ||
|
|
99
|
+
path.posix.join(p, 'colors_and_type.css'),
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
return { ...cfg, designSystems };
|
|
82
103
|
}
|
|
83
104
|
|
|
84
105
|
const CFG = loadConfig();
|
|
@@ -962,9 +983,22 @@ function parseTokens(css) {
|
|
|
962
983
|
return tokens;
|
|
963
984
|
}
|
|
964
985
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
986
|
+
/**
|
|
987
|
+
* Build the System view payload. When `dsName` is set, scope to that DS entry
|
|
988
|
+
* (per-DS tokens + per-DS preview/ui_kits gallery). Returns null when the
|
|
989
|
+
* name is set but not registered in `CFG.designSystems` so the caller can
|
|
990
|
+
* 404 instead of silently falling back to shell defaults. DDR-048.
|
|
991
|
+
*/
|
|
992
|
+
async function buildSystemData(dsName) {
|
|
993
|
+
let dsEntry = null;
|
|
994
|
+
if (dsName) {
|
|
995
|
+
dsEntry = (CFG.designSystems || []).find((d) => d.name === dsName) || null;
|
|
996
|
+
if (!dsEntry) return null;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const scopedSystemRel = dsEntry ? dsEntry.path : SYSTEM_DIR_REL;
|
|
1000
|
+
const sysAbs = path.join(DESIGN_ROOT, scopedSystemRel);
|
|
1001
|
+
const sysRel = path.posix.join(DESIGN_REL, scopedSystemRel);
|
|
968
1002
|
|
|
969
1003
|
// README — try canonical locations (designRoot/README.md, system/README.md, system/<project>/README.md)
|
|
970
1004
|
let readme = null, readmePath = null;
|
|
@@ -986,11 +1020,13 @@ async function buildSystemData() {
|
|
|
986
1020
|
} catch {}
|
|
987
1021
|
}
|
|
988
1022
|
|
|
989
|
-
// Tokens
|
|
1023
|
+
// Tokens — per-DS path wins; top-level CFG.tokensCssRel is the fallback
|
|
1024
|
+
// for legacy single-DS configs that don't declare `designSystems[]`.
|
|
1025
|
+
const tokensCssRel = (dsEntry && dsEntry.tokensCssRel) || CFG.tokensCssRel;
|
|
990
1026
|
let tokens = [];
|
|
991
1027
|
let tokensPath = null;
|
|
992
1028
|
try {
|
|
993
|
-
const tokensAbs = path.join(DESIGN_ROOT,
|
|
1029
|
+
const tokensAbs = path.join(DESIGN_ROOT, tokensCssRel);
|
|
994
1030
|
const css = await fs.readFile(tokensAbs, 'utf8');
|
|
995
1031
|
tokens = parseTokens(css);
|
|
996
1032
|
tokensPath = path.relative(REPO_ROOT, tokensAbs);
|
|
@@ -1005,14 +1041,19 @@ async function buildSystemData() {
|
|
|
1005
1041
|
// Look one level deep — system/<project>/<folderName>/...
|
|
1006
1042
|
const matches = [];
|
|
1007
1043
|
try {
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
if (
|
|
1015
|
-
|
|
1044
|
+
// When scoped to one DS (sysAbs IS the DS folder), only check the
|
|
1045
|
+
// DS-relative `<folderName>` subdir. Scanning sub-dirs here would
|
|
1046
|
+
// misread the DS's own `preview/` as a sibling DS root.
|
|
1047
|
+
if (!dsEntry) {
|
|
1048
|
+
const subs = await fs.readdir(sysAbs, { withFileTypes: true });
|
|
1049
|
+
for (const s of subs) {
|
|
1050
|
+
if (!s.isDirectory()) continue;
|
|
1051
|
+
const candidate = path.join(sysAbs, s.name, folderName);
|
|
1052
|
+
try {
|
|
1053
|
+
const stat = await fs.stat(candidate);
|
|
1054
|
+
if (stat.isDirectory()) matches.push({ abs: candidate, rel: path.posix.join(sysRel, s.name, folderName) });
|
|
1055
|
+
} catch {}
|
|
1056
|
+
}
|
|
1016
1057
|
}
|
|
1017
1058
|
// Also accept top-level system/<folderName>/
|
|
1018
1059
|
try {
|
|
@@ -1022,9 +1063,9 @@ async function buildSystemData() {
|
|
|
1022
1063
|
} catch {}
|
|
1023
1064
|
const items = [];
|
|
1024
1065
|
for (const m of matches) {
|
|
1025
|
-
const files = await findFiles(m.abs, m.rel, ['.html']);
|
|
1066
|
+
const files = await findFiles(m.abs, m.rel, ['.tsx', '.html']);
|
|
1026
1067
|
for (const f of files) {
|
|
1027
|
-
const fname = f.split('/').pop().replace(/\.html$/i, '');
|
|
1068
|
+
const fname = f.split('/').pop().replace(/\.(tsx|html)$/i, '');
|
|
1028
1069
|
const group = f.split('/').slice(-2, -1)[0] || folderName;
|
|
1029
1070
|
// For "index.html" prefer the group name as the label (e.g. "desktop", "mobile")
|
|
1030
1071
|
const label = (fname.toLowerCase() === 'index') ? group : fname;
|
|
@@ -1037,10 +1078,27 @@ async function buildSystemData() {
|
|
|
1037
1078
|
const previewGallery = await galleryFor('preview');
|
|
1038
1079
|
const uiKitsGallery = await galleryFor('ui_kits');
|
|
1039
1080
|
|
|
1081
|
+
const availableDesignSystems = (CFG.designSystems || []).map((d) => ({
|
|
1082
|
+
name: d.name,
|
|
1083
|
+
path: d.path,
|
|
1084
|
+
description: d.description || null,
|
|
1085
|
+
}));
|
|
1086
|
+
|
|
1040
1087
|
return {
|
|
1041
1088
|
project: CFG.name,
|
|
1042
1089
|
designRoot: DESIGN_REL,
|
|
1043
1090
|
systemDir: sysRel,
|
|
1091
|
+
ds: dsEntry
|
|
1092
|
+
? {
|
|
1093
|
+
name: dsEntry.name,
|
|
1094
|
+
path: dsEntry.path,
|
|
1095
|
+
description: dsEntry.description || null,
|
|
1096
|
+
rootClass: dsEntry.rootClass || null,
|
|
1097
|
+
themeDefault: dsEntry.themeDefault || null,
|
|
1098
|
+
}
|
|
1099
|
+
: null,
|
|
1100
|
+
availableDesignSystems,
|
|
1101
|
+
defaultDesignSystem: CFG.defaultDesignSystem || availableDesignSystems[0]?.name || null,
|
|
1044
1102
|
readme,
|
|
1045
1103
|
readmePath,
|
|
1046
1104
|
tokens,
|
|
@@ -1048,8 +1106,8 @@ async function buildSystemData() {
|
|
|
1048
1106
|
tokensPath,
|
|
1049
1107
|
previewGallery,
|
|
1050
1108
|
uiKitsGallery,
|
|
1051
|
-
rootClass: CFG.rootClass,
|
|
1052
|
-
themeDefault: CFG.themeDefault,
|
|
1109
|
+
rootClass: (dsEntry && dsEntry.rootClass) || CFG.rootClass,
|
|
1110
|
+
themeDefault: (dsEntry && dsEntry.themeDefault) || CFG.themeDefault,
|
|
1053
1111
|
teamAccentDefault: CFG.teamAccentDefault,
|
|
1054
1112
|
};
|
|
1055
1113
|
}
|
|
@@ -1105,8 +1163,16 @@ const server = http.createServer(async (req, res) => {
|
|
|
1105
1163
|
res.end(JSON.stringify(data));
|
|
1106
1164
|
return;
|
|
1107
1165
|
}
|
|
1108
|
-
if (reqPath === '/_system-data') {
|
|
1109
|
-
|
|
1166
|
+
if (reqPath === '/_system-data' || reqPath.startsWith('/_system-data?')) {
|
|
1167
|
+
// DDR-048 — `?ds=<name>` scopes to one DS; omitted = legacy unscoped.
|
|
1168
|
+
const sysUrl = new URL(reqPath, 'http://x');
|
|
1169
|
+
const dsName = sysUrl.searchParams.get('ds');
|
|
1170
|
+
const data = await buildSystemData(dsName);
|
|
1171
|
+
if (data === null) {
|
|
1172
|
+
res.writeHead(404, { 'Content-Type': MIME['.json'], 'Cache-Control': 'no-store' });
|
|
1173
|
+
res.end(JSON.stringify({ error: 'unknown design system', ds: dsName }));
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1110
1176
|
res.writeHead(200, { 'Content-Type': MIME['.json'], 'Cache-Control': 'no-store' });
|
|
1111
1177
|
res.end(JSON.stringify(data));
|
|
1112
1178
|
return;
|
|
@@ -46,41 +46,87 @@ const http = createHttp(ctx, api, inspect);
|
|
|
46
46
|
const fsWatch = createFsWatch(ctx);
|
|
47
47
|
|
|
48
48
|
// Port: --port arg > $PORT > $MDCC_DEV_PORT > 4399.
|
|
49
|
-
|
|
49
|
+
// When the port wasn't explicitly chosen and the default is busy (another
|
|
50
|
+
// project's dev-server is running on the same machine), walk up to 4408 before
|
|
51
|
+
// giving up. Explicit ports stay fatal so users notice their own collisions.
|
|
52
|
+
function resolvePort(): { port: number; explicit: boolean } {
|
|
50
53
|
const i = process.argv.indexOf('--port');
|
|
51
|
-
if (i !== -1 && process.argv[i + 1]) return Number(process.argv[i + 1]);
|
|
54
|
+
if (i !== -1 && process.argv[i + 1]) return { port: Number(process.argv[i + 1]), explicit: true };
|
|
52
55
|
const env = process.env.PORT ?? process.env.MDCC_DEV_PORT;
|
|
53
|
-
if (env) return Number(env);
|
|
54
|
-
return 4399;
|
|
56
|
+
if (env) return { port: Number(env), explicit: true };
|
|
57
|
+
return { port: 4399, explicit: false };
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
60
|
+
const { port: BASE_PORT, explicit: PORT_EXPLICIT } = resolvePort();
|
|
61
|
+
|
|
62
|
+
type BunServer = ReturnType<typeof Bun.serve<{ id: string; remote: string }, never>>;
|
|
63
|
+
|
|
64
|
+
function startServer(port: number): BunServer {
|
|
65
|
+
return Bun.serve<{ id: string; remote: string }, never>({
|
|
66
|
+
port,
|
|
67
|
+
hostname: '127.0.0.1',
|
|
68
|
+
development: process.env.NODE_ENV !== 'production',
|
|
69
|
+
routes: http.routes,
|
|
70
|
+
async fetch(req, srv) {
|
|
71
|
+
// WebSocket upgrade.
|
|
72
|
+
if (new URL(req.url).pathname.startsWith('/_ws')) {
|
|
73
|
+
const ok = srv.upgrade(req, {
|
|
74
|
+
data: {
|
|
75
|
+
id: crypto.randomUUID(),
|
|
76
|
+
remote: req.headers.get('x-forwarded-for') ?? '127.0.0.1',
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
if (ok) return undefined as unknown as Response;
|
|
80
|
+
return new Response('Upgrade failed', { status: 400 });
|
|
81
|
+
}
|
|
82
|
+
return http.fetch(req);
|
|
83
|
+
},
|
|
84
|
+
websocket: ws.handler,
|
|
85
|
+
error(e) {
|
|
86
|
+
console.error('[bun.serve error]', e);
|
|
87
|
+
return new Response('Server error', { status: 500 });
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isAddrInUse(e: unknown): boolean {
|
|
93
|
+
if (!e || typeof e !== 'object') return false;
|
|
94
|
+
const err = e as { code?: string; errno?: number };
|
|
95
|
+
return err.code === 'EADDRINUSE';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let server: BunServer;
|
|
99
|
+
{
|
|
100
|
+
const MAX_TRIES = PORT_EXPLICIT ? 1 : 10;
|
|
101
|
+
let lastErr: unknown;
|
|
102
|
+
let bound: BunServer | null = null;
|
|
103
|
+
for (let i = 0; i < MAX_TRIES; i++) {
|
|
104
|
+
const tryPort = BASE_PORT + i;
|
|
105
|
+
try {
|
|
106
|
+
bound = startServer(tryPort);
|
|
107
|
+
if (i > 0) {
|
|
108
|
+
console.log(`[port] ${BASE_PORT} busy, using ${tryPort} instead.`);
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
} catch (e) {
|
|
112
|
+
lastErr = e;
|
|
113
|
+
if (!isAddrInUse(e)) throw e;
|
|
75
114
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
115
|
+
}
|
|
116
|
+
if (!bound) {
|
|
117
|
+
if (PORT_EXPLICIT) {
|
|
118
|
+
console.error(
|
|
119
|
+
`\n Port ${BASE_PORT} is in use. Pick a different one with --port <N> or $PORT.\n`
|
|
120
|
+
);
|
|
121
|
+
} else {
|
|
122
|
+
console.error(
|
|
123
|
+
`\n Ports ${BASE_PORT}-${BASE_PORT + MAX_TRIES - 1} are all in use. Stop a running dev-server or pass --port <N>.\n`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
throw lastErr;
|
|
127
|
+
}
|
|
128
|
+
server = bound;
|
|
129
|
+
}
|
|
84
130
|
|
|
85
131
|
await Bun.write(
|
|
86
132
|
ctx.paths.serverInfoFile,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Per-DS tokensCssRel auto-resolution — load-bearing for DDR-048. When a
|
|
2
|
+
// config entry doesn't spell out `tokensCssRel`, the dev-server must derive
|
|
3
|
+
// it from `<entry.path>/colors_and_type.css` so the System view can read the
|
|
4
|
+
// scaffolded layout without forcing every config author to repeat the path.
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from 'bun:test';
|
|
7
|
+
import { normalizeDesignSystems } from '../context';
|
|
8
|
+
|
|
9
|
+
const baseCfg = {
|
|
10
|
+
name: 'fixture',
|
|
11
|
+
projectLabel: null,
|
|
12
|
+
designRoot: '.design',
|
|
13
|
+
canvasGroups: [],
|
|
14
|
+
rootClass: 'app',
|
|
15
|
+
themeDefault: 'dark' as const,
|
|
16
|
+
tokensCssRel: 'system/colors_and_type.css',
|
|
17
|
+
teamAccentDefault: null,
|
|
18
|
+
handoffTargets: [],
|
|
19
|
+
newCanvasDir: 'ui',
|
|
20
|
+
newComponentDir: 'ui/components',
|
|
21
|
+
_source: '.design/config.json' as const,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('normalizeDesignSystems', () => {
|
|
25
|
+
test('no designSystems → passthrough (top-level tokensCssRel is the only source)', () => {
|
|
26
|
+
const out = normalizeDesignSystems({ ...baseCfg, designSystems: undefined });
|
|
27
|
+
expect(out.designSystems).toBeUndefined();
|
|
28
|
+
expect(out.tokensCssRel).toBe('system/colors_and_type.css');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('empty designSystems array → passthrough', () => {
|
|
32
|
+
const out = normalizeDesignSystems({ ...baseCfg, designSystems: [] });
|
|
33
|
+
expect(out.designSystems).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('entry without tokensCssRel → derived from <path>/colors_and_type.css', () => {
|
|
37
|
+
const out = normalizeDesignSystems({
|
|
38
|
+
...baseCfg,
|
|
39
|
+
designSystems: [{ name: 'studyfi', path: 'system/studyfi' }],
|
|
40
|
+
});
|
|
41
|
+
expect(out.designSystems?.[0].tokensCssRel).toBe('system/studyfi/colors_and_type.css');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('entry with explicit tokensCssRel → preserved (leading slash trimmed)', () => {
|
|
45
|
+
const out = normalizeDesignSystems({
|
|
46
|
+
...baseCfg,
|
|
47
|
+
designSystems: [
|
|
48
|
+
{ name: 'studyfi', path: 'system/studyfi', tokensCssRel: '/system/studyfi/tokens.css' },
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
expect(out.designSystems?.[0].tokensCssRel).toBe('system/studyfi/tokens.css');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('path with leading + trailing slashes → normalized', () => {
|
|
55
|
+
const out = normalizeDesignSystems({
|
|
56
|
+
...baseCfg,
|
|
57
|
+
designSystems: [{ name: 'alpha', path: '/system/alpha/' }],
|
|
58
|
+
});
|
|
59
|
+
expect(out.designSystems?.[0].path).toBe('system/alpha');
|
|
60
|
+
expect(out.designSystems?.[0].tokensCssRel).toBe('system/alpha/colors_and_type.css');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('multiple entries → each resolved independently', () => {
|
|
64
|
+
const out = normalizeDesignSystems({
|
|
65
|
+
...baseCfg,
|
|
66
|
+
designSystems: [
|
|
67
|
+
{ name: 'alpha', path: 'system/alpha' },
|
|
68
|
+
{ name: 'beta', path: 'system/beta', tokensCssRel: 'system/beta/custom.css' },
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
expect(out.designSystems?.[0].tokensCssRel).toBe('system/alpha/colors_and_type.css');
|
|
72
|
+
expect(out.designSystems?.[1].tokensCssRel).toBe('system/beta/custom.css');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('preserves unrelated entry fields (description, rootClass, themes)', () => {
|
|
76
|
+
const out = normalizeDesignSystems({
|
|
77
|
+
...baseCfg,
|
|
78
|
+
designSystems: [
|
|
79
|
+
{
|
|
80
|
+
name: 'studyfi',
|
|
81
|
+
path: 'system/studyfi',
|
|
82
|
+
description: 'StudyFi production mirror',
|
|
83
|
+
rootClass: 'studyfi',
|
|
84
|
+
themeDefault: 'light',
|
|
85
|
+
themes: ['light', 'dark'],
|
|
86
|
+
newCanvasDir: 'ui/studyfi',
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
const entry = out.designSystems?.[0];
|
|
91
|
+
expect(entry?.description).toBe('StudyFi production mirror');
|
|
92
|
+
expect(entry?.rootClass).toBe('studyfi');
|
|
93
|
+
expect(entry?.themeDefault).toBe('light');
|
|
94
|
+
expect(entry?.themes).toEqual(['light', 'dark']);
|
|
95
|
+
expect(entry?.newCanvasDir).toBe('ui/studyfi');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// snap-distance-pill — DDR-046. Verifies SnapGuide.delta + .kind population
|
|
2
|
+
// in `computeSnap`. The visual `Δ{Math.round(delta)}` pill rendering lives in
|
|
3
|
+
// `SnapGuideOverlay` (`canvas-lib.tsx`); this file covers only the pure-function
|
|
4
|
+
// contract that the overlay reads.
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from 'bun:test';
|
|
7
|
+
|
|
8
|
+
import { type Rect, type SnapOptions, computeSnap } from '../use-snap-guides.tsx';
|
|
9
|
+
|
|
10
|
+
const DEFAULTS: SnapOptions = { gridSize: 40, tolerance: 8, disabled: false };
|
|
11
|
+
const rect = (x: number, y: number, w = 100, h = 80): Rect => ({ x, y, w, h });
|
|
12
|
+
|
|
13
|
+
describe('SnapGuide.delta + .kind (DDR-046)', () => {
|
|
14
|
+
test('grid snap emits delta + kind=grid', () => {
|
|
15
|
+
const r = computeSnap(rect(33, 200), [], DEFAULTS);
|
|
16
|
+
const xg = r.guides.find((g) => g.axis === 'x');
|
|
17
|
+
expect(xg?.delta).toBe(7); // 40 − 33
|
|
18
|
+
expect(xg?.kind).toBe('grid');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('grid snap with zero delta (already on grid line) still emits kind=grid', () => {
|
|
22
|
+
const r = computeSnap(rect(40, 200), [], DEFAULTS);
|
|
23
|
+
const xg = r.guides.find((g) => g.axis === 'x');
|
|
24
|
+
expect(xg?.delta).toBe(0);
|
|
25
|
+
expect(xg?.kind).toBe('grid');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('sibling snap beats grid when both fire (closest |delta| wins)', () => {
|
|
29
|
+
// proposed x=37: grid candidate at 40 (delta +3), sibling at 35 (delta −2).
|
|
30
|
+
// |−2| < |3| → sibling wins.
|
|
31
|
+
const r = computeSnap(rect(37, 200), [rect(35, 0, 50, 50)], DEFAULTS);
|
|
32
|
+
const xg = r.guides.find((g) => g.axis === 'x');
|
|
33
|
+
expect(xg?.kind).toBe('sibling');
|
|
34
|
+
expect(xg?.delta).toBe(-2);
|
|
35
|
+
expect(r.x).toBe(35);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('sibling snap emits delta + kind=sibling', () => {
|
|
39
|
+
// proposed x=503, sibling at x=500. Snap left↔left.
|
|
40
|
+
const r = computeSnap(rect(503, 200), [rect(500, 50, 100, 80)], DEFAULTS);
|
|
41
|
+
const xg = r.guides.find((g) => g.axis === 'x');
|
|
42
|
+
expect(xg?.delta).toBe(-3);
|
|
43
|
+
expect(xg?.kind).toBe('sibling');
|
|
44
|
+
expect(r.x).toBe(500);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('merged-at-pos guide keeps the larger |delta|', () => {
|
|
48
|
+
// Two siblings both at x=500 — same edge, different y so merged guide
|
|
49
|
+
// unions their from/to. Both candidates have identical delta here, but
|
|
50
|
+
// we still expect the merge code path to preserve a single delta value.
|
|
51
|
+
const r = computeSnap(
|
|
52
|
+
rect(503, 200),
|
|
53
|
+
[rect(500, 50, 100, 50), rect(500, 400, 100, 50)],
|
|
54
|
+
DEFAULTS
|
|
55
|
+
);
|
|
56
|
+
const xg = r.guides.find((g) => g.axis === 'x');
|
|
57
|
+
expect(xg?.delta).toBe(-3);
|
|
58
|
+
expect(xg?.kind).toBe('sibling');
|
|
59
|
+
// Merged span covers both siblings' Y extents.
|
|
60
|
+
expect(xg?.from).toBe(50);
|
|
61
|
+
expect(xg?.to).toBe(450);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('disabled mode emits no guides (delta + kind absent)', () => {
|
|
65
|
+
const r = computeSnap(rect(33, 200), [rect(35, 0)], { ...DEFAULTS, disabled: true });
|
|
66
|
+
expect(r.guides).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
});
|