@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
|
@@ -1049,7 +1049,7 @@ function Wordmark({ project, port, version }) {
|
|
|
1049
1049
|
);
|
|
1050
1050
|
}
|
|
1051
1051
|
|
|
1052
|
-
function Viewport({ tabs, activePath, registerIframe, systemData, onOpenFromSystem, project, cfg }) {
|
|
1052
|
+
function Viewport({ tabs, activePath, registerIframe, systemData, onOpenFromSystem, onSelectDs, project, cfg }) {
|
|
1053
1053
|
return (
|
|
1054
1054
|
<div className="viewport">
|
|
1055
1055
|
{tabs.length === 0 && (
|
|
@@ -1073,7 +1073,7 @@ function Viewport({ tabs, activePath, registerIframe, systemData, onOpenFromSyst
|
|
|
1073
1073
|
if (t.path === SYSTEM_TAB) {
|
|
1074
1074
|
return (
|
|
1075
1075
|
<div key={t.path} className={'system-view' + (t.path === activePath ? ' active' : '')}>
|
|
1076
|
-
<SystemView data={systemData} onOpen={onOpenFromSystem} cfg={cfg} />
|
|
1076
|
+
<SystemView data={systemData} onOpen={onOpenFromSystem} cfg={cfg} onSelectDs={onSelectDs} />
|
|
1077
1077
|
</div>
|
|
1078
1078
|
);
|
|
1079
1079
|
}
|
|
@@ -1092,83 +1092,184 @@ function Viewport({ tabs, activePath, registerIframe, systemData, onOpenFromSyst
|
|
|
1092
1092
|
}
|
|
1093
1093
|
|
|
1094
1094
|
// ---------- SystemView ----------
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1095
|
+
//
|
|
1096
|
+
// DDR-048 — the System view renders the USER's design-system tokens. It does
|
|
1097
|
+
// NOT read from `document.documentElement` (that would surface the dev-server
|
|
1098
|
+
// shell's amber-rust chrome theme from styles/1-tokens.css, which is NOT a
|
|
1099
|
+
// user template) and it does NOT assume any canonical token-name contract.
|
|
1100
|
+
// Whatever the user's `colors_and_type.css` declared — names, values, theme
|
|
1101
|
+
// blocks — is what shows up here.
|
|
1102
|
+
|
|
1103
|
+
// Order kinds match the typical reading flow of a tokens file. Unknown kinds
|
|
1104
|
+
// fall through to `other` so a custom token group still renders, just last.
|
|
1105
|
+
const TOKEN_GROUP_ORDER = [
|
|
1106
|
+
'color', 'space', 'radius', 'shadow', 'leading', 'weight', 'motion', 'font', 'other',
|
|
1102
1107
|
];
|
|
1103
|
-
const
|
|
1108
|
+
const TOKEN_GROUP_LABELS = {
|
|
1109
|
+
color: 'colors',
|
|
1110
|
+
space: 'spacing',
|
|
1111
|
+
radius: 'radii',
|
|
1112
|
+
shadow: 'shadows',
|
|
1113
|
+
leading: 'leading',
|
|
1114
|
+
weight: 'weights',
|
|
1115
|
+
motion: 'motion',
|
|
1116
|
+
font: 'font stacks',
|
|
1117
|
+
other: 'other',
|
|
1118
|
+
};
|
|
1104
1119
|
|
|
1105
|
-
function
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1120
|
+
function isSwatchKind(kind) { return kind === 'color'; }
|
|
1121
|
+
|
|
1122
|
+
function TokenLadder({ tokens, tokenGroups, tokensPath }) {
|
|
1123
|
+
if (!tokens || tokens.length === 0) {
|
|
1124
|
+
return (
|
|
1125
|
+
<section className="sv-section sv-section-tokens">
|
|
1126
|
+
<h2>tokens<span className="sv-h-num">0</span></h2>
|
|
1127
|
+
<div className="sv-empty">
|
|
1128
|
+
<p>
|
|
1129
|
+
No tokens parsed from {tokensPath ? <code>{tokensPath}</code> : 'the configured tokens file'}.
|
|
1130
|
+
Does the file exist and contain CSS custom properties (<code>--name: value;</code>)?
|
|
1131
|
+
</p>
|
|
1132
|
+
</div>
|
|
1133
|
+
</section>
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const groups = tokenGroups || {};
|
|
1138
|
+
const kinds = Array.from(
|
|
1139
|
+
new Set([...TOKEN_GROUP_ORDER, ...Object.keys(groups)])
|
|
1140
|
+
).filter((k) => groups[k]?.length);
|
|
1110
1141
|
|
|
1111
|
-
function TokenLadder() {
|
|
1112
|
-
const [tokens, setTokens] = useState(() => readTokens(TOKEN_NAMES));
|
|
1113
|
-
useEffect(() => {
|
|
1114
|
-
setTokens(readTokens(TOKEN_NAMES));
|
|
1115
|
-
const obs = new MutationObserver(() => setTokens(readTokens(TOKEN_NAMES)));
|
|
1116
|
-
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
1117
|
-
return () => obs.disconnect();
|
|
1118
|
-
}, []);
|
|
1119
1142
|
return (
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
<
|
|
1127
|
-
|
|
1128
|
-
<span className="sv-
|
|
1143
|
+
<>
|
|
1144
|
+
{kinds.map((kind) => {
|
|
1145
|
+
const list = groups[kind];
|
|
1146
|
+
const swatch = isSwatchKind(kind);
|
|
1147
|
+
return (
|
|
1148
|
+
<section className={'sv-section sv-section-tokens sv-section-' + kind} key={kind}>
|
|
1149
|
+
<h2>
|
|
1150
|
+
tokens · {TOKEN_GROUP_LABELS[kind] || kind}
|
|
1151
|
+
<span className="sv-h-num">{list.length}</span>
|
|
1152
|
+
</h2>
|
|
1153
|
+
<div className="sv-tokens-ladder">
|
|
1154
|
+
{list.map((t) => (
|
|
1155
|
+
<div className="sv-tok-cell" key={t.name + '|' + t.value}>
|
|
1156
|
+
{swatch ? (
|
|
1157
|
+
<div className="sv-tok-swatch" style={{ background: t.value }} />
|
|
1158
|
+
) : null}
|
|
1159
|
+
<div className="sv-tok-meta">
|
|
1160
|
+
<code className="sv-tok-name">{t.name}</code>
|
|
1161
|
+
<span className="sv-tok-value">{t.value || '—'}</span>
|
|
1162
|
+
</div>
|
|
1163
|
+
</div>
|
|
1164
|
+
))}
|
|
1129
1165
|
</div>
|
|
1130
|
-
</
|
|
1131
|
-
)
|
|
1132
|
-
|
|
1133
|
-
|
|
1166
|
+
</section>
|
|
1167
|
+
);
|
|
1168
|
+
})}
|
|
1169
|
+
</>
|
|
1134
1170
|
);
|
|
1135
1171
|
}
|
|
1136
1172
|
|
|
1137
|
-
|
|
1173
|
+
// Find a leading partner by name suffix: --fs-base → --lh-base, --type-xl → --lh-xl.
|
|
1174
|
+
// Returns null when no convention match exists; caller omits the lineHeight style.
|
|
1175
|
+
function findLeadingFor(typeToken, leadingTokens) {
|
|
1176
|
+
const m = typeToken.name.match(/^--(?:type|fs|text)-(.+)$/);
|
|
1177
|
+
if (!m) return null;
|
|
1178
|
+
const suffix = m[1];
|
|
1179
|
+
return (leadingTokens || []).find((t) =>
|
|
1180
|
+
/^--(?:lh|leading|line-height)-/.test(t.name) && t.name.endsWith('-' + suffix)
|
|
1181
|
+
)?.value ?? null;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Best-effort sample font: prefer body / sans / display tokens, fall back to
|
|
1185
|
+
// the first font-kind token, fall back to system-ui. Avoids the shell's
|
|
1186
|
+
// Berkeley Mono leaking into user-facing previews.
|
|
1187
|
+
function sampleFontFamily(fontTokens) {
|
|
1188
|
+
if (!fontTokens?.length) return undefined;
|
|
1189
|
+
const prefer = ['body', 'sans', 'display', 'text', 'family'];
|
|
1190
|
+
for (const tag of prefer) {
|
|
1191
|
+
const hit = fontTokens.find((t) => t.name.includes(tag));
|
|
1192
|
+
if (hit) return hit.value;
|
|
1193
|
+
}
|
|
1194
|
+
return fontTokens[0].value;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function TypeLadder({ tokenGroups }) {
|
|
1198
|
+
const typeTokens = tokenGroups?.fontsize || [];
|
|
1199
|
+
if (typeTokens.length === 0) return null;
|
|
1200
|
+
const leadingTokens = tokenGroups?.leading || [];
|
|
1201
|
+
const sampleFont = sampleFontFamily(tokenGroups?.font);
|
|
1202
|
+
|
|
1138
1203
|
return (
|
|
1139
1204
|
<section className="sv-section sv-section-type">
|
|
1140
|
-
<h2>
|
|
1205
|
+
<h2>
|
|
1206
|
+
type · ladder<span className="sv-h-num">{typeTokens.length}</span>
|
|
1207
|
+
</h2>
|
|
1141
1208
|
<div className="sv-type-list">
|
|
1142
|
-
{
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1209
|
+
{typeTokens.map((t) => {
|
|
1210
|
+
const lh = findLeadingFor(t, leadingTokens);
|
|
1211
|
+
const style = { fontSize: t.value };
|
|
1212
|
+
if (lh) style.lineHeight = lh;
|
|
1213
|
+
if (sampleFont) style.fontFamily = sampleFont;
|
|
1214
|
+
return (
|
|
1215
|
+
<div className="sv-type-row" key={t.name}>
|
|
1216
|
+
<code className="sv-type-tok">{t.name}</code>
|
|
1217
|
+
<span className="sv-type-sample" style={style}>
|
|
1218
|
+
The catalog is the system.
|
|
1219
|
+
</span>
|
|
1220
|
+
</div>
|
|
1221
|
+
);
|
|
1222
|
+
})}
|
|
1150
1223
|
</div>
|
|
1151
1224
|
</section>
|
|
1152
1225
|
);
|
|
1153
1226
|
}
|
|
1154
1227
|
|
|
1155
|
-
function SystemView({ data, onOpen, cfg }) {
|
|
1228
|
+
function SystemView({ data, onOpen, cfg, onSelectDs }) {
|
|
1156
1229
|
if (!data) {
|
|
1157
1230
|
return <div className="sv-empty"><p>Loading design system…</p></div>;
|
|
1158
1231
|
}
|
|
1159
|
-
const {
|
|
1232
|
+
const {
|
|
1233
|
+
previewGallery,
|
|
1234
|
+
uiKitsGallery,
|
|
1235
|
+
systemDir,
|
|
1236
|
+
tokens,
|
|
1237
|
+
tokenGroups,
|
|
1238
|
+
tokensPath,
|
|
1239
|
+
ds,
|
|
1240
|
+
availableDesignSystems,
|
|
1241
|
+
} = data;
|
|
1160
1242
|
const empty = (!previewGallery || !previewGallery.length) && (!uiKitsGallery || !uiKitsGallery.length);
|
|
1243
|
+
const hasPicker = Array.isArray(availableDesignSystems) && availableDesignSystems.length > 1;
|
|
1244
|
+
const selectedName = ds?.name ?? availableDesignSystems?.[0]?.name ?? '';
|
|
1161
1245
|
|
|
1162
1246
|
return (
|
|
1163
1247
|
<div className="sv">
|
|
1164
1248
|
<header className="sv-header">
|
|
1165
1249
|
<span className="sv-sku">MDCC-DSN/01</span>
|
|
1166
1250
|
<span className="sv-title">design system view</span>
|
|
1251
|
+
{hasPicker ? (
|
|
1252
|
+
<label className="sv-ds-picker">
|
|
1253
|
+
<span className="sv-ds-picker-label">DS</span>
|
|
1254
|
+
<select
|
|
1255
|
+
value={selectedName}
|
|
1256
|
+
onChange={(e) => onSelectDs && onSelectDs(e.target.value)}
|
|
1257
|
+
>
|
|
1258
|
+
{availableDesignSystems.map((d) => (
|
|
1259
|
+
<option key={d.name} value={d.name}>{d.name}</option>
|
|
1260
|
+
))}
|
|
1261
|
+
</select>
|
|
1262
|
+
</label>
|
|
1263
|
+
) : null}
|
|
1167
1264
|
<span className="sv-loc"><code>{systemDir}</code></span>
|
|
1168
1265
|
</header>
|
|
1169
1266
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1267
|
+
{ds?.description ? (
|
|
1268
|
+
<p className="sv-ds-description">{ds.description}</p>
|
|
1269
|
+
) : null}
|
|
1270
|
+
|
|
1271
|
+
<TokenLadder tokens={tokens} tokenGroups={tokenGroups} tokensPath={tokensPath} />
|
|
1272
|
+
<TypeLadder tokenGroups={tokenGroups} />
|
|
1172
1273
|
|
|
1173
1274
|
{empty ? (
|
|
1174
1275
|
<div className="sv-empty">
|
|
@@ -1494,10 +1595,29 @@ function App() {
|
|
|
1494
1595
|
useEffect(() => { loadTree(); }, [loadTree]);
|
|
1495
1596
|
|
|
1496
1597
|
// ----- System data (lazy) -----
|
|
1497
|
-
|
|
1598
|
+
// `dsName` scopes to a single design-system entry (DDR-048). The initial
|
|
1599
|
+
// call is unscoped — server returns `availableDesignSystems[]` + a default
|
|
1600
|
+
// — so the picker can render without a probe round-trip. Subsequent calls
|
|
1601
|
+
// (e.g. picker change) pass the chosen DS name and we replace systemData
|
|
1602
|
+
// wholesale (tokens + previews + ds metadata all shift together).
|
|
1603
|
+
const loadSystemData = useCallback(async (dsName) => {
|
|
1498
1604
|
try {
|
|
1499
|
-
const
|
|
1605
|
+
const url = dsName ? `/_system-data?ds=${encodeURIComponent(dsName)}` : '/_system-data';
|
|
1606
|
+
const r = await fetch(url);
|
|
1607
|
+
if (!r.ok) {
|
|
1608
|
+
console.error('failed to load system-data', r.status);
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1500
1611
|
const data = await r.json();
|
|
1612
|
+
// If the initial unscoped fetch has a defaultDesignSystem but no `ds`
|
|
1613
|
+
// attached (multi-DS project), kick off a scoped fetch so the visible
|
|
1614
|
+
// tokens + previews match the default DS, not the union root scan.
|
|
1615
|
+
if (!dsName && data?.defaultDesignSystem && !data.ds) {
|
|
1616
|
+
setSystemData(data);
|
|
1617
|
+
const r2 = await fetch(`/_system-data?ds=${encodeURIComponent(data.defaultDesignSystem)}`);
|
|
1618
|
+
if (r2.ok) setSystemData(await r2.json());
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1501
1621
|
setSystemData(data);
|
|
1502
1622
|
} catch (e) {
|
|
1503
1623
|
console.error('failed to load system-data', e);
|
|
@@ -1918,6 +2038,7 @@ function App() {
|
|
|
1918
2038
|
registerIframe={registerIframe}
|
|
1919
2039
|
systemData={systemData}
|
|
1920
2040
|
onOpenFromSystem={openTab}
|
|
2041
|
+
onSelectDs={loadSystemData}
|
|
1921
2042
|
project={project}
|
|
1922
2043
|
cfg={cfg}
|
|
1923
2044
|
/>
|
|
@@ -65,6 +65,34 @@ function handle(evt) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (evt.type === 'fs:json' && evt.file) {
|
|
71
|
+
// Phase 20 (DDR-049) — external edits to canvas `.meta.json` or
|
|
72
|
+
// `.annotations.svg` invalidate the iframe's in-memory undo stack
|
|
73
|
+
// (stale before/after snapshots would otherwise restore wrong state).
|
|
74
|
+
// The iframe's UndoStackProvider self-echo-dampens our own PATCH-es
|
|
75
|
+
// via window.__maude_last_meta_self_write_at, so we forward
|
|
76
|
+
// indiscriminately here.
|
|
77
|
+
const file = evt.file;
|
|
78
|
+
const isMeta = file.endsWith('.meta.json');
|
|
79
|
+
if (!isMeta) return;
|
|
80
|
+
const iframes = document.querySelectorAll('iframe[data-canvas-path]');
|
|
81
|
+
for (const f of iframes) {
|
|
82
|
+
if (!f.dataset.canvasPath) continue;
|
|
83
|
+
// Match by the canvas filename root (foo.tsx ↔ foo.meta.json).
|
|
84
|
+
const root = f.dataset.canvasPath.replace(/\.(tsx|jsx|ts|js|html)$/i, '');
|
|
85
|
+
if (!file.includes(`${root}.meta.json`)) continue;
|
|
86
|
+
try {
|
|
87
|
+
f.contentWindow?.dispatchEvent(
|
|
88
|
+
new CustomEvent('maude:invalidate-undo', {
|
|
89
|
+
detail: { reason: 'Edit history reset (external change)' },
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
} catch {
|
|
93
|
+
/* cross-origin / detached — ignore */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
68
96
|
}
|
|
69
97
|
}
|
|
70
98
|
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
/* @layer tokens —
|
|
1
|
+
/* @layer tokens — SHELL theme for the dev-server UI itself.
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* manually and keep the values verbatim.
|
|
3
|
+
* ⚠ This is NOT a template for user design systems and NOT a fallback for
|
|
4
|
+
* the System view's token ladder. It themes the dev-server chrome — sidebar,
|
|
5
|
+
* status bar, header, menubar, comments panel, System tab outer shell. The
|
|
6
|
+
* amber-rust "catalog stamp" aesthetic below belongs to Maude as a tool, not
|
|
7
|
+
* to any project that runs against it.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
9
|
+
* Per DDR-048, the System view (MDCC-DSN/01) reads user tokens from the
|
|
10
|
+
* project's own `colors_and_type.css` (parsed by the server, returned via
|
|
11
|
+
* /_system-data) and renders them by raw value — never by `var(--name)`
|
|
12
|
+
* resolving against this document. That isolation is load-bearing: prior
|
|
13
|
+
* to DDR-048 the System view leaked these shell tokens into the user's
|
|
14
|
+
* "design system overview" because both happened to share the canonical
|
|
15
|
+
* `--bg-0..4 / --fg-0..3 / --accent*` naming.
|
|
16
|
+
*
|
|
17
|
+
* Two shell themes — both first-class, both equal:
|
|
11
18
|
* :root + [data-theme="light"] — paper-light (cream + ink)
|
|
12
19
|
* [data-theme="dark"] — phosphor-dark (default for dev-server)
|
|
13
20
|
*
|
|
@@ -36,14 +36,44 @@
|
|
|
36
36
|
|
|
37
37
|
/* CV-09 header — SKU framing, hairline divider underneath. */
|
|
38
38
|
.sv-header {
|
|
39
|
-
display:
|
|
40
|
-
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-wrap: wrap;
|
|
41
41
|
align-items: baseline;
|
|
42
42
|
gap: var(--space-5);
|
|
43
43
|
border-bottom: 2px solid var(--u-fg-0);
|
|
44
44
|
padding-bottom: var(--u-s-3);
|
|
45
45
|
margin-bottom: var(--u-s-5);
|
|
46
46
|
}
|
|
47
|
+
.sv-header .sv-title { flex: 1 1 auto; }
|
|
48
|
+
.sv-ds-picker {
|
|
49
|
+
display: inline-flex;
|
|
50
|
+
align-items: baseline;
|
|
51
|
+
gap: var(--u-s-2);
|
|
52
|
+
font-family: var(--u-font-mono);
|
|
53
|
+
font-size: 10px;
|
|
54
|
+
letter-spacing: var(--tracking-sku);
|
|
55
|
+
text-transform: uppercase;
|
|
56
|
+
color: var(--u-fg-2);
|
|
57
|
+
}
|
|
58
|
+
.sv-ds-picker select {
|
|
59
|
+
font-family: var(--u-font-mono);
|
|
60
|
+
font-size: 11px;
|
|
61
|
+
color: var(--u-fg-0);
|
|
62
|
+
background: var(--u-bg-1);
|
|
63
|
+
border: 1px solid var(--u-border);
|
|
64
|
+
border-radius: var(--u-r-sm);
|
|
65
|
+
padding: 2px 6px;
|
|
66
|
+
text-transform: none;
|
|
67
|
+
letter-spacing: 0;
|
|
68
|
+
}
|
|
69
|
+
.sv-ds-description {
|
|
70
|
+
font-family: var(--u-font-body);
|
|
71
|
+
font-size: var(--type-sm);
|
|
72
|
+
line-height: 1.5;
|
|
73
|
+
color: var(--u-fg-1);
|
|
74
|
+
margin: 0 0 var(--u-s-5);
|
|
75
|
+
max-width: 72ch;
|
|
76
|
+
}
|
|
47
77
|
.sv-sku {
|
|
48
78
|
font-family: var(--u-font-mono);
|
|
49
79
|
font-size: var(--type-lg);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file commands/annotation-strokes-command.ts — undo entry for annotation
|
|
3
|
+
* stroke add / erase / batch-translate.
|
|
4
|
+
* @scope plugins/design/dev-server/commands/annotation-strokes-command.ts
|
|
5
|
+
* @purpose Records a `Stroke[]` pair (before/after) and routes both
|
|
6
|
+
* directions through a single `putFn`. The server endpoint is
|
|
7
|
+
* `PUT /_api/annotations` which replaces the entire SVG; we
|
|
8
|
+
* match that shape rather than diffing.
|
|
9
|
+
*
|
|
10
|
+
* Per DDR-050 rev 2 the runtime command is rebuilt per iframe
|
|
11
|
+
* mount from a serializable `CommandRecord` so the stack
|
|
12
|
+
* survives canvas switches. The `putFn` here also updates the
|
|
13
|
+
* iframe's local React strokes state (see annotations-layer
|
|
14
|
+
* `putStrokes`) so undo/redo visibly refresh, not just PUT.
|
|
15
|
+
*
|
|
16
|
+
* Why per-stroke commands (not coalesced into a 300 ms window):
|
|
17
|
+
* matches Figma/FigJam — Cmd+Z reverts the most recent tap or
|
|
18
|
+
* pen stroke individually. Coalescing into windows led to a
|
|
19
|
+
* matoucí "why did Cmd+Z erase two of my last lines?" UX.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { Stroke } from '../annotations-layer.tsx';
|
|
23
|
+
import type { CommandRecord, CommandSinks, EditCommand } from '../undo-stack.ts';
|
|
24
|
+
import { registerCommand } from '../undo-stack.ts';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Push-once callable that submits a full `Stroke[]` to the server (or its
|
|
28
|
+
* test stub) AND updates the iframe's local strokes state. The
|
|
29
|
+
* annotations-layer `putStrokes` is the production binding.
|
|
30
|
+
*/
|
|
31
|
+
export type StrokesPutFn = (next: readonly Stroke[]) => void | Promise<void>;
|
|
32
|
+
|
|
33
|
+
export interface AnnotationStrokesPayload {
|
|
34
|
+
before: readonly Stroke[];
|
|
35
|
+
after: readonly Stroke[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const ANNOTATION_STROKES_KIND = 'annotation-strokes';
|
|
39
|
+
|
|
40
|
+
export interface AnnotationStrokesCommandInit {
|
|
41
|
+
before: readonly Stroke[];
|
|
42
|
+
after: readonly Stroke[];
|
|
43
|
+
putFn: StrokesPutFn;
|
|
44
|
+
/** Optional label override (eraser → "erase 1 stroke"). */
|
|
45
|
+
label?: string;
|
|
46
|
+
/** Telemetry kind. */
|
|
47
|
+
kind?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createAnnotationStrokesCommand(init: AnnotationStrokesCommandInit): EditCommand {
|
|
51
|
+
const { putFn } = init;
|
|
52
|
+
const before = init.before.map(cloneStroke);
|
|
53
|
+
const after = init.after.map(cloneStroke);
|
|
54
|
+
const label = init.label ?? defaultLabel(before, after);
|
|
55
|
+
const kind = init.kind ?? ANNOTATION_STROKES_KIND;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
kind,
|
|
59
|
+
label,
|
|
60
|
+
async do() {
|
|
61
|
+
await putFn(after.map(cloneStroke));
|
|
62
|
+
},
|
|
63
|
+
async undo() {
|
|
64
|
+
await putFn(before.map(cloneStroke));
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build a persistable record from the same inputs as `createAnnotationStrokesCommand`.
|
|
71
|
+
* Used by the consumer alongside the EditCommand so the runtime side-effect
|
|
72
|
+
* and the stored shape share one snapshot.
|
|
73
|
+
*/
|
|
74
|
+
export function buildAnnotationStrokesRecord(opts: {
|
|
75
|
+
before: readonly Stroke[];
|
|
76
|
+
after: readonly Stroke[];
|
|
77
|
+
label?: string;
|
|
78
|
+
}): CommandRecord<AnnotationStrokesPayload> {
|
|
79
|
+
const before = opts.before.map(cloneStroke);
|
|
80
|
+
const after = opts.after.map(cloneStroke);
|
|
81
|
+
return {
|
|
82
|
+
kind: ANNOTATION_STROKES_KIND,
|
|
83
|
+
label: opts.label ?? defaultLabel(before, after),
|
|
84
|
+
payload: { before, after },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
89
|
+
// Registry
|
|
90
|
+
|
|
91
|
+
registerCommand<AnnotationStrokesPayload>(ANNOTATION_STROKES_KIND, (record, sinks) => {
|
|
92
|
+
const putFn = sinks.strokesPutFn as StrokesPutFn | undefined;
|
|
93
|
+
if (!putFn) return null;
|
|
94
|
+
return createAnnotationStrokesCommand({
|
|
95
|
+
before: record.payload.before,
|
|
96
|
+
after: record.payload.after,
|
|
97
|
+
putFn,
|
|
98
|
+
label: record.label,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
// Internals
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Structured clone — works for every Stroke variant because they're plain
|
|
107
|
+
* JSON-shaped (no Date / Map / DOM nodes). The pen `points: WorldPoint[]`
|
|
108
|
+
* array is nested-cloned too so callers can't poison the snapshot by
|
|
109
|
+
* mutating the source.
|
|
110
|
+
*/
|
|
111
|
+
function cloneStroke<T extends Stroke>(s: T): T {
|
|
112
|
+
// structuredClone is available on every runtime we ship to (Bun, modern
|
|
113
|
+
// browsers, Node 17+). Fall back to JSON for the rare older harness.
|
|
114
|
+
if (typeof structuredClone === 'function') return structuredClone(s);
|
|
115
|
+
return JSON.parse(JSON.stringify(s)) as T;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function defaultLabel(before: readonly Stroke[], after: readonly Stroke[]): string {
|
|
119
|
+
const added = after.length - before.length;
|
|
120
|
+
if (added > 0) return `add ${added} stroke${added === 1 ? '' : 's'}`;
|
|
121
|
+
if (added < 0) {
|
|
122
|
+
const erased = -added;
|
|
123
|
+
return `erase ${erased} stroke${erased === 1 ? '' : 's'}`;
|
|
124
|
+
}
|
|
125
|
+
// Same count — could be translate / edit / fill-change.
|
|
126
|
+
return `edit ${after.length} stroke${after.length === 1 ? '' : 's'}`;
|
|
127
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file commands/equal-spacing-command.ts — undo label helpers for
|
|
3
|
+
* equal-spacing / align gestures.
|
|
4
|
+
* @scope plugins/design/dev-server/commands/equal-spacing-command.ts
|
|
5
|
+
* @purpose Distribute + align both call `dragBus.commitPositions(moved)`,
|
|
6
|
+
* which wraps the move in a generic MoveArtboardsCommand. Per
|
|
7
|
+
* DDR-050 the underlying command type is shared (the inverse
|
|
8
|
+
* payload is identical — a layout snapshot pair) and only the
|
|
9
|
+
* HUD label differs. These helpers compute the labels.
|
|
10
|
+
*
|
|
11
|
+
* Why not a separate Command class? The wrapping logic would
|
|
12
|
+
* duplicate `before`/`after`/`patchFn` + add an indirection.
|
|
13
|
+
* A label is a one-line string; a factory is overkill.
|
|
14
|
+
* Keeping `MoveArtboardsCommand` as the only artboard-layout
|
|
15
|
+
* command keeps the future Yjs swap (Phase 8) one-for-one.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type AlignMode = 'left' | 'right' | 'center-x' | 'top' | 'bottom' | 'center-y';
|
|
19
|
+
|
|
20
|
+
/** "equal-space 3 artboards" — N must be ≥ 3 (distribute requires it). */
|
|
21
|
+
export function equalSpacingLabel(n: number): string {
|
|
22
|
+
return `equal-space ${n} artboard${n === 1 ? '' : 's'}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** "align left 4 artboards" — N must be ≥ 2 (align requires it). */
|
|
26
|
+
export function alignLabel(mode: AlignMode, n: number): string {
|
|
27
|
+
return `align ${mode} ${n} artboard${n === 1 ? '' : 's'}`;
|
|
28
|
+
}
|