@askewly/design 0.3.0 → 0.4.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/data/tokens.css CHANGED
@@ -16,9 +16,27 @@
16
16
  --accent: #B9FAF8;
17
17
  --accent-foreground: oklch(0.16 0.015 270);
18
18
  --destructive: oklch(0.58 0.22 27);
19
+ --destructive-foreground: oklch(1 0 0);
19
20
  --border: oklch(0.88 0.015 270);
20
21
  --input: oklch(0.88 0.015 270);
21
22
  --ring: #6F2DBD;
23
+ --primary-hover: #5F22A8;
24
+ --emphasis-surface: oklch(0.962 0.018 272.314);
25
+ --emphasis-foreground: oklch(0.457 0.24 277.023);
26
+ --emphasis-solid: oklch(0.511 0.262 276.966);
27
+ --emphasis-solid-hover: oklch(0.585 0.233 277.117);
28
+ --emphasis-ring: oklch(0.585 0.233 277.117);
29
+ --emphasis-on-solid: oklch(1 0 0);
30
+ --info-surface: oklch(0.951 0.026 236.824);
31
+ --info-foreground: oklch(0.5 0.134 242.749);
32
+ --info-link: oklch(0.685 0.169 237.323);
33
+ --info-link-hover: oklch(0.588 0.158 241.966);
34
+ --success-surface: oklch(0.979 0.021 166.113);
35
+ --success-foreground: oklch(0.508 0.118 165.612);
36
+ --danger-surface: oklch(0.941 0.03 12.58);
37
+ --danger-foreground: oklch(0.586 0.253 17.585);
38
+ --danger-solid: oklch(0.586 0.253 17.585);
39
+ --danger-solid-hover: oklch(0.645 0.246 16.439);
22
40
  --askewly-violet: #6F2DBD;
23
41
  --askewly-orchid: #A663CC;
24
42
  --askewly-lavender: #B298DC;
@@ -30,11 +48,16 @@
30
48
  --space-8: 32px;
31
49
  --space-12: 48px;
32
50
  --space-16: 64px;
51
+ --font-size-2xs: 0.625rem;
52
+ --font-size-xs: 0.75rem;
33
53
  --font-size-sm: 14px;
34
54
  --font-size-base: 16px;
35
55
  --font-size-lg: 20px;
36
56
  --font-size-xl: 28px;
37
57
  --font-size-2xl: 40px;
58
+ --font-size-3xl: 1.875rem;
59
+ --font-size-5xl: 3rem;
60
+ --font-size-7xl: 4.5rem;
38
61
  --font-weight-regular: 400;
39
62
  --font-weight-medium: 500;
40
63
  --font-sans: Geist, "Noto Sans KR", ui-sans-serif, system-ui, sans-serif;
@@ -61,7 +84,25 @@
61
84
  --accent: oklch(0.35 0.03 270);
62
85
  --accent-foreground: oklch(0.985 0 0);
63
86
  --destructive: oklch(0.58 0.22 27);
87
+ --destructive-foreground: oklch(1 0 0);
64
88
  --border: oklch(0.35 0.03 270);
65
89
  --input: oklch(0.35 0.03 270);
66
90
  --ring: #B298DC;
91
+ --primary-hover: #A663CC;
92
+ --emphasis-surface: oklch(0.257 0.09 281.288);
93
+ --emphasis-foreground: oklch(0.785 0.115 274.713);
94
+ --emphasis-solid: oklch(0.585 0.233 277.117);
95
+ --emphasis-solid-hover: oklch(0.673 0.182 276.935);
96
+ --emphasis-ring: oklch(0.673 0.182 276.935);
97
+ --emphasis-on-solid: oklch(1 0 0);
98
+ --info-surface: oklch(0.293 0.066 243.157);
99
+ --info-foreground: oklch(0.828 0.111 230.318);
100
+ --info-link: oklch(0.746 0.16 232.661);
101
+ --info-link-hover: oklch(0.828 0.111 230.318);
102
+ --success-surface: oklch(0.262 0.051 172.552);
103
+ --success-foreground: oklch(0.845 0.143 164.978);
104
+ --danger-surface: oklch(0.271 0.105 12.094);
105
+ --danger-foreground: oklch(0.712 0.194 13.428);
106
+ --danger-solid: oklch(0.586 0.253 17.585);
107
+ --danger-solid-hover: oklch(0.645 0.246 16.439);
67
108
  }
package/data/tokens.json CHANGED
@@ -1 +1 @@
1
- {"$description":"Askewly Design token SSOT (DTCG-compatible, stable 2025.10 subset). Tier rule: component -> semantic -> primitive, one direction only. Dark mode lives in $extensions['com.askewly.modes'].dark on semantic tokens. Generated artifacts: DESIGN.md frontmatter, examples/ui-vocabulary-site/src/tokens.css. Do not edit generated files by hand.","color":{"primitive":{"$description":"Raw values only. gray is the site's violet-tinted neutral (hue 270) formalized as a 12-step ramp; usage convention: 1-2 app background, 3-5 interactive component states, 6-8 borders, 9-10 solid/low-contrast foreground, 11-12 text.","white":{"$type":"color","$value":{"colorSpace":"oklch","components":[1,0,0]}},"gray":{"1":{"$value":{"colorSpace":"oklch","components":[0.985,0,0]}},"2":{"$value":{"colorSpace":"oklch","components":[0.97,0.005,270]}},"3":{"$value":{"colorSpace":"oklch","components":[0.95,0.015,270]}},"4":{"$value":{"colorSpace":"oklch","components":[0.94,0.02,270]}},"5":{"$value":{"colorSpace":"oklch","components":[0.91,0.015,270]}},"6":{"$value":{"colorSpace":"oklch","components":[0.88,0.015,270]}},"7":{"$value":{"colorSpace":"oklch","components":[0.72,0.02,270]}},"8":{"$value":{"colorSpace":"oklch","components":[0.6,0.025,270]}},"9":{"$value":{"colorSpace":"oklch","components":[0.46,0.03,270]}},"10":{"$value":{"colorSpace":"oklch","components":[0.35,0.03,270]}},"11":{"$value":{"colorSpace":"oklch","components":[0.25,0.03,270]}},"12":{"$value":{"colorSpace":"oklch","components":[0.16,0.015,270]}},"$type":"color"},"askewly":{"$type":"color","$description":"Brand palette extracted from the landing planning reference image (2026-07-05). Canonical purple: violet.","violet":{"$value":{"colorSpace":"srgb","components":[0.435294,0.176471,0.741176],"hex":"#6F2DBD"}},"orchid":{"$value":{"colorSpace":"srgb","components":[0.65098,0.388235,0.8],"hex":"#A663CC"}},"lavender":{"$value":{"colorSpace":"srgb","components":[0.698039,0.596078,0.862745],"hex":"#B298DC"}},"sky":{"$value":{"colorSpace":"srgb","components":[0.721569,0.815686,0.921569],"hex":"#B8D0EB"}},"mint":{"$value":{"colorSpace":"srgb","components":[0.72549,0.980392,0.972549],"hex":"#B9FAF8"}}},"red":{"9":{"$value":{"colorSpace":"oklch","components":[0.58,0.22,27]}},"$type":"color"}},"semantic":{"$description":"Role tokens. Components and site CSS consume this layer only. Dark values in $extensions['com.askewly.modes'].dark.","surface":{"$type":"color","base":{"$value":"{color.primitive.gray.1}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.12}"}}},"raised":{"$description":"Cards, panels sitting on base.","$value":"{color.primitive.white}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.11}"}}},"overlay":{"$description":"Popovers, dropdowns, floating layers.","$value":"{color.primitive.white}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.11}"}}},"muted":{"$value":"{color.primitive.gray.3}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.10}"}}},"secondary":{"$description":"Secondary button / subtle filled surface.","$value":"{color.primitive.gray.4}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.10}"}}},"tint":{"$description":"Informational wash, agent-ready signal surface.","$value":"{color.primitive.askewly.mint}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.10}"}}}},"text":{"$type":"color","default":{"$value":"{color.primitive.gray.12}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.1}"}}},"muted":{"$value":"{color.primitive.gray.9}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.7}"}}},"secondary":{"$description":"Foreground on surface.secondary.","$value":"{color.primitive.gray.11}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.3}"}}},"on-accent":{"$description":"Text/icon on action.primary solid fills.","$value":"{color.primitive.gray.1}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.12}"}}}},"action":{"$type":"color","primary":{"$value":"{color.primitive.askewly.violet}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.1}"}}},"secondary":{"$value":"{color.primitive.askewly.orchid}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.askewly.mint}"}}},"destructive":{"$value":"{color.primitive.red.9}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.red.9}"}}}},"accent":{"$type":"color","base":{"$description":"Informational accent wash (mint family).","$value":"{color.primitive.askewly.mint}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.10}"}}},"foreground":{"$value":"{color.primitive.gray.12}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.1}"}}}},"border":{"$type":"color","default":{"$value":"{color.primitive.gray.6}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.10}"}}},"input":{"$value":"{color.primitive.gray.6}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.10}"}}},"focus":{"$description":"Focus ring.","$value":"{color.primitive.askewly.violet}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.askewly.lavender}"}}},"accent":{"$description":"Selected/active edges only.","$value":"{color.primitive.askewly.lavender}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.askewly.orchid}"}}}}},"component":{"$description":"Minimal by policy (survey criterion A-5): only overrides that semantic roles cannot express. Exemplar tier for the lint chain.","button":{"$type":"color","bg":{"$value":"{color.semantic.action.primary}"},"text":{"$value":"{color.semantic.text.on-accent}"}}}},"dimension":{"space":{"1":{"$value":{"value":4,"unit":"px"}},"2":{"$value":{"value":8,"unit":"px"}},"4":{"$value":{"value":16,"unit":"px"}},"8":{"$value":{"value":32,"unit":"px"}},"12":{"$value":{"value":48,"unit":"px"}},"16":{"$value":{"value":64,"unit":"px"}},"$type":"dimension","$description":"4/8 grid. Reference variables only - not bound into the Tailwind theme namespace."},"radius":{"$type":"dimension","$description":"Canonical = shipped site values (PSS1 visually approved), not the older 2/4px draft.","sm":{"$value":{"value":4,"unit":"px"}},"md":{"$value":{"value":6,"unit":"px"}},"lg":{"$value":{"value":8,"unit":"px"}},"xl":{"$value":{"value":12,"unit":"px"}},"2xl":{"$description":"SD gen2 (2026-07-12): large-surface corner radius for sheets/overlays that sit above the base scale — device-frame inner viewport, bottom-sheet-detents. Recurs identically in >=2 mobile components as Tailwind's built-in rounded-2xl (16px), currently outside the tokenized sm-xl range.","$value":{"value":16,"unit":"px"}}},"size":{"$type":"dimension","$description":"Fixed-purpose sizing constants that are not part of the 4/8 spacing grid. SD gen2 (2026-07-12): touch-target minimum recurs across mobile surfaces (large-title-collapsing-header back button, mobile-signup-field-stack inputs/submit, plus pre-existing search-autocomplete/showcase-card/term-visual usage) as Tailwind size-11/h-11/min-h-11 (44px) — the WCAG 2.5.5 / iOS HIG minimum hit target.","touch-target-min":{"$value":{"value":44,"unit":"px"}}},"z-index":{"$type":"number","$description":"SD gen2 (2026-07-12): layering scale distilled from recurring hardcoded z-index values across mobile overlays and dense-data sticky headers. 'sticky' = content pinned within its own scroll container (collapsing header bar, bottom-sheet/action-sheet root within DeviceFrame, table sticky header row). 'elevated' = a second layer above a sticky layer in the same local stacking context (permission-matrix-editor-grid's sticky corner cell, which must sit above both its own header row and column). 'overlay' = page-level modal/scrim/popover stacking context (ui/dialog, ui/sheet, ui/popover, ui/tooltip, full-screen confirm overlays).","sticky":{"$value":10},"elevated":{"$value":20},"overlay":{"$value":50}}},"typography":{"font":{"$type":"fontFamily","sans":{"$value":["Geist","Noto Sans KR","ui-sans-serif","system-ui","sans-serif"]},"mono":{"$value":["Geist Mono","ui-monospace","monospace"]}},"scale":{"$type":"dimension","sm":{"$value":{"value":14,"unit":"px"}},"base":{"$value":{"value":16,"unit":"px"}},"lg":{"$value":{"value":20,"unit":"px"}},"xl":{"$value":{"value":28,"unit":"px"}},"2xl":{"$value":{"value":40,"unit":"px"}}},"weight":{"$type":"fontWeight","regular":{"$value":400},"medium":{"$value":500}}},"motion":{"$description":"SD gen2 (2026-07-12): motion timing constants. Kept minimal by the same policy as color.component — only the value that actually recurs, not a full scale invented ahead of need.","duration":{"$type":"duration","overlay":{"$description":"Open/close transition for modal-style surfaces. Recurs identically (200ms) in bottom-sheet-detents.tsx and ui/dialog.tsx.","$value":{"value":200,"unit":"ms"}}}}}
1
+ {"$description":"Askewly Design token SSOT (DTCG-compatible, stable 2025.10 subset). Tier rule: component -> semantic -> primitive, one direction only. Dark mode lives in $extensions['com.askewly.modes'].dark on semantic tokens. Generated artifacts: DESIGN.md frontmatter, examples/ui-vocabulary-site/src/tokens.css. Do not edit generated files by hand.","color":{"primitive":{"$description":"Raw values only. gray is the site's violet-tinted neutral (hue 270) formalized as a 12-step ramp; usage convention: 1-2 app background, 3-5 interactive component states, 6-8 borders, 9-10 solid/low-contrast foreground, 11-12 text.","white":{"$type":"color","$value":{"colorSpace":"oklch","components":[1,0,0]}},"gray":{"1":{"$value":{"colorSpace":"oklch","components":[0.985,0,0]}},"2":{"$value":{"colorSpace":"oklch","components":[0.97,0.005,270]}},"3":{"$value":{"colorSpace":"oklch","components":[0.95,0.015,270]}},"4":{"$value":{"colorSpace":"oklch","components":[0.94,0.02,270]}},"5":{"$value":{"colorSpace":"oklch","components":[0.91,0.015,270]}},"6":{"$value":{"colorSpace":"oklch","components":[0.88,0.015,270]}},"7":{"$value":{"colorSpace":"oklch","components":[0.72,0.02,270]}},"8":{"$value":{"colorSpace":"oklch","components":[0.6,0.025,270]}},"9":{"$value":{"colorSpace":"oklch","components":[0.46,0.03,270]}},"10":{"$value":{"colorSpace":"oklch","components":[0.35,0.03,270]}},"11":{"$value":{"colorSpace":"oklch","components":[0.25,0.03,270]}},"12":{"$value":{"colorSpace":"oklch","components":[0.16,0.015,270]}},"$type":"color"},"askewly":{"$type":"color","$description":"Brand palette extracted from the landing planning reference image (2026-07-05). Canonical purple: violet.","violet":{"$value":{"colorSpace":"srgb","components":[0.435294,0.176471,0.741176],"hex":"#6F2DBD"}},"orchid":{"$value":{"colorSpace":"srgb","components":[0.65098,0.388235,0.8],"hex":"#A663CC"}},"lavender":{"$value":{"colorSpace":"srgb","components":[0.698039,0.596078,0.862745],"hex":"#B298DC"}},"sky":{"$value":{"colorSpace":"srgb","components":[0.721569,0.815686,0.921569],"hex":"#B8D0EB"}},"mint":{"$value":{"colorSpace":"srgb","components":[0.72549,0.980392,0.972549],"hex":"#B9FAF8"}},"violet-deep":{"$description":"Hover shade of the canonical brand violet (M2 2026-08-01, promoted from hardcoded #5f22a8).","$value":{"colorSpace":"srgb","components":[0.372549,0.133333,0.658824],"hex":"#5F22A8"}}},"red":{"9":{"$value":{"colorSpace":"oklch","components":[0.58,0.22,27]}},"$type":"color"},"indigo":{"50":{"$value":{"colorSpace":"oklch","components":[0.962,0.018,272.314]}},"300":{"$value":{"colorSpace":"oklch","components":[0.785,0.115,274.713]}},"400":{"$value":{"colorSpace":"oklch","components":[0.673,0.182,276.935]}},"500":{"$value":{"colorSpace":"oklch","components":[0.585,0.233,277.117]}},"600":{"$value":{"colorSpace":"oklch","components":[0.511,0.262,276.966]}},"700":{"$value":{"colorSpace":"oklch","components":[0.457,0.24,277.023]}},"950":{"$value":{"colorSpace":"oklch","components":[0.257,0.09,281.288]}},"$type":"color","$description":"Emphasis/selection accent ramp (M2 2026-08-01). Values = Tailwind v4 defaults the site already rendered, promoted verbatim for light-mode visual losslessness."},"skyx":{"100":{"$value":{"colorSpace":"oklch","components":[0.951,0.026,236.824]}},"300":{"$value":{"colorSpace":"oklch","components":[0.828,0.111,230.318]}},"400":{"$value":{"colorSpace":"oklch","components":[0.746,0.16,232.661]}},"500":{"$value":{"colorSpace":"oklch","components":[0.685,0.169,237.323]}},"600":{"$value":{"colorSpace":"oklch","components":[0.588,0.158,241.966]}},"700":{"$value":{"colorSpace":"oklch","components":[0.5,0.134,242.749]}},"950":{"$value":{"colorSpace":"oklch","components":[0.293,0.066,243.157]}},"$type":"color","$description":"Informational accent ramp (M2 2026-08-01, Tailwind v4 sky — named skyx to avoid clashing with brand askewly.sky)."},"emerald":{"50":{"$value":{"colorSpace":"oklch","components":[0.979,0.021,166.113]}},"300":{"$value":{"colorSpace":"oklch","components":[0.845,0.143,164.978]}},"700":{"$value":{"colorSpace":"oklch","components":[0.508,0.118,165.612]}},"950":{"$value":{"colorSpace":"oklch","components":[0.262,0.051,172.552]}},"$type":"color","$description":"Success accent ramp (M2 2026-08-01, Tailwind v4 defaults)."},"rose":{"100":{"$value":{"colorSpace":"oklch","components":[0.941,0.03,12.58]}},"400":{"$value":{"colorSpace":"oklch","components":[0.712,0.194,13.428]}},"500":{"$value":{"colorSpace":"oklch","components":[0.645,0.246,16.439]}},"600":{"$value":{"colorSpace":"oklch","components":[0.586,0.253,17.585]}},"950":{"$value":{"colorSpace":"oklch","components":[0.271,0.105,12.094]}},"$type":"color","$description":"Danger accent ramp (M2 2026-08-01, Tailwind v4 defaults). Distinct from red.9 (--destructive): the site's danger demos render rose, and the shades intentionally differ."}},"semantic":{"$description":"Role tokens. Components and site CSS consume this layer only. Dark values in $extensions['com.askewly.modes'].dark.","surface":{"$type":"color","base":{"$value":"{color.primitive.gray.1}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.12}"}}},"raised":{"$description":"Cards, panels sitting on base.","$value":"{color.primitive.white}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.11}"}}},"overlay":{"$description":"Popovers, dropdowns, floating layers.","$value":"{color.primitive.white}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.11}"}}},"muted":{"$value":"{color.primitive.gray.3}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.10}"}}},"secondary":{"$description":"Secondary button / subtle filled surface.","$value":"{color.primitive.gray.4}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.10}"}}},"tint":{"$description":"Informational wash, agent-ready signal surface.","$value":"{color.primitive.askewly.mint}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.10}"}}}},"text":{"$type":"color","default":{"$value":"{color.primitive.gray.12}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.1}"}}},"muted":{"$value":"{color.primitive.gray.9}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.7}"}}},"secondary":{"$description":"Foreground on surface.secondary.","$value":"{color.primitive.gray.11}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.3}"}}},"on-accent":{"$description":"Text/icon on action.primary solid fills.","$value":"{color.primitive.gray.1}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.12}"}}},"on-destructive":{"$description":"Text/icon on action.destructive solid fills. Theme-invariant white (M2: promotes the settled 'text-white is correct' marker verdict into a token).","$value":"{color.primitive.white}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.white}"}}}},"action":{"$type":"color","primary":{"$value":"{color.primitive.askewly.violet}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.1}"}}},"primary-hover":{"$description":"Hover state for brand-violet fills/text (M2). Dark uses the brighter orchid — darkening on hover loses contrast on dark surfaces.","$value":"{color.primitive.askewly.violet-deep}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.askewly.orchid}"}}},"secondary":{"$value":"{color.primitive.askewly.orchid}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.askewly.mint}"}}},"destructive":{"$value":"{color.primitive.red.9}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.red.9}"}}}},"emphasis":{"$type":"color","$description":"Selection/emphasis accent (indigo family, M2 2026-08-01). Distinct from 'accent' (shadcn interaction-state wash, mint family) — do not merge (group-semantics collision, fresh-verifier finding).","surface":{"$description":"Chip/selected-row wash.","$value":"{color.primitive.indigo.50}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.indigo.950}"}}},"foreground":{"$description":"Text on emphasis.surface.","$value":"{color.primitive.indigo.700}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.indigo.300}"}}},"solid":{"$description":"Solid emphasis fill (buttons).","$value":"{color.primitive.indigo.600}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.indigo.500}"}}},"solid-hover":{"$value":"{color.primitive.indigo.500}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.indigo.400}"}}},"ring":{"$description":"Focus border/ring on emphasis-accented inputs.","$value":"{color.primitive.indigo.500}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.indigo.400}"}}},"on-solid":{"$description":"Text/icon on emphasis.solid fills. White holds contrast on indigo-600 (light) and indigo-500 (dark).","$value":"{color.primitive.white}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.white}"}}}},"status":{"$type":"color","$description":"Status colors (M2 2026-08-01). info=sky, success=emerald, danger=rose. Kept out of 'accent' (interaction-state group).","info-surface":{"$value":"{color.primitive.skyx.100}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.skyx.950}"}}},"info-foreground":{"$value":"{color.primitive.skyx.700}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.skyx.300}"}}},"info-link":{"$description":"Standalone info-accented link/action text.","$value":"{color.primitive.skyx.500}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.skyx.400}"}}},"info-link-hover":{"$value":"{color.primitive.skyx.600}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.skyx.300}"}}},"success-surface":{"$value":"{color.primitive.emerald.50}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.emerald.950}"}}},"success-foreground":{"$value":"{color.primitive.emerald.700}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.emerald.300}"}}},"danger-surface":{"$value":"{color.primitive.rose.100}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.rose.950}"}}},"danger-foreground":{"$value":"{color.primitive.rose.600}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.rose.400}"}}},"danger-solid":{"$value":"{color.primitive.rose.600}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.rose.600}"}}},"danger-solid-hover":{"$value":"{color.primitive.rose.500}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.rose.500}"}}}},"accent":{"$type":"color","base":{"$description":"Informational accent wash (mint family).","$value":"{color.primitive.askewly.mint}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.10}"}}},"foreground":{"$value":"{color.primitive.gray.12}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.1}"}}}},"border":{"$type":"color","default":{"$value":"{color.primitive.gray.6}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.10}"}}},"input":{"$value":"{color.primitive.gray.6}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.gray.10}"}}},"focus":{"$description":"Focus ring.","$value":"{color.primitive.askewly.violet}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.askewly.lavender}"}}},"accent":{"$description":"Selected/active edges only.","$value":"{color.primitive.askewly.lavender}","$extensions":{"com.askewly.modes":{"dark":"{color.primitive.askewly.orchid}"}}}}},"component":{"$description":"Minimal by policy (survey criterion A-5): only overrides that semantic roles cannot express. Exemplar tier for the lint chain.","button":{"$type":"color","bg":{"$value":"{color.semantic.action.primary}"},"text":{"$value":"{color.semantic.text.on-accent}"}}}},"dimension":{"space":{"1":{"$value":{"value":4,"unit":"px"}},"2":{"$value":{"value":8,"unit":"px"}},"4":{"$value":{"value":16,"unit":"px"}},"8":{"$value":{"value":32,"unit":"px"}},"12":{"$value":{"value":48,"unit":"px"}},"16":{"$value":{"value":64,"unit":"px"}},"$type":"dimension","$description":"4/8 grid. Reference variables only - not bound into the Tailwind theme namespace."},"radius":{"$type":"dimension","$description":"Canonical = shipped site values (PSS1 visually approved), not the older 2/4px draft.","sm":{"$value":{"value":4,"unit":"px"}},"md":{"$value":{"value":6,"unit":"px"}},"lg":{"$value":{"value":8,"unit":"px"}},"xl":{"$value":{"value":12,"unit":"px"}},"2xl":{"$description":"SD gen2 (2026-07-12): large-surface corner radius for sheets/overlays that sit above the base scale — device-frame inner viewport, bottom-sheet-detents. Recurs identically in >=2 mobile components as Tailwind's built-in rounded-2xl (16px), currently outside the tokenized sm-xl range.","$value":{"value":16,"unit":"px"}}},"size":{"$type":"dimension","$description":"Fixed-purpose sizing constants that are not part of the 4/8 spacing grid. SD gen2 (2026-07-12): touch-target minimum recurs across mobile surfaces (large-title-collapsing-header back button, mobile-signup-field-stack inputs/submit, plus pre-existing search-autocomplete/showcase-card/term-visual usage) as Tailwind size-11/h-11/min-h-11 (44px) — the WCAG 2.5.5 / iOS HIG minimum hit target.","touch-target-min":{"$value":{"value":44,"unit":"px"}}},"z-index":{"$type":"number","$description":"SD gen2 (2026-07-12): layering scale distilled from recurring hardcoded z-index values across mobile overlays and dense-data sticky headers. 'sticky' = content pinned within its own scroll container (collapsing header bar, bottom-sheet/action-sheet root within DeviceFrame, table sticky header row). 'elevated' = a second layer above a sticky layer in the same local stacking context (permission-matrix-editor-grid's sticky corner cell, which must sit above both its own header row and column). 'overlay' = page-level modal/scrim/popover stacking context (ui/dialog, ui/sheet, ui/popover, ui/tooltip, full-screen confirm overlays).","sticky":{"$value":10},"elevated":{"$value":20},"overlay":{"$value":50}}},"typography":{"font":{"$type":"fontFamily","sans":{"$value":["Geist","Noto Sans KR","ui-sans-serif","system-ui","sans-serif"]},"mono":{"$value":["Geist Mono","ui-monospace","monospace"]}},"scale":{"$type":"dimension","$description":"M6 2026-08-01: expanded to the 9 steps screens actually render (M1 closeout measurement). New steps (2xs/xs/3xl/5xl/7xl) carry Tailwind v4 default values verbatim in rem — they are wired into @theme --text-* so existing utility classes route through the SSOT with zero render change. Legacy 5 names (sm/base/lg/xl/2xl, px) are kept for backward compat; lg(20) and 2xl(40) differ from Tailwind's same-named defaults and are NOT wired.","2xs":{"$description":"Micro labels (10px at default root). New utility text-2xs — Tailwind has no built-in 2xs.","$value":{"value":0.625,"unit":"rem"}},"xs":{"$value":{"value":0.75,"unit":"rem"}},"sm":{"$value":{"value":14,"unit":"px"}},"base":{"$value":{"value":16,"unit":"px"}},"lg":{"$value":{"value":20,"unit":"px"}},"xl":{"$description":"Unused by live screens as of M1 measurement (2026-08-01) — kept as record for llms consumers.","$value":{"value":28,"unit":"px"}},"2xl":{"$value":{"value":40,"unit":"px"}},"3xl":{"$value":{"value":1.875,"unit":"rem"}},"5xl":{"$value":{"value":3,"unit":"rem"}},"7xl":{"$value":{"value":4.5,"unit":"rem"}}},"weight":{"$type":"fontWeight","regular":{"$value":400},"medium":{"$value":500}}},"motion":{"$description":"SD gen2 (2026-07-12): motion timing constants. Kept minimal by the same policy as color.component — only the value that actually recurs, not a full scale invented ahead of need.","duration":{"$type":"duration","overlay":{"$description":"Open/close transition for modal-style surfaces. Recurs identically (200ms) in bottom-sheet-detents.tsx and ui/dialog.tsx.","$value":{"value":200,"unit":"ms"}}}}}
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { Command } from "commander";
5
5
  import { addRecipe, initProject, InjectError } from "./inject.js";
6
+ import { runKickstart, KickstartError } from "./kickstart.js";
6
7
  import { DEFAULT_TYPOGRAPHY_THRESHOLD } from "./typography.js";
7
8
  import { verifyDir } from "./verify.js";
8
9
  import { loadRecipes, loadTerms, loadTokens, loadTokensCss, searchTerms, tokensForTier, } from "./load.js";
@@ -104,8 +105,25 @@ program
104
105
  .command("init")
105
106
  .argument("[dir]", "target directory", ".")
106
107
  .option("--force", "overwrite existing files")
107
- .description("write DESIGN.md and tokens.css into a project")
108
- .action((dir, options) => {
108
+ .option("--block <name>", "kickstart: brief → DESIGN.md → tokens → transplant this block → verify (block-contract §8)")
109
+ .option("--yes", "kickstart: accept recommended defaults for unanswered brief questions")
110
+ .option("--tone <tone>", "kickstart brief answer (agent path)")
111
+ .option("--color <color>", "kickstart brief answer (agent path)")
112
+ .option("--type <type>", "kickstart brief answer (agent path)")
113
+ .option("--registry <baseUrl>", "kickstart: registry base URL", "https://ui.askewly.com")
114
+ .description("write DESIGN.md and tokens.css into a project; with --block, run the one-command kickstart")
115
+ .action(async (dir, options) => {
116
+ if (options.block) {
117
+ try {
118
+ await runKickstart(dir, { block: options.block, yes: options.yes, tone: options.tone, color: options.color, type: options.type, registry: options.registry, force: options.force });
119
+ }
120
+ catch (error) {
121
+ if (error instanceof KickstartError)
122
+ fail(error.message);
123
+ throw error;
124
+ }
125
+ return;
126
+ }
109
127
  try {
110
128
  const written = initProject(path.resolve(dir), Boolean(options.force));
111
129
  for (const file of written)
@@ -0,0 +1,410 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { createInterface } from "node:readline/promises";
4
+ import { stdin, stdout } from "node:process";
5
+ import { verifyDir } from "./verify.js";
6
+ export class KickstartError extends Error {
7
+ }
8
+ const REGISTRY_BASE = "https://ui.askewly.com";
9
+ const SHADCN_BASE = "https://ui.shadcn.com/r/styles/new-york-v4";
10
+ const BRIEF_QUESTIONS = [
11
+ {
12
+ key: "tone",
13
+ label: "Tone — how should the product feel?",
14
+ options: ["minimal-clean", "warm-editorial", "dark-technical"],
15
+ recommended: "minimal-clean",
16
+ },
17
+ {
18
+ key: "color",
19
+ label: "Color direction — the single accent family",
20
+ options: ["blue", "teal", "violet", "amber", "cosmos"],
21
+ recommended: "blue",
22
+ },
23
+ {
24
+ key: "type",
25
+ label: "Type direction — one family, hierarchy by weight/size",
26
+ options: ["system-sans", "geist-sans", "humanist-serif-display"],
27
+ recommended: "system-sans",
28
+ },
29
+ ];
30
+ const FONT_STACKS = {
31
+ "system-sans": "system-ui, 'Noto Sans KR', sans-serif",
32
+ "geist-sans": "'Geist', 'Noto Sans KR', system-ui, sans-serif",
33
+ "humanist-serif-display": "'Source Serif 4', 'Noto Serif KR', Georgia, serif",
34
+ };
35
+ // Accent hue pairs. Hex only — the flat-colors DESIGN.md parser (slide-theme
36
+ // converter, parser A) accepts hex exclusively; the template stays canonical,
37
+ // the generator conforms (M17 decision carried over).
38
+ const ACCENTS = {
39
+ blue: { light: "#2256C4", onLight: "#F8FAFD", dark: "#6D9DF6", onDark: "#0E1626", ring: "#2256C4", darkRing: "#6D9DF6" },
40
+ teal: { light: "#1C877E", onLight: "#F6FBFA", dark: "#37BEAD", onDark: "#0A1F1D", ring: "#1C877E", darkRing: "#37BEAD" },
41
+ violet: { light: "#6231C4", onLight: "#FAF8FD", dark: "#A683EE", onDark: "#160E26", ring: "#6231C4", darkRing: "#A683EE" },
42
+ amber: { light: "#BC6E0F", onLight: "#FDFBF6", dark: "#F3A324", onDark: "#211405", ring: "#BC6E0F", darkRing: "#F3A324" },
43
+ // cosmos: harvested from second-brain/poc-graph (brain.askewly.com) knowledge-graph
44
+ // palette — muted navy primary over a near-black ground family (harvest-contract §4, 2026-08-04).
45
+ cosmos: { light: "#3E5F94", onLight: "#F7F9FC", dark: "#6E8FC4", onDark: "#0E1420", ring: "#3E5F94", darkRing: "#6E8FC4" },
46
+ };
47
+ const CANVASES = {
48
+ "minimal-clean": {
49
+ light: { bg: "#F9FAFB", fg: "#171B26", card: "#FFFFFF", muted: "#EDF0F4", mutedFg: "#5C6472", border: "#DEE2E9" },
50
+ dark: { bg: "#0F1219", fg: "#E7EAEF", card: "#14181F", muted: "#1E242E", mutedFg: "#949CA9", border: "#262D3A" },
51
+ radius: "0.5rem",
52
+ },
53
+ "warm-editorial": {
54
+ light: { bg: "#FAF7F1", fg: "#2B241C", card: "#FDFBF7", muted: "#EFEAE1", mutedFg: "#746A5C", border: "#E1DACF" },
55
+ dark: { bg: "#191410", fg: "#EBE5DB", card: "#211B16", muted: "#2B241E", mutedFg: "#A69C8F", border: "#352D25" },
56
+ radius: "0.625rem",
57
+ },
58
+ "dark-technical": {
59
+ light: { bg: "#EFF1F4", fg: "#14181F", card: "#F8F9FB", muted: "#E2E5EA", mutedFg: "#59616D", border: "#D2D6DD" },
60
+ dark: { bg: "#080B12", fg: "#DFE3E9", card: "#0D1119", muted: "#161C26", mutedFg: "#8B93A1", border: "#232B3A" },
61
+ radius: "0.375rem",
62
+ },
63
+ };
64
+ export async function collectBrief(opts) {
65
+ const answers = {};
66
+ const assumed = [];
67
+ for (const q of BRIEF_QUESTIONS) {
68
+ const injected = opts[q.key];
69
+ if (injected) {
70
+ if (!q.options.includes(injected)) {
71
+ throw new KickstartError(`--${q.key} "${injected}" is not an option — use one of: ${q.options.join(", ")}`);
72
+ }
73
+ answers[q.key] = injected;
74
+ continue;
75
+ }
76
+ if (opts.yes) {
77
+ answers[q.key] = q.recommended;
78
+ assumed.push(q.key);
79
+ continue;
80
+ }
81
+ const rl = createInterface({ input: stdin, output: stdout });
82
+ try {
83
+ const menu = q.options.map((o, i) => ` ${i + 1}. ${o}${o === q.recommended ? " (recommended)" : ""}`).join("\n");
84
+ const raw = (await rl.question(`${q.label}\n${menu}\n> [1-${q.options.length}, Enter = recommended] `)).trim();
85
+ if (raw === "") {
86
+ answers[q.key] = q.recommended;
87
+ }
88
+ else {
89
+ const idx = Number(raw);
90
+ const picked = Number.isInteger(idx) && idx >= 1 && idx <= q.options.length ? q.options[idx - 1] : q.options.includes(raw) ? raw : null;
91
+ if (!picked)
92
+ throw new KickstartError(`"${raw}" is not a valid choice for ${q.key}`);
93
+ answers[q.key] = picked;
94
+ }
95
+ }
96
+ finally {
97
+ rl.close();
98
+ }
99
+ }
100
+ return { answers, assumed };
101
+ }
102
+ // ---------------------------------------------------------------------------
103
+ // DESIGN.md generation — flat-colors form of templates/DESIGN.md.tmpl
104
+ // (the form the slide-theme converter already reads; do not invent new keys).
105
+ // ---------------------------------------------------------------------------
106
+ export function renderDesignMd(project, answers, assumed) {
107
+ const accent = ACCENTS[answers.color];
108
+ const canvas = CANVASES[answers.tone];
109
+ const font = FONT_STACKS[answers.type];
110
+ const date = new Date().toISOString().slice(0, 10);
111
+ const assumedNote = assumed.length > 0
112
+ ? `기본값 채택(추정): ${assumed.join(", ")} — 비대화 실행으로 추천값을 그대로 썼다.`
113
+ : "세 축(tone·color·type) 모두 사용자 답변으로 결정됐다.";
114
+ return `---
115
+ # 공식 스키마: google-labs-code/design.md spec (version: alpha)
116
+ # generated by \`askewly-design init --block\` — tone=${answers.tone} color=${answers.color} type=${answers.type}
117
+ name: "${project}"
118
+ description: "Kickstart brief: ${answers.tone} tone, ${answers.color} accent, ${answers.type} type."
119
+ colors:
120
+ primary: "${accent.light}"
121
+ on-primary: "${accent.onLight}"
122
+ surface: "${canvas.light.bg}"
123
+ on-surface: "${canvas.light.fg}"
124
+ surface-raised: "${canvas.light.card}"
125
+ muted: "${canvas.light.mutedFg}"
126
+ border: "${canvas.light.border}"
127
+ typography:
128
+ headline-lg:
129
+ fontFamily: "${font}"
130
+ fontSize: 48px
131
+ fontWeight: "800"
132
+ lineHeight: 1.1
133
+ letterSpacing: -0.03em
134
+ body-md:
135
+ fontFamily: "${font}"
136
+ fontSize: 16px
137
+ fontWeight: "400"
138
+ lineHeight: 1.65
139
+ label-md:
140
+ fontFamily: "${font}"
141
+ fontSize: 14px
142
+ fontWeight: "500"
143
+ lineHeight: 1.4
144
+ rounded:
145
+ sm: 4px
146
+ md: 8px
147
+ lg: 16px
148
+ full: 9999px
149
+ spacing:
150
+ unit: 4px
151
+ gap: 16px
152
+ container-padding: 24px
153
+ section-margin: 64px
154
+ components:
155
+ button-primary:
156
+ backgroundColor: "{colors.primary}"
157
+ textColor: "{colors.on-primary}"
158
+ rounded: "{rounded.sm}"
159
+ padding: 12px
160
+ card:
161
+ backgroundColor: "{colors.surface-raised}"
162
+ rounded: "{rounded.lg}"
163
+ padding: "{spacing.container-padding}"
164
+ ---
165
+
166
+ # ${project} — DESIGN.md
167
+
168
+ ## Overview
169
+
170
+ Kickstart 축약 브리프 산출물 (block-contract §8). ${assumedNote}
171
+ 구조·인터랙션 레벨은 블록(saas-app-shell 등)이 결정하므로 묻지 않았다 — 나머지 브리프 영역(콘텐츠·모션 등)은 추천 기본값이며, 바뀌면 이 파일이 정본이다.
172
+
173
+ ## Colors
174
+
175
+ - 코드에서는 의미 이름(\`surface\`, \`on-surface\`, \`muted\`)만 참조 — 값 하드코딩 금지.
176
+ - \`primary\`는 유일한 액센트: CTA·핵심 신호에만. WCAG AA(4.5:1) 통과 필수.
177
+
178
+ ## Typography
179
+
180
+ - 서체 1종(${answers.type}). 위계는 굵기·크기·색으로만.
181
+ - 한글: \`word-break: keep-all\`.
182
+
183
+ ## Do's and Don'ts
184
+
185
+ - Do: 상태 완비(hover/focus-visible/active/disabled/loading/error), 다크 모드, reduced-motion 분기.
186
+ - Don't: 이모지 아이콘, 좌측 컬러 라인 카드, 그라디언트 남발, primary의 비-CTA 사용.
187
+
188
+ <!-- Changelog: ${date} kickstart 초기화 (축약 브리프 산출) -->
189
+ `;
190
+ }
191
+ // ---------------------------------------------------------------------------
192
+ // Token derivation — the CSS variable definition layer the block's
193
+ // required-CSS-variable list (registry meta.requiredCssVars) is checked against.
194
+ // ---------------------------------------------------------------------------
195
+ export function renderBrandCss(answers) {
196
+ const a = ACCENTS[answers.color];
197
+ const c = CANVASES[answers.tone];
198
+ const font = FONT_STACKS[answers.type];
199
+ const vars = (m, primary, onPrimary, ring) => ` --background: ${m.bg};
200
+ --foreground: ${m.fg};
201
+ --card: ${m.card};
202
+ --card-foreground: ${m.fg};
203
+ --popover: ${m.card};
204
+ --popover-foreground: ${m.fg};
205
+ --primary: ${primary};
206
+ --primary-foreground: ${onPrimary};
207
+ --secondary: ${m.muted};
208
+ --secondary-foreground: ${m.fg};
209
+ --muted: ${m.muted};
210
+ --muted-foreground: ${m.mutedFg};
211
+ --accent: ${m.muted};
212
+ --accent-foreground: ${m.fg};
213
+ --destructive: hsl(0 72% 45%);
214
+ --destructive-foreground: ${m.card};
215
+ --border: ${m.border};
216
+ --input: ${m.border};
217
+ --ring: ${ring};
218
+ --radius: ${c.radius};
219
+ --sidebar: ${m.bg};
220
+ --sidebar-foreground: ${m.fg};
221
+ --sidebar-primary: ${primary};
222
+ --sidebar-primary-foreground: ${onPrimary};
223
+ --sidebar-accent: ${m.muted};
224
+ --sidebar-accent-foreground: ${m.fg};
225
+ --sidebar-border: ${m.border};
226
+ --sidebar-ring: ${ring};`;
227
+ return `/* Generated by \`askewly-design init --block\` from DESIGN.md (tone=${answers.tone}, color=${answers.color}).
228
+ * This file is the project's token definition layer — the block's required
229
+ * CSS variables (block-contract §4/§6) are defined here, nowhere else. */
230
+ :root {
231
+ font-family: ${font};
232
+ ${vars(c.light, a.light, a.onLight, a.ring)}
233
+ }
234
+
235
+ .dark {
236
+ ${vars(c.dark, a.dark, a.onDark, a.darkRing)}
237
+ }
238
+
239
+ @theme inline {
240
+ --color-background: var(--background);
241
+ --color-foreground: var(--foreground);
242
+ --color-card: var(--card);
243
+ --color-card-foreground: var(--card-foreground);
244
+ --color-popover: var(--popover);
245
+ --color-popover-foreground: var(--popover-foreground);
246
+ --color-primary: var(--primary);
247
+ --color-primary-foreground: var(--primary-foreground);
248
+ --color-secondary: var(--secondary);
249
+ --color-secondary-foreground: var(--secondary-foreground);
250
+ --color-muted: var(--muted);
251
+ --color-muted-foreground: var(--muted-foreground);
252
+ --color-accent: var(--accent);
253
+ --color-accent-foreground: var(--accent-foreground);
254
+ --color-destructive: var(--destructive);
255
+ --color-destructive-foreground: var(--destructive-foreground);
256
+ --color-border: var(--border);
257
+ --color-input: var(--input);
258
+ --color-ring: var(--ring);
259
+ --color-sidebar: var(--sidebar);
260
+ --color-sidebar-foreground: var(--sidebar-foreground);
261
+ --color-sidebar-primary: var(--sidebar-primary);
262
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
263
+ --color-sidebar-accent: var(--sidebar-accent);
264
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
265
+ --color-sidebar-border: var(--sidebar-border);
266
+ --color-sidebar-ring: var(--sidebar-ring);
267
+ --radius-sm: calc(var(--radius) - 4px);
268
+ --radius-md: calc(var(--radius) - 2px);
269
+ --radius-lg: var(--radius);
270
+ --radius-xl: calc(var(--radius) + 4px);
271
+ }
272
+ `;
273
+ }
274
+ /** Presence-only check (block-contract §8-5): every required var must be *defined* in css. */
275
+ export function missingRequiredVars(css, required) {
276
+ return required.filter((name) => !new RegExp(`${name.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&")}\\s*:`).test(css));
277
+ }
278
+ async function fetchJson(url) {
279
+ let res;
280
+ try {
281
+ res = await fetch(url);
282
+ }
283
+ catch (error) {
284
+ throw new KickstartError(`fetch failed: ${url} — ${error.message}`);
285
+ }
286
+ if (!res.ok)
287
+ throw new KickstartError(`registry fetch ${res.status}: ${url}`);
288
+ return (await res.json());
289
+ }
290
+ function rewriteImports(content) {
291
+ return content
292
+ .replaceAll(/@\/registry\/[a-z0-9-]+\/hooks\//g, "@/hooks/")
293
+ .replaceAll(/@\/registry\/[a-z0-9-]+\/lib\/utils/g, "@/lib/utils")
294
+ .replaceAll(/@\/registry\/[a-z0-9-]+\/ui\//g, "@/components/ui/")
295
+ .replaceAll(/@\/registry\/[a-z0-9-]+\/blocks\/[a-z0-9-]+\/components\//g, "./");
296
+ }
297
+ function destFor(file, srcRoot) {
298
+ if (file.target)
299
+ return path.join(srcRoot, file.target);
300
+ const p = file.path;
301
+ const uiMatch = p.match(/\/ui\/([^/]+)$/);
302
+ if (uiMatch)
303
+ return path.join(srcRoot, "components", "ui", uiMatch[1]);
304
+ const hookMatch = p.match(/\/hooks\/([^/]+)$/);
305
+ if (hookMatch)
306
+ return path.join(srcRoot, "hooks", hookMatch[1]);
307
+ const libMatch = p.match(/\/lib\/([^/]+)$/);
308
+ if (libMatch)
309
+ return path.join(srcRoot, "lib", libMatch[1]);
310
+ return path.join(srcRoot, "components", path.basename(p));
311
+ }
312
+ export async function fetchBlock(blockName, registryBase) {
313
+ const block = await fetchJson(`${registryBase}/r/${blockName}.json`);
314
+ if (block.meta?.tier !== "block") {
315
+ throw new KickstartError(`"${blockName}" is not a block-tier asset (meta.tier missing) — see block-contract §1`);
316
+ }
317
+ return block;
318
+ }
319
+ export async function transplantBlock(block, targetDir) {
320
+ const srcRoot = existsSync(path.join(targetDir, "src")) ? path.join(targetDir, "src") : targetDir;
321
+ const written = [];
322
+ const npmDeps = new Set(["clsx", "tailwind-merge"]);
323
+ const visited = new Set();
324
+ async function resolve(item) {
325
+ for (const dep of item.dependencies ?? [])
326
+ npmDeps.add(dep.split("@").slice(0, dep.startsWith("@") ? 2 : 1).join("@"));
327
+ for (const ref of item.registryDependencies ?? []) {
328
+ const url = ref.startsWith("http") ? ref : `${SHADCN_BASE}/${ref}.json`;
329
+ const key = url;
330
+ if (visited.has(key))
331
+ continue;
332
+ visited.add(key);
333
+ const child = await fetchJson(url);
334
+ await resolve(child);
335
+ for (const file of child.files ?? []) {
336
+ const dest = destFor(file, srcRoot);
337
+ mkdirSync(path.dirname(dest), { recursive: true });
338
+ writeFileSync(dest, rewriteImports(file.content));
339
+ written.push(dest);
340
+ }
341
+ }
342
+ }
343
+ await resolve(block);
344
+ for (const file of block.files ?? []) {
345
+ const dest = destFor(file, srcRoot);
346
+ mkdirSync(path.dirname(dest), { recursive: true });
347
+ writeFileSync(dest, rewriteImports(file.content));
348
+ written.push(dest);
349
+ }
350
+ // cn() — shadcn init would have created it; transplant guarantees it (M18 step-5 friction).
351
+ const utils = path.join(srcRoot, "lib", "utils.ts");
352
+ if (!existsSync(utils)) {
353
+ mkdirSync(path.dirname(utils), { recursive: true });
354
+ writeFileSync(utils, `import { clsx, type ClassValue } from "clsx"\nimport { twMerge } from "tailwind-merge"\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n`);
355
+ written.push(utils);
356
+ }
357
+ return { written, npmDeps, requiredCssVars: block.meta?.requiredCssVars ?? [] };
358
+ }
359
+ // ---------------------------------------------------------------------------
360
+ // Orchestrator — brief → DESIGN.md → tokens → transplant → required-var check → verify
361
+ // ---------------------------------------------------------------------------
362
+ export async function runKickstart(dir, opts) {
363
+ const targetDir = path.resolve(dir);
364
+ const registryBase = (opts.registry ?? REGISTRY_BASE).replace(/\/$/, "");
365
+ const designPath = path.join(targetDir, "DESIGN.md");
366
+ if (existsSync(designPath) && !opts.force) {
367
+ throw new KickstartError(`refusing to overwrite ${designPath} (use --force) — an existing DESIGN.md owns the look; kickstart is for blank projects`);
368
+ }
369
+ // 0. fetch the block first — an unknown block name must fail before any file
370
+ // is written (no half-initialized project on exit 1).
371
+ const block = await fetchBlock(opts.block, registryBase);
372
+ // 1. brief
373
+ const { answers, assumed } = await collectBrief(opts);
374
+ console.log(`brief: tone=${answers.tone} color=${answers.color} type=${answers.type}${assumed.length ? ` (defaults: ${assumed.join(", ")})` : ""}`);
375
+ // 2. DESIGN.md
376
+ mkdirSync(targetDir, { recursive: true });
377
+ writeFileSync(designPath, renderDesignMd(path.basename(targetDir), answers, assumed));
378
+ console.log(`wrote ${designPath}`);
379
+ // 3. token layer
380
+ const srcRoot = existsSync(path.join(targetDir, "src")) ? path.join(targetDir, "src") : targetDir;
381
+ const brandCss = renderBrandCss(answers);
382
+ const cssPath = path.join(srcRoot, "askewly-brand.css");
383
+ writeFileSync(cssPath, brandCss);
384
+ console.log(`wrote ${cssPath}`);
385
+ // 4. transplant
386
+ const result = await transplantBlock(block, targetDir);
387
+ console.log(`transplanted ${result.written.length} file(s) from ${registryBase}/r/${opts.block}.json`);
388
+ // 5. required-var check (presence only — values are settled by human eyes)
389
+ const missing = missingRequiredVars(brandCss, result.requiredCssVars);
390
+ if (missing.length > 0) {
391
+ throw new KickstartError(`token layer is missing required CSS variables (block-contract §4): ${missing.join(", ")}`);
392
+ }
393
+ console.log(`required CSS variables: ${result.requiredCssVars.length}/${result.requiredCssVars.length} defined (presence check only — look at the rendered result before shipping)`);
394
+ // 6. verify
395
+ const blockDir = path.join(srcRoot, "components", "blocks", opts.block);
396
+ const { files, violations } = verifyDir(blockDir, ["tsx", "ts", "css"], {});
397
+ if (violations.length > 0) {
398
+ console.error(`verify FAIL — ${violations.length} violation(s) in ${files} file(s) under ${blockDir}`);
399
+ for (const violation of violations)
400
+ console.error(` ${violation.file}:${violation.line} [${violation.rule}] ${violation.excerpt}`);
401
+ throw new KickstartError("transplanted block bypasses the token system — fix before continuing");
402
+ }
403
+ console.log(`verify PASS — ${files} block file(s), no color literals`);
404
+ // 7. handoff
405
+ console.log(`\nNext steps:`);
406
+ console.log(` 1. npm i ${[...result.npmDeps].sort().join(" ")}`);
407
+ console.log(` 2. import "./askewly-brand.css" after \`@import "tailwindcss";\` in your global stylesheet`);
408
+ console.log(` 3. render the block: import { SaasAppShell } from "@/components/blocks/${opts.block}/page"`);
409
+ console.log(` 4. wire routing yourself — the block deliberately ships none (block-contract §3)`);
410
+ }
package/dist/verify.js CHANGED
@@ -14,7 +14,7 @@ const TYPOGRAPHY_MARKER = /askewly-typography-ok(?::([^\n]*))?/;
14
14
  const DEFAULT_EXTENSIONS = ["tsx", "ts", "jsx", "js", "css", "html"];
15
15
  const SKIP_DIRS = new Set(["node_modules", "dist", "build", ".git", ".next"]);
16
16
  // Generated token files legitimately contain raw color values.
17
- const SKIP_FILES = new Set(["tokens.css", "askewly.css"]);
17
+ const SKIP_FILES = new Set(["tokens.css", "askewly.css", "askewly-brand.css"]);
18
18
  const HEX_RULE = /#[0-9a-fA-F]{3,8}\b/;
19
19
  const COLOR_FN_RULE = /\b(?:rgb|rgba|hsl|hsla|oklch)\(/;
20
20
  // Every rule is checked independently per line. A single ternary would report
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askewly/design",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Query, inject, and verify the Askewly Design system (tokens, UI vocabulary, recipes) from your terminal or coding agent.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://ui.askewly.com",