@decantr/cli 1.7.6 → 1.7.8
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/README.md +51 -3
- package/dist/bin.js +2 -2
- package/dist/{chunk-H4H3IQJK.js → chunk-74G2RQDO.js} +100 -13
- package/dist/{chunk-KAEQTVAM.js → chunk-ODJQZXHQ.js} +702 -352
- package/dist/index.js +2 -2
- package/dist/{upgrade-XNUAON3G.js → upgrade-UXY2WUJH.js} +1 -1
- package/package.json +4 -2
- package/src/templates/DECANTR.md.template +6 -6
package/README.md
CHANGED
|
@@ -14,12 +14,22 @@ npm install -D @decantr/cli
|
|
|
14
14
|
Or run it without installing:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
npx @decantr/cli
|
|
17
|
+
npx @decantr/cli new my-app --blueprint=agent-marketplace
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
Use `decantr new` for a greenfield workspace in a fresh directory.
|
|
21
|
+
Use `decantr analyze` first when you already have an app and want Decantr governance without adopting a blueprint.
|
|
22
|
+
Use `decantr init` to attach Decantr contract/context files to an existing project or to create a contract-only workspace.
|
|
23
|
+
|
|
24
|
+
Current starter adapter availability:
|
|
25
|
+
|
|
26
|
+
- `react-vite` is the runnable bootstrap adapter in this wave
|
|
27
|
+
- other contract targets remain valid Decantr targets, but `decantr new` will keep them in contract-only mode until their adapters land
|
|
28
|
+
|
|
20
29
|
## What It Does
|
|
21
30
|
|
|
22
31
|
- scaffolds Decantr projects from blueprints, archetypes, or prompts
|
|
32
|
+
- supports three workflow lanes: greenfield blueprint, brownfield adoption, and hybrid composition
|
|
23
33
|
- generates execution-pack context files for AI coding assistants
|
|
24
34
|
- audits projects against Decantr contracts
|
|
25
35
|
- searches the registry and showcase benchmark corpus
|
|
@@ -28,7 +38,10 @@ npx @decantr/cli init --blueprint=agent-marketplace --yes
|
|
|
28
38
|
## Common Commands
|
|
29
39
|
|
|
30
40
|
```bash
|
|
31
|
-
decantr
|
|
41
|
+
decantr new my-app --blueprint=agent-marketplace
|
|
42
|
+
decantr analyze
|
|
43
|
+
decantr init --existing --yes
|
|
44
|
+
decantr init --existing --blueprint=agent-marketplace
|
|
32
45
|
decantr magic "AI-native analytics workspace"
|
|
33
46
|
decantr audit
|
|
34
47
|
decantr check
|
|
@@ -36,14 +49,49 @@ decantr registry summary --namespace @official --json
|
|
|
36
49
|
decantr showcase verification --json
|
|
37
50
|
```
|
|
38
51
|
|
|
52
|
+
## Greenfield Certification
|
|
53
|
+
|
|
54
|
+
Use the built-in certification harness before releases when you want to prove that representative blueprints still scaffold into runnable starter projects:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pnpm --filter @decantr/cli certify:blueprints
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
By default it certifies `portfolio`, `producer-studio`, and `agent-marketplace` by:
|
|
61
|
+
|
|
62
|
+
- running `decantr new` in fresh temp directories
|
|
63
|
+
- seeding offline content from `DECANTR_CONTENT_DIR` or a sibling `decantr-content` checkout
|
|
64
|
+
- verifying the starter runtime files and router mode match the generated essence
|
|
65
|
+
- running `npm run build` in each scaffolded project
|
|
66
|
+
|
|
67
|
+
Override the matrix or emit JSON when needed:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pnpm --filter @decantr/cli certify:blueprints -- --blueprints=portfolio,legal-research --json
|
|
71
|
+
```
|
|
72
|
+
|
|
39
73
|
Offline blueprint scaffolding expects a real local content source:
|
|
40
74
|
|
|
41
75
|
```bash
|
|
42
|
-
DECANTR_CONTENT_DIR=/path/to/decantr-content decantr
|
|
76
|
+
DECANTR_CONTENT_DIR=/path/to/decantr-content decantr new my-app --blueprint=agent-marketplace --offline
|
|
43
77
|
```
|
|
44
78
|
|
|
45
79
|
If a requested offline blueprint, archetype, or theme cannot be resolved from local cache/custom content or `DECANTR_CONTENT_DIR`, the CLI now stops explicitly instead of silently falling back to the default scaffold.
|
|
46
80
|
|
|
81
|
+
## Workflow Certification
|
|
82
|
+
|
|
83
|
+
The broader workflow matrix now has its own certification entrypoint:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pnpm --filter @decantr/cli certify:workflows
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
It covers:
|
|
90
|
+
|
|
91
|
+
- greenfield blueprint bootstrap
|
|
92
|
+
- brownfield `analyze -> init --existing`
|
|
93
|
+
- hybrid follow-up composition via Decantr mutation commands
|
|
94
|
+
|
|
47
95
|
## Generated Context
|
|
48
96
|
|
|
49
97
|
Scaffolded projects include compiled execution packs under `.decantr/context/`, including:
|
package/dist/bin.js
CHANGED
|
@@ -6,8 +6,9 @@ import { computeSpatialTokens } from "@decantr/essence-spec";
|
|
|
6
6
|
import { compileExecutionPackBundle } from "@decantr/core";
|
|
7
7
|
|
|
8
8
|
// src/treatments.ts
|
|
9
|
-
function generateTreatmentCSS(spatialTokens, treatmentOverrides, themeDecorators, themeName) {
|
|
9
|
+
function generateTreatmentCSS(spatialTokens, treatmentOverrides, themeDecorators, themeName, themeDecoratorDefinitions) {
|
|
10
10
|
const lines = [];
|
|
11
|
+
const decoratorAnimationNames = /* @__PURE__ */ new Set();
|
|
11
12
|
lines.push("/* Generated by @decantr/cli \u2014 Visual Treatment System */");
|
|
12
13
|
lines.push("");
|
|
13
14
|
lines.push("@layer treatments {");
|
|
@@ -195,9 +196,9 @@ ${themeBody}
|
|
|
195
196
|
["font-size", "0.75rem"],
|
|
196
197
|
["font-weight", "500"],
|
|
197
198
|
["padding", "0.125rem 0.5rem"],
|
|
198
|
-
["margin-top", "calc(var(--d-annotation-mt) * var(--d-density-scale, 1))"],
|
|
199
199
|
["border-radius", "var(--d-radius-full)"],
|
|
200
|
-
["background", "var(--d-surface)"],
|
|
200
|
+
["background", "color-mix(in srgb, var(--d-surface-raised) 88%, transparent)"],
|
|
201
|
+
["border", "1px solid color-mix(in srgb, var(--d-border) 72%, transparent)"],
|
|
201
202
|
["color", "var(--d-text-muted)"],
|
|
202
203
|
["white-space", "nowrap"]
|
|
203
204
|
]);
|
|
@@ -247,10 +248,47 @@ ${themeBody}
|
|
|
247
248
|
lines.push("}");
|
|
248
249
|
lines.push("");
|
|
249
250
|
lines.push("} /* end @layer treatments */");
|
|
251
|
+
const decoratorRules = [];
|
|
252
|
+
if (themeDecoratorDefinitions) {
|
|
253
|
+
for (const [className, definition] of Object.entries(themeDecoratorDefinitions)) {
|
|
254
|
+
const props = definition?.suggested_properties ?? {};
|
|
255
|
+
const entries = Object.entries(props);
|
|
256
|
+
if (entries.length === 0) continue;
|
|
257
|
+
decoratorRules.push(`.${className} {`);
|
|
258
|
+
for (const [prop, value] of entries) {
|
|
259
|
+
decoratorRules.push(` ${prop}: ${value};`);
|
|
260
|
+
if (prop === "animation") {
|
|
261
|
+
const animationName = value.split(/\s+/)[0]?.trim();
|
|
262
|
+
if (animationName) decoratorAnimationNames.add(animationName);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
decoratorRules.push("}");
|
|
266
|
+
decoratorRules.push("");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const decoratorKeyframes = [];
|
|
270
|
+
if (decoratorAnimationNames.has("carbon-fade-slide")) {
|
|
271
|
+
decoratorKeyframes.push("@keyframes carbon-fade-slide {");
|
|
272
|
+
decoratorKeyframes.push(" from { opacity: 0; transform: translateY(12px); }");
|
|
273
|
+
decoratorKeyframes.push(" to { opacity: 1; transform: translateY(0); }");
|
|
274
|
+
decoratorKeyframes.push("}");
|
|
275
|
+
decoratorKeyframes.push("");
|
|
276
|
+
}
|
|
277
|
+
if (decoratorAnimationNames.has("pulse")) {
|
|
278
|
+
decoratorKeyframes.push("@keyframes pulse {");
|
|
279
|
+
decoratorKeyframes.push(" 0%, 100% { opacity: 1; }");
|
|
280
|
+
decoratorKeyframes.push(" 50% { opacity: 0.5; }");
|
|
281
|
+
decoratorKeyframes.push("}");
|
|
282
|
+
decoratorKeyframes.push("");
|
|
283
|
+
}
|
|
284
|
+
const decoratorComments = themeDecorators ? Object.entries(themeDecorators).map(([name, description]) => ` /* .${name}: ${description} */`).join("\n") : " /* No theme decorators available. */";
|
|
285
|
+
const decoratorBody = decoratorRules.length > 0 ? `${decoratorRules.join("\n")}${decoratorKeyframes.length > 0 ? `
|
|
286
|
+
${decoratorKeyframes.join("\n")}` : ""}${decoratorComments ? `
|
|
287
|
+
${decoratorComments}` : ""}` : `${decoratorComments}
|
|
288
|
+
/* Canonical decorator CSS should be derived from theme decorator definitions when available. */`;
|
|
250
289
|
const decoratorBlock = `
|
|
251
290
|
@layer decorators {
|
|
252
|
-
|
|
253
|
-
/* See .decantr/context/section-*.md for intent, suggested properties, and usage guidance. */
|
|
291
|
+
${decoratorBody}
|
|
254
292
|
}
|
|
255
293
|
`;
|
|
256
294
|
lines.push(decoratorBlock);
|
|
@@ -598,6 +636,7 @@ function mapRegistryThemeToThemeData(theme) {
|
|
|
598
636
|
typography: theme.typography,
|
|
599
637
|
motion: theme.motion,
|
|
600
638
|
decorators: theme.decorators,
|
|
639
|
+
decorator_definitions: theme.decorator_definitions,
|
|
601
640
|
treatments: theme.treatments,
|
|
602
641
|
spatial: theme.spatial,
|
|
603
642
|
radius: theme.radius,
|
|
@@ -777,11 +816,15 @@ body {
|
|
|
777
816
|
transform: translateY(0);
|
|
778
817
|
}
|
|
779
818
|
|
|
780
|
-
img, picture, video, canvas
|
|
819
|
+
img, picture, video, canvas {
|
|
781
820
|
display: block;
|
|
782
821
|
max-width: 100%;
|
|
783
822
|
}
|
|
784
823
|
|
|
824
|
+
svg {
|
|
825
|
+
max-width: 100%;
|
|
826
|
+
}
|
|
827
|
+
|
|
785
828
|
input, button, textarea, select {
|
|
786
829
|
font: inherit;
|
|
787
830
|
color: inherit;
|
|
@@ -1009,10 +1052,14 @@ import './styles/global.css'; // Resets
|
|
|
1009
1052
|
### Runtime Rules
|
|
1010
1053
|
|
|
1011
1054
|
- Use the real \`@decantr/css\` runtime for atoms. If \`package.json\` does not already depend on \`@decantr/css\`, add it before building.
|
|
1055
|
+
- If \`package.json\`, app entry files, or router/runtime files are absent, create them explicitly for the declared target instead of assuming a hidden starter already exists.
|
|
1012
1056
|
- Do **not** create local atom-runtime substitutes such as \`src/lib/css.js\`, \`src/lib/css.ts\`, or hand-written \`src/styles/atoms.css\` files unless the task explicitly asks for a fallback runtime.
|
|
1013
1057
|
- Keep atoms in \`css(...)\`, treatments as semantic classes, and theme decorators as additive classes. Do not blur those roles together.
|
|
1058
|
+
- Do **not** use inline visual style values or component-scoped \`<style>\` tags as the primary styling path. Colors, spacing, borders, shadows, gradients, and transitions should come from atoms, treatments, decorators, or CSS variables. Inline styles are only acceptable for truly dynamic geometry that cannot be expressed through the contract.
|
|
1014
1059
|
- Use \`d-control\` as the default semantic treatment for inputs, selects, and textareas. Theme decorators such as \`carbon-input\` are additive and should only layer on when the section or theme contract explicitly calls for them.
|
|
1015
1060
|
- Use loading decorators such as \`carbon-skeleton\` as optional enhancement on top of a structurally correct loading state \u2014 they do not replace the need for a real loading/skeleton branch.
|
|
1061
|
+
- Shells own spacing, centering, and scroll containers. Pages should not duplicate shell responsibilities with extra full-height wrappers, max-width wrappers, or page-local padding unless the route contract explicitly requires it.
|
|
1062
|
+
- If a required decorator class is referenced in the generated contract but missing from generated CSS, report that contract gap instead of inventing a parallel visual system.
|
|
1016
1063
|
|
|
1017
1064
|
### Visual Treatments
|
|
1018
1065
|
|
|
@@ -1044,12 +1091,19 @@ Atoms + treatment + theme decorator:
|
|
|
1044
1091
|
|
|
1045
1092
|
\`\`\`tsx
|
|
1046
1093
|
// Responsive prefix \u2014 applies at breakpoint and above:
|
|
1047
|
-
css('_col
|
|
1094
|
+
css('_col _sm:row')
|
|
1048
1095
|
|
|
1049
1096
|
// Pseudo prefix:
|
|
1050
|
-
css('
|
|
1097
|
+
css('_bgprimary _h:bgprimary/80')
|
|
1051
1098
|
\`\`\`
|
|
1052
1099
|
|
|
1100
|
+
### Prefix and Arbitrary Value Syntax
|
|
1101
|
+
|
|
1102
|
+
- Responsive prefixes are part of the atom token itself: \`_sm:gc2\`, \`_md:flex\`, \`_lg:row\`.
|
|
1103
|
+
- Pseudo prefixes are also token-prefixed: \`_h:bgprimary/80\`, \`_f:borderprimary\`, \`_fv:shadowmd\`.
|
|
1104
|
+
- Arbitrary values use square brackets when the standard scale is not enough: \`_w[512px]\`, \`_h[100vh]\`, \`_p[clamp(1rem,3vw,2rem)]\`, \`_z[40]\`.
|
|
1105
|
+
- When you see bracket atoms in shell or page contracts, treat them as first-class Decantr syntax, not as an error or a cue to fall back to inline styles.
|
|
1106
|
+
|
|
1053
1107
|
### Atom Reference
|
|
1054
1108
|
|
|
1055
1109
|
#### Display
|
|
@@ -1226,7 +1280,7 @@ css('hover:_opacity80')
|
|
|
1226
1280
|
| \`_trans\` | \`transition:all 0.15s ease\` |
|
|
1227
1281
|
| \`_visible\`, \`_invisible\` | visibility |
|
|
1228
1282
|
|
|
1229
|
-
Responsive prefixes: \`_sm:\`, \`_md:\`, \`_lg:\` (e.g. \`
|
|
1283
|
+
Responsive prefixes: \`_sm:\`, \`_md:\`, \`_lg:\`, \`_xl:\` (e.g. \`_sm:gc2\`, \`_md:flex\`, \`_lg:row\`).
|
|
1230
1284
|
|
|
1231
1285
|
### Section Labels
|
|
1232
1286
|
|
|
@@ -1245,6 +1299,7 @@ If the theme provides motion tokens, apply the \`entrance-fade\` class to page c
|
|
|
1245
1299
|
### Navigation Shortcuts
|
|
1246
1300
|
|
|
1247
1301
|
If the essence defines hotkeys or command_palette, implement as keyboard event listeners (useEffect + keydown) \u2014 not as visible UI text.
|
|
1302
|
+
Missing declared navigation features are contract drift, not optional polish.
|
|
1248
1303
|
|
|
1249
1304
|
### Design Tokens
|
|
1250
1305
|
|
|
@@ -1320,7 +1375,23 @@ function generateDecantrMdV31(params) {
|
|
|
1320
1375
|
const template = loadTemplate("DECANTR.md.template");
|
|
1321
1376
|
const body = renderTemplate(template, {
|
|
1322
1377
|
GUARD_MODE: params.guardMode,
|
|
1323
|
-
CSS_APPROACH: params.cssApproach
|
|
1378
|
+
CSS_APPROACH: params.cssApproach,
|
|
1379
|
+
WORKFLOW_MODE: params.workflowMode === "brownfield-attach" ? "brownfield attach" : "greenfield scaffold",
|
|
1380
|
+
WORKFLOW_GUIDANCE: params.workflowMode === "brownfield-attach" ? `This project is using Decantr in **brownfield attach** mode.
|
|
1381
|
+
|
|
1382
|
+
Read \`.decantr/analysis.json\` first for the detected framework, routes, styling, layout, and dependency facts.
|
|
1383
|
+
Then read \`.decantr/init-seed.json\` for the recommended attach defaults.
|
|
1384
|
+
Then read \`.decantr/context/scaffold-pack.md\` and \`.decantr/context/scaffold.md\` to understand the Decantr contract you are layering onto the existing app.
|
|
1385
|
+
|
|
1386
|
+
Preserve the current framework, package manager, router, and working runtime structure unless the contract gives you a reviewed reason to change them. Map existing routes and components onto the declared Decantr sections/pages before creating new files. Registry content is optional in this workflow unless the task explicitly asks for it.` : `This project is using Decantr in **greenfield scaffold** mode.
|
|
1387
|
+
|
|
1388
|
+
Treat the compiled execution-pack files as the primary source of truth.
|
|
1389
|
+
Use narrative docs only as secondary explanation when the compiled packs are not enough.
|
|
1390
|
+
Use only files present in this workspace as the source of truth. If local scaffold files disagree, stop and report the mismatch instead of relying on external Decantr assumptions or prior examples.
|
|
1391
|
+
|
|
1392
|
+
Read \`.decantr/context/scaffold-pack.md\` first for the compact compiled shell, theme, feature, and route contract.
|
|
1393
|
+
Then read \`.decantr/context/scaffold.md\` for the fuller app overview, topology, route map, and voice guidance.
|
|
1394
|
+
Start implementation from the shell layouts and shared route structure before filling in section pages.`
|
|
1324
1395
|
});
|
|
1325
1396
|
const briefLines = [];
|
|
1326
1397
|
briefLines.push("## Project Brief");
|
|
@@ -1328,6 +1399,7 @@ function generateDecantrMdV31(params) {
|
|
|
1328
1399
|
briefLines.push(`- **Blueprint:** ${params.blueprintId || "custom"}`);
|
|
1329
1400
|
const themeDesc = `${params.themeName || "default"} (${params.themeMode || "dark"} mode${params.themeShape ? `, ${params.themeShape} shape` : ""})`;
|
|
1330
1401
|
briefLines.push(`- **Theme:** ${themeDesc}`);
|
|
1402
|
+
briefLines.push(`- **Workflow:** ${params.workflowMode === "brownfield-attach" ? "brownfield attach" : "greenfield scaffold"}`);
|
|
1331
1403
|
if (params.personality && params.personality.length > 0) {
|
|
1332
1404
|
briefLines.push(`- **Personality:** ${params.personality.join(". ")}`);
|
|
1333
1405
|
}
|
|
@@ -1414,7 +1486,8 @@ function generateProjectJson(detected, options, registrySource) {
|
|
|
1414
1486
|
at: now,
|
|
1415
1487
|
via: "cli",
|
|
1416
1488
|
version: CLI_VERSION,
|
|
1417
|
-
flags: buildFlagsString(options)
|
|
1489
|
+
flags: buildFlagsString(options),
|
|
1490
|
+
workflowMode: options.workflowMode || "greenfield-scaffold"
|
|
1418
1491
|
}
|
|
1419
1492
|
};
|
|
1420
1493
|
if (options.blueprint) {
|
|
@@ -1854,7 +1927,8 @@ function scaffoldMinimal(projectRoot) {
|
|
|
1854
1927
|
at: now,
|
|
1855
1928
|
via: "cli",
|
|
1856
1929
|
version: CLI_VERSION,
|
|
1857
|
-
flags: "--offline --minimal"
|
|
1930
|
+
flags: "--offline --minimal",
|
|
1931
|
+
workflowMode: "greenfield-scaffold"
|
|
1858
1932
|
}
|
|
1859
1933
|
};
|
|
1860
1934
|
const projectJsonPath = join(decantrDir, "project.json");
|
|
@@ -2054,6 +2128,7 @@ async function refreshDerivedFiles(projectRoot, essence, registry, prefetchedThe
|
|
|
2054
2128
|
mkdirSync(contextDir, { recursive: true });
|
|
2055
2129
|
let storedBlueprintId;
|
|
2056
2130
|
let storedVoice;
|
|
2131
|
+
let storedWorkflowMode;
|
|
2057
2132
|
const projectJsonFilePath = join(decantrDir, "project.json");
|
|
2058
2133
|
let projectJsonData = {};
|
|
2059
2134
|
if (existsSync(projectJsonFilePath)) {
|
|
@@ -2061,6 +2136,7 @@ async function refreshDerivedFiles(projectRoot, essence, registry, prefetchedThe
|
|
|
2061
2136
|
projectJsonData = JSON.parse(readFileSync(projectJsonFilePath, "utf-8"));
|
|
2062
2137
|
if (projectJsonData.blueprintId) storedBlueprintId = projectJsonData.blueprintId;
|
|
2063
2138
|
if (projectJsonData.voice) storedVoice = projectJsonData.voice;
|
|
2139
|
+
if (projectJsonData.initialized?.workflowMode) storedWorkflowMode = projectJsonData.initialized.workflowMode;
|
|
2064
2140
|
} catch {
|
|
2065
2141
|
}
|
|
2066
2142
|
}
|
|
@@ -2141,7 +2217,8 @@ async function refreshDerivedFiles(projectRoot, essence, registry, prefetchedThe
|
|
|
2141
2217
|
spatialTokens,
|
|
2142
2218
|
themeData?.treatments,
|
|
2143
2219
|
themeData?.decorators,
|
|
2144
|
-
themeName
|
|
2220
|
+
themeName,
|
|
2221
|
+
themeData?.decorator_definitions
|
|
2145
2222
|
);
|
|
2146
2223
|
const personalityCSS = generatePersonalityCSS(personality || [], themeData || {});
|
|
2147
2224
|
treatmentCSS += personalityCSS;
|
|
@@ -2178,6 +2255,7 @@ async function refreshDerivedFiles(projectRoot, essence, registry, prefetchedThe
|
|
|
2178
2255
|
writeFileSync(decantrMdPath, generateDecantrMdV31({
|
|
2179
2256
|
guardMode,
|
|
2180
2257
|
cssApproach: CSS_APPROACH_CONTENT,
|
|
2258
|
+
workflowMode: storedWorkflowMode,
|
|
2181
2259
|
blueprintId: storedBlueprintId || getLegacyBlueprintId(essence.meta) || void 0,
|
|
2182
2260
|
themeName,
|
|
2183
2261
|
themeMode: mode,
|
|
@@ -3112,9 +3190,18 @@ function generateScaffoldContext(input) {
|
|
|
3112
3190
|
lines.push("");
|
|
3113
3191
|
if (navigation.command_palette) {
|
|
3114
3192
|
lines.push("- Command palette: enabled");
|
|
3193
|
+
lines.push("- Requirement: implement a real keyboard-triggered command palette, not just placeholder UI text.");
|
|
3115
3194
|
}
|
|
3116
3195
|
if (navigation.hotkeys && navigation.hotkeys.length > 0) {
|
|
3117
3196
|
lines.push(`- Hotkeys: ${navigation.hotkeys.length} configured`);
|
|
3197
|
+
for (const hotkey of navigation.hotkeys) {
|
|
3198
|
+
if (typeof hotkey === "object" && hotkey !== null && typeof hotkey.key === "string") {
|
|
3199
|
+
const target = [hotkey.label, hotkey.route || hotkey.action].filter(Boolean).join(" \u2014 ");
|
|
3200
|
+
lines.push(` - \`${hotkey.key}\`${target ? `: ${target}` : ""}`);
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
lines.push("- Requirement: implement these bindings as real keyboard shortcuts, not as decorative text.");
|
|
3204
|
+
lines.push("- Presentation rule: do not append hotkey text to persistent nav labels, breadcrumbs, or page titles unless the shell or route contract explicitly requests visible shortcut hints.");
|
|
3118
3205
|
}
|
|
3119
3206
|
lines.push("");
|
|
3120
3207
|
}
|