@urbicon-ui/design 6.3.1 → 6.3.6
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 +291 -37
- package/package.json +4 -4
- 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 = {
|
|
@@ -1740,6 +1835,53 @@ var KNOWN_BAD_NAMESPACES = {
|
|
|
1740
1835
|
"status-": "Use a `feedback-*` token (feedback-success, feedback-error, …) or a bare intent (`success`, `danger`).",
|
|
1741
1836
|
"-fg": "Use `text-on-primary` / `text-on-surface` for foreground-on-intent text."
|
|
1742
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
|
+
}
|
|
1743
1885
|
|
|
1744
1886
|
// ../design-engine/src/linter/rules.ts
|
|
1745
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`).";
|
|
@@ -1951,6 +2093,81 @@ var dynamicClassInterpolation = {
|
|
|
1951
2093
|
return dedupeByLine(findings);
|
|
1952
2094
|
}
|
|
1953
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
|
+
};
|
|
1954
2171
|
var tokenHallucination = {
|
|
1955
2172
|
id: "token-hallucination",
|
|
1956
2173
|
severity: "warning",
|
|
@@ -1963,8 +2180,21 @@ var tokenHallucination = {
|
|
|
1963
2180
|
lines.forEach((line, i) => {
|
|
1964
2181
|
for (const m of line.matchAll(re)) {
|
|
1965
2182
|
const core = m[2];
|
|
1966
|
-
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
|
+
}
|
|
1967
2196
|
continue;
|
|
2197
|
+
}
|
|
1968
2198
|
if (validCores.has(core))
|
|
1969
2199
|
continue;
|
|
1970
2200
|
findings.push({
|
|
@@ -2022,6 +2252,8 @@ var RULES = [
|
|
|
2022
2252
|
darkModeOverride,
|
|
2023
2253
|
focusNotVisible,
|
|
2024
2254
|
hardcodedZIndex,
|
|
2255
|
+
hardcodedMotion,
|
|
2256
|
+
deepInternalImport,
|
|
2025
2257
|
dynamicClassInterpolation,
|
|
2026
2258
|
tokenHallucination,
|
|
2027
2259
|
...MARKUP_RULES
|
|
@@ -2146,7 +2378,7 @@ async function runHook(_positionals, flags) {
|
|
|
2146
2378
|
for (const p of paths) {
|
|
2147
2379
|
let code;
|
|
2148
2380
|
try {
|
|
2149
|
-
code = await readFile5(
|
|
2381
|
+
code = await readFile5(resolve5(p), "utf-8");
|
|
2150
2382
|
} catch {
|
|
2151
2383
|
continue;
|
|
2152
2384
|
}
|
|
@@ -2171,21 +2403,21 @@ Fix the issues above and re-save. — urbicon design gate`);
|
|
|
2171
2403
|
|
|
2172
2404
|
// src/cli/commands/init.ts
|
|
2173
2405
|
import { mkdir, readFile as readFile7, writeFile as writeFile2 } from "node:fs/promises";
|
|
2174
|
-
import { dirname as
|
|
2406
|
+
import { dirname as dirname5, join as join3, relative as relative2, resolve as resolve7 } from "node:path";
|
|
2175
2407
|
|
|
2176
2408
|
// src/cli/package-root.ts
|
|
2177
2409
|
import { readFile as readFile6 } from "node:fs/promises";
|
|
2178
|
-
import { dirname as
|
|
2410
|
+
import { dirname as dirname4, resolve as resolve6 } from "node:path";
|
|
2179
2411
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2180
2412
|
async function findPackageRoot() {
|
|
2181
|
-
let dir =
|
|
2413
|
+
let dir = dirname4(fileURLToPath2(import.meta.url));
|
|
2182
2414
|
for (let i = 0;i < 6; i++) {
|
|
2183
2415
|
try {
|
|
2184
|
-
const pkg = JSON.parse(await readFile6(
|
|
2416
|
+
const pkg = JSON.parse(await readFile6(resolve6(dir, "package.json"), "utf-8"));
|
|
2185
2417
|
if (pkg.name === "@urbicon-ui/design")
|
|
2186
2418
|
return dir;
|
|
2187
2419
|
} catch {}
|
|
2188
|
-
const parent =
|
|
2420
|
+
const parent = dirname4(dir);
|
|
2189
2421
|
if (parent === dir)
|
|
2190
2422
|
break;
|
|
2191
2423
|
dir = parent;
|
|
@@ -2196,11 +2428,31 @@ async function findPackageRoot() {
|
|
|
2196
2428
|
// src/cli/commands/init.ts
|
|
2197
2429
|
var BLOCK_START = "<!-- urbicon:start";
|
|
2198
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
|
+
}
|
|
2199
2451
|
async function readTemplate(name) {
|
|
2200
2452
|
const root = await findPackageRoot();
|
|
2201
2453
|
if (!root)
|
|
2202
2454
|
throw new Error("could not locate the @urbicon-ui/design package root");
|
|
2203
|
-
return readFile7(
|
|
2455
|
+
return readFile7(join3(root, "templates", name), "utf-8");
|
|
2204
2456
|
}
|
|
2205
2457
|
async function readOrNull(path) {
|
|
2206
2458
|
try {
|
|
@@ -2248,7 +2500,7 @@ async function mergeHook(settingsPath) {
|
|
|
2248
2500
|
matcher: "Edit|MultiEdit|Write",
|
|
2249
2501
|
hooks: [{ type: "command", command: "urbicon hook" }]
|
|
2250
2502
|
});
|
|
2251
|
-
await mkdir(
|
|
2503
|
+
await mkdir(dirname5(settingsPath), { recursive: true });
|
|
2252
2504
|
await writeFile2(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
2253
2505
|
`, "utf-8");
|
|
2254
2506
|
return "added";
|
|
@@ -2265,7 +2517,7 @@ async function runInit(_positionals, flags) {
|
|
|
2265
2517
|
printError(err.message);
|
|
2266
2518
|
return EXIT.FAIL;
|
|
2267
2519
|
}
|
|
2268
|
-
const agentsPath =
|
|
2520
|
+
const agentsPath = resolve7(stringFlag(flags, "agents-file") ?? "AGENTS.md");
|
|
2269
2521
|
const existingAgents = await readOrNull(agentsPath) ?? "";
|
|
2270
2522
|
let upserted;
|
|
2271
2523
|
try {
|
|
@@ -2285,7 +2537,7 @@ async function runInit(_positionals, flags) {
|
|
|
2285
2537
|
done.push(`${rel(manifestPath)} — scaffolded`);
|
|
2286
2538
|
}
|
|
2287
2539
|
if (boolFlag(flags, "hook")) {
|
|
2288
|
-
const settingsPath =
|
|
2540
|
+
const settingsPath = resolve7(".claude", "settings.json");
|
|
2289
2541
|
try {
|
|
2290
2542
|
const result = await mergeHook(settingsPath);
|
|
2291
2543
|
(result === "added" ? done : skipped).push(`${rel(settingsPath)} — ${result === "added" ? "wired" : "already has"} the PostToolUse \`urbicon hook\``);
|
|
@@ -2294,12 +2546,12 @@ async function runInit(_positionals, flags) {
|
|
|
2294
2546
|
}
|
|
2295
2547
|
}
|
|
2296
2548
|
if (boolFlag(flags, "ci")) {
|
|
2297
|
-
const ciPath =
|
|
2549
|
+
const ciPath = resolve7(".github", "workflows", "design-gate.yml");
|
|
2298
2550
|
if (await readOrNull(ciPath)) {
|
|
2299
2551
|
skipped.push(`${rel(ciPath)} — already present`);
|
|
2300
2552
|
} else {
|
|
2301
2553
|
const ci = await readTemplate("ci-github.yml");
|
|
2302
|
-
await mkdir(
|
|
2554
|
+
await mkdir(dirname5(ciPath), { recursive: true });
|
|
2303
2555
|
await writeFile2(ciPath, ci, "utf-8");
|
|
2304
2556
|
done.push(`${rel(ciPath)} — wrote the design-gate workflow`);
|
|
2305
2557
|
}
|
|
@@ -2312,6 +2564,8 @@ async function runInit(_positionals, flags) {
|
|
|
2312
2564
|
console.log(` · ${s}`);
|
|
2313
2565
|
console.log(`
|
|
2314
2566
|
Next steps:`);
|
|
2567
|
+
for (const line of tailwindSteps(readConsumerDependencies()))
|
|
2568
|
+
console.log(line);
|
|
2315
2569
|
console.log(" • Make sure your agent reads AGENTS.md (or paste the block into CLAUDE.md / .cursorrules).");
|
|
2316
2570
|
console.log(" • Seed the design memory: `bunx urbicon verb adopt` (brownfield) or `onboard` (greenfield) — the guided intake.");
|
|
2317
2571
|
if (!boolFlag(flags, "hook")) {
|
|
@@ -2368,7 +2622,7 @@ async function runRecordDecision(_positionals, flags) {
|
|
|
2368
2622
|
}
|
|
2369
2623
|
|
|
2370
2624
|
// src/cli/commands/sync-manifest.ts
|
|
2371
|
-
import { dirname as
|
|
2625
|
+
import { dirname as dirname6 } from "node:path";
|
|
2372
2626
|
async function runSyncManifest(_positionals, flags) {
|
|
2373
2627
|
const path = resolveManifestPath(stringFlag(flags, "manifest"));
|
|
2374
2628
|
if (!path.endsWith(".md")) {
|
|
@@ -2376,7 +2630,7 @@ async function runSyncManifest(_positionals, flags) {
|
|
|
2376
2630
|
return EXIT.USAGE;
|
|
2377
2631
|
}
|
|
2378
2632
|
const src = resolveSourceDir(stringFlag(flags, "src"));
|
|
2379
|
-
const usages = await scanMarkers(src,
|
|
2633
|
+
const usages = await scanMarkers(src, dirname6(path));
|
|
2380
2634
|
const { content, created } = await readOrCreateManifest(path);
|
|
2381
2635
|
const updated = upsertUsagesSection(content, usages);
|
|
2382
2636
|
try {
|
|
@@ -2405,7 +2659,7 @@ async function runSyncManifest(_positionals, flags) {
|
|
|
2405
2659
|
|
|
2406
2660
|
// src/cli/commands/validate.ts
|
|
2407
2661
|
import { readdir as readdir2, readFile as readFile8, stat as stat2 } from "node:fs/promises";
|
|
2408
|
-
import { join as
|
|
2662
|
+
import { join as join4, relative as relative3, resolve as resolve8, sep as sep2 } from "node:path";
|
|
2409
2663
|
var SKIP_DIRS2 = new Set([
|
|
2410
2664
|
"node_modules",
|
|
2411
2665
|
".svelte-kit",
|
|
@@ -2434,9 +2688,9 @@ async function collectSvelte(dir, depth = 0) {
|
|
|
2434
2688
|
if (entry.isDirectory()) {
|
|
2435
2689
|
if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith("."))
|
|
2436
2690
|
continue;
|
|
2437
|
-
files.push(...await collectSvelte(
|
|
2691
|
+
files.push(...await collectSvelte(join4(dir, entry.name), depth + 1));
|
|
2438
2692
|
} else if (entry.isFile() && entry.name.endsWith(".svelte")) {
|
|
2439
|
-
files.push(
|
|
2693
|
+
files.push(join4(dir, entry.name));
|
|
2440
2694
|
}
|
|
2441
2695
|
}
|
|
2442
2696
|
return files;
|
|
@@ -2457,7 +2711,7 @@ async function gather(positionals) {
|
|
|
2457
2711
|
units.push({ label: "<stdin>", code: await readStdin2() });
|
|
2458
2712
|
continue;
|
|
2459
2713
|
}
|
|
2460
|
-
const abs =
|
|
2714
|
+
const abs = resolve8(p);
|
|
2461
2715
|
let info;
|
|
2462
2716
|
try {
|
|
2463
2717
|
info = await stat2(abs);
|
|
@@ -2549,11 +2803,11 @@ FAIL — ${reason}.`);
|
|
|
2549
2803
|
|
|
2550
2804
|
// src/cli/commands/verb.ts
|
|
2551
2805
|
import { readdir as readdir3, readFile as readFile9 } from "node:fs/promises";
|
|
2552
|
-
import { resolve as
|
|
2806
|
+
import { resolve as resolve9 } from "node:path";
|
|
2553
2807
|
var SAFE_VERB = /^[a-z][a-z0-9-]*$/;
|
|
2554
2808
|
async function resolveVerbsDir() {
|
|
2555
2809
|
const root = await findPackageRoot();
|
|
2556
|
-
return root ?
|
|
2810
|
+
return root ? resolve9(root, "skill", "verbs") : null;
|
|
2557
2811
|
}
|
|
2558
2812
|
function purposeOf(body) {
|
|
2559
2813
|
const heading = body.split(`
|
|
@@ -2579,7 +2833,7 @@ async function runVerbList(_positionals, _flags) {
|
|
|
2579
2833
|
const name = file.replace(/\.md$/, "");
|
|
2580
2834
|
let purpose = "";
|
|
2581
2835
|
try {
|
|
2582
|
-
purpose = purposeOf(await readFile9(
|
|
2836
|
+
purpose = purposeOf(await readFile9(resolve9(dir, file), "utf-8"));
|
|
2583
2837
|
} catch {}
|
|
2584
2838
|
console.log(` ${name.padEnd(10)} ${purpose}`);
|
|
2585
2839
|
}
|
|
@@ -2601,7 +2855,7 @@ async function runVerb(positionals, _flags) {
|
|
|
2601
2855
|
return EXIT.FAIL;
|
|
2602
2856
|
}
|
|
2603
2857
|
try {
|
|
2604
|
-
console.log(await readFile9(
|
|
2858
|
+
console.log(await readFile9(resolve9(dir, `${name}.md`), "utf-8"));
|
|
2605
2859
|
return EXIT.OK;
|
|
2606
2860
|
} catch {
|
|
2607
2861
|
printError(`unknown verb "${name}" — list the available verbs with \`urbicon verbs\``);
|
|
@@ -2684,7 +2938,7 @@ async function readVersion() {
|
|
|
2684
2938
|
if (!root)
|
|
2685
2939
|
return "unknown";
|
|
2686
2940
|
try {
|
|
2687
|
-
const pkg = JSON.parse(await readFile10(
|
|
2941
|
+
const pkg = JSON.parse(await readFile10(resolve10(root, "package.json"), "utf-8"));
|
|
2688
2942
|
return pkg.version ?? "unknown";
|
|
2689
2943
|
} catch {
|
|
2690
2944
|
return "unknown";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@urbicon-ui/design",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.6",
|
|
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,12 +38,12 @@
|
|
|
38
38
|
"test:run": "vitest run"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@urbicon-ui/design-content": "6.3.
|
|
42
|
-
"@urbicon-ui/design-engine": "6.3.
|
|
41
|
+
"@urbicon-ui/design-content": "6.3.6",
|
|
42
|
+
"@urbicon-ui/design-engine": "6.3.6"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"typescript": "^6.0.3",
|
|
46
|
-
"@types/node": "^25.9.
|
|
46
|
+
"@types/node": "^25.9.4",
|
|
47
47
|
"vitest": "^4.1.9"
|
|
48
48
|
},
|
|
49
49
|
"author": "Urbicon UI Team"
|
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
|
|