@1agh/maude 0.19.0 → 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/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 +70 -17
- package/plugins/design/dev-server/artboard-marquee.tsx +170 -0
- 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 +173 -52
- 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 +212 -103
- 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 +84 -18
- 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 {
|
|
@@ -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;
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// /_system-data — bias-free token rendering depends on per-DS scoping.
|
|
2
|
+
// Covers DDR-048: ?ds=<name> scopes payload to one designSystem entry;
|
|
3
|
+
// unknown ds → 404; omitted ds → legacy unscoped behavior for single-DS
|
|
4
|
+
// projects that don't declare `designSystems[]`.
|
|
5
|
+
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
7
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import type { Subprocess } from 'bun';
|
|
11
|
+
import { bootServer, killProc, nextPort } from './_helpers';
|
|
12
|
+
|
|
13
|
+
interface MultiDsSandbox {
|
|
14
|
+
root: string;
|
|
15
|
+
designRoot: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function makeMultiDsSandbox(): MultiDsSandbox {
|
|
19
|
+
const root = mkdtempSync(join(tmpdir(), 'mdcc-multi-ds-'));
|
|
20
|
+
const designRoot = join(root, '.design');
|
|
21
|
+
mkdirSync(designRoot, { recursive: true });
|
|
22
|
+
|
|
23
|
+
writeFileSync(
|
|
24
|
+
join(designRoot, 'config.json'),
|
|
25
|
+
JSON.stringify(
|
|
26
|
+
{
|
|
27
|
+
name: 'multi-ds',
|
|
28
|
+
designRoot: '.design',
|
|
29
|
+
canvasGroups: [
|
|
30
|
+
{ label: 'Design system', path: 'system' },
|
|
31
|
+
{ label: 'UI', path: 'ui' },
|
|
32
|
+
],
|
|
33
|
+
designSystems: [
|
|
34
|
+
{ name: 'alpha', path: 'system/alpha', description: 'Alpha brand' },
|
|
35
|
+
{ name: 'beta', path: 'system/beta', description: 'Beta brand' },
|
|
36
|
+
],
|
|
37
|
+
defaultDesignSystem: 'alpha',
|
|
38
|
+
},
|
|
39
|
+
null,
|
|
40
|
+
2
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Alpha — periwinkle accent, cream surfaces (mimics StudyFi case).
|
|
45
|
+
mkdirSync(join(designRoot, 'system', 'alpha', 'preview'), { recursive: true });
|
|
46
|
+
writeFileSync(
|
|
47
|
+
join(designRoot, 'system', 'alpha', 'colors_and_type.css'),
|
|
48
|
+
":root {\n --color-bg: #fbfaf7;\n --accent: #9CACFF;\n --color-text-primary: #000000;\n --fs-base: 1rem;\n --font-body: 'Inter', sans-serif;\n}\n"
|
|
49
|
+
);
|
|
50
|
+
writeFileSync(
|
|
51
|
+
join(designRoot, 'system', 'alpha', 'preview', 'colors-accent.tsx'),
|
|
52
|
+
'export default function ColorsAccent() { return <div>alpha accent</div>; }\n'
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Beta — different palette + names.
|
|
56
|
+
mkdirSync(join(designRoot, 'system', 'beta', 'preview'), { recursive: true });
|
|
57
|
+
writeFileSync(
|
|
58
|
+
join(designRoot, 'system', 'beta', 'colors_and_type.css'),
|
|
59
|
+
':root {\n --bg-0: oklch(13% 0.012 60);\n --accent: oklch(72% 0.16 55);\n --type-base: 14px;\n}\n'
|
|
60
|
+
);
|
|
61
|
+
writeFileSync(
|
|
62
|
+
join(designRoot, 'system', 'beta', 'preview', 'colors-accent.tsx'),
|
|
63
|
+
'export default function ColorsAccent() { return <div>beta accent</div>; }\n'
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
mkdirSync(join(designRoot, 'ui'), { recursive: true });
|
|
67
|
+
return { root, designRoot };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe('/_system-data — per-DS scope (DDR-048)', () => {
|
|
71
|
+
let proc: Subprocess | null = null;
|
|
72
|
+
let sb: MultiDsSandbox;
|
|
73
|
+
let port: number;
|
|
74
|
+
|
|
75
|
+
beforeEach(async () => {
|
|
76
|
+
sb = makeMultiDsSandbox();
|
|
77
|
+
port = nextPort();
|
|
78
|
+
proc = await bootServer(sb.root, port);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterEach(async () => {
|
|
82
|
+
if (proc) await killProc(proc);
|
|
83
|
+
proc = null;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('?ds=alpha → tokens parsed from alpha colors_and_type.css', async () => {
|
|
87
|
+
const r = await fetch(`http://localhost:${port}/_system-data?ds=alpha`);
|
|
88
|
+
expect(r.status).toBe(200);
|
|
89
|
+
const data = (await r.json()) as {
|
|
90
|
+
tokens: { name: string; value: string }[];
|
|
91
|
+
ds: { name: string; description: string };
|
|
92
|
+
tokensPath: string;
|
|
93
|
+
previewGallery: { label: string }[];
|
|
94
|
+
};
|
|
95
|
+
expect(data.ds.name).toBe('alpha');
|
|
96
|
+
expect(data.ds.description).toBe('Alpha brand');
|
|
97
|
+
expect(data.tokensPath).toContain('system/alpha/colors_and_type.css');
|
|
98
|
+
const accent = data.tokens.find((t) => t.name === '--accent');
|
|
99
|
+
expect(accent?.value).toBe('#9CACFF');
|
|
100
|
+
const colorBg = data.tokens.find((t) => t.name === '--color-bg');
|
|
101
|
+
expect(colorBg?.value).toBe('#fbfaf7');
|
|
102
|
+
// Alpha previews only — no beta leakage.
|
|
103
|
+
expect(data.previewGallery.some((p) => p.label === 'colors-accent')).toBe(true);
|
|
104
|
+
expect(data.previewGallery.every((p) => !p.label.includes('beta'))).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('?ds=beta → distinct tokens, distinct previews', async () => {
|
|
108
|
+
const r = await fetch(`http://localhost:${port}/_system-data?ds=beta`);
|
|
109
|
+
expect(r.status).toBe(200);
|
|
110
|
+
const data = (await r.json()) as {
|
|
111
|
+
tokens: { name: string; value: string }[];
|
|
112
|
+
ds: { name: string };
|
|
113
|
+
tokensPath: string;
|
|
114
|
+
};
|
|
115
|
+
expect(data.ds.name).toBe('beta');
|
|
116
|
+
expect(data.tokensPath).toContain('system/beta/colors_and_type.css');
|
|
117
|
+
const accent = data.tokens.find((t) => t.name === '--accent');
|
|
118
|
+
expect(accent?.value).toBe('oklch(72% 0.16 55)');
|
|
119
|
+
const bg0 = data.tokens.find((t) => t.name === '--bg-0');
|
|
120
|
+
expect(bg0?.value).toBe('oklch(13% 0.012 60)');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('?ds=ghost → 404 + error payload (no silent shell fallback)', async () => {
|
|
124
|
+
const r = await fetch(`http://localhost:${port}/_system-data?ds=ghost`);
|
|
125
|
+
expect(r.status).toBe(404);
|
|
126
|
+
const body = (await r.json()) as { error: string; ds: string };
|
|
127
|
+
expect(body.error).toBe('unknown design system');
|
|
128
|
+
expect(body.ds).toBe('ghost');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('unscoped (no ?ds) → availableDesignSystems list + defaultDesignSystem', async () => {
|
|
132
|
+
const r = await fetch(`http://localhost:${port}/_system-data`);
|
|
133
|
+
expect(r.status).toBe(200);
|
|
134
|
+
const data = (await r.json()) as {
|
|
135
|
+
availableDesignSystems: { name: string; description: string | null }[];
|
|
136
|
+
defaultDesignSystem: string;
|
|
137
|
+
ds: unknown;
|
|
138
|
+
};
|
|
139
|
+
expect(data.availableDesignSystems).toHaveLength(2);
|
|
140
|
+
expect(data.availableDesignSystems.map((d) => d.name)).toEqual(['alpha', 'beta']);
|
|
141
|
+
expect(data.defaultDesignSystem).toBe('alpha');
|
|
142
|
+
expect(data.ds).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
});
|