@litfamily/litmuse 0.1.0 → 0.1.2
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/CHANGELOG.md +17 -0
- package/README.md +115 -26
- package/README_ko-KR.md +112 -23
- package/bin/litmuse.mjs +4 -0
- package/docs/assets/card/after.webp +0 -0
- package/docs/assets/card/idle.webp +0 -0
- package/docs/assets/card/working.webp +0 -0
- package/docs/assets/handoff/latest.webp +0 -0
- package/docs/assets/handoff/saved.webp +0 -0
- package/docs/assets/readme/demo.webp +0 -0
- package/lib/stage-card.mjs +180 -5
- package/package.json +7 -1
- package/plugin/.muse-plugin/plugin.json +26 -11
- package/plugin/commands/lit-status.md +2 -1
- package/plugin/commands/lit.md +1 -1
- package/plugin/hooks/lib/digest.mjs +1 -1
- package/plugin/hooks/lib/next-step.mjs +68 -0
- package/plugin/hooks/lib/routes.mjs +59 -0
- package/plugin/hooks/lib/workflow-stack.mjs +318 -0
- package/plugin/hooks/session-start.mjs +29 -2
- package/plugin/hooks/user-prompt-submit.mjs +138 -7
- package/plugin/lib/capped-fetch.mjs +25 -0
- package/plugin/lib/pinned-fetch.mjs +79 -0
- package/plugin/lib/plan-task-rows.mjs +32 -0
- package/plugin/lib/render_pages.py +29 -1
- package/plugin/lib/rewind/collect.mjs +679 -0
- package/plugin/lib/rewind/gauge.mjs +72 -0
- package/plugin/lib/rewind/next-step.mjs +91 -0
- package/plugin/lib/rewind/redact.mjs +83 -0
- package/plugin/lib/rewind/transcripts.mjs +227 -0
- package/plugin/skills/frontend-ui-ux/SKILL.md +24 -9
- package/plugin/skills/frontend-ui-ux/kit/components.css +414 -0
- package/plugin/skills/frontend-ui-ux/kit/fonts.json +22 -0
- package/plugin/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
- package/plugin/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
- package/plugin/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
- package/plugin/skills/frontend-ui-ux/kit/ui.js +226 -0
- package/plugin/skills/frontend-ui-ux/references/complete-contract.md +2 -1
- package/plugin/skills/frontend-ui-ux/references/craft-floor.md +9 -4
- package/plugin/skills/frontend-ui-ux/references/default-editorial-pixel.json +59 -7
- package/plugin/skills/frontend-ui-ux/references/house-styles.md +234 -0
- package/plugin/skills/frontend-ui-ux/references/motion-guide.md +3 -3
- package/plugin/skills/frontend-ui-ux/references/slop-register.md +7 -7
- package/plugin/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe-lock.mjs +196 -0
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe-page.mjs +31 -10
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe-rules.mjs +19 -7
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe-static.mjs +4 -0
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe.mjs +59 -7
- package/plugin/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
- package/plugin/skills/lit-adhd/SKILL.md +188 -0
- package/plugin/skills/lit-adhd/references/card-examples.md +121 -0
- package/plugin/skills/lit-adhd/references/status-taxonomy.md +43 -0
- package/plugin/skills/lit-adhd/scripts/rewind.mjs +11 -0
- package/plugin/skills/lit-diagram-drawer/SKILL.md +3 -3
- package/plugin/skills/lit-diagram-drawer/scripts/diagram-craft-rules.mjs +1 -2
- package/plugin/skills/lit-diagram-drawer/scripts/visual-quality.mjs +0 -1
- package/plugin/skills/lit-docx/SKILL.md +1 -1
- package/plugin/skills/lit-hijack/SKILL.md +589 -0
- package/plugin/skills/lit-hijack/references/capture-and-provenance.md +123 -0
- package/plugin/skills/lit-hijack/references/clean-room-build.md +133 -0
- package/plugin/skills/lit-hijack/references/distil-and-licence.md +128 -0
- package/plugin/skills/lit-hijack/references/gap-and-port-design.md +152 -0
- package/plugin/skills/lit-hijack/references/parity-and-report.md +133 -0
- package/plugin/skills/lit-hijack/scripts/hijack.mjs +879 -0
- package/plugin/skills/lit-pptx/SKILL.md +20 -9
- package/plugin/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
- package/plugin/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
- package/plugin/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
- package/plugin/skills/lit-pptx/fonts/provenance.json +32 -1
- package/plugin/skills/lit-pptx/references/direction-step.md +19 -2
- package/plugin/skills/lit-pptx/references/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
- package/plugin/skills/lit-pptx/references/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
- package/plugin/skills/lit-pptx/references/examples/15-design-lecture-bauhaus-ko.md +139 -0
- package/plugin/skills/lit-pptx/references/examples/16-community-programme-riso-ko.md +158 -0
- package/plugin/skills/lit-pptx/references/examples/17-public-data-briefing-newsroom-ko.md +179 -0
- package/plugin/skills/lit-pptx/references/tonalities/bauhaus.md +112 -0
- package/plugin/skills/lit-pptx/references/tonalities/computational-brutalism.md +118 -0
- package/plugin/skills/lit-pptx/references/tonalities/neo-brutalism.md +125 -0
- package/plugin/skills/lit-pptx/references/tonalities/newsroom.md +115 -0
- package/plugin/skills/lit-pptx/references/tonalities/riso.md +109 -0
- package/plugin/skills/lit-pptx/scripts/compile-deck.js +1 -1
- package/plugin/skills/lit-pptx/scripts/craft_extras.py +136 -92
- package/plugin/skills/lit-pptx/scripts/embed_fonts.py +13 -1
- package/plugin/skills/lit-pptx/scripts/lib/grid-resolver.js +105 -116
- package/plugin/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
- package/plugin/skills/lit-pptx/scripts/lib/render-pack.js +368 -561
- package/plugin/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
- package/plugin/skills/lit-pptx/scripts/lib/template-registry.js +57 -6
- package/plugin/skills/lit-pptx/scripts/qa_deck.py +4 -4
- package/plugin/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
- package/plugin/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
- package/plugin/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
- package/plugin/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
- package/plugin/skills/lit-pptx/templates/tonalities/night/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/paper/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
- package/plugin/skills/lit-pptx/templates/tonalities/signal/pack.yaml +2 -2
- package/plugin/skills/lit-pptx/templates/tonalities/studio/pack.yaml +1 -1
- package/plugin/skills/lit-recap/SKILL.md +3 -0
- package/plugin/skills/lit-schematic/SKILL.md +241 -0
- package/plugin/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +14 -0
- package/plugin/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +8 -0
- package/plugin/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +22 -0
- package/plugin/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +73 -0
- package/plugin/skills/lit-schematic/references/cutaway-outputs.md +25 -0
- package/plugin/skills/lit-schematic/references/isometric-craft.md +24 -0
- package/plugin/skills/lit-schematic/references/journal-figures.md +19 -0
- package/plugin/skills/lit-schematic/references/kit-api.md +27 -0
- package/plugin/skills/lit-schematic/references/pfd-and-layout.md +20 -0
- package/plugin/skills/lit-schematic/references/pid-drafting.md +26 -0
- package/plugin/skills/lit-schematic/references/pid-symbols.md +27 -0
- package/plugin/skills/lit-schematic/references/verification.md +24 -0
- package/plugin/skills/lit-schematic/scripts/doctor.mjs +13 -0
- package/plugin/skills/lit-schematic/scripts/lib/figure.mjs +49 -0
- package/plugin/skills/lit-schematic/scripts/lib/isa.mjs +18 -0
- package/plugin/skills/lit-schematic/scripts/lib/iso.mjs +58 -0
- package/plugin/skills/lit-schematic/scripts/lib/legend-sheet.mjs +10 -0
- package/plugin/skills/lit-schematic/scripts/lib/qa-core.mjs +241 -0
- package/plugin/skills/lit-schematic/scripts/lib/sheet.mjs +446 -0
- package/plugin/skills/lit-schematic/scripts/lib/svg.mjs +39 -0
- package/plugin/skills/lit-schematic/scripts/lib/theme.mjs +25 -0
- package/plugin/skills/lit-schematic/scripts/motion.mjs +8 -0
- package/plugin/skills/lit-schematic/scripts/qa.mjs +21 -0
- package/plugin/skills/lit-schematic/scripts/render.mjs +38 -0
- package/plugin/skills/lit-schematic/scripts/scaffold.mjs +73 -0
- package/plugin/skills/lit-scientific-visualization/SKILL.md +2 -0
- package/plugin/skills/lit-typographic-motion/SKILL.md +12 -8
- package/plugin/skills/lit-typographic-motion/engine/THIRD_PARTY_NOTICES +11 -0
- package/plugin/skills/lit-typographic-motion/engine/blender/runner.py +218 -0
- package/plugin/skills/lit-typographic-motion/engine/core/presets.mjs +1 -1
- package/plugin/skills/lit-typographic-motion/engine/node/assets.mjs +218 -0
- package/plugin/skills/lit-typographic-motion/engine/node/blender.mjs +144 -0
- package/plugin/skills/lit-typographic-motion/engine/node/board.mjs +136 -0
- package/plugin/skills/lit-typographic-motion/engine/node/done.mjs +42 -3
- package/plugin/skills/lit-typographic-motion/engine/node/gallery.mjs +120 -0
- package/plugin/skills/lit-typographic-motion/engine/node/house.mjs +44 -0
- package/plugin/skills/lit-typographic-motion/engine/node/pipeline.mjs +18 -7
- package/plugin/skills/lit-typographic-motion/engine/node/runtime.mjs +6 -3
- package/plugin/skills/lit-typographic-motion/engine/node/scaffold.mjs +210 -0
- package/plugin/skills/lit-typographic-motion/engine/node/score.mjs +639 -0
- package/plugin/skills/lit-typographic-motion/engine/node/sound-cli.mjs +21 -4
- package/plugin/skills/lit-typographic-motion/engine/node/sound.mjs +126 -27
- package/plugin/skills/lit-typographic-motion/engine/node/stage-serve.mjs +29 -5
- package/plugin/skills/lit-typographic-motion/engine/node/stage.mjs +82 -14
- package/plugin/skills/lit-typographic-motion/engine/node/styles.mjs +214 -0
- package/plugin/skills/lit-typographic-motion/engine/node/treatment.mjs +311 -10
- package/plugin/skills/lit-typographic-motion/engine/vendor/three/LICENSE +21 -0
- package/plugin/skills/lit-typographic-motion/engine/vendor/three/addons/environments/RoomEnvironment.js +144 -0
- package/plugin/skills/lit-typographic-motion/engine/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
- package/plugin/skills/lit-typographic-motion/engine/vendor/three/three.module.js +54571 -0
- package/plugin/skills/lit-typographic-motion/recipes/hero-3d/expected.json +69 -0
- package/plugin/skills/lit-typographic-motion/recipes/hero-3d/index.html +139 -0
- package/plugin/skills/lit-typographic-motion/references/depth.md +122 -0
- package/plugin/skills/lit-typographic-motion/references/sound.md +74 -16
- package/plugin/skills/lit-typographic-motion/references/stage.md +29 -5
- package/plugin/skills/lit-typographic-motion/references/studio.md +245 -0
- package/plugin/skills/lit-typographic-motion/references/treatment.md +31 -8
- package/plugin/skills/lit-typographic-motion/scripts/motion.mjs +48 -7
- package/plugin/skills/lit-typographic-motion/styles/paper.json +41 -0
- package/plugin/skills/lit-typographic-motion/styles/swiss-signal.json +41 -0
- package/plugin/skills/lit-typographic-motion/styles/terminalcore.json +42 -0
- package/plugin/skills/lit-typographic-motion/styles/tidal.json +41 -0
- package/plugin/skills/readme-studio/SKILL.md +1 -1
- package/plugin/skills/readme-studio/templates/hyperframes-cover/package-lock.json +3 -3
- package/plugin/skills/readme-studio/templates/remotion-cover/package-lock.json +3 -3
- package/plugin/skills/start-work/SKILL.md +6 -0
|
@@ -44,13 +44,13 @@ or *checklist* (review by eye).
|
|
|
44
44
|
| Id | Tell | Signal and threshold | Severity | Fix | Coverage |
|
|
45
45
|
|---|---|---|---|---|---|
|
|
46
46
|
| SLOP-008 | The reflex violet accent | a gradient stop, or heading text (h1 to h3 or ≥ 20 px), with hue 260° to 310° and an RGB channel spread ≥ 50, unless that hue is the brand's own action colour in ≥ 2 regions | MEDIUM | Neutral surface plus one real brand hue | both |
|
|
47
|
-
| SLOP-009 |
|
|
48
|
-
| SLOP-010 | The same coloured glow repeated | CF-404's glow test (≤ 2 px offset, ≥ 16 px blur, saturation > 0.4) on ≥
|
|
49
|
-
| SLOP-011 |
|
|
50
|
-
| SLOP-012 | Dot or line grid texture next to other decoration | ≥ 2 gradient layers or a radial dot on a fixed px cell ≤ 48 px, no canvas, table, grid, `role=img` or SVG over 48 px in its section or landmark, beside a SLOP-008 or SLOP-011 signal | MEDIUM | Remove unless it stands in for real data | probe |
|
|
47
|
+
| SLOP-009 | Reflex gradient-filled text | `background-clip: text` with a gradient that has a violet stop (SLOP-008's band), or on more than one element, or with a stop under 4.5:1 (3:1 at ≥ 24 px) on its surface; one gradient headline in the palette's own hues is craft | MEDIUM | Keep one, in palette hues with every stop readable, or use a solid colour | both (static: the violet or repeated form) |
|
|
48
|
+
| SLOP-010 | The same coloured glow repeated | CF-404's glow test (≤ 2 px offset, ≥ 16 px blur, saturation > 0.4) on ≥ 3 unrelated controls or headings, not a focus state; one or two glows are CF-404, and a glow on a surface (`ui-glow`) is craft | MEDIUM | Keep the glow for the surface it lights | probe |
|
|
49
|
+
| SLOP-011 | Faint floating blobs with no focal job | ≥ 2 absolutely placed radial gradients under 0.3 opacity that paint only themselves (no text, media, control or link inside); one atmosphere layer behind the focal point is craft | MEDIUM | Keep one layer tied to the focal point | probe (judgment) |
|
|
50
|
+
| SLOP-012 | Dot or line grid texture next to other decoration | ≥ 2 gradient layers or a radial dot on a fixed px cell ≤ 48 px, no canvas, table, grid, `role=img` or SVG over 48 px in its section or landmark, beside a SLOP-008 or SLOP-011 signal (so notebook dots under one atmosphere layer pass) | MEDIUM | Remove unless it stands in for real data | probe |
|
|
51
51
|
| SLOP-013 | Grain or noise dense enough to hide the layout | layout reads better with the layer hidden | LOW | Lighten it or remove it | checklist |
|
|
52
52
|
| SLOP-014 | Glass blur as a default surface | no stated reason and no solid fallback for reduced transparency | LOW | Solid surface unless the brief earns it | checklist |
|
|
53
|
-
| SLOP-015 | Coloured side stripes faking emphasis on cards | on rounded elements: a single-side accent (one side ≥ 2 px saturated, the other three 0 or ≤ 1 px neutral; an all-round outline is not a stripe), an absolutely placed 3 to 12 px `::before`/`::after` bar pinned to one edge (tab and nav indicators skipped), ≥
|
|
53
|
+
| SLOP-015 | Coloured side stripes faking emphasis on cards | on rounded elements: a single-side accent (one side ≥ 2 px saturated, the other three 0 or ≤ 1 px neutral; an all-round outline is not a stripe), an absolutely placed 3 to 12 px `::before`/`::after` bar pinned to one edge (tab and nav indicators skipped), ≥ 3 per scope; an inset 3 to 12 px chromatic shadow counts at 2; status regions and callouts excepted | MEDIUM | Carry emphasis with the fill or an icon | probe |
|
|
54
54
|
|
|
55
55
|
## Typography
|
|
56
56
|
|
|
@@ -67,7 +67,7 @@ or *checklist* (review by eye).
|
|
|
67
67
|
| Id | Tell | Signal and threshold | Severity | Fix | Coverage |
|
|
68
68
|
|---|---|---|---|---|---|
|
|
69
69
|
| SLOP-021 | Neutral mid-gray text on a coloured surface | text lightness 40 % to 70 % and saturation < 10 % on a surface with saturation ≥ 25 % (see CF-201 for the contrast floor) | MEDIUM | Tint the text from the surface hue | probe |
|
|
70
|
-
| SLOP-022 |
|
|
70
|
+
| SLOP-022 | Too many accent hues competing | more than four saturated clusters among filled surfaces (more than one in a calm lock); measured as CF-205 | MEDIUM | Fold the extras into a family the page already uses | via CF-205 |
|
|
71
71
|
| SLOP-023 | Pure #000 or #fff as the base | exact black or white body background or ink (a deliberate all-black dark theme excepted) | LOW | Move a few percent toward the brand neutral | probe |
|
|
72
72
|
|
|
73
73
|
## Motion
|
|
@@ -121,7 +121,7 @@ or *checklist* (review by eye).
|
|
|
121
121
|
|
|
122
122
|
| Id | Tell | Signal and threshold | Severity | Fix | Coverage |
|
|
123
123
|
|---|---|---|---|---|---|
|
|
124
|
-
| SLOP-051 | An illustration stacked from primitives | an inline SVG ≥ 200 × 200 px with ≥ 8 rect, circle, ellipse or polygon shapes, ≥ 3 fills, ≤ 2 text elements and no
|
|
124
|
+
| SLOP-051 | An unlabelled illustration stacked from primitives | an inline SVG ≥ 200 × 200 px with ≥ 8 rect, circle, ellipse or polygon shapes, ≥ 3 fills, ≤ 2 text elements, no pattern and no name (`role="img"` with `aria-label`/`aria-labelledby`, or a `<title>`); a named many-shape drawing of the subject is an authored illustration | MEDIUM | Name what it shows, or draw the subject instead of a generic stack | probe (judgment: an honest diagram clears it) |
|
|
125
125
|
| SLOP-052 | A many-vertex mask faking a photo edge | `clip-path: polygon()` with ≥ 10 vertices, at least half its coordinates off the 25 % grid, or `path()` with ≥ 3 curves | LOW | A real crop or a simple shape | probe |
|
|
126
126
|
| SLOP-053 | Emoji in place of an icon set | emoji as the whole content of a small control, or leading a nav item, badge, list item or heading | LOW | The project's icon set | probe (judgment) |
|
|
127
127
|
| SLOP-054 | Hover zoom on the image itself | a `:hover` transform on `img` | LOW | Transform the container | checklist |
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Design lock: the one place a build picks its look. It writes the chosen house style into the
|
|
3
|
+
// project as design.lock.json plus ui/tokens.css, ui/components.css, ui/ui.js and the fonts, and
|
|
4
|
+
// the interface probe then checks the rendered page against that lock (DS-* rules).
|
|
5
|
+
//
|
|
6
|
+
// node design-lock.mjs list
|
|
7
|
+
// node design-lock.mjs init --family <landing|workspace|settings> --dir <project> [--style <id>]
|
|
8
|
+
// [--brief "<request text>"] [--density rich|calm] [--moment <kind> --moment-note "<what it is>"
|
|
9
|
+
// [--moment-place "<where it sits>"]] [--scaffold] [--from <style.json>] [--force]
|
|
10
|
+
// node design-lock.mjs moment --dir <project> --kind <kind> --note "<what it is>" [--place "<where>"]
|
|
11
|
+
// Without --style a style the brief names by keyword applies (styleForBrief), else the family
|
|
12
|
+
// default (DEFAULT_STYLE_BY_FAMILY).
|
|
13
|
+
// node design-lock.mjs match "<request text>"
|
|
14
|
+
// node design-lock.mjs show --dir <project>
|
|
15
|
+
//
|
|
16
|
+
// `--from` takes a brand's own style file in the same schema as kit/styles/*.json; its id must not
|
|
17
|
+
// reuse a kit style id. Nothing here fetches from the network or installs anything.
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { dirname, join, resolve } from "node:path";
|
|
21
|
+
import process from "node:process";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
|
|
24
|
+
const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
25
|
+
const KIT = join(SKILL_ROOT, "kit");
|
|
26
|
+
export const LOCK_FILE = "design.lock.json";
|
|
27
|
+
export const LOCK_SCHEMA = "litfamily.design-lock/v1";
|
|
28
|
+
export const STYLE_SCHEMA = "litfamily.frontend-style/v1";
|
|
29
|
+
export const FAMILIES = Object.freeze({
|
|
30
|
+
landing: "read-once pages: landing, product, launch, portfolio, docs home",
|
|
31
|
+
workspace: "screens people work in: dashboards, consoles, admin, tools, tables",
|
|
32
|
+
settings: "guided forms: settings, account, onboarding, checkout steps",
|
|
33
|
+
});
|
|
34
|
+
// The user-decided defaults (2026-10-25) when a brief gives no visual direction: read-once pages
|
|
35
|
+
// keep the authored field-notes profile; work surfaces use graphite. meadow is never a default.
|
|
36
|
+
export const DEFAULT_STYLE_BY_FAMILY = Object.freeze({ landing: "field-notes", workspace: "graphite", settings: "graphite" });
|
|
37
|
+
// Round 2 (user decision A, 2026-10-25): the lock fixes the look, and every page names the one place
|
|
38
|
+
// it is allowed to be bold. Read-once pages take a bold moment above the fold; work surfaces stay
|
|
39
|
+
// calm and take a small one. The probe checks the declared kind against [data-ui-moment] (DS-008).
|
|
40
|
+
export const MOMENT_SCALE_BY_FAMILY = Object.freeze({ landing: "bold", workspace: "small", settings: "small" });
|
|
41
|
+
export const MOMENT_KINDS = Object.freeze({
|
|
42
|
+
bold: Object.freeze(["hero-composition", "type-set-piece", "data-moment", "diagram", "motion-detail", "interactive-object"]),
|
|
43
|
+
small: Object.freeze(["chart", "empty-state", "header-detail"]),
|
|
44
|
+
});
|
|
45
|
+
export const MOMENT_SELECTOR = "[data-ui-moment]";
|
|
46
|
+
export const COLOR_ROLES = Object.freeze(["canvas", "surface", "sunken", "ink", "ink-2", "ink-3", "line", "line-strong", "control", "accent", "accent-hover", "on-accent", "accent-wash", "accent-ink", "accent-2", "accent-2-ink", "accent-2-wash", "on-accent-2", "support", "support-ink", "focus", "positive", "positive-wash", "warning", "warning-wash", "negative", "negative-wash", "info", "info-wash", "chart-1", "chart-2", "chart-3", "chart-4", "chart-5"]);
|
|
47
|
+
const SCALE_KEYS = ["xs", "sm", "md", "base", "lg", "xl", "2xl", "3xl", "4xl", "5xl"];
|
|
48
|
+
const RADIUS_KEYS = ["xs", "sm", "md", "lg", "pill"];
|
|
49
|
+
const HEX = /^#[0-9a-f]{6}$/u;
|
|
50
|
+
// The shape block carries what makes a register more than a palette: border weights, which role
|
|
51
|
+
// draws outlines, the elevation grammar the probe enforces (DS-007), how a button presses, and the
|
|
52
|
+
// case and family of meta text. Every value is closed so components.css can stay token-only.
|
|
53
|
+
export const SHAPE_CHOICES = Object.freeze({
|
|
54
|
+
surface_line: ["line", "line-strong", "ink"],
|
|
55
|
+
control_edge: ["control", "ink"],
|
|
56
|
+
elevation: ["soft", "offset", "flat"],
|
|
57
|
+
press: ["scale", "shift", "none"],
|
|
58
|
+
meta_font: ["text", "mono"],
|
|
59
|
+
meta_case: ["none", "uppercase"],
|
|
60
|
+
display_case: ["none", "uppercase"],
|
|
61
|
+
canvas_pattern: ["none", "dots", "grid", "plot"],
|
|
62
|
+
heading_font: ["display", "mono"],
|
|
63
|
+
panel_head: ["plain", "inverse", "accent"],
|
|
64
|
+
section_marks: ["none", "coord"],
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export function kitStyles() {
|
|
68
|
+
return readdirSync(join(KIT, "styles")).filter((name) => name.endsWith(".json")).sort()
|
|
69
|
+
.map((name) => JSON.parse(readFileSync(join(KIT, "styles", name), "utf8")));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Returns a list of problems; an empty list means the style can be locked.
|
|
73
|
+
export function validateStyle(style) {
|
|
74
|
+
const issues = [];
|
|
75
|
+
if (!style || typeof style !== "object") return ["style is not an object"];
|
|
76
|
+
if (style.schema_id !== STYLE_SCHEMA) issues.push(`schema_id must be ${STYLE_SCHEMA}`);
|
|
77
|
+
if (!/^[a-z][a-z0-9-]{1,31}$/u.test(style.id ?? "")) issues.push("id must be a short kebab-case name");
|
|
78
|
+
for (const role of ["text", "display", "mono"]) if (typeof style.fonts?.[role] !== "string" || !style.fonts[role].trim()) issues.push(`fonts.${role} is required`);
|
|
79
|
+
for (const key of SCALE_KEYS) if (!Number.isFinite(style.type?.scale_px?.[key])) issues.push(`type.scale_px.${key} is required`);
|
|
80
|
+
if (!(style.type?.display_px?.min > 0 && style.type.display_px.max >= style.type.display_px.min)) issues.push("type.display_px needs min ≤ max");
|
|
81
|
+
if (!(style.type?.moment_px?.max > style.type?.display_px?.max)) issues.push("type.moment_px.max must sit above type.display_px.max");
|
|
82
|
+
for (const key of ["text", "medium", "strong", "display"]) if (!Number.isFinite(style.type?.weights?.[key])) issues.push(`type.weights.${key} is required`);
|
|
83
|
+
for (const key of RADIUS_KEYS) if (!Number.isFinite(style.radius_px?.[key])) issues.push(`radius_px.${key} is required`);
|
|
84
|
+
if (!Array.isArray(style.space_px) || style.space_px.some((v) => !Number.isInteger(v) || v % 4 !== 0)) issues.push("space_px must be multiples of 4");
|
|
85
|
+
for (const theme of ["light", "dark"]) {
|
|
86
|
+
for (const role of COLOR_ROLES) if (!HEX.test(style.color?.[theme]?.[role] ?? "")) issues.push(`color.${theme}.${role} must be #rrggbb`);
|
|
87
|
+
}
|
|
88
|
+
const shape = style.shape;
|
|
89
|
+
for (const key of ["rule", "surface", "control"]) if (!(Number.isInteger(shape?.border_px?.[key]) && shape.border_px[key] >= 1 && shape.border_px[key] <= 4)) issues.push(`shape.border_px.${key} must be 1 to 4`);
|
|
90
|
+
for (const [key, allowed] of Object.entries(SHAPE_CHOICES)) if (!allowed.includes(shape?.[key])) issues.push(`shape.${key} must be one of ${allowed.join(", ")}`);
|
|
91
|
+
for (const key of ["surface", "control"]) if (!(Number.isInteger(shape?.offset_px?.[key]) && shape.offset_px[key] >= 0)) issues.push(`shape.offset_px.${key} is required`);
|
|
92
|
+
if (shape?.elevation === "offset" && !(shape.offset_px?.surface > 0 && shape.offset_px?.control > 0)) issues.push("an offset elevation needs offset_px above 0");
|
|
93
|
+
if (shape?.elevation !== "offset" && (shape?.offset_px?.surface || shape?.offset_px?.control || shape?.press === "shift")) issues.push("offset_px and press shift belong to an offset elevation");
|
|
94
|
+
for (const key of ["keywords", "signature", "forbid"]) if (!Array.isArray(style[key]) || !style[key].length || style[key].some((item) => typeof item !== "string" || !item.trim())) issues.push(`${key} needs at least one entry`);
|
|
95
|
+
return issues;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// The kit style a brief names, or null. A brief that names a register by one of a style's keywords
|
|
99
|
+
// picks it; when two styles match ("neo-brutalism" also contains "brutalism") the longer keyword
|
|
100
|
+
// wins. Latin keywords must start at a word boundary so "crt" does not match inside a word.
|
|
101
|
+
export function styleForBrief(text, styles = kitStyles()) {
|
|
102
|
+
const haystack = String(text ?? "").toLowerCase();
|
|
103
|
+
let best = null;
|
|
104
|
+
for (const style of styles) {
|
|
105
|
+
for (const keyword of style.keywords ?? []) {
|
|
106
|
+
const word = keyword.toLowerCase();
|
|
107
|
+
const escaped = word.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
108
|
+
const pattern = /^[a-z]/u.test(word) ? new RegExp(`(?<![a-z0-9])${escaped}`, "u") : new RegExp(escaped, "u");
|
|
109
|
+
if (pattern.test(haystack) && (!best || word.length > best.keyword.length)) best = { id: style.id, keyword: word };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return best;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Density (user rule, 2026-10-25): rich is the default register; calm only when the request asks
|
|
116
|
+
// for a minimal, quiet or restrained page, or --density calm. Calm keeps the broad, flat page the
|
|
117
|
+
// kit shipped before; rich tightens the section rhythm and turns on texture and a glow token.
|
|
118
|
+
export const DENSITIES = Object.freeze(["rich", "calm"]);
|
|
119
|
+
const CALM_WORDS = [/(?<![a-z])minimal(?:ist|ism)?(?![a-z])/u, /(?<![a-z])(?:calm|quiet|restrained|understated|pared[- ]back|sparse)(?![a-z])/u, /미니멀/u, /차분/u, /절제/u, /담백/u, /간결/u];
|
|
120
|
+
export function densityForBrief(text) {
|
|
121
|
+
const haystack = String(text ?? "").toLowerCase();
|
|
122
|
+
return CALM_WORDS.some((pattern) => pattern.test(haystack)) ? "calm" : "rich";
|
|
123
|
+
}
|
|
124
|
+
const checkDensity = (density) => {
|
|
125
|
+
if (!DENSITIES.includes(density)) throw new Error(`--density must be rich or calm, not "${density}"`);
|
|
126
|
+
return density;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const fontStack = (family, kind) => {
|
|
130
|
+
const tail = kind === "mono"
|
|
131
|
+
? `ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace`
|
|
132
|
+
: `"Pretendard Variable", -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Noto Sans KR", "Segoe UI", sans-serif`;
|
|
133
|
+
return `"${family}", ${tail}`;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export function tokensCss(style, fontFiles, density = "rich") {
|
|
137
|
+
const calm = checkDensity(density) === "calm";
|
|
138
|
+
const t = style.type;
|
|
139
|
+
const { min, max } = t.display_px;
|
|
140
|
+
const slope = (max - min) / (1440 - 390);
|
|
141
|
+
const intercept = min - slope * 390;
|
|
142
|
+
const colors = (theme, indent = " ") => COLOR_ROLES.map((role) => `${indent}--c-${role}: ${style.color[theme][role]};`).join("\n");
|
|
143
|
+
const shape = style.shape;
|
|
144
|
+
const control = shape.offset_px.control;
|
|
145
|
+
const press = { scale: "scale(0.96)", shift: `translate(${control}px, ${control}px)`, none: "none" }[shape.press];
|
|
146
|
+
const grid = "linear-gradient(color-mix(in srgb, var(--c-line) 70%, transparent) 1px, transparent 1px), linear-gradient(90deg, color-mix(in srgb, var(--c-line) 70%, transparent) 1px, transparent 1px)";
|
|
147
|
+
// The plot grid: minor lines lighter than the grid style, a stronger line every five cells.
|
|
148
|
+
// Both stay on --c-line so body text keeps its contrast over every stop (CF-204) in light and dark.
|
|
149
|
+
const minor = grid.replaceAll("70%", "50%");
|
|
150
|
+
const major = grid.replaceAll("70%", "85%");
|
|
151
|
+
// Notebook dots are texture, so the calm register drops them; grid and plot are a register's structure and stay.
|
|
152
|
+
const dots = "radial-gradient(circle, color-mix(in srgb, var(--c-line-strong) 55%, transparent) 1px, transparent 1.5px)";
|
|
153
|
+
const pattern = { none: ["none", "auto"], dots: calm ? ["none", "auto"] : [dots, "24px 24px"], grid: [grid, "32px 32px"], plot: [`${minor}, ${major}`, "32px 32px, 32px 32px, 160px 160px, 160px 160px"] }[shape.canvas_pattern];
|
|
154
|
+
// A soft accent glow for one atmosphere surface; flat and offset registers keep their own elevation grammar (DS-007).
|
|
155
|
+
const glow = calm || shape.elevation !== "soft" ? "none" : "0 0 48px color-mix(in srgb, var(--c-accent) 30%, transparent), 0 0 96px color-mix(in srgb, var(--c-accent-2) 18%, transparent)";
|
|
156
|
+
const pad = calm ? { section: [64, 96, 128], hero: [48, 64, 80] } : { section: [48, 72, 96], hero: [40, 56, 64] };
|
|
157
|
+
const head = { plain: ["transparent", "inherit", "initial", 0], inverse: ["var(--c-ink)", "var(--c-canvas)", "var(--c-canvas)", 1], accent: ["var(--c-accent)", "var(--c-on-accent)", "var(--c-on-accent)", 1] }[shape.panel_head];
|
|
158
|
+
const moment = t.moment_px.max;
|
|
159
|
+
const momentSlope = (moment - min) / (1440 - 390);
|
|
160
|
+
const faces = fontFiles.map((font) => `@font-face {\n font-family: "${font.family}";\n src: url("fonts/${font.file}") format("woff2");\n font-weight: ${font.weight};\n font-style: normal;\n font-display: swap;\n}`).join("\n");
|
|
161
|
+
return `/* Generated by frontend-ui-ux scripts/design-lock.mjs for the "${style.id}" style. Do not edit by hand:
|
|
162
|
+
re-run design-lock.mjs, or change the style file and lock again. */
|
|
163
|
+
${faces}
|
|
164
|
+
:root {
|
|
165
|
+
--ui-lock: "${style.id}";
|
|
166
|
+
--density: "${calm ? "calm" : "rich"}";
|
|
167
|
+
--font-text: ${fontStack(style.fonts.text, "text")};
|
|
168
|
+
--font-display: ${fontStack(style.fonts.display, "text")};
|
|
169
|
+
--font-mono: ${fontStack(style.fonts.mono, "mono")};
|
|
170
|
+
--font-text-ko: ${style.fonts.text === style.fonts.mono ? fontStack(style.fonts.display, "text") : "var(--font-text)"};
|
|
171
|
+
${SCALE_KEYS.map((key) => ` --fs-${key}: ${t.scale_px[key]}px;`).join("\n")}
|
|
172
|
+
--fs-display: clamp(${min}px, ${intercept.toFixed(2)}px + ${(slope * 100).toFixed(3)}vw, ${max}px);
|
|
173
|
+
--fs-moment: clamp(${min}px, ${(min - momentSlope * 390).toFixed(2)}px + ${(momentSlope * 100).toFixed(3)}vw, ${moment}px);
|
|
174
|
+
--fw-text: ${t.weights.text};
|
|
175
|
+
--fw-medium: ${t.weights.medium};
|
|
176
|
+
--fw-strong: ${t.weights.strong};
|
|
177
|
+
--fw-display: ${t.weights.display};
|
|
178
|
+
--track-display: ${t.tracking_em.display}em;
|
|
179
|
+
--track-label: ${t.tracking_em.label}em;
|
|
180
|
+
--lh-body: ${t.leading.body};
|
|
181
|
+
--lh-tight: ${t.leading.tight};
|
|
182
|
+
--lh-display: ${t.leading.display_latin};
|
|
183
|
+
--lh-display-ko: ${t.leading.display_korean};
|
|
184
|
+
${style.space_px.map((value) => ` --sp-${value}: ${value}px;`).join("\n")}
|
|
185
|
+
${RADIUS_KEYS.map((key) => ` --radius-${key}: ${style.radius_px[key]}px;`).join("\n")}
|
|
186
|
+
--shadow-raised: ${style.shadow.raised};
|
|
187
|
+
--shadow-overlay: ${style.shadow.overlay};
|
|
188
|
+
--shadow-control: ${shape.elevation === "offset" ? `${control}px ${control}px 0 0 var(--c-ink)` : "none"};
|
|
189
|
+
--shadow-control-pressed: ${shape.elevation === "offset" ? "0 0 0 0 var(--c-ink)" : "none"};
|
|
190
|
+
--press: ${press};
|
|
191
|
+
--bw-rule: ${shape.border_px.rule}px;
|
|
192
|
+
--bw-surface: ${shape.border_px.surface}px;
|
|
193
|
+
--bw-control: ${shape.border_px.control}px;
|
|
194
|
+
--c-frame: var(--c-${shape.surface_line});
|
|
195
|
+
--control-edge: ${shape.control_edge === "ink" ? "var(--c-ink)" : "initial"};
|
|
196
|
+
--font-meta: ${shape.meta_font === "mono" ? "var(--font-mono)" : "var(--font-text)"};
|
|
197
|
+
--font-heading: ${shape.heading_font === "mono" ? "var(--font-mono)" : "var(--font-display)"};
|
|
198
|
+
--font-heading-ko: ${shape.heading_font === "mono" ? "var(--font-display)" : "var(--font-heading)"};
|
|
199
|
+
--panel-head-bg: ${head[0]};
|
|
200
|
+
--panel-head-ink: ${head[1]};
|
|
201
|
+
--panel-head-status: ${head[2]};
|
|
202
|
+
--panel-head-bleed: ${head[3]};
|
|
203
|
+
--section-marks: ${shape.section_marks === "coord" ? "block" : "none"};
|
|
204
|
+
--meta-case: ${shape.meta_case};
|
|
205
|
+
--display-case: ${shape.display_case};
|
|
206
|
+
--canvas-pattern: ${pattern[0]};
|
|
207
|
+
--canvas-pattern-size: ${pattern[1]};
|
|
208
|
+
--glow: ${glow};
|
|
209
|
+
--dur-fast: ${style.motion.fast_ms}ms;
|
|
210
|
+
--dur-ui: ${style.motion.ui_ms}ms;
|
|
211
|
+
--dur-enter: ${style.motion.enter_ms}ms;
|
|
212
|
+
--dur-exit: ${style.motion.exit_ms}ms;
|
|
213
|
+
--ease: ${style.motion.ease};
|
|
214
|
+
--gutter: 16px;
|
|
215
|
+
--work-gutter: 16px;
|
|
216
|
+
--section-pad: ${pad.section[0]}px;
|
|
217
|
+
--hero-pad: ${pad.hero[0]}px;
|
|
218
|
+
${colors("light")}
|
|
219
|
+
}
|
|
220
|
+
@media (min-width: 640px) { :root { --gutter: 24px; --work-gutter: 24px; --section-pad: ${pad.section[1]}px; --hero-pad: ${pad.hero[1]}px; } }
|
|
221
|
+
@media (min-width: 1024px) { :root { --gutter: 40px; --work-gutter: 32px; --section-pad: ${pad.section[2]}px; --hero-pad: ${pad.hero[2]}px; } }
|
|
222
|
+
@media (prefers-color-scheme: dark) {
|
|
223
|
+
:root:not([data-theme="light"]) {
|
|
224
|
+
${colors("dark", " ")}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
:root[data-theme="dark"] {
|
|
228
|
+
${colors("dark")}
|
|
229
|
+
}
|
|
230
|
+
`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const sha256 = (path) => createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
234
|
+
|
|
235
|
+
function kitFonts() {
|
|
236
|
+
const manifest = JSON.parse(readFileSync(join(KIT, "fonts.json"), "utf8"));
|
|
237
|
+
return manifest.fonts.map((font) => {
|
|
238
|
+
const source = resolve(KIT, font.source);
|
|
239
|
+
const licence = resolve(KIT, font.licence.source);
|
|
240
|
+
if (!existsSync(source) || sha256(source) !== font.sha256) throw new Error(`font ${font.file} is missing or does not match kit/fonts.json`);
|
|
241
|
+
if (!existsSync(licence) || sha256(licence) !== font.licence.sha256) throw new Error(`licence for ${font.family} is missing or does not match kit/fonts.json`);
|
|
242
|
+
return { ...font, sourcePath: source, licencePath: licence };
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// What a moment is and where it sits. A lock without a kind is valid to write (the page is not
|
|
247
|
+
// built yet) but DS-008 fails every render until the moment is declared and present.
|
|
248
|
+
function momentRecord(family, { kind = null, note = null, place = null } = {}) {
|
|
249
|
+
const scale = MOMENT_SCALE_BY_FAMILY[family];
|
|
250
|
+
if (kind !== null) {
|
|
251
|
+
const all = [...MOMENT_KINDS.bold, ...MOMENT_KINDS.small];
|
|
252
|
+
if (!all.includes(kind)) throw new Error(`unknown moment "${kind}"; one of ${all.join(", ")}`);
|
|
253
|
+
if (!MOMENT_KINDS[scale].includes(kind)) throw new Error(`a ${scale === "small" ? "work surface takes a small" : "read-once page takes a bold"} moment: ${MOMENT_KINDS[scale].join(", ")}`);
|
|
254
|
+
if (!String(note ?? "").trim()) throw new Error("a moment needs --moment-note: what it shows, in a few words");
|
|
255
|
+
}
|
|
256
|
+
return { scale, kind, note: kind === null ? null : String(note).trim(), place: kind === null || !String(place ?? "").trim() ? null : String(place).trim(), selector: MOMENT_SELECTOR, above_fold: scale === "bold" };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function lockRecord(style, family, moment = {}, density = "rich") {
|
|
260
|
+
return {
|
|
261
|
+
schema_id: LOCK_SCHEMA,
|
|
262
|
+
style: style.id,
|
|
263
|
+
family,
|
|
264
|
+
density: checkDensity(density),
|
|
265
|
+
fonts: [...new Set([style.fonts.text, style.fonts.display, style.fonts.mono])],
|
|
266
|
+
type: { scale_px: Object.values(style.type.scale_px), display_px: style.type.display_px, moment_px: style.type.moment_px },
|
|
267
|
+
radius_px: [...new Set([0, ...RADIUS_KEYS.map((key) => style.radius_px[key])])],
|
|
268
|
+
space_px: style.space_px,
|
|
269
|
+
colors: { light: style.color.light, dark: style.color.dark },
|
|
270
|
+
elevation: style.shape.elevation,
|
|
271
|
+
files: { tokens: "ui/tokens.css", components: "ui/components.css", script: "ui/ui.js" },
|
|
272
|
+
accent_art: style.accent_art ?? "none",
|
|
273
|
+
moment: momentRecord(family, moment),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function setMoment({ dir, kind, note, place = null }) {
|
|
278
|
+
const lockPath = join(resolve(dir), LOCK_FILE);
|
|
279
|
+
const lock = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
280
|
+
lock.moment = momentRecord(lock.family, { kind, note, place });
|
|
281
|
+
writeFileSync(lockPath, `${JSON.stringify(lock, null, 2)}\n`);
|
|
282
|
+
return lock.moment;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function initLock({ styleId = null, family, dir, scaffold = false, from = null, force = false, brief = null, moment = null, momentNote = null, momentPlace = null, density = null }) {
|
|
286
|
+
if (!FAMILIES[family]) throw new Error(`--family must be one of ${Object.keys(FAMILIES).join(", ")}`);
|
|
287
|
+
const register = checkDensity(density ?? densityForBrief(brief));
|
|
288
|
+
const declared = momentRecord(family, { kind: moment, note: momentNote, place: momentPlace });
|
|
289
|
+
const styles = kitStyles();
|
|
290
|
+
let style;
|
|
291
|
+
if (from) {
|
|
292
|
+
style = JSON.parse(readFileSync(resolve(from), "utf8"));
|
|
293
|
+
if (styles.some((kit) => kit.id === style.id)) throw new Error(`--from style id "${style.id}" reuses a kit style id; give the brand style its own id`);
|
|
294
|
+
} else {
|
|
295
|
+
style = styles.find((kit) => kit.id === (styleId ?? styleForBrief(brief, styles)?.id ?? DEFAULT_STYLE_BY_FAMILY[family]));
|
|
296
|
+
if (!style) throw new Error(`unknown style "${styleId}"; run \`design-lock.mjs list\``);
|
|
297
|
+
}
|
|
298
|
+
const issues = validateStyle(style);
|
|
299
|
+
if (issues.length) throw new Error(`style ${style.id} cannot be locked:\n- ${issues.join("\n- ")}`);
|
|
300
|
+
const project = resolve(dir);
|
|
301
|
+
mkdirSync(join(project, "ui", "fonts"), { recursive: true });
|
|
302
|
+
const lockPath = join(project, LOCK_FILE);
|
|
303
|
+
if (existsSync(lockPath) && !force) {
|
|
304
|
+
const current = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
305
|
+
if (current.style !== style.id) throw new Error(`${LOCK_FILE} already locks "${current.style}"; pass --force only if the user changed the direction`);
|
|
306
|
+
}
|
|
307
|
+
const fonts = kitFonts();
|
|
308
|
+
for (const font of fonts) {
|
|
309
|
+
copyFileSync(font.sourcePath, join(project, "ui", "fonts", font.file));
|
|
310
|
+
copyFileSync(font.licencePath, join(project, "ui", "fonts", font.licence.file));
|
|
311
|
+
}
|
|
312
|
+
writeFileSync(join(project, "ui", "tokens.css"), tokensCss(style, fonts, register));
|
|
313
|
+
copyFileSync(join(KIT, "components.css"), join(project, "ui", "components.css"));
|
|
314
|
+
copyFileSync(join(KIT, "ui.js"), join(project, "ui", "ui.js"));
|
|
315
|
+
writeFileSync(lockPath, `${JSON.stringify({ ...lockRecord(style, family, {}, register), moment: declared }, null, 2)}\n`);
|
|
316
|
+
let scaffolded = null;
|
|
317
|
+
if (scaffold) {
|
|
318
|
+
const page = join(project, "index.html");
|
|
319
|
+
if (existsSync(page) && !force) scaffolded = "kept existing index.html";
|
|
320
|
+
else {
|
|
321
|
+
copyFileSync(join(KIT, "layouts", `${family}.html`), page);
|
|
322
|
+
scaffolded = "index.html";
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return { style, family, project, scaffolded, density: register };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function parse(argv) {
|
|
329
|
+
const [command, ...rest] = argv;
|
|
330
|
+
const options = { command };
|
|
331
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
332
|
+
const arg = rest[i];
|
|
333
|
+
if (arg === "--scaffold" || arg === "--force") options[arg.slice(2)] = true;
|
|
334
|
+
else if (["--style", "--family", "--dir", "--from", "--brief", "--moment", "--moment-note", "--moment-place", "--density", "--kind", "--note", "--place"].includes(arg)) options[arg.slice(2)] = rest[++i];
|
|
335
|
+
else if (command === "match" && options.text === undefined) options.text = arg;
|
|
336
|
+
else throw new Error(`unknown argument ${arg}`);
|
|
337
|
+
}
|
|
338
|
+
return options;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function main(argv) {
|
|
342
|
+
const options = parse(argv);
|
|
343
|
+
if (options.command === "list") {
|
|
344
|
+
for (const style of kitStyles()) {
|
|
345
|
+
console.log(`${style.id} — ${style.name}\n ${style.summary}\n use when: ${style.use_when.join("; ")}\n avoid when: ${style.avoid_when.join("; ")}\n picked by: ${style.keywords.join(", ")}\n signature: ${style.signature.join("; ")}\n never: ${style.forbid.join("; ")}\n`);
|
|
346
|
+
}
|
|
347
|
+
console.log("Families (layout starters in kit/layouts/):");
|
|
348
|
+
for (const [id, text] of Object.entries(FAMILIES)) console.log(` ${id} — ${text} (default style: ${DEFAULT_STYLE_BY_FAMILY[id]})\n moments (${id}): ${MOMENT_KINDS[MOMENT_SCALE_BY_FAMILY[id]].join(", ")}`);
|
|
349
|
+
return 0;
|
|
350
|
+
}
|
|
351
|
+
if (options.command === "init") {
|
|
352
|
+
if (!options.dir || !options.family) throw new Error("init needs --family and --dir (--style or --from only to override the family default)");
|
|
353
|
+
const result = initLock({ styleId: options.style, family: options.family, dir: options.dir, scaffold: options.scaffold, from: options.from, force: options.force, brief: options.brief, density: options.density ?? null, moment: options.moment ?? null, momentNote: options["moment-note"] ?? null, momentPlace: options["moment-place"] ?? null });
|
|
354
|
+
console.log(`Locked "${result.style.id}" (${result.family}, ${result.density}) in ${result.project}`);
|
|
355
|
+
console.log(`Wrote ${LOCK_FILE}, ui/tokens.css, ui/components.css, ui/ui.js and ui/fonts/.${result.scaffolded ? ` Scaffold: ${result.scaffolded}.` : ""}`);
|
|
356
|
+
console.log("Link ui/tokens.css then ui/components.css (and ui/ui.js with defer when you use charts, sortable tables or tabs).");
|
|
357
|
+
console.log("Build with the kit's ui-* classes and var(--c-*), var(--fs-*), var(--sp-*), var(--radius-*) only; the interface probe checks the page against this lock.");
|
|
358
|
+
const declared = JSON.parse(readFileSync(join(result.project, LOCK_FILE), "utf8")).moment;
|
|
359
|
+
console.log(declared.kind
|
|
360
|
+
? `Signature moment: ${declared.kind} (${declared.note}). Mark that one element with data-ui-moment="${declared.kind}"${declared.above_fold ? " in the first screen" : ""}.`
|
|
361
|
+
: `No signature moment yet: decide the ${declared.scale} moment (${MOMENT_KINDS[declared.scale].join(", ")}) and record it with \`design-lock.mjs moment\`; the probe fails the page until it is declared and present (DS-008).`);
|
|
362
|
+
return 0;
|
|
363
|
+
}
|
|
364
|
+
if (options.command === "moment") {
|
|
365
|
+
if (!options.dir || !options.kind) throw new Error("moment needs --dir, --kind and --note");
|
|
366
|
+
const moment = setMoment({ dir: options.dir, kind: options.kind, note: options.note, place: options.place ?? null });
|
|
367
|
+
console.log(`Recorded ${moment.scale} moment ${moment.kind}: mark it with data-ui-moment="${moment.kind}"${moment.above_fold ? " in the first screen" : ""}.`);
|
|
368
|
+
return 0;
|
|
369
|
+
}
|
|
370
|
+
if (options.command === "match") {
|
|
371
|
+
const hit = styleForBrief(options.text);
|
|
372
|
+
console.log(hit ? `${hit.id} (keyword "${hit.keyword}")` : "no style named; the family default applies");
|
|
373
|
+
const density = densityForBrief(options.text);
|
|
374
|
+
console.log(`density: ${density}${density === "calm" ? " (the request asks for a calm page)" : " (the default; calm only when the request asks)"}`);
|
|
375
|
+
return 0;
|
|
376
|
+
}
|
|
377
|
+
if (options.command === "show") {
|
|
378
|
+
const lock = JSON.parse(readFileSync(join(resolve(options.dir ?? "."), LOCK_FILE), "utf8"));
|
|
379
|
+
console.log(JSON.stringify({ style: lock.style, family: lock.family, density: lock.density ?? null, fonts: lock.fonts, type: lock.type, radius_px: lock.radius_px, moment: lock.moment ?? null }, null, 2));
|
|
380
|
+
return 0;
|
|
381
|
+
}
|
|
382
|
+
throw new Error("usage: design-lock.mjs list | init [--style <id>] [--brief <text>] [--density rich|calm] --family <landing|workspace|settings> --dir <project> [--moment <kind> --moment-note <text> [--moment-place <text>]] [--scaffold] [--from <style.json>] [--force] | moment --dir <project> --kind <kind> --note <text> [--place <text>] | match <text> | show --dir <project>");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const startedDirectly = () => {
|
|
386
|
+
try {
|
|
387
|
+
return Boolean(process.argv[1]) && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
388
|
+
} catch {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
if (startedDirectly()) {
|
|
394
|
+
try {
|
|
395
|
+
process.exitCode = main(process.argv.slice(2));
|
|
396
|
+
} catch (error) {
|
|
397
|
+
console.error(`design-lock: ${error.message}`);
|
|
398
|
+
process.exitCode = 1;
|
|
399
|
+
}
|
|
400
|
+
}
|