@urbicon-ui/design 6.2.0 → 6.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,17 @@ bun add -d @urbicon-ui/design # dev tooling — not a runtime dependency
20
20
  This exposes the `urbicon` command (a self-contained, Node-runnable bundle — no
21
21
  Bun required at the consumer side).
22
22
 
23
+ > **Running it standalone (no local install).** The bin is `urbicon` but the package
24
+ > is `@urbicon-ui/design`, so a bare `bunx urbicon …` from a project that hasn't
25
+ > installed it fails with `GET …/urbicon 404` (it looks for a package literally named
26
+ > `urbicon`). To run the CLI without a local install, name both the package and the bin:
27
+ >
28
+ > ```bash
29
+ > bunx --package @urbicon-ui/design urbicon validate src/ # or: npx --package @urbicon-ui/design urbicon …
30
+ > ```
31
+ >
32
+ > Inside a project that already has `@urbicon-ui/design` installed, plain `bunx urbicon …` resolves fine.
33
+
23
34
  ## Onboarding a consumer project
24
35
 
25
36
  ```bash
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { readFile as readFile10 } from "node:fs/promises";
5
- import { resolve as resolve9 } from "node:path";
5
+ import { resolve as resolve10 } from "node:path";
6
6
 
7
7
  // src/cli/args.ts
8
8
  var BOOLEAN_FLAGS = new Set([
@@ -168,12 +168,22 @@ function parseIntent(body) {
168
168
  return emptyIntent();
169
169
  const lines = section.split(`
170
170
  `);
171
+ const isFieldOrHeading = (l) => /^\*\*[^*]+:\*\*/.test(l) || /^#{1,6}\s/.test(l);
171
172
  const inlineField = (label) => {
172
- const re = new RegExp(`^\\*\\*${label}:\\*\\*\\s*(.+)$`);
173
- for (const l of lines) {
174
- const m = l.match(re);
175
- if (m)
176
- return m[1].trim();
173
+ const labelRe = new RegExp(`^\\*\\*${label}:\\*\\*\\s*(.*)$`);
174
+ for (let i = 0;i < lines.length; i++) {
175
+ const m = lines[i].match(labelRe);
176
+ if (!m)
177
+ continue;
178
+ const parts = m[1].trim() ? [m[1].trim()] : [];
179
+ for (let j = i + 1;j < lines.length; j++) {
180
+ const l = lines[j];
181
+ if (l.trim() === "" || isFieldOrHeading(l))
182
+ break;
183
+ parts.push(l.trim());
184
+ }
185
+ const value = parts.join(" ").trim();
186
+ return value === "" ? undefined : value;
177
187
  }
178
188
  return;
179
189
  };
@@ -181,22 +191,33 @@ function parseIntent(body) {
181
191
  const items = [];
182
192
  const labelRe = new RegExp(`^\\*\\*${label}:\\*\\*\\s*(.*)$`);
183
193
  let capturing = false;
194
+ let inInlineRun = false;
184
195
  for (const l of lines) {
185
196
  if (!capturing) {
186
197
  const m = l.match(labelRe);
187
198
  if (m) {
188
199
  capturing = true;
200
+ inInlineRun = true;
189
201
  const inline = m[1].trim();
190
202
  if (inline)
191
203
  items.push(...splitList(inline));
192
204
  }
193
205
  continue;
194
206
  }
195
- if (/^\*\*[^*]+:\*\*/.test(l))
207
+ if (isFieldOrHeading(l))
196
208
  break;
197
209
  const bullet = l.match(/^\s*[-*]\s+(.+)$/);
198
- if (bullet)
210
+ if (bullet) {
199
211
  items.push(bullet[1].trim());
212
+ inInlineRun = false;
213
+ continue;
214
+ }
215
+ if (l.trim() === "") {
216
+ inInlineRun = false;
217
+ continue;
218
+ }
219
+ if (inInlineRun)
220
+ items.push(...splitList(l.trim()));
200
221
  }
201
222
  return items;
202
223
  };
@@ -811,6 +832,51 @@ async function loadComponentLlm(slug) {
811
832
  return null;
812
833
  }
813
834
 
835
+ // src/cli/installed.ts
836
+ import { readFileSync } from "node:fs";
837
+ import { dirname as dirname3, join as join2, parse, resolve as resolve4 } from "node:path";
838
+ var DEP_FIELDS = [
839
+ "dependencies",
840
+ "devDependencies",
841
+ "peerDependencies",
842
+ "optionalDependencies"
843
+ ];
844
+ function readConsumerDependencies(cwd = process.cwd()) {
845
+ let dir = resolve4(cwd);
846
+ const { root } = parse(dir);
847
+ for (;; ) {
848
+ try {
849
+ const pkg = JSON.parse(readFileSync(join2(dir, "package.json"), "utf-8"));
850
+ const names = new Set;
851
+ for (const field of DEP_FIELDS) {
852
+ const deps = pkg[field];
853
+ if (deps && typeof deps === "object") {
854
+ for (const name of Object.keys(deps))
855
+ names.add(name);
856
+ }
857
+ }
858
+ return names;
859
+ } catch {}
860
+ if (dir === root)
861
+ return null;
862
+ dir = dirname3(dir);
863
+ }
864
+ }
865
+ function installStateFor(pkg, deps) {
866
+ if (!deps)
867
+ return "unknown";
868
+ let hasUrbiconContext = false;
869
+ for (const d of deps) {
870
+ if (d.startsWith("@urbicon-ui/")) {
871
+ hasUrbiconContext = true;
872
+ break;
873
+ }
874
+ }
875
+ if (!hasUrbiconContext)
876
+ return "unknown";
877
+ return deps.has(pkg) ? "installed" : "missing";
878
+ }
879
+
814
880
  // src/cli/commands/find.ts
815
881
  function variantSummary(entry) {
816
882
  return entry.variants.filter((v) => !v.values.every((x) => x === "true" || x === "false")).map((v) => `${v.name}: ${v.values.join("/")}`).join(" · ");
@@ -820,8 +886,14 @@ function shortDescription(description) {
820
886
  `)[0]?.trim() ?? "";
821
887
  return firstLine.length > 140 ? `${firstLine.slice(0, 139)}…` : firstLine;
822
888
  }
823
- function formatEntry(entry) {
824
- const lines = [` ${entry.name} · ${entry.slug}`, ` ${shortDescription(entry.description)}`];
889
+ function packageTag(entry, state) {
890
+ return state === "missing" ? `${entry.package} · not installed` : entry.package;
891
+ }
892
+ function formatEntry(entry, state) {
893
+ const lines = [
894
+ ` ${entry.name} · ${entry.slug} · ${packageTag(entry, state)}`,
895
+ ` ${shortDescription(entry.description)}`
896
+ ];
825
897
  const variants = variantSummary(entry);
826
898
  if (variants)
827
899
  lines.push(` ${variants}`);
@@ -850,8 +922,14 @@ async function runFind(positionals, flags) {
850
922
  return EXIT.FAIL;
851
923
  }
852
924
  const results = query ? matchComponents(components, query, tags, limit) : components.filter((c) => !tags || c.tags.some((t) => tags.includes(t)));
925
+ const deps = readConsumerDependencies();
926
+ const stateOf = (entry) => installStateFor(entry.package, deps);
853
927
  if (asJson) {
854
- console.log(JSON.stringify(results, null, 2));
928
+ const annotated = results.map((entry) => {
929
+ const state = stateOf(entry);
930
+ return { ...entry, installed: state === "unknown" ? null : state === "installed" };
931
+ });
932
+ console.log(JSON.stringify(annotated, null, 2));
855
933
  return EXIT.OK;
856
934
  }
857
935
  if (results.length === 0) {
@@ -862,15 +940,31 @@ async function runFind(positionals, flags) {
862
940
  console.log(`${header}
863
941
  `);
864
942
  for (const entry of results) {
865
- console.log(`${formatEntry(entry)}
943
+ console.log(`${formatEntry(entry, stateOf(entry))}
866
944
  `);
867
945
  }
946
+ const missing = [
947
+ ...new Set(results.filter((e) => stateOf(e) === "missing").map((e) => e.package))
948
+ ];
949
+ if (missing.length > 0) {
950
+ console.log(`⚠ Not in your dependencies: ${missing.join(", ")} — install before importing (e.g. \`bun add ${missing[0]}\`).`);
951
+ }
868
952
  console.log("→ `urbicon get-component <slug>` for the full API · `get_css_reference` for tokens.");
869
953
  return EXIT.OK;
870
954
  }
871
955
 
872
956
  // src/cli/commands/get-component.ts
873
957
  var SECTIONS = ["overview", "examples", "variants", "api", "slots"];
958
+ async function warnIfNotInstalled(slug) {
959
+ try {
960
+ const entry = (await loadCatalog()).components.find((c) => c.slug === slug);
961
+ if (!entry)
962
+ return;
963
+ if (installStateFor(entry.package, readConsumerDependencies()) === "missing") {
964
+ console.error(`⚠ ${entry.name} ships from ${entry.package}, which isn't in your dependencies — ` + `install it before importing (e.g. \`bun add ${entry.package}\`).`);
965
+ }
966
+ } catch {}
967
+ }
874
968
  async function runGetComponent(positionals, flags) {
875
969
  const slug = positionals[0];
876
970
  if (!slug) {
@@ -893,6 +987,7 @@ async function runGetComponent(positionals, flags) {
893
987
  printError(`component "${slug}" not found. Run \`urbicon find <query>\` to discover the slug.`);
894
988
  return EXIT.FAIL;
895
989
  }
990
+ await warnIfNotInstalled(slug);
896
991
  if (!section || section === "full") {
897
992
  console.log(content.trim());
898
993
  return EXIT.OK;
@@ -908,7 +1003,7 @@ async function runGetComponent(positionals, flags) {
908
1003
 
909
1004
  // src/cli/commands/hook.ts
910
1005
  import { readFile as readFile5 } from "node:fs/promises";
911
- import { resolve as resolve4 } from "node:path";
1006
+ import { resolve as resolve5 } from "node:path";
912
1007
  // ../design-engine/src/linter/heuristics.ts
913
1008
  var CHROMATIC_INTENTS = ["primary", "secondary", "success", "warning", "danger", "info"];
914
1009
  var HEURISTIC_THRESHOLDS = {
@@ -1671,6 +1766,7 @@ var INTERACTIVE_CORES = [
1671
1766
  "interactive-disabled"
1672
1767
  ];
1673
1768
  var CHART_CORES = ["chart-1", "chart-2", "chart-3", "chart-4", "chart-5", "chart-6"];
1769
+ var SKELETON_CORES = ["skeleton-shimmer"];
1674
1770
  function buildIntentCores() {
1675
1771
  const cores = [];
1676
1772
  for (const intent of INTENT_NAMES) {
@@ -1691,7 +1787,8 @@ var VALID_TOKEN_CORES = new Set([
1691
1787
  ...WARM_NEUTRAL_STEPS.map((s) => `warm-neutral-${s}`),
1692
1788
  ...FEEDBACK_CORES,
1693
1789
  ...INTERACTIVE_CORES,
1694
- ...CHART_CORES
1790
+ ...CHART_CORES,
1791
+ ...SKELETON_CORES
1695
1792
  ]);
1696
1793
  function normalizeExtraTokens(extra) {
1697
1794
  return extra.map((token) => token.trim()).filter((token) => token.length > 0);
@@ -1738,6 +1835,53 @@ var KNOWN_BAD_NAMESPACES = {
1738
1835
  "status-": "Use a `feedback-*` token (feedback-success, feedback-error, …) or a bare intent (`success`, `danger`).",
1739
1836
  "-fg": "Use `text-on-primary` / `text-on-surface` for foreground-on-intent text."
1740
1837
  };
1838
+ function isSingleEditApart(a, b) {
1839
+ if (a === b)
1840
+ return false;
1841
+ if (a.length === b.length) {
1842
+ let diffs = 0;
1843
+ let at = -1;
1844
+ for (let i2 = 0;i2 < a.length; i2++) {
1845
+ if (a[i2] !== b[i2]) {
1846
+ diffs++;
1847
+ if (at === -1)
1848
+ at = i2;
1849
+ }
1850
+ }
1851
+ if (diffs === 1)
1852
+ return true;
1853
+ return diffs === 2 && at >= 0 && a[at] === b[at + 1] && a[at + 1] === b[at];
1854
+ }
1855
+ if (Math.abs(a.length - b.length) !== 1)
1856
+ return false;
1857
+ const [short, long] = a.length < b.length ? [a, b] : [b, a];
1858
+ let i = 0;
1859
+ let j = 0;
1860
+ let skipped = false;
1861
+ while (i < short.length && j < long.length) {
1862
+ if (short[i] === long[j]) {
1863
+ i++;
1864
+ j++;
1865
+ } else if (!skipped) {
1866
+ skipped = true;
1867
+ j++;
1868
+ } else {
1869
+ return false;
1870
+ }
1871
+ }
1872
+ return true;
1873
+ }
1874
+ function suggestIntentTypo(core) {
1875
+ if (core.includes("-"))
1876
+ return null;
1877
+ if (INTENT_NAMES.includes(core))
1878
+ return null;
1879
+ for (const intent of INTENT_NAMES) {
1880
+ if (isSingleEditApart(core, intent))
1881
+ return intent;
1882
+ }
1883
+ return null;
1884
+ }
1741
1885
 
1742
1886
  // ../design-engine/src/linter/rules.ts
1743
1887
  var SHADCN_FIX = "This is shadcn/ui vocabulary, not Urbicon UI. Use surface tokens (`bg-surface-base`/`-elevated`), text tokens (`text-text-primary`/`-secondary`), or intents (`bg-primary`, `text-success`).";
@@ -1949,6 +2093,81 @@ var dynamicClassInterpolation = {
1949
2093
  return dedupeByLine(findings);
1950
2094
  }
1951
2095
  };
2096
+ var INTERNAL_SUBPATH_SEGMENTS = new Set([
2097
+ "primitives",
2098
+ "components",
2099
+ "lib",
2100
+ "dist",
2101
+ "src",
2102
+ "icons"
2103
+ ]);
2104
+ function isDeepInternalSubpath(subpath) {
2105
+ if (/\.svelte(\.[jt]s)?$|\.[jt]s$/.test(subpath))
2106
+ return true;
2107
+ return subpath.split("/").some((seg) => INTERNAL_SUBPATH_SEGMENTS.has(seg));
2108
+ }
2109
+ var deepInternalImport = {
2110
+ id: "deep-internal-import",
2111
+ severity: "error",
2112
+ description: "Deep/internal import into an `@urbicon-ui` package instead of its public root.",
2113
+ check(lines) {
2114
+ const re = /['"](@urbicon-ui\/[a-z-]+)\/([^'"]+)['"]/g;
2115
+ const findings = [];
2116
+ lines.forEach((line, i) => {
2117
+ for (const m of line.matchAll(re)) {
2118
+ const pkg = m[1];
2119
+ const subpath = m[2];
2120
+ if (!isDeepInternalSubpath(subpath))
2121
+ continue;
2122
+ findings.push({
2123
+ ruleId: this.id,
2124
+ severity: this.severity,
2125
+ kind: "deterministic",
2126
+ message: `Deep import \`${pkg}/${subpath}\` reaches into ${pkg}'s internals — they can move between releases.`,
2127
+ fix: `Import from the package root: \`import { … } from '${pkg}'\`.`,
2128
+ line: i + 1,
2129
+ match: `${pkg}/${subpath}`
2130
+ });
2131
+ }
2132
+ });
2133
+ return dedupeByLine(findings);
2134
+ }
2135
+ };
2136
+ var hardcodedMotion = {
2137
+ id: "hardcoded-motion",
2138
+ severity: "error",
2139
+ description: "Hardcoded transition duration or `cubic-bezier()` easing instead of a motion token.",
2140
+ check(lines) {
2141
+ const duration = /\bduration-\[\d+(?:\.\d+)?m?s\]/g;
2142
+ const easing = /\bease-\[cubic-bezier\([^\]]*\)\]/g;
2143
+ const findings = [];
2144
+ lines.forEach((line, i) => {
2145
+ for (const m of line.matchAll(duration)) {
2146
+ findings.push({
2147
+ ruleId: this.id,
2148
+ severity: this.severity,
2149
+ kind: "deterministic",
2150
+ message: `Hardcoded transition duration \`${m[0]}\` bypasses the motion scale (no global speed / reduced-motion control).`,
2151
+ fix: "Use a duration token: `duration-[var(--blocks-duration-fast)]` / `-normal` / `-slow`.",
2152
+ line: i + 1,
2153
+ match: m[0]
2154
+ });
2155
+ }
2156
+ for (const m of line.matchAll(easing)) {
2157
+ findings.push({
2158
+ ruleId: this.id,
2159
+ severity: this.severity,
2160
+ kind: "deterministic",
2161
+ message: `Hardcoded \`cubic-bezier()\` easing \`${m[0]}\` bypasses the motion system's easing tokens.`,
2162
+ fix: "Use an easing token: `ease-[var(--blocks-ease-smooth)]` / `-snappy` / `-gentle`, or a named Tailwind ease (`ease-out`).",
2163
+ line: i + 1,
2164
+ match: m[0]
2165
+ });
2166
+ }
2167
+ });
2168
+ return dedupeByLine(findings);
2169
+ }
2170
+ };
1952
2171
  var tokenHallucination = {
1953
2172
  id: "token-hallucination",
1954
2173
  severity: "warning",
@@ -1961,8 +2180,21 @@ var tokenHallucination = {
1961
2180
  lines.forEach((line, i) => {
1962
2181
  for (const m of line.matchAll(re)) {
1963
2182
  const core = m[2];
1964
- if (!looksSemantic(core))
2183
+ if (!looksSemantic(core)) {
2184
+ const intended = suggestIntentTypo(core);
2185
+ if (intended) {
2186
+ findings.push({
2187
+ ruleId: this.id,
2188
+ severity: this.severity,
2189
+ kind: "deterministic",
2190
+ message: `\`${m[1]}-${core}\` looks like a typo of \`${m[1]}-${intended}\`.`,
2191
+ fix: `Did you mean \`${m[1]}-${intended}\`? Valid intents: ${INTENT_NAMES.join(", ")}.`,
2192
+ line: i + 1,
2193
+ match: m[0]
2194
+ });
2195
+ }
1965
2196
  continue;
2197
+ }
1966
2198
  if (validCores.has(core))
1967
2199
  continue;
1968
2200
  findings.push({
@@ -2020,6 +2252,8 @@ var RULES = [
2020
2252
  darkModeOverride,
2021
2253
  focusNotVisible,
2022
2254
  hardcodedZIndex,
2255
+ hardcodedMotion,
2256
+ deepInternalImport,
2023
2257
  dynamicClassInterpolation,
2024
2258
  tokenHallucination,
2025
2259
  ...MARKUP_RULES
@@ -2144,7 +2378,7 @@ async function runHook(_positionals, flags) {
2144
2378
  for (const p of paths) {
2145
2379
  let code;
2146
2380
  try {
2147
- code = await readFile5(resolve4(p), "utf-8");
2381
+ code = await readFile5(resolve5(p), "utf-8");
2148
2382
  } catch {
2149
2383
  continue;
2150
2384
  }
@@ -2169,21 +2403,21 @@ Fix the issues above and re-save. — urbicon design gate`);
2169
2403
 
2170
2404
  // src/cli/commands/init.ts
2171
2405
  import { mkdir, readFile as readFile7, writeFile as writeFile2 } from "node:fs/promises";
2172
- import { dirname as dirname4, join as join2, relative as relative2, resolve as resolve6 } from "node:path";
2406
+ import { dirname as dirname5, join as join3, relative as relative2, resolve as resolve7 } from "node:path";
2173
2407
 
2174
2408
  // src/cli/package-root.ts
2175
2409
  import { readFile as readFile6 } from "node:fs/promises";
2176
- import { dirname as dirname3, resolve as resolve5 } from "node:path";
2410
+ import { dirname as dirname4, resolve as resolve6 } from "node:path";
2177
2411
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2178
2412
  async function findPackageRoot() {
2179
- let dir = dirname3(fileURLToPath2(import.meta.url));
2413
+ let dir = dirname4(fileURLToPath2(import.meta.url));
2180
2414
  for (let i = 0;i < 6; i++) {
2181
2415
  try {
2182
- const pkg = JSON.parse(await readFile6(resolve5(dir, "package.json"), "utf-8"));
2416
+ const pkg = JSON.parse(await readFile6(resolve6(dir, "package.json"), "utf-8"));
2183
2417
  if (pkg.name === "@urbicon-ui/design")
2184
2418
  return dir;
2185
2419
  } catch {}
2186
- const parent = dirname3(dir);
2420
+ const parent = dirname4(dir);
2187
2421
  if (parent === dir)
2188
2422
  break;
2189
2423
  dir = parent;
@@ -2194,11 +2428,31 @@ async function findPackageRoot() {
2194
2428
  // src/cli/commands/init.ts
2195
2429
  var BLOCK_START = "<!-- urbicon:start";
2196
2430
  var BLOCK_END = "<!-- urbicon:end -->";
2431
+ function tailwindSteps(deps) {
2432
+ const has = (p) => deps?.has(p) ?? false;
2433
+ const tailwindWired = has("@tailwindcss/vite") || has("tailwindcss");
2434
+ if (tailwindWired) {
2435
+ return [
2436
+ " • Tailwind is installed — ensure your `app.css` has `@source '../node_modules/@urbicon-ui/blocks/dist';`",
2437
+ " (relative to app.css) so the components' utility classes are generated. Easy to miss."
2438
+ ];
2439
+ }
2440
+ return [
2441
+ " • Wire up Tailwind 4 — REQUIRED, or components render unstyled (they emit Tailwind classes):",
2442
+ " 1. bun add -D tailwindcss @tailwindcss/vite",
2443
+ " 2. vite.config.ts → add the `tailwindcss()` plugin",
2444
+ " 3. src/app.css →",
2445
+ " @import 'tailwindcss';",
2446
+ " @import '@urbicon-ui/blocks/style/index.css';",
2447
+ " @source '../node_modules/@urbicon-ui/blocks/dist'; /* generates component classes */",
2448
+ " 4. import './app.css' in your root +layout.svelte"
2449
+ ];
2450
+ }
2197
2451
  async function readTemplate(name) {
2198
2452
  const root = await findPackageRoot();
2199
2453
  if (!root)
2200
2454
  throw new Error("could not locate the @urbicon-ui/design package root");
2201
- return readFile7(join2(root, "templates", name), "utf-8");
2455
+ return readFile7(join3(root, "templates", name), "utf-8");
2202
2456
  }
2203
2457
  async function readOrNull(path) {
2204
2458
  try {
@@ -2246,7 +2500,7 @@ async function mergeHook(settingsPath) {
2246
2500
  matcher: "Edit|MultiEdit|Write",
2247
2501
  hooks: [{ type: "command", command: "urbicon hook" }]
2248
2502
  });
2249
- await mkdir(dirname4(settingsPath), { recursive: true });
2503
+ await mkdir(dirname5(settingsPath), { recursive: true });
2250
2504
  await writeFile2(settingsPath, `${JSON.stringify(settings, null, 2)}
2251
2505
  `, "utf-8");
2252
2506
  return "added";
@@ -2263,7 +2517,7 @@ async function runInit(_positionals, flags) {
2263
2517
  printError(err.message);
2264
2518
  return EXIT.FAIL;
2265
2519
  }
2266
- const agentsPath = resolve6(stringFlag(flags, "agents-file") ?? "AGENTS.md");
2520
+ const agentsPath = resolve7(stringFlag(flags, "agents-file") ?? "AGENTS.md");
2267
2521
  const existingAgents = await readOrNull(agentsPath) ?? "";
2268
2522
  let upserted;
2269
2523
  try {
@@ -2283,7 +2537,7 @@ async function runInit(_positionals, flags) {
2283
2537
  done.push(`${rel(manifestPath)} — scaffolded`);
2284
2538
  }
2285
2539
  if (boolFlag(flags, "hook")) {
2286
- const settingsPath = resolve6(".claude", "settings.json");
2540
+ const settingsPath = resolve7(".claude", "settings.json");
2287
2541
  try {
2288
2542
  const result = await mergeHook(settingsPath);
2289
2543
  (result === "added" ? done : skipped).push(`${rel(settingsPath)} — ${result === "added" ? "wired" : "already has"} the PostToolUse \`urbicon hook\``);
@@ -2292,12 +2546,12 @@ async function runInit(_positionals, flags) {
2292
2546
  }
2293
2547
  }
2294
2548
  if (boolFlag(flags, "ci")) {
2295
- const ciPath = resolve6(".github", "workflows", "design-gate.yml");
2549
+ const ciPath = resolve7(".github", "workflows", "design-gate.yml");
2296
2550
  if (await readOrNull(ciPath)) {
2297
2551
  skipped.push(`${rel(ciPath)} — already present`);
2298
2552
  } else {
2299
2553
  const ci = await readTemplate("ci-github.yml");
2300
- await mkdir(dirname4(ciPath), { recursive: true });
2554
+ await mkdir(dirname5(ciPath), { recursive: true });
2301
2555
  await writeFile2(ciPath, ci, "utf-8");
2302
2556
  done.push(`${rel(ciPath)} — wrote the design-gate workflow`);
2303
2557
  }
@@ -2310,6 +2564,8 @@ async function runInit(_positionals, flags) {
2310
2564
  console.log(` · ${s}`);
2311
2565
  console.log(`
2312
2566
  Next steps:`);
2567
+ for (const line of tailwindSteps(readConsumerDependencies()))
2568
+ console.log(line);
2313
2569
  console.log(" • Make sure your agent reads AGENTS.md (or paste the block into CLAUDE.md / .cursorrules).");
2314
2570
  console.log(" • Seed the design memory: `bunx urbicon verb adopt` (brownfield) or `onboard` (greenfield) — the guided intake.");
2315
2571
  if (!boolFlag(flags, "hook")) {
@@ -2366,7 +2622,7 @@ async function runRecordDecision(_positionals, flags) {
2366
2622
  }
2367
2623
 
2368
2624
  // src/cli/commands/sync-manifest.ts
2369
- import { dirname as dirname5 } from "node:path";
2625
+ import { dirname as dirname6 } from "node:path";
2370
2626
  async function runSyncManifest(_positionals, flags) {
2371
2627
  const path = resolveManifestPath(stringFlag(flags, "manifest"));
2372
2628
  if (!path.endsWith(".md")) {
@@ -2374,7 +2630,7 @@ async function runSyncManifest(_positionals, flags) {
2374
2630
  return EXIT.USAGE;
2375
2631
  }
2376
2632
  const src = resolveSourceDir(stringFlag(flags, "src"));
2377
- const usages = await scanMarkers(src, dirname5(path));
2633
+ const usages = await scanMarkers(src, dirname6(path));
2378
2634
  const { content, created } = await readOrCreateManifest(path);
2379
2635
  const updated = upsertUsagesSection(content, usages);
2380
2636
  try {
@@ -2403,7 +2659,7 @@ async function runSyncManifest(_positionals, flags) {
2403
2659
 
2404
2660
  // src/cli/commands/validate.ts
2405
2661
  import { readdir as readdir2, readFile as readFile8, stat as stat2 } from "node:fs/promises";
2406
- import { join as join3, relative as relative3, resolve as resolve7, sep as sep2 } from "node:path";
2662
+ import { join as join4, relative as relative3, resolve as resolve8, sep as sep2 } from "node:path";
2407
2663
  var SKIP_DIRS2 = new Set([
2408
2664
  "node_modules",
2409
2665
  ".svelte-kit",
@@ -2432,9 +2688,9 @@ async function collectSvelte(dir, depth = 0) {
2432
2688
  if (entry.isDirectory()) {
2433
2689
  if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith("."))
2434
2690
  continue;
2435
- files.push(...await collectSvelte(join3(dir, entry.name), depth + 1));
2691
+ files.push(...await collectSvelte(join4(dir, entry.name), depth + 1));
2436
2692
  } else if (entry.isFile() && entry.name.endsWith(".svelte")) {
2437
- files.push(join3(dir, entry.name));
2693
+ files.push(join4(dir, entry.name));
2438
2694
  }
2439
2695
  }
2440
2696
  return files;
@@ -2455,7 +2711,7 @@ async function gather(positionals) {
2455
2711
  units.push({ label: "<stdin>", code: await readStdin2() });
2456
2712
  continue;
2457
2713
  }
2458
- const abs = resolve7(p);
2714
+ const abs = resolve8(p);
2459
2715
  let info;
2460
2716
  try {
2461
2717
  info = await stat2(abs);
@@ -2547,11 +2803,11 @@ FAIL — ${reason}.`);
2547
2803
 
2548
2804
  // src/cli/commands/verb.ts
2549
2805
  import { readdir as readdir3, readFile as readFile9 } from "node:fs/promises";
2550
- import { resolve as resolve8 } from "node:path";
2806
+ import { resolve as resolve9 } from "node:path";
2551
2807
  var SAFE_VERB = /^[a-z][a-z0-9-]*$/;
2552
2808
  async function resolveVerbsDir() {
2553
2809
  const root = await findPackageRoot();
2554
- return root ? resolve8(root, "skill", "verbs") : null;
2810
+ return root ? resolve9(root, "skill", "verbs") : null;
2555
2811
  }
2556
2812
  function purposeOf(body) {
2557
2813
  const heading = body.split(`
@@ -2577,7 +2833,7 @@ async function runVerbList(_positionals, _flags) {
2577
2833
  const name = file.replace(/\.md$/, "");
2578
2834
  let purpose = "";
2579
2835
  try {
2580
- purpose = purposeOf(await readFile9(resolve8(dir, file), "utf-8"));
2836
+ purpose = purposeOf(await readFile9(resolve9(dir, file), "utf-8"));
2581
2837
  } catch {}
2582
2838
  console.log(` ${name.padEnd(10)} ${purpose}`);
2583
2839
  }
@@ -2599,7 +2855,7 @@ async function runVerb(positionals, _flags) {
2599
2855
  return EXIT.FAIL;
2600
2856
  }
2601
2857
  try {
2602
- console.log(await readFile9(resolve8(dir, `${name}.md`), "utf-8"));
2858
+ console.log(await readFile9(resolve9(dir, `${name}.md`), "utf-8"));
2603
2859
  return EXIT.OK;
2604
2860
  } catch {
2605
2861
  printError(`unknown verb "${name}" — list the available verbs with \`urbicon verbs\``);
@@ -2682,7 +2938,7 @@ async function readVersion() {
2682
2938
  if (!root)
2683
2939
  return "unknown";
2684
2940
  try {
2685
- const pkg = JSON.parse(await readFile10(resolve9(root, "package.json"), "utf-8"));
2941
+ const pkg = JSON.parse(await readFile10(resolve10(root, "package.json"), "utf-8"));
2686
2942
  return pkg.version ?? "unknown";
2687
2943
  } catch {
2688
2944
  return "unknown";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urbicon-ui/design",
3
- "version": "6.2.0",
3
+ "version": "6.3.3",
4
4
  "description": "The urbicon CLI — version-pinned design validation and design-manifest tooling for projects built with Urbicon UI. Wraps @urbicon-ui/design-engine for editor hooks and CI.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,8 +38,8 @@
38
38
  "test:run": "vitest run"
39
39
  },
40
40
  "dependencies": {
41
- "@urbicon-ui/design-content": "6.2.0",
42
- "@urbicon-ui/design-engine": "6.2.0"
41
+ "@urbicon-ui/design-content": "6.3.3",
42
+ "@urbicon-ui/design-engine": "6.3.3"
43
43
  },
44
44
  "devDependencies": {
45
45
  "typescript": "^6.0.3",
@@ -78,6 +78,8 @@ app root and reference it by name; never force colours with inline `!` overrides
78
78
  </BlocksProvider>
79
79
  ```
80
80
 
81
+ The full override ladder (weakest → strongest): `class` (root slot only) → instance `slotClasses={{ <slot>: … }}` → `BlocksProvider` `defaults`/`presets` → prop-conditional `overrides` (one variant/intent/state) → `unstyled` + `slotClasses` (strip & rebuild).
82
+
81
83
  **Svelte 5** — `$props()` not `export let`; `{#snippet}` / `{@render}` not `<slot>`; callback props
82
84
  (`onValueChange`) not `createEventDispatcher`; lowercase DOM events (`onclick`).
83
85