kortext 2.2.3 → 3.1.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/LICENSE +21 -0
- package/README.md +175 -65
- package/agents/architect.md +85 -0
- package/agents/compliance-expert.md +36 -75
- package/agents/copywriter.md +36 -92
- package/agents/db-admin.md +43 -91
- package/agents/designer.md +40 -77
- package/agents/devops-engineer.md +37 -113
- package/agents/growth-expert.md +45 -88
- package/agents/product-manager.md +38 -83
- package/agents/qa-engineer.md +51 -99
- package/agents/security-engineer.md +47 -92
- package/bin/kortext.js +2 -0
- package/dist/app.js +829 -0
- package/dist/cli-spawn.js +137 -0
- package/dist/daemon.js +40 -0
- package/dist/db.js +78 -0
- package/dist/design-preview.js +533 -0
- package/dist/docs.js +701 -0
- package/dist/engines.js +225 -0
- package/dist/index.js +109 -0
- package/dist/pick-directory.js +38 -0
- package/dist/projects.js +189 -0
- package/dist/readiness.js +288 -0
- package/dist/runner.js +771 -0
- package/dist/update.js +55 -0
- package/docs/CHANGELOG.md +11 -0
- package/package.json +53 -13
- package/templates/AGENTS.md +62 -0
- package/templates/docs/API.md +67 -0
- package/templates/docs/ARCHITECTURE.md +31 -0
- package/templates/docs/BRIEF.md +27 -0
- package/templates/docs/CONTENT.md +66 -0
- package/templates/docs/DATABASE.md +63 -0
- package/templates/docs/DESIGN.md +124 -0
- package/templates/docs/ENGINEERING.md +66 -0
- package/templates/docs/ENVIRONMENT.md +102 -0
- package/templates/docs/GROWTH.md +44 -0
- package/templates/docs/LEGAL.md +48 -0
- package/templates/docs/PRODUCT.md +43 -0
- package/templates/docs/SECURITY.md +65 -0
- package/templates/docs/STACK.md +57 -0
- package/templates/docs/STRUCTURE.md +73 -0
- package/templates/docs/TEST.md +62 -0
- package/ui/dist/assets/abnfDiagram-VCTEODGH-Bm7Q2BML.js +1 -0
- package/ui/dist/assets/arc-BV9hs0Hu.js +1 -0
- package/ui/dist/assets/architecture-7GRP2DOG-RckR3gBI.js +1 -0
- package/ui/dist/assets/architectureDiagram-5GKGNRK7-CHiJ6JFT.js +36 -0
- package/ui/dist/assets/array-BifhSqXX.js +1 -0
- package/ui/dist/assets/blockDiagram-I7D4REHJ-qCbaKVhd.js +129 -0
- package/ui/dist/assets/c4Diagram-7LVT6UL2-DidCDJrv.js +38 -0
- package/ui/dist/assets/channel-CNUQ11ne.js +1 -0
- package/ui/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/ui/dist/assets/chunk-4HAMMTFA-CDGFjfVp.js +62 -0
- package/ui/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/ui/dist/assets/chunk-75Z2AOVW-CbNT4sDh.js +2 -0
- package/ui/dist/assets/chunk-DU6HZSFF-Cq2BVv26.js +127 -0
- package/ui/dist/assets/chunk-F27PBJKO-CE365m0v.js +1 -0
- package/ui/dist/assets/chunk-FOHPRMQF-DHwB1DNv.js +161 -0
- package/ui/dist/assets/chunk-GMAD6QVW-DxWL1VC-.js +72 -0
- package/ui/dist/assets/chunk-GVQU2GXP-aARV8YbK.js +1 -0
- package/ui/dist/assets/chunk-IMKFNOWR-CC790Kt2.js +231 -0
- package/ui/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/ui/dist/assets/chunk-L3NEJ4N5-BfE4VE8J.js +1 -0
- package/ui/dist/assets/chunk-OSK3NFVY-BImQbSWH.js +10 -0
- package/ui/dist/assets/chunk-P2QGCYS3-CN3XD313.js +1 -0
- package/ui/dist/assets/chunk-POPQ4Y6H-Dqb6jOrX.js +1 -0
- package/ui/dist/assets/chunk-PWAF6VOD-DfXX6rBk.js +1 -0
- package/ui/dist/assets/chunk-SHT3W25Y-DATyyqPK.js +168 -0
- package/ui/dist/assets/chunk-SVP7TREG-r1chwRmg.js +88 -0
- package/ui/dist/assets/chunk-TICWLB2K-DL4atHUC.js +206 -0
- package/ui/dist/assets/chunk-XXDRQBXY-DXrM6jh7.js +1 -0
- package/ui/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/ui/dist/assets/classDiagram-ZZMXUADV-BpQxfIQ7.js +1 -0
- package/ui/dist/assets/classDiagram-v2-VYDZK3BY-BpQxfIQ7.js +1 -0
- package/ui/dist/assets/cose-bilkent-JH36ORCC-Bhe-ZBlS.js +1 -0
- package/ui/dist/assets/cynefin-OW5HDTMX-D1koLd-G.js +1 -0
- package/ui/dist/assets/cynefinDiagram-5FMLGOSQ-BMcw40hV.js +62 -0
- package/ui/dist/assets/cytoscape.esm-CGd-uY3x.js +321 -0
- package/ui/dist/assets/dagre-GXQ25YYZ-DBun-Rs2.js +4 -0
- package/ui/dist/assets/dagre-PrKaheQc.js +1 -0
- package/ui/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/ui/dist/assets/diagram-S7CK7UJ4-C1fUIGvo.js +30 -0
- package/ui/dist/assets/diagram-UQ7AKVKN-B5FDfkvB.js +41 -0
- package/ui/dist/assets/diagram-VSXAHHWV-CCIpOCNC.js +3 -0
- package/ui/dist/assets/diagram-VX7I27RA-DV0q2xo_.js +24 -0
- package/ui/dist/assets/diagram-Z3DM3KII-B3LCdNgI.js +24 -0
- package/ui/dist/assets/dist-EcW76Zvr.js +1 -0
- package/ui/dist/assets/ebnfDiagram-PWID7BFC-De_oRbYe.js +1 -0
- package/ui/dist/assets/erDiagram-RLTQ6QDP-B9NikMr1.js +99 -0
- package/ui/dist/assets/eventmodeling-NTZA5JFV-DZ4t581k.js +1 -0
- package/ui/dist/assets/flowDiagram-HODETNUW-DuTOJp1C.js +1 -0
- package/ui/dist/assets/ganttDiagram-EL5Y4UJY-BVTqqlPw.js +292 -0
- package/ui/dist/assets/gitGraph-4MIJSDKK-xRFaEJdZ.js +1 -0
- package/ui/dist/assets/gitGraphDiagram-WWUBYQGX-CK5_GCmX.js +106 -0
- package/ui/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/ui/dist/assets/index-CjkpNftW.js +46 -0
- package/ui/dist/assets/index-DVIDTWin.css +1 -0
- package/ui/dist/assets/info-A6RAGUB7-Dg7z-CTI.js +1 -0
- package/ui/dist/assets/infoDiagram-27XIBGKW-YaqsOXh0.js +2 -0
- package/ui/dist/assets/init-C-OQMol4.js +1 -0
- package/ui/dist/assets/ishikawaDiagram-5VMMS53U-C-wtxZPa.js +70 -0
- package/ui/dist/assets/journeyDiagram-3NMN7TZE-DsXSXf16.js +139 -0
- package/ui/dist/assets/kanban-definition-UXKFOSKX-Ct59fuWk.js +89 -0
- package/ui/dist/assets/katex-CXMH3UgJ.js +257 -0
- package/ui/dist/assets/line-BUVw9Mhs.js +1 -0
- package/ui/dist/assets/linear-D8ncLO0X.js +1 -0
- package/ui/dist/assets/mermaid-parser.core-BDC4bG-O.js +7 -0
- package/ui/dist/assets/mermaid.core-DZY3WwzX.js +44 -0
- package/ui/dist/assets/mindmap-definition-YA3MSWOX-DtiuF6Ce.js +96 -0
- package/ui/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/ui/dist/assets/packet-AYTQ26CC-BX2-w9rD.js +1 -0
- package/ui/dist/assets/path-fybaL0A-.js +1 -0
- package/ui/dist/assets/pegDiagram-XKGWAZYB-CotwuIHB.js +1 -0
- package/ui/dist/assets/pie-WAS4IAKB-Hth8ZVpm.js +1 -0
- package/ui/dist/assets/pieDiagram-E7YTZNPT-D9-INs8B.js +39 -0
- package/ui/dist/assets/quadrantDiagram-AXDQQJYC-CMtBXZGV.js +7 -0
- package/ui/dist/assets/radar-RG4KPBEZ-6UuwQPsw.js +1 -0
- package/ui/dist/assets/railroad-74A4TZTK-BF5TZgj0.js +1 -0
- package/ui/dist/assets/railroad-abnf-HS5TGJTU-3KYovtYT.js +1 -0
- package/ui/dist/assets/railroad-ebnf-LZEXJU2U-DeP4WedE.js +1 -0
- package/ui/dist/assets/railroad-peg-WCYAUIDC-i42N1-yk.js +1 -0
- package/ui/dist/assets/railroadDiagram-O6MQD6OU-BS6zkU3Z.js +1 -0
- package/ui/dist/assets/requirementDiagram-BXWQKSXE-J4IOB-kP.js +84 -0
- package/ui/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/ui/dist/assets/sankeyDiagram-P5KCCOFB-jYTCuYzQ.js +40 -0
- package/ui/dist/assets/sequenceDiagram-WJ2MYXX4-Cd2r6PW-.js +162 -0
- package/ui/dist/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
- package/ui/dist/assets/src-7TsfZXRO.js +1 -0
- package/ui/dist/assets/stateDiagram-D77RDMKH-DXOdtOKe.js +1 -0
- package/ui/dist/assets/stateDiagram-v2-MP3YSRHH-9h7sIf3R.js +1 -0
- package/ui/dist/assets/swimlanes-42K2YHIH-BvJGmGs1.js +1 -0
- package/ui/dist/assets/swimlanesDiagram-VR7AAH4N-QA4gPi6a.js +8 -0
- package/ui/dist/assets/timeline-definition-24CTP7MA-CAaD6-yd.js +120 -0
- package/ui/dist/assets/treeView-Q6P3EWNA-4gDEXIyJ.js +1 -0
- package/ui/dist/assets/treemap-WGGIJYW6-CGel5TwW.js +1 -0
- package/ui/dist/assets/vennDiagram-4TSXK5OY-DpxsKboL.js +34 -0
- package/ui/dist/assets/wardley-WFR3VGLG-Ck38v5ns.js +1 -0
- package/ui/dist/assets/wardleyDiagram-VM6X3IG4-Ba0grrqs.js +78 -0
- package/ui/dist/assets/xychartDiagram-S5SC5T6Z-DH_gC96K.js +7 -0
- package/ui/dist/index.html +13 -0
- package/ui/dist/kortext-logo-dark.svg +41 -0
- package/ui/dist/kortext-logo.svg +41 -0
- package/workflows/existing-project-analysis.md +87 -0
- package/workflows/new-project-analysis.md +98 -0
- package/workflows/planning-pipeline.md +61 -0
- package/AGENTS.md +0 -28
- package/agents/backend-developer.md +0 -132
- package/agents/delivery-manager.md +0 -121
- package/agents/engineering-manager.md +0 -208
- package/agents/frontend-developer.md +0 -174
- package/agents/operation-manager.md +0 -122
- package/hooks/audit-logger.sh +0 -25
- package/hooks/auto-locker.sh +0 -74
- package/hooks/auto-unlocker.sh +0 -17
- package/hooks/backlog-sync-guard.sh +0 -12
- package/hooks/branch-guard.sh +0 -32
- package/hooks/commit-msg-guard.sh +0 -43
- package/hooks/git-pre-commit.sh +0 -76
- package/hooks/git-pre-push.sh +0 -17
- package/hooks/handover-guard.sh +0 -33
- package/hooks/kortext-init.sh +0 -194
- package/hooks/kortext-lib.sh +0 -151
- package/hooks/lint-guard.sh +0 -50
- package/hooks/secret-scanner.sh +0 -89
- package/hooks/size-guard.sh +0 -48
- package/hooks/snapshot-guard.sh +0 -39
- package/hooks/write-guard.sh +0 -77
- package/rules/behavior.md +0 -252
- package/rules/branching.md +0 -82
- package/rules/commands.md +0 -112
- package/rules/emergency.md +0 -160
- package/rules/mcp.md +0 -47
- package/rules/models.md +0 -93
- package/scripts/kortext-backlog-add.py +0 -123
- package/scripts/kortext-backlog-health.py +0 -112
- package/scripts/kortext-backlog-sync.py +0 -117
- package/scripts/kortext-bulk-plan.py +0 -63
- package/scripts/kortext-cli.py +0 -376
- package/scripts/kortext-consistency-check.py +0 -74
- package/scripts/kortext-context-check.py +0 -146
- package/scripts/kortext-handover.py +0 -107
- package/scripts/kortext-item-check.py +0 -61
- package/scripts/kortext-item-start.py +0 -70
- package/scripts/kortext-item-transition.py +0 -78
- package/scripts/kortext-lock.py +0 -174
- package/scripts/kortext-session-start.py +0 -127
- package/scripts/lock_kortext.sh +0 -34
- package/settings/.claude-settings.template.json +0 -48
- package/settings/CHANGELOG.md +0 -176
- package/settings/INTEGRATION-MAP.md +0 -276
- package/settings/README.md +0 -255
- package/settings/USER-GUIDE.md +0 -502
- package/settings/VERSION +0 -1
- package/settings/config.md +0 -8
- package/settings/runtime-adapters.md +0 -104
- package/skills/backend-developer/.gitkeep +0 -0
- package/skills/compliance-expert/.gitkeep +0 -0
- package/skills/copywriter/.gitkeep +0 -0
- package/skills/db-admin/.gitkeep +0 -0
- package/skills/delivery-manager/.gitkeep +0 -0
- package/skills/designer/.gitkeep +0 -0
- package/skills/devops-engineer/.gitkeep +0 -0
- package/skills/engineering-manager/.gitkeep +0 -0
- package/skills/frontend-developer/.gitkeep +0 -0
- package/skills/growth-expert/.gitkeep +0 -0
- package/skills/operation-manager/.gitkeep +0 -0
- package/skills/product-manager/.gitkeep +0 -0
- package/skills/qa-engineer/.gitkeep +0 -0
- package/skills/security-engineer/.gitkeep +0 -0
- package/workflows/00-kortext-setup.md +0 -53
- package/workflows/01a-analysis-pipeline.md +0 -117
- package/workflows/01b-onboarding-pipeline.md +0 -110
- package/workflows/02-planning-pipeline.md +0 -146
- package/workflows/02b-spike-workflow.md +0 -120
- package/workflows/03-environment-setup.md +0 -99
- package/workflows/04-development-cycle.md +0 -203
- package/workflows/05-test-cycle.md +0 -123
- package/workflows/06-deployment-cycle.md +0 -224
- package/workflows/07-rollback-pipeline.md +0 -203
- package/workflows/08-hotfix-pipeline.md +0 -196
- package/workflows/09-maintenance-cycle.md +0 -133
- package/workspace/archive/.gitkeep +0 -0
- package/workspace/backups/.gitkeep +0 -0
- package/workspace/memory/backlog/.gitkeep +0 -0
- package/workspace/memory/backlog/BXX-[bug-name].md +0 -2
- package/workspace/memory/backlog/DXX-[debt-name].md +0 -2
- package/workspace/memory/backlog/README.md +0 -31
- package/workspace/memory/backlog/TXX-[task-name].md +0 -2
- package/workspace/memory/backlog/debt-dashboard.md +0 -3
- package/workspace/memory/backlog/epic-dashboard.md +0 -10
- package/workspace/memory/backlog/version-dashboard.md +0 -7
- package/workspace/memory/context/.gitkeep +0 -0
- package/workspace/memory/context/README.md +0 -35
- package/workspace/memory/context/[agent-name]-active.md +0 -2
- package/workspace/memory/decisions.md +0 -17
- package/workspace/memory/handover.md +0 -40
- package/workspace/memory/learned.md +0 -21
- package/workspace/references/access.md +0 -47
- package/workspace/references/api-reference.md +0 -59
- package/workspace/references/blueprint.md +0 -29
- package/workspace/references/content-strategy.md +0 -39
- package/workspace/references/db-schema.md +0 -51
- package/workspace/references/design-system.md +0 -107
- package/workspace/references/dictionary.md +0 -42
- package/workspace/references/file-system.md +0 -42
- package/workspace/references/growth-strategy.md +0 -46
- package/workspace/references/legal-strategy.md +0 -35
- package/workspace/references/required-skills.md +0 -11
- package/workspace/references/security-rules.md +0 -34
- package/workspace/references/tech-stack.md +0 -49
- package/workspace/references/test-strategy.md +0 -48
- package/workspace/reports/analysis-reports.md +0 -40
- package/workspace/reports/content-reports.md +0 -29
- package/workspace/reports/delivery-reports.md +0 -26
- package/workspace/reports/growth-reports.md +0 -9
- package/workspace/reports/legal-reports.md +0 -29
- package/workspace/reports/product-requirements.md +0 -28
- package/workspace/reports/release-notes.md +0 -20
- package/workspace/reports/security-reports.md +0 -27
- package/workspace/reports/status-reports.md +0 -48
- package/workspace/reports/tech-requirements.md +0 -30
- package/workspace/reports/test-reports.md +0 -61
- package/workspace/templates/BXX-[bug-name].md +0 -34
- package/workspace/templates/DXX-[debt-name].md +0 -35
- package/workspace/templates/EXX-[epic-name].md +0 -48
- package/workspace/templates/HXX-[hotfix-name].md +0 -73
- package/workspace/templates/SXX-[spike-name].md +0 -51
- package/workspace/templates/TXX-[task-name].md +0 -34
- package/workspace/templates/[agent-name]-active.md +0 -4
- package/workspace/templates/[original-name]_[YYYY-MM-DD_HHMMSS].md +0 -5
- package/workspace/templates/adr-XXX.md +0 -66
- package/workspace/templates/debt-dashboard.md +0 -10
- package/workspace/templates/epic-dashboard.md +0 -10
- package/workspace/templates/version-dashboard.md +0 -10
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
// Render a preview from the tokens declared in DESIGN.md without inventing missing values.
|
|
2
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { readFrontmatter } from './docs.js';
|
|
5
|
+
const COLOR = /^(#[0-9a-f]{3,8}|rgba?\([^)]*\)|hsla?\([^)]*\)|oklch\([^)]*\)|oklab\([^)]*\)|color-mix\([^)]*\))$/i;
|
|
6
|
+
const LENGTH = /^-?\d*\.?\d+(px|rem|em|%|vh|vw|ch)$/i;
|
|
7
|
+
// Validate agent-written values before placing them in style attributes.
|
|
8
|
+
const CSS_SAFE = /^[#\w%.,()\-+\s/'"]*$/;
|
|
9
|
+
const unwrap = (s) => s.trim().replace(/^`|`$/g, '').trim();
|
|
10
|
+
function safe(value) {
|
|
11
|
+
const v = unwrap(value);
|
|
12
|
+
return v && v.length < 200 && CSS_SAFE.test(v) ? v : null;
|
|
13
|
+
}
|
|
14
|
+
function esc(s) {
|
|
15
|
+
return s
|
|
16
|
+
.replace(/&/g, '&')
|
|
17
|
+
.replace(/</g, '<')
|
|
18
|
+
.replace(/>/g, '>')
|
|
19
|
+
.replace(/"/g, '"');
|
|
20
|
+
}
|
|
21
|
+
function cells(line) {
|
|
22
|
+
return line
|
|
23
|
+
.trim()
|
|
24
|
+
.replace(/^\|/, '')
|
|
25
|
+
.replace(/\|$/, '')
|
|
26
|
+
.split('|')
|
|
27
|
+
.map((c) => unwrap(c));
|
|
28
|
+
}
|
|
29
|
+
const isSeparator = (row) => row.every((c) => /^:?-{2,}:?$/.test(c));
|
|
30
|
+
// Ignore unresolved template placeholders.
|
|
31
|
+
const decided = (v) => v !== '' && !/^\[.*\]$/.test(v);
|
|
32
|
+
function bucket(tokens, t) {
|
|
33
|
+
const n = t.name.toLowerCase();
|
|
34
|
+
if (COLOR.test(t.value))
|
|
35
|
+
tokens.colors.push(t);
|
|
36
|
+
else if (/shadow|elevation/.test(n))
|
|
37
|
+
tokens.shadows.push(t);
|
|
38
|
+
else if (/transition|duration|ease|motion/.test(n))
|
|
39
|
+
tokens.motion.push(t);
|
|
40
|
+
else if (/font|family/.test(n))
|
|
41
|
+
tokens.fonts.push(t);
|
|
42
|
+
else if (/radius|rounded/.test(n))
|
|
43
|
+
tokens.radius.push(t);
|
|
44
|
+
else if (/space|gap|gutter/.test(n) && LENGTH.test(t.value))
|
|
45
|
+
tokens.spacing.push(t);
|
|
46
|
+
else if (/container|screen|breakpoint|safe|max|width/.test(n) && LENGTH.test(t.value))
|
|
47
|
+
tokens.layout.push(t);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Reads token declarations wherever they sit: table rows (`| --color-primary |
|
|
51
|
+
* #111 | buttons |`) and bullets (`- --space-md: 16px`). Rows the designer left
|
|
52
|
+
* as prose contribute nothing rather than a broken swatch.
|
|
53
|
+
*/
|
|
54
|
+
export function parseDesignTokens(md) {
|
|
55
|
+
const tokens = {
|
|
56
|
+
colors: [],
|
|
57
|
+
spacing: [],
|
|
58
|
+
radius: [],
|
|
59
|
+
shadows: [],
|
|
60
|
+
motion: [],
|
|
61
|
+
layout: [],
|
|
62
|
+
fonts: [],
|
|
63
|
+
type: [],
|
|
64
|
+
};
|
|
65
|
+
const seen = new Map();
|
|
66
|
+
let header = null;
|
|
67
|
+
// A dark palette is declared one of three ways, and all three occur: a second
|
|
68
|
+
// value column, a heading that says dark, or a `-dark` suffix on the name.
|
|
69
|
+
let darkColumn = null;
|
|
70
|
+
let darkSection = false;
|
|
71
|
+
// Merge dark-mode declarations into the corresponding token, regardless of declaration format.
|
|
72
|
+
const attach = (name, value, note, dark) => {
|
|
73
|
+
const base = name
|
|
74
|
+
.replace(/-?dark-?/, '-')
|
|
75
|
+
.replace(/--+$/, '')
|
|
76
|
+
.replace(/-$/, '');
|
|
77
|
+
const restates = darkSection
|
|
78
|
+
? (seen.get(name) ?? seen.get(base))
|
|
79
|
+
: base !== name
|
|
80
|
+
? seen.get(base)
|
|
81
|
+
: undefined;
|
|
82
|
+
if (restates) {
|
|
83
|
+
restates.dark = value;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (seen.has(name))
|
|
87
|
+
return;
|
|
88
|
+
const token = dark ? { name, value, note, dark } : { name, value, note };
|
|
89
|
+
seen.set(name, token);
|
|
90
|
+
bucket(tokens, token);
|
|
91
|
+
};
|
|
92
|
+
for (const line of md.split('\n')) {
|
|
93
|
+
const heading = line.match(/^#{1,6}\s+(.*)$/);
|
|
94
|
+
if (heading)
|
|
95
|
+
darkSection = /\bdark\b/i.test(heading[1] ?? '');
|
|
96
|
+
if (/^\s*\|/.test(line)) {
|
|
97
|
+
const row = cells(line);
|
|
98
|
+
if (isSeparator(row))
|
|
99
|
+
continue;
|
|
100
|
+
const lower = row.map((c) => c.toLowerCase());
|
|
101
|
+
if (lower.some((c) => /size/.test(c)) && lower.some((c) => /line-?height|weight/.test(c))) {
|
|
102
|
+
header = lower;
|
|
103
|
+
darkColumn = null;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
// `| Token | Light | Dark | Usage |` — the header is the only place that
|
|
107
|
+
// says which column is which, so it is read before the rows.
|
|
108
|
+
if (lower.some((c) => /\bdark\b/.test(c)) && lower.some((c) => /token|name|light/.test(c))) {
|
|
109
|
+
darkColumn = lower.findIndex((c) => /\bdark\b/.test(c));
|
|
110
|
+
header = null;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (header) {
|
|
114
|
+
const at = (re) => {
|
|
115
|
+
const i = header.findIndex((h) => re.test(h));
|
|
116
|
+
return i === -1 ? '' : (row[i] ?? '');
|
|
117
|
+
};
|
|
118
|
+
const role = row[0] ?? '';
|
|
119
|
+
if (decided(role)) {
|
|
120
|
+
tokens.type.push({
|
|
121
|
+
role,
|
|
122
|
+
family: at(/family|font/),
|
|
123
|
+
size: at(/size/),
|
|
124
|
+
lineHeight: at(/line-?height/),
|
|
125
|
+
weight: at(/weight/),
|
|
126
|
+
tracking: at(/letter|tracking/),
|
|
127
|
+
});
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
header = null;
|
|
131
|
+
}
|
|
132
|
+
// A token row: the name cell, then the first cell holding a real value.
|
|
133
|
+
const name = row[0] ?? '';
|
|
134
|
+
if (!/^--?[\w-]+$/.test(name))
|
|
135
|
+
continue;
|
|
136
|
+
const usable = (c, i) => i !== darkColumn && decided(c) && (COLOR.test(c) || LENGTH.test(c));
|
|
137
|
+
const value = row.slice(1).find((c, i) => usable(c, i + 1));
|
|
138
|
+
if (!value)
|
|
139
|
+
continue;
|
|
140
|
+
const darkCell = darkColumn === null ? '' : (row[darkColumn] ?? '');
|
|
141
|
+
const dark = decided(darkCell) && COLOR.test(darkCell) ? darkCell : undefined;
|
|
142
|
+
const note = row[row.length - 1] === value ? '' : (row[row.length - 1] ?? '');
|
|
143
|
+
if (darkSection && !dark)
|
|
144
|
+
attach(name, value, note);
|
|
145
|
+
else
|
|
146
|
+
attach(name, value, note, dark);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
header = null;
|
|
150
|
+
darkColumn = null;
|
|
151
|
+
const m = line.match(/^\s*[-*+]\s+`?(--[\w-]+)`?\s*:\s*(.+)$/);
|
|
152
|
+
if (!m)
|
|
153
|
+
continue;
|
|
154
|
+
const name = m[1];
|
|
155
|
+
const rest = m[2];
|
|
156
|
+
const value = unwrap(rest.split(/\s{2,}|\s+\(|\s+—|\s+#\s/)[0] ?? '');
|
|
157
|
+
if (!decided(value))
|
|
158
|
+
continue;
|
|
159
|
+
const note = rest.slice(rest.indexOf(value) + value.length).replace(/^[\s(]+|[)\s]+$/g, '');
|
|
160
|
+
attach(name, value, note);
|
|
161
|
+
}
|
|
162
|
+
return tokens;
|
|
163
|
+
}
|
|
164
|
+
// --- contrast -------------------------------------------------------------
|
|
165
|
+
// Calculate contrast only for hex colors; unsupported formats receive no ratio.
|
|
166
|
+
function luminance(hex) {
|
|
167
|
+
const h = hex.replace('#', '');
|
|
168
|
+
const full = h.length === 3
|
|
169
|
+
? h
|
|
170
|
+
.split('')
|
|
171
|
+
.map((c) => c + c)
|
|
172
|
+
.join('')
|
|
173
|
+
: h.slice(0, 6);
|
|
174
|
+
if (!/^[0-9a-f]{6}$/i.test(full))
|
|
175
|
+
return null;
|
|
176
|
+
const channel = (v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
|
|
177
|
+
const [r, g, b] = [0, 2, 4].map((i) => channel(parseInt(full.slice(i, i + 2), 16) / 255));
|
|
178
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
179
|
+
}
|
|
180
|
+
export function contrast(a, b) {
|
|
181
|
+
const la = luminance(a);
|
|
182
|
+
const lb = luminance(b);
|
|
183
|
+
if (la === null || lb === null)
|
|
184
|
+
return null;
|
|
185
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
186
|
+
}
|
|
187
|
+
// --- rendering ------------------------------------------------------------
|
|
188
|
+
// Every color reaches the page as a variable, never as a literal, so one
|
|
189
|
+
// attribute on <html> repaints swatches, buttons and chrome together.
|
|
190
|
+
function varName(index) {
|
|
191
|
+
return `--kx-c${index}`;
|
|
192
|
+
}
|
|
193
|
+
// What a label on this color would actually be set in. On a light-blue button
|
|
194
|
+
// in dark mode that is the page's own near-black, not white and not the pale
|
|
195
|
+
// body text — grading it against those two reported a failure the design does
|
|
196
|
+
// not have.
|
|
197
|
+
function inks(theme) {
|
|
198
|
+
return [theme.fg, '#ffffff', theme.bg, '#111111'];
|
|
199
|
+
}
|
|
200
|
+
function bestInk(color, theme) {
|
|
201
|
+
let best = null;
|
|
202
|
+
for (const ink of inks(theme)) {
|
|
203
|
+
const ratio = contrast(color, ink);
|
|
204
|
+
if (ratio !== null && (!best || ratio > best.ratio))
|
|
205
|
+
best = { ink, ratio };
|
|
206
|
+
}
|
|
207
|
+
return best;
|
|
208
|
+
}
|
|
209
|
+
function readable(color, theme) {
|
|
210
|
+
return bestInk(color, theme)?.ink ?? theme.fg;
|
|
211
|
+
}
|
|
212
|
+
function palette(colors, light, dark) {
|
|
213
|
+
const painted = [];
|
|
214
|
+
const lightVars = [];
|
|
215
|
+
const darkVars = [];
|
|
216
|
+
colors.forEach((token, i) => {
|
|
217
|
+
const value = safe(token.value);
|
|
218
|
+
if (!value)
|
|
219
|
+
return;
|
|
220
|
+
const darkValue = token.dark ? (safe(token.dark) ?? value) : value;
|
|
221
|
+
lightVars.push(`${varName(i)}:${value};${varName(i)}-on:${readable(token.value, light)}`);
|
|
222
|
+
darkVars.push(`${varName(i)}:${darkValue};${varName(i)}-on:${readable(token.dark ?? token.value, dark)}`);
|
|
223
|
+
painted.push({ token, ref: `var(${varName(i)})`, on: `var(${varName(i)}-on)` });
|
|
224
|
+
});
|
|
225
|
+
return { painted, lightVars, darkVars };
|
|
226
|
+
}
|
|
227
|
+
// A badge only where a threshold actually applies. Measuring a background
|
|
228
|
+
// against itself produced a red 1.00:1 that meant nothing — a failing grade on
|
|
229
|
+
// a token that was never text.
|
|
230
|
+
function verdict(token, value, theme) {
|
|
231
|
+
const n = token.name.toLowerCase();
|
|
232
|
+
if (/text|fg|foreground/.test(n)) {
|
|
233
|
+
const ratio = contrast(value, theme.bg);
|
|
234
|
+
return ratio === null ? null : { ratio, label: 'on background', min: 4.5 };
|
|
235
|
+
}
|
|
236
|
+
if (/border|divider|outline/.test(n)) {
|
|
237
|
+
const ratio = contrast(value, theme.bg);
|
|
238
|
+
return ratio === null ? null : { ratio, label: 'on background', min: 3 };
|
|
239
|
+
}
|
|
240
|
+
if (/bg|background|surface|card/.test(n)) {
|
|
241
|
+
const ratio = contrast(value, theme.fg);
|
|
242
|
+
return ratio === null ? null : { ratio, label: 'text on it', min: 4.5 };
|
|
243
|
+
}
|
|
244
|
+
// Primary, semantic and accent colors carry a label — grade the text an
|
|
245
|
+
// implementation would actually put on them, light or dark.
|
|
246
|
+
const best = bestInk(value, theme);
|
|
247
|
+
if (!best)
|
|
248
|
+
return null;
|
|
249
|
+
const light = (contrast(best.ink, '#000000') ?? 0) > 8;
|
|
250
|
+
return { ratio: best.ratio, label: `${light ? 'light' : 'dark'} text on it`, min: 4.5 };
|
|
251
|
+
}
|
|
252
|
+
function badge(v, cls) {
|
|
253
|
+
if (!v)
|
|
254
|
+
return '';
|
|
255
|
+
const grade = v.ratio >= v.min ? 'ok' : v.ratio >= v.min - 1.5 ? 'warn' : 'bad';
|
|
256
|
+
return `<span class="badge ${grade} ${cls}">${v.ratio.toFixed(2)}:1 ${esc(v.label)}</span>`;
|
|
257
|
+
}
|
|
258
|
+
function colorSection(painted, light, dark) {
|
|
259
|
+
if (painted.length === 0)
|
|
260
|
+
return '';
|
|
261
|
+
const cards = painted
|
|
262
|
+
.map(({ token, ref }) => {
|
|
263
|
+
const darkValue = token.dark ?? token.value;
|
|
264
|
+
return `<div class="tok">
|
|
265
|
+
<div class="chip" style="background:${ref}"></div>
|
|
266
|
+
<div class="tk">
|
|
267
|
+
<b>${esc(token.name)}</b>
|
|
268
|
+
<code class="only-light">${esc(token.value)}</code>
|
|
269
|
+
<code class="only-dark">${esc(darkValue)}${token.dark ? '' : ' <span class="same">(same in dark)</span>'}</code>
|
|
270
|
+
${token.note ? `<span class="note">${esc(token.note)}</span>` : ''}
|
|
271
|
+
${badge(verdict(token, token.value, light), 'only-light')}
|
|
272
|
+
${badge(verdict(token, darkValue, dark), 'only-dark')}
|
|
273
|
+
</div>
|
|
274
|
+
</div>`;
|
|
275
|
+
})
|
|
276
|
+
.join('');
|
|
277
|
+
return `<section><h2>Color</h2>
|
|
278
|
+
<p class="desc">Each token is graded against the contrast that actually applies to it: text against the background (WCAG AA wants 4.5:1), borders against the background (3:1), and label colors against the text that sits on them. Switch the mode above to grade the dark palette.</p>
|
|
279
|
+
<div class="tokrow">${cards}</div></section>`;
|
|
280
|
+
}
|
|
281
|
+
function typeSection(type, fonts) {
|
|
282
|
+
if (type.length === 0)
|
|
283
|
+
return '';
|
|
284
|
+
const fallback = fonts.map((f) => safe(f.value)).find(Boolean) ?? 'system-ui, sans-serif';
|
|
285
|
+
const rows = type
|
|
286
|
+
.map((r) => {
|
|
287
|
+
const family = safe(r.family) ?? fallback;
|
|
288
|
+
const size = safe(r.size) ?? '16px';
|
|
289
|
+
const lh = safe(r.lineHeight) ?? '1.5';
|
|
290
|
+
const weight = safe(r.weight) ?? '400';
|
|
291
|
+
const tracking = safe(r.tracking) ?? 'normal';
|
|
292
|
+
const style = `font-family:${esc(family)};font-size:${esc(size)};line-height:${esc(lh)};font-weight:${esc(weight)};letter-spacing:${esc(tracking)}`;
|
|
293
|
+
return `<div class="spec">
|
|
294
|
+
<div class="tag">${esc(r.role)}<span>${esc([r.size, r.weight, r.lineHeight].filter(Boolean).join(' · '))}</span></div>
|
|
295
|
+
<div class="sample" style="${style}">Grumpy wizards make toxic brew — 0123</div>
|
|
296
|
+
</div>`;
|
|
297
|
+
})
|
|
298
|
+
.join('');
|
|
299
|
+
return `<section><h2>Typography</h2><div class="surface">${rows}</div></section>`;
|
|
300
|
+
}
|
|
301
|
+
// 'bar' draws the value at size — honest for a spacing scale, meaningless for
|
|
302
|
+
// a 1200px container, which is why layout is listed rather than drawn.
|
|
303
|
+
function scaleSection(title, tokens, kind) {
|
|
304
|
+
if (tokens.length === 0)
|
|
305
|
+
return '';
|
|
306
|
+
const items = tokens
|
|
307
|
+
.map((t) => {
|
|
308
|
+
const value = safe(t.value);
|
|
309
|
+
if (!value)
|
|
310
|
+
return '';
|
|
311
|
+
const demo = kind === 'bar'
|
|
312
|
+
? `<div class="bar" style="width:${esc(value)}"></div>`
|
|
313
|
+
: kind === 'box'
|
|
314
|
+
? `<div class="box" style="border-radius:${esc(value)}"></div>`
|
|
315
|
+
: `<span class="note">${esc(t.note)}</span>`;
|
|
316
|
+
return `<div class="scale-row"><code>${esc(t.name)}</code><span class="val">${esc(t.value)}</span>${demo}</div>`;
|
|
317
|
+
})
|
|
318
|
+
.join('');
|
|
319
|
+
return `<section><h2>${esc(title)}</h2><div class="surface">${items}</div></section>`;
|
|
320
|
+
}
|
|
321
|
+
function shadowSection(shadows) {
|
|
322
|
+
if (shadows.length === 0)
|
|
323
|
+
return '';
|
|
324
|
+
const items = shadows
|
|
325
|
+
.map((t) => {
|
|
326
|
+
const value = safe(t.value);
|
|
327
|
+
if (!value)
|
|
328
|
+
return '';
|
|
329
|
+
return `<div class="shadow-cell"><div class="shadow-box" style="box-shadow:${esc(value)}"></div><code>${esc(t.name)}</code></div>`;
|
|
330
|
+
})
|
|
331
|
+
.join('');
|
|
332
|
+
return `<section><h2>Elevation</h2><div class="surface grid">${items}</div></section>`;
|
|
333
|
+
}
|
|
334
|
+
function paintedPick(painted, ...res) {
|
|
335
|
+
for (const re of res) {
|
|
336
|
+
const hit = painted.find((p) => re.test(p.token.name.toLowerCase()));
|
|
337
|
+
if (hit)
|
|
338
|
+
return hit;
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
function componentSection(painted, radius) {
|
|
343
|
+
const primary = paintedPick(painted, /primary(?!-hover)/, /accent/, /brand/);
|
|
344
|
+
if (!primary)
|
|
345
|
+
return '';
|
|
346
|
+
const hover = paintedPick(painted, /primary-hover|accent-hover/) ?? primary;
|
|
347
|
+
const border = paintedPick(painted, /border/);
|
|
348
|
+
const error = paintedPick(painted, /error|danger|destructive/);
|
|
349
|
+
const success = paintedPick(painted, /success/);
|
|
350
|
+
const surface = paintedPick(painted, /surface|card/);
|
|
351
|
+
const r = radius.map((t) => safe(t.value)).find(Boolean) ?? '8px';
|
|
352
|
+
const line = border?.ref ?? 'var(--line)';
|
|
353
|
+
const bad = error?.ref ?? '#dc2626';
|
|
354
|
+
const good = success?.ref ?? '#16a34a';
|
|
355
|
+
const b = (label, extra) => `<div class="demo"><button style="background:${primary.ref};color:${primary.on};border-radius:${esc(r)};${extra}">Primary action</button><span>${esc(label)}</span></div>`;
|
|
356
|
+
return `<section><h2>Components</h2>
|
|
357
|
+
<p class="desc">Built from the tokens above — a state that looks wrong here is a token that is wrong in the document.</p>
|
|
358
|
+
<div class="surface"${surface ? ` style="background:${surface.ref}"` : ''}>
|
|
359
|
+
<div class="grid">
|
|
360
|
+
${b('default', '')}
|
|
361
|
+
${b('hover', `background:${hover.ref}`)}
|
|
362
|
+
${b('active', 'transform:scale(0.98)')}
|
|
363
|
+
${b('disabled', 'opacity:0.5;cursor:not-allowed')}
|
|
364
|
+
${b('focus', `outline:2px solid ${primary.ref};outline-offset:2px`)}
|
|
365
|
+
</div>
|
|
366
|
+
<div class="grid" style="margin-top:18px">
|
|
367
|
+
<div class="demo"><input placeholder="Input — default" style="border:1px solid ${line};border-radius:${esc(r)}"><span>default</span></div>
|
|
368
|
+
<div class="demo"><input placeholder="Input — focus" style="border:1px solid ${primary.ref};border-radius:${esc(r)};box-shadow:0 0 0 3px color-mix(in srgb, ${primary.ref} 30%, transparent)"><span>focus</span></div>
|
|
369
|
+
<div class="demo"><input placeholder="Input — error" style="border:1px solid ${bad};border-radius:${esc(r)}"><span>error</span></div>
|
|
370
|
+
</div>
|
|
371
|
+
<div class="grid" style="margin-top:18px">
|
|
372
|
+
<div class="demo"><span class="pill" style="background:color-mix(in srgb, ${good} 15%, transparent);color:${good};border-radius:${esc(r)}">Saved</span><span>success</span></div>
|
|
373
|
+
<div class="demo"><span class="pill" style="background:color-mix(in srgb, ${bad} 15%, transparent);color:${bad};border-radius:${esc(r)}">Could not save</span><span>error</span></div>
|
|
374
|
+
</div>
|
|
375
|
+
</div></section>`;
|
|
376
|
+
}
|
|
377
|
+
function pick(tokens, ...res) {
|
|
378
|
+
for (const re of res) {
|
|
379
|
+
const hit = tokens.find((t) => re.test(t.name.toLowerCase()));
|
|
380
|
+
if (hit)
|
|
381
|
+
return safe(hit.value);
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
function pickDark(tokens, ...res) {
|
|
386
|
+
for (const re of res) {
|
|
387
|
+
const hit = tokens.find((t) => re.test(t.name.toLowerCase()));
|
|
388
|
+
if (hit)
|
|
389
|
+
return safe(hit.dark ?? hit.value);
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
export function renderDesignPreview(md, projectName = 'project') {
|
|
394
|
+
const t = parseDesignTokens(md);
|
|
395
|
+
const status = readFrontmatter(md).status ?? 'uninitialized';
|
|
396
|
+
const light = {
|
|
397
|
+
bg: pick(t.colors, /bg-main|background|bg(?!-)/) ?? '#ffffff',
|
|
398
|
+
fg: pick(t.colors, /text-base|text(?!-muted)|fg/) ?? '#111111',
|
|
399
|
+
};
|
|
400
|
+
const dark = {
|
|
401
|
+
bg: pickDark(t.colors, /bg-main|background|bg(?!-)/) ?? '#111113',
|
|
402
|
+
fg: pickDark(t.colors, /text-base|text(?!-muted)|fg/) ?? '#f4f4f5',
|
|
403
|
+
};
|
|
404
|
+
const { painted, lightVars, darkVars } = palette(t.colors, light, dark);
|
|
405
|
+
const hasDark = t.colors.some((c) => c.dark);
|
|
406
|
+
const empty = t.colors.length === 0 && t.type.length === 0 && t.spacing.length === 0 && t.radius.length === 0;
|
|
407
|
+
const body = empty
|
|
408
|
+
? `<section><div class="surface"><p class="desc">DESIGN.md carries no decided tokens yet — every value is still a <code>[placeholder]</code>. Run the designer step, then reopen this page.</p></div></section>`
|
|
409
|
+
: [
|
|
410
|
+
hasDark
|
|
411
|
+
? ''
|
|
412
|
+
: `<div class="warn-strip only-dark">DESIGN.md declares no dark palette — only this page switches. Give each color a dark value (a <code>Dark</code> column, a <code>## Dark mode</code> table, or a <code>-dark</code> token) and the swatches below switch with it.</div>`,
|
|
413
|
+
colorSection(painted, light, dark),
|
|
414
|
+
typeSection(t.type, t.fonts),
|
|
415
|
+
scaleSection('Spacing', t.spacing, 'bar'),
|
|
416
|
+
scaleSection('Radius', t.radius, 'box'),
|
|
417
|
+
shadowSection(t.shadows),
|
|
418
|
+
componentSection(painted, t.radius),
|
|
419
|
+
scaleSection('Layout', t.layout, 'list'),
|
|
420
|
+
].join('\n');
|
|
421
|
+
return `<!doctype html>
|
|
422
|
+
<html lang="en" data-theme="light"><head><meta charset="utf-8">
|
|
423
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
424
|
+
<title>${esc(projectName)} — Design tokens</title>
|
|
425
|
+
<style>
|
|
426
|
+
:root { --paper:#fbfbfa; --ink:#18181b; --muted:#71717a; --line:#e4e4e7; --card:#fff; ${lightVars.join(';')} }
|
|
427
|
+
:root[data-theme="dark"] { --paper:#111113; --ink:#f4f4f5; --muted:#a1a1aa; --line:#27272a; --card:#18181b; ${darkVars.join(';')} }
|
|
428
|
+
:root[data-theme="dark"] .only-light, :root[data-theme="light"] .only-dark { display:none; }
|
|
429
|
+
* { box-sizing: border-box; }
|
|
430
|
+
body { margin:0; background:var(--paper); color:var(--ink); font:14px/1.6 ui-sans-serif, system-ui, -apple-system, sans-serif; }
|
|
431
|
+
.wrap { max-width: 980px; margin: 0 auto; padding: 28px 22px 100px; }
|
|
432
|
+
.bar-top { display:flex; align-items:center; gap:12px; justify-content:space-between; margin-bottom:26px; }
|
|
433
|
+
.modes { display:inline-flex; border:1px solid var(--line); border-radius:999px; padding:3px; background:var(--card); }
|
|
434
|
+
.modes button { border:0; background:transparent; color:var(--muted); font:inherit; font-size:12px; padding:5px 14px; border-radius:999px; cursor:pointer; }
|
|
435
|
+
.modes button[aria-pressed="true"] { background:var(--ink); color:var(--paper); font-weight:600; }
|
|
436
|
+
header h1 { font-size: 28px; letter-spacing:-0.02em; margin: 0; }
|
|
437
|
+
header p { color: var(--muted); margin: 8px 0 0; }
|
|
438
|
+
.meta { font: 11px ui-monospace, monospace; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; }
|
|
439
|
+
section { margin-top: 48px; }
|
|
440
|
+
section > h2 { font-size:12px; letter-spacing:.08em; text-transform:uppercase; color:var(--muted); margin:0 0 10px; }
|
|
441
|
+
.desc { color: var(--muted); margin: 0 0 14px; }
|
|
442
|
+
.warn-strip { margin-top:32px; padding:12px 16px; border:1px solid var(--line); border-left:3px solid #f59e0b; border-radius:8px; color:var(--muted); font-size:13px; }
|
|
443
|
+
.surface { background: var(--card); border:1px solid var(--line); border-radius:12px; padding:22px; }
|
|
444
|
+
.tokrow { display:grid; grid-template-columns:repeat(auto-fill,minmax(230px,1fr)); gap:10px; }
|
|
445
|
+
.tok { display:flex; gap:12px; align-items:flex-start; background:var(--card); border:1px solid var(--line); border-radius:10px; padding:10px; }
|
|
446
|
+
.chip { width:38px; height:38px; border-radius:8px; flex:none; box-shadow: inset 0 0 0 1px rgba(128,128,128,.25); }
|
|
447
|
+
.tk b { display:block; font-weight:600; font-size:12.5px; }
|
|
448
|
+
.tk code, .val, code { font-family: ui-monospace, SFMono-Regular, monospace; font-size:11.5px; color:var(--muted); }
|
|
449
|
+
.tk code { display:block; }
|
|
450
|
+
.tk .same { opacity:.65; }
|
|
451
|
+
.tk .note, .scale-row .note { color:var(--muted); font-size:11.5px; }
|
|
452
|
+
.tk .note { display:block; }
|
|
453
|
+
.badge { display:inline-block; margin-top:6px; padding:1px 7px; border-radius:999px; font-size:11px; font-weight:600; }
|
|
454
|
+
.badge.ok { background:#16a34a26; color:#22a05a; } .badge.warn { background:#f59e0b26; color:#c1820c; } .badge.bad { background:#dc262626; color:#e0574f; }
|
|
455
|
+
.spec { display:flex; gap:20px; align-items:baseline; padding:14px 0; border-bottom:1px solid var(--line); }
|
|
456
|
+
.spec:last-child { border-bottom:0; }
|
|
457
|
+
.spec .tag { width:150px; flex:none; font-family:ui-monospace,monospace; font-size:11.5px; color:var(--muted); }
|
|
458
|
+
.spec .tag span { display:block; font-size:10.5px; opacity:.75; }
|
|
459
|
+
.spec .sample { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
460
|
+
.scale-row { display:flex; align-items:center; gap:14px; padding:7px 0; }
|
|
461
|
+
.scale-row code { width:150px; flex:none; }
|
|
462
|
+
.scale-row .val { width:70px; flex:none; }
|
|
463
|
+
.bar { height:14px; background:currentColor; opacity:.75; border-radius:3px; max-width:100%; }
|
|
464
|
+
.box { width:56px; height:40px; border:2px solid currentColor; opacity:.7; }
|
|
465
|
+
.grid { display:flex; flex-wrap:wrap; gap:18px; }
|
|
466
|
+
.shadow-cell { text-align:center; }
|
|
467
|
+
.shadow-box { width:110px; height:66px; background:var(--card); border-radius:10px; margin-bottom:8px; border:1px solid var(--line); }
|
|
468
|
+
.demo { display:flex; flex-direction:column; gap:6px; align-items:flex-start; }
|
|
469
|
+
.demo > span { font-size:11px; color:var(--muted); font-family:ui-monospace,monospace; }
|
|
470
|
+
.demo button { border:0; padding:0 18px; height:44px; font:inherit; font-weight:600; cursor:pointer; }
|
|
471
|
+
.demo input { height:44px; padding:0 12px; font:inherit; background:transparent; color:inherit; min-width:220px; }
|
|
472
|
+
.pill { display:inline-block; padding:5px 12px; font-size:12.5px; font-weight:600; }
|
|
473
|
+
footer { margin-top:60px; color:var(--muted); font-size:12px; border-top:1px solid var(--line); padding-top:14px; }
|
|
474
|
+
</style></head>
|
|
475
|
+
<body><div class="wrap">
|
|
476
|
+
<div class="bar-top">
|
|
477
|
+
<div class="meta">${esc(status)} · generated from DESIGN.md</div>
|
|
478
|
+
<div class="modes" role="group" aria-label="Preview mode">
|
|
479
|
+
<button type="button" data-mode="light" aria-pressed="true">Light</button>
|
|
480
|
+
<button type="button" data-mode="dark" aria-pressed="false">Dark</button>
|
|
481
|
+
<button type="button" data-mode="system" aria-pressed="false">System</button>
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
<header>
|
|
485
|
+
<h1>${esc(projectName)} — design tokens</h1>
|
|
486
|
+
<p>Every value on this page is read out of <code>.kortext/DESIGN.md</code>. Edit the document, not this file.</p>
|
|
487
|
+
</header>
|
|
488
|
+
${body}
|
|
489
|
+
<footer>Generated by kortext on ${new Date().toISOString().slice(0, 10)} — regenerated each time DESIGN.md changes.</footer>
|
|
490
|
+
</div>
|
|
491
|
+
<script>
|
|
492
|
+
// The mode is the reader's, not the document's: it survives a reload, and
|
|
493
|
+
// "system" keeps following the OS while the page is open.
|
|
494
|
+
(function () {
|
|
495
|
+
var root = document.documentElement;
|
|
496
|
+
var media = window.matchMedia('(prefers-color-scheme: dark)');
|
|
497
|
+
var mode = 'system';
|
|
498
|
+
try { mode = localStorage.getItem('kortext-preview-mode') || 'system'; } catch (e) {}
|
|
499
|
+
function paint() {
|
|
500
|
+
root.dataset.theme = mode === 'system' ? (media.matches ? 'dark' : 'light') : mode;
|
|
501
|
+
document.querySelectorAll('.modes button').forEach(function (b) {
|
|
502
|
+
b.setAttribute('aria-pressed', String(b.dataset.mode === mode));
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
document.querySelectorAll('.modes button').forEach(function (b) {
|
|
506
|
+
b.addEventListener('click', function () {
|
|
507
|
+
mode = b.dataset.mode;
|
|
508
|
+
try { localStorage.setItem('kortext-preview-mode', mode); } catch (e) {}
|
|
509
|
+
paint();
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
media.addEventListener('change', function () { if (mode === 'system') paint(); });
|
|
513
|
+
paint();
|
|
514
|
+
})();
|
|
515
|
+
</script>
|
|
516
|
+
</body></html>`;
|
|
517
|
+
}
|
|
518
|
+
/** Writes `.kortext/DESIGN.html` next to the document. No document, no page. */
|
|
519
|
+
export function writeDesignPreview(project) {
|
|
520
|
+
const md = join(project.repo_path, '.kortext', 'DESIGN.md');
|
|
521
|
+
const html = join(project.repo_path, '.kortext', 'DESIGN.html');
|
|
522
|
+
if (!existsSync(md))
|
|
523
|
+
return;
|
|
524
|
+
const content = readFileSync(md, 'utf8');
|
|
525
|
+
// A skeleton has nothing to preview: a page rendered from placeholders is a
|
|
526
|
+
// file nobody asked for, and the next author reports it as one.
|
|
527
|
+
if (readFrontmatter(content).status === 'uninitialized') {
|
|
528
|
+
if (existsSync(html))
|
|
529
|
+
unlinkSync(html);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
writeFileSync(html, renderDesignPreview(content, project.name), 'utf8');
|
|
533
|
+
}
|