@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 +11 -0
- package/dist/cli.js +294 -38
- package/package.json +3 -3
- package/templates/AGENTS.md +2 -0
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
|
|
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
|
|
173
|
-
for (
|
|
174
|
-
const m =
|
|
175
|
-
if (m)
|
|
176
|
-
|
|
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 (
|
|
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
|
|
824
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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,
|
|
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
|
|
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(
|
|
2691
|
+
files.push(...await collectSvelte(join4(dir, entry.name), depth + 1));
|
|
2436
2692
|
} else if (entry.isFile() && entry.name.endsWith(".svelte")) {
|
|
2437
|
-
files.push(
|
|
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 =
|
|
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
|
|
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 ?
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
42
|
-
"@urbicon-ui/design-engine": "6.
|
|
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",
|
package/templates/AGENTS.md
CHANGED
|
@@ -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
|
|