roast-my-design-system 9.4.0 → 9.5.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/README.md +2 -2
- package/package.json +1 -1
- package/skills/roast-my-design-system/scripts/diagnose/index.mjs +2 -2
- package/skills/roast-my-design-system/scripts/diagnose/score.mjs +2 -2
- package/skills/roast-my-design-system/scripts/harvest/index.mjs +24 -12
- package/skills/roast-my-design-system/scripts/harvest/paint.mjs +24 -3
- package/skills/roast-my-design-system/scripts/lib/guard-api.mjs +16 -8
- package/skills/roast-my-design-system/scripts/lib/palette.mjs +21 -4
- package/skills/roast-my-design-system/scripts/lib/version.mjs +1 -1
- package/skills/roast-my-design-system/scripts/mcp/engine.mjs +7 -4
- package/skills/roast-my-design-system/scripts/mcp/knowledge.mjs +5 -2
- package/skills/roast-my-design-system/scripts/profiles/index.mjs +37 -12
- package/skills/roast-my-design-system/scripts/profiles/installed.mjs +74 -0
- package/skills/roast-my-design-system/scripts/rules/build.mjs +71 -6
package/README.md
CHANGED
|
@@ -142,11 +142,11 @@ Eleven reports, hosted exactly as the tool writes them. Every number determinist
|
|
|
142
142
|
|
|
143
143
|
The full report for vercel/ai-chatbot. The verdict answers what the agent will learn here, the gap section comes first, then "What the repo teaches the agent", Claude's read of the scan:
|
|
144
144
|
|
|
145
|
-

|
|
146
146
|
|
|
147
147
|
The same report in light mode (one file, built-in toggle):
|
|
148
148
|
|
|
149
|
-

|
|
150
150
|
|
|
151
151
|
## Why the numbers hold
|
|
152
152
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roast-my-design-system",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.5.0",
|
|
4
4
|
"mcpName": "io.github.gregkozakiewicz/roast-my-design-system",
|
|
5
5
|
"description": "Your AI can write the UI. This makes sure it writes your UI. A deterministic scanner scores your design system 0-100 against 112 public repos, reads React and web components (Stencil, Lit), hands you a copy-paste fix prompt for each top finding, writes rules for Claude, Cursor, Copilot and Windsurf with --apply, and runs as a local MCP server with --mcp.",
|
|
6
6
|
"keywords": [
|
|
@@ -32,7 +32,7 @@ import { fixPrompt } from '../lib/fixprompt.mjs';
|
|
|
32
32
|
import { WHY } from './why.mjs';
|
|
33
33
|
import { parseColor, luminance, isGrey } from '../lib/color.mjs';
|
|
34
34
|
import { loadBenchmark, benchHelpers, makeHealthOf, coreMetrics, tileHealths, scoreOfTiles, scoreBreakdown, scorePackage as scorePackageOf, ZERO_IDEAL, WARN_TOLERANCE, SCORE_OF, SCHEMA_VERSION } from './score.mjs';
|
|
35
|
-
import { ownSpacing, profileOf,
|
|
35
|
+
import { ownSpacing, profileOf, installedOf, splitArbitrary } from '../profiles/index.mjs';
|
|
36
36
|
import { KITS } from '../profiles/kit-common.mjs';
|
|
37
37
|
import { rulesOn as lintRulesOn, describeRule as lintDescribe, entryMatcher as lintMatcher, RULE_TILE as LINT_TILE } from '../profiles/shadcn-lint.mjs';
|
|
38
38
|
import { publishesLine } from '../profiles/registry.mjs';
|
|
@@ -257,7 +257,7 @@ const P = profileOf(h);
|
|
|
257
257
|
const fresh = P.isShadcn && P.shadcn?.fresh?.fresh === true;
|
|
258
258
|
// On a shadcn repo the bracket list is the team's own; installed brackets are
|
|
259
259
|
// named in the side panel instead (see coreMetrics for the count).
|
|
260
|
-
if (P.isShadcn) arbitrary = splitArbitrary(arbitrary,
|
|
260
|
+
if (P.isShadcn) arbitrary = splitArbitrary(arbitrary, installedOf(P)).own;
|
|
261
261
|
const breakdown = scoreBreakdown(h, bench);
|
|
262
262
|
const neverImported = neverImportedComponents(h.components, P.uiDir);
|
|
263
263
|
|
|
@@ -26,7 +26,7 @@ import { isGrey } from '../lib/color.mjs';
|
|
|
26
26
|
import { nearColorPairs } from '../lib/nearpairs.mjs';
|
|
27
27
|
import { neverImportedComponents } from '../lib/neverimported.mjs';
|
|
28
28
|
import { SCHEMA_VERSION } from '../lib/version.mjs';
|
|
29
|
-
import { profileOf,
|
|
29
|
+
import { profileOf, installedOf, splitArbitrary, ownSpacing } from '../profiles/index.mjs';
|
|
30
30
|
|
|
31
31
|
export { SCHEMA_VERSION };
|
|
32
32
|
|
|
@@ -146,7 +146,7 @@ export function coreMetrics(h, opts = {}) {
|
|
|
146
146
|
const P = profileOf(h);
|
|
147
147
|
// On a shadcn repo the bracket count is the team's own: brackets inside the
|
|
148
148
|
// catalogue, kit blocks and registries are installed choices, not drift.
|
|
149
|
-
const arbitraryEntries = P.isShadcn ? splitArbitrary(h.tokens?.tailwind?.arbitrary ?? [],
|
|
149
|
+
const arbitraryEntries = P.isShadcn ? splitArbitrary(h.tokens?.tailwind?.arbitrary ?? [], installedOf(P)).own : (h.tokens?.tailwind?.arbitrary ?? []);
|
|
150
150
|
// ownCode: the breakdown under the score. Same metrics, paint counted on
|
|
151
151
|
// own code only, so the difference is what installed registries cost.
|
|
152
152
|
const paint = P.isTailwind ? P.tailwind?.paint
|
|
@@ -29,9 +29,9 @@ import { ruleStaleness } from '../lib/staleness.mjs';
|
|
|
29
29
|
import { neverImportedComponents } from '../lib/neverimported.mjs';
|
|
30
30
|
import { lastTouchedDates } from '../lib/lasttouched.mjs';
|
|
31
31
|
import { SCHEMA_VERSION } from '../lib/version.mjs';
|
|
32
|
-
import { decideProfile, decideFresh, profileOf,
|
|
32
|
+
import { decideProfile, decideFresh, profileOf, installedOf, doorFile, installedFile, splitArbitrary, scopeFiles } from '../profiles/index.mjs';
|
|
33
33
|
import { publishesLine } from '../profiles/registry.mjs';
|
|
34
|
-
import { countPaint } from './paint.mjs';
|
|
34
|
+
import { countPaint, doorShades } from './paint.mjs';
|
|
35
35
|
|
|
36
36
|
function arg(name, fallback) {
|
|
37
37
|
const i = process.argv.indexOf(`--${name}`);
|
|
@@ -120,17 +120,18 @@ if (profileOf(profile).isRegistry) {
|
|
|
120
120
|
const P = profileOf(profile);
|
|
121
121
|
if (P.isShadcn) {
|
|
122
122
|
const regDirs = profile.shadcn.registryDirs ?? [];
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
123
|
+
// installed code, file by file (profiles/installed.mjs): a component of
|
|
124
|
+
// the team's own kept in the catalogue folder is own code (9.5.0)
|
|
125
|
+
const installed = installedOf(P);
|
|
126
|
+
const doorFiles = new Set(files.code.filter(installed));
|
|
126
127
|
const kitNames = new Set(components.filter((c) => doorFiles.has(c.file)).map((c) => c.name));
|
|
127
128
|
// The score reads the repo as the agent meets it: own code plus installed
|
|
128
129
|
// registries, because a palette colour in ai-elements teaches the same
|
|
129
|
-
// wrong lesson as one in own code.
|
|
130
|
-
// editing them is the intended use.
|
|
131
|
-
profile.shadcn.paint = countPaint(target, files.code, {
|
|
130
|
+
// wrong lesson as one in own code. shadcn's own components and kit blocks
|
|
131
|
+
// stay out: editing them is the intended use.
|
|
132
|
+
profile.shadcn.paint = countPaint(target, files.code, { isDoor: (f) => doorFile(P, f), kitNames, email });
|
|
132
133
|
// Own code alone, for the breakdown line under the score.
|
|
133
|
-
profile.shadcn.paintOwn = regDirs.length ? countPaint(target, files.code, {
|
|
134
|
+
profile.shadcn.paintOwn = regDirs.length ? countPaint(target, files.code, { isDoor: installed, kitNames, email }) : profile.shadcn.paint;
|
|
134
135
|
if (regDirs.length) {
|
|
135
136
|
const regFiles = files.code.filter((f) => regDirs.some((d) => f.startsWith(`${d}/`)));
|
|
136
137
|
const rp = countPaint(target, regFiles, { uiDirs: [], kitNames, email });
|
|
@@ -138,7 +139,18 @@ if (profileOf(profile).isRegistry) {
|
|
|
138
139
|
}
|
|
139
140
|
// Bracket values inside installed code are shadcn's (or a registry's)
|
|
140
141
|
// choices: a true lesson, badly framed. Kept out of the count, named.
|
|
141
|
-
|
|
142
|
+
// Utility-class mode (9.5.0), for the rules file only: the shades shadcn's
|
|
143
|
+
// own components paint with, and, where the repo keeps a theme of its
|
|
144
|
+
// own, the palette classes in own code that theme does not own.
|
|
145
|
+
if (P.designSystem?.cssVariables === false) {
|
|
146
|
+
profile.shadcn.doorShades = doorShades(target, files.code.filter((f) => /\.(tsx|jsx)$/.test(f) && doorFile(P, f)));
|
|
147
|
+
const own = profile.shadcn.ownTheme;
|
|
148
|
+
if (own) {
|
|
149
|
+
const p = countPaint(target, files.code, { isDoor: installed, retuned: own.retuned, families: own.families, email });
|
|
150
|
+
profile.shadcn.ownThemePaint = { uses: p.tin.uses, samples: p.tin.samples.slice(0, 6) };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const split = splitArbitrary(tokens.tailwind?.arbitrary ?? [], installed);
|
|
142
154
|
profile.shadcn.arbitraryInstalled = split.installed;
|
|
143
155
|
// A fresh kit (the install command's output, nothing built yet) is
|
|
144
156
|
// decided here, once, from the facts above.
|
|
@@ -162,8 +174,8 @@ const context = harvestContext(target);
|
|
|
162
174
|
// The chart palette the repo keeps, or the charts painting by hand without
|
|
163
175
|
// one, and the gaps an agent will fill by inventing (lib/charts, lib/gaps).
|
|
164
176
|
const charts = (() => {
|
|
165
|
-
const
|
|
166
|
-
const own = (f) => !
|
|
177
|
+
const P = profileOf(profile);
|
|
178
|
+
const own = (f) => !installedFile(P, f);
|
|
167
179
|
return chartSystemOf(files, (f) => readSource(resolve(target, f)), { own });
|
|
168
180
|
})();
|
|
169
181
|
const gaps = designGaps({ charts, tokenFile: tokens.tokenFile ?? null });
|
|
@@ -60,11 +60,13 @@ export const DEMO_PATH_RE = /(^|\/)(stories|storybook|__stories__|examples?|demo
|
|
|
60
60
|
/**
|
|
61
61
|
* @param root repo root
|
|
62
62
|
* @param codeFiles relative code paths from the walk
|
|
63
|
-
* @param opts { uiDirs: string[], kitNames: Set<string> } — catalogue folders
|
|
63
|
+
* @param opts { uiDirs: string[], kitNames: Set<string> } — catalogue folders;
|
|
64
|
+
* `isDoor(file)` in place of uiDirs decides file by file (9.5.0: a team
|
|
65
|
+
* component kept in the catalogue folder is own code, profiles/installed.mjs)
|
|
64
66
|
* and the component names defined in them
|
|
65
67
|
*/
|
|
66
|
-
export function countPaint(root, codeFiles, { uiDirs = [], kitNames = new Set(), retuned = [], families = null, email = null } = {}) {
|
|
67
|
-
const inCatalogue = (f) => uiDirs.some((d) => f === d || f.startsWith(`${d}/`));
|
|
68
|
+
export function countPaint(root, codeFiles, { uiDirs = [], isDoor = null, kitNames = new Set(), retuned = [], families = null, email = null } = {}) {
|
|
69
|
+
const inCatalogue = isDoor ?? ((f) => uiDirs.some((d) => f === d || f.startsWith(`${d}/`)));
|
|
68
70
|
const names = [...kitNames].filter((n) => /^[A-Z][A-Za-z0-9]*$/.test(n));
|
|
69
71
|
const doorOpen = names.length
|
|
70
72
|
? new RegExp(`<(${names.join('|')})\\b[^>]*?className=(?:"([^"]*)"|\\{cn\\(\\s*["'\`]([^"'\`]*)["'\`])`, 'g')
|
|
@@ -141,3 +143,22 @@ export function countPaint(root, codeFiles, { uiDirs = [], kitNames = new Set(),
|
|
|
141
143
|
});
|
|
142
144
|
return { ownFiles, tin: finish(tin), doors: finish(doors), typo: finish(typo) };
|
|
143
145
|
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The palette shades shadcn's own components paint with, most used first,
|
|
149
|
+
* with variant prefixes and opacity dropped (hover:bg-zinc-900/90 is
|
|
150
|
+
* bg-zinc-900). On a utility-class install these are the system: the rules
|
|
151
|
+
* file tells the agent to stay with them (9.5.0).
|
|
152
|
+
*/
|
|
153
|
+
export function doorShades(root, doorFiles) {
|
|
154
|
+
const tally = new Map();
|
|
155
|
+
for (const f of doorFiles) {
|
|
156
|
+
const raw = readSource(join(root, f));
|
|
157
|
+
if (raw === null) continue;
|
|
158
|
+
for (const m of blankComments(raw).matchAll(PALETTE_CLASS_RE)) {
|
|
159
|
+
const cls = m[0].replace(/^(?:[\w-]+:)*/, '').replace(/\/\d+$/, '');
|
|
160
|
+
tally.set(cls, (tally.get(cls) ?? 0) + 1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return [...tally.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 8).map(([value, count]) => ({ value, count }));
|
|
164
|
+
}
|
|
@@ -14,7 +14,7 @@ import { walkRepo, readSource, profileRepo } from '../harvest/walk.mjs';
|
|
|
14
14
|
import { chartSystemOf } from './charts.mjs';
|
|
15
15
|
import { designGaps } from './gaps.mjs';
|
|
16
16
|
export { designGaps } from './gaps.mjs';
|
|
17
|
-
import { decideProfile, profileOf, installedDirs } from '../profiles/index.mjs';
|
|
17
|
+
import { decideProfile, profileOf, installedDirs, installedFile, installedFrom } from '../profiles/index.mjs';
|
|
18
18
|
import { KITS } from '../profiles/kit-common.mjs';
|
|
19
19
|
import { kitPaintFindings } from './kitpaint.mjs';
|
|
20
20
|
import { paletteFindings } from './palette.mjs';
|
|
@@ -76,6 +76,11 @@ export { PALETTE_CLASS_RE, DEMO_PATH_RE, blankComments };
|
|
|
76
76
|
// a guard that calls this agrees with the report's live checks by
|
|
77
77
|
// construction, on a Tailwind theme and on a shadcn kit alike.
|
|
78
78
|
export { paletteFindings };
|
|
79
|
+
// Installed code, file by file (9.5.0, profiles/installed.mjs): a guard
|
|
80
|
+
// leaves installedFile(system.profile.installedFrom, file, under) alone and
|
|
81
|
+
// judges everything else, including the team's own components kept in the
|
|
82
|
+
// shadcn catalogue folder. isShadcnFile(file) is the name test on its own.
|
|
83
|
+
export { installedFile, doorFile, isShadcnFile } from '../profiles/installed.mjs';
|
|
79
84
|
// A product built on a kit (MUI, Mantine, Chakra UI, Ant Design), 8.4.6: a
|
|
80
85
|
// colour or a pixel size written onto a kit component where the theme has a
|
|
81
86
|
// value. kitPaintFindings(fileText, system.profile.kit, { file }) returns the
|
|
@@ -144,8 +149,7 @@ export function learnSystem(repoRoot, { exclude = [] } = {}) {
|
|
|
144
149
|
const read = (f) => readSource(join(repoRoot, f));
|
|
145
150
|
const hardDupes = (findDuplicates(ledger, profile.uiDir, repoRoot, profile.uiDirs ?? null).exactDuplicates ?? []).filter((d) => !d.wrapped);
|
|
146
151
|
const P = profileOf(profile);
|
|
147
|
-
const
|
|
148
|
-
const charts = chartSystemOf(files, read, { own: (f) => !installedNow.some((d) => f === d || f.startsWith(`${d}/`)) });
|
|
152
|
+
const charts = chartSystemOf(files, read, { own: (f) => !installedFile(P, f) });
|
|
149
153
|
// the kit the product is built on, with its definition attached, the way
|
|
150
154
|
// the MCP knowledge reads it (mcp/knowledge.mjs): the theme's colours are
|
|
151
155
|
// the token set on a kit repo, whatever stylesheet holds the most literals
|
|
@@ -225,12 +229,16 @@ export function learnSystem(repoRoot, { exclude = [] } = {}) {
|
|
|
225
229
|
profile: {
|
|
226
230
|
kind: P.kind,
|
|
227
231
|
role: P.role,
|
|
228
|
-
// folders
|
|
229
|
-
// registries, kit blocks)
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
// plain product is the team's own (the report splits nothing there either)
|
|
232
|
+
// folders that hold installed code (a shadcn catalogue, installed
|
|
233
|
+
// registries, kit blocks). Empty on a registry: what it publishes is
|
|
234
|
+
// its own work. Only a shadcn kit has installed code; a hand-written
|
|
235
|
+
// components/ui on a plain product is the team's own.
|
|
233
236
|
installedDirs: P.isShadcn && !P.isRegistry ? installedDirs(P) : [],
|
|
237
|
+
// Which files in those folders are installed, file by file (9.5.0): a
|
|
238
|
+
// component of the team's own kept in the catalogue folder is the
|
|
239
|
+
// team's, and judged. installedFile(profile.installedFrom, file, under)
|
|
240
|
+
// answers; `under` is how the caller matches a file to a folder.
|
|
241
|
+
installedFrom: installedFrom(P),
|
|
234
242
|
// a product built on a kit: name, theme files and values, spacing step,
|
|
235
243
|
// the team's own layers over it, and the definition kitPaintFindings reads
|
|
236
244
|
kit,
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
import { PALETTE_CLASS_RE, GREY_HUE_RE, DARK_WB_RE, DEMO_PATH_RE, blankComments } from '../harvest/paint.mjs';
|
|
35
35
|
import { TAILWIND_DEFAULTS } from '../profiles/tailwind-defaults.mjs';
|
|
36
36
|
import { oklab } from './color.mjs';
|
|
37
|
+
import { doorFile } from '../profiles/installed.mjs';
|
|
37
38
|
|
|
38
39
|
// shadcn's names, one per role the utilities want; the sheet always has
|
|
39
40
|
// greys and colours, so no family check
|
|
@@ -59,9 +60,10 @@ export function decidePalette(profile, P, readTheme = null) {
|
|
|
59
60
|
const sc = profile.shadcn;
|
|
60
61
|
// utility-class mode: the palette is the theme, and the report scores it so
|
|
61
62
|
if (P.designSystem?.cssVariables === false) return null;
|
|
62
|
-
//
|
|
63
|
-
// intended use, and the report's tile leaves them out
|
|
64
|
-
|
|
63
|
+
// shadcn's own components and kit blocks are the kit's doors: editing them
|
|
64
|
+
// is the intended use, and the report's tile leaves them out. A component
|
|
65
|
+
// of the team's own kept in the catalogue folder is not a door (9.5.0).
|
|
66
|
+
const doors = { uiDirs: [...(P.uiDirs ?? [])], shadcn: { blockFiles: [...(sc.blockFiles ?? [])] } };
|
|
65
67
|
const rows = Math.max(sc.sheet?.shadcnPresent ?? 0, sc.contract?.rows ?? 0);
|
|
66
68
|
if (rows >= CONTRACT_MIN) {
|
|
67
69
|
return {
|
|
@@ -83,7 +85,7 @@ export function decidePalette(profile, P, readTheme = null) {
|
|
|
83
85
|
export function paletteFindings(text, palette, { file = null, css = null } = {}) {
|
|
84
86
|
if (!palette || !text) return [];
|
|
85
87
|
if (css ?? (file ? CSS_FILE_RE.test(file) : false)) return [];
|
|
86
|
-
if (file && (DEMO_PATH_RE.test(file) || (palette.doors
|
|
88
|
+
if (file && (DEMO_PATH_RE.test(file) || doorFile(palette.doors, file))) return [];
|
|
87
89
|
const retuned = new Set(palette.retuned ?? []);
|
|
88
90
|
const fam = palette.families ?? null;
|
|
89
91
|
const hasFamily = (cls) => !fam || (GREY_HUE_RE.test(cls) ? fam.grey : fam.colour);
|
|
@@ -124,6 +126,21 @@ export function paletteFindings(text, palette, { file = null, css = null } = {})
|
|
|
124
126
|
return out;
|
|
125
127
|
}
|
|
126
128
|
|
|
129
|
+
/**
|
|
130
|
+
* The theme class that stands in for a palette class, or null: the theme's
|
|
131
|
+
* own word for the hue when it has one (text-red-500 on a theme naming
|
|
132
|
+
* destructive is text-destructive), else the suitable theme colour nearest
|
|
133
|
+
* by value. For the rules file's example (9.5.0).
|
|
134
|
+
*/
|
|
135
|
+
export function themeClassFor(cls, names = [], values = {}) {
|
|
136
|
+
const util = cls.replace(/^((?:[\w-]+:)*[a-z]+)-.*$/, '$1').replace(/^(?:[\w-]+:)*/, '');
|
|
137
|
+
const shade = cls.replace(/^(?:[\w-]+:)*[a-z]+-/, '').replace(/\/\d+$/, '');
|
|
138
|
+
const word = hueWord(shade, names);
|
|
139
|
+
if (names.includes(word)) return `${util}-${word}`;
|
|
140
|
+
const near = nearestThemeName(shade, utilRole(util), values);
|
|
141
|
+
return near ? `${util}-${near}` : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
127
144
|
// ---------- the theme colour a palette class stands in for ----------
|
|
128
145
|
// Which theme names suit a utility, read from the name: a text class wants an
|
|
129
146
|
// ink, a bg class a surface, a border class an edge. A name that says none of
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Single version constant for the engine — imported by diagnose (report
|
|
2
2
|
// footer) and rules (generated-by line). This is the bump spot that used to
|
|
3
3
|
// live as a const inside diagnose/index.mjs.
|
|
4
|
-
export const VERSION = '9.
|
|
4
|
+
export const VERSION = '9.5.0';
|
|
5
5
|
// The shape of harvest.json and summary.json. Bumped only when a field is
|
|
6
6
|
// renamed, removed or changes meaning; a new field is not a new schema. A
|
|
7
7
|
// tool comparing two scans compares like with like by this number, not by VERSION.
|
|
@@ -18,6 +18,7 @@ import { exemptReason, isDrawing, SVG_MARKUP_RE } from '../lib/exempt.mjs';
|
|
|
18
18
|
import { extraDeclarations, fontDeclarations } from '../lib/declarations.mjs';
|
|
19
19
|
import { typefaceOf, GENERIC_FONTS } from '../lib/typefaces.mjs';
|
|
20
20
|
import { kitPaintFindings } from '../lib/kitpaint.mjs';
|
|
21
|
+
import { installedFile } from '../profiles/installed.mjs';
|
|
21
22
|
import { paletteFindings } from '../lib/palette.mjs';
|
|
22
23
|
import { tokenTwinFindings } from '../lib/tokentwins.mjs';
|
|
23
24
|
import { avoidedImportFindings } from '../lib/avoidedimports.mjs';
|
|
@@ -246,10 +247,12 @@ export function validateContent(content, k) {
|
|
|
246
247
|
// a raw value the repo already uses, in a non-Tailwind repo, is consistency,
|
|
247
248
|
// not a new offence — silence
|
|
248
249
|
}
|
|
249
|
-
// Bracket values inside installed code (
|
|
250
|
-
// registries) are the kit's own choices. The report names them and
|
|
251
|
-
// them out of the count; a file under review is judged by the same
|
|
252
|
-
|
|
250
|
+
// Bracket values inside installed code (shadcn's own components, kit
|
|
251
|
+
// blocks, registries) are the kit's own choices. The report names them and
|
|
252
|
+
// keeps them out of the count; a file under review is judged by the same
|
|
253
|
+
// line, file by file: a component of the team's own kept in the catalogue
|
|
254
|
+
// folder is the team's (profiles/installed.mjs, 9.5.0).
|
|
255
|
+
const installed = !!file && installedFile(k.installedFrom, file);
|
|
253
256
|
const localArb = new Map();
|
|
254
257
|
for (const a of got.arbitrary) localArb.set(a.value, (localArb.get(a.value) ?? 0) + 1);
|
|
255
258
|
for (const a of installed ? [] : got.arbitrary) {
|
|
@@ -24,7 +24,7 @@ import { tsconfigAliases } from '../harvest/components.mjs';
|
|
|
24
24
|
import { neverImportedComponents } from '../lib/neverimported.mjs';
|
|
25
25
|
import { typefaceOf } from '../lib/typefaces.mjs';
|
|
26
26
|
import { hexRgb } from '../lib/nearpairs.mjs';
|
|
27
|
-
import { decideProfile, profileOf, installedDirs } from '../profiles/index.mjs';
|
|
27
|
+
import { decideProfile, profileOf, installedDirs, installedFile, installedFrom } from '../profiles/index.mjs';
|
|
28
28
|
import { KITS } from '../profiles/kit-common.mjs';
|
|
29
29
|
import { repoTokenDefs } from '../lib/tokentwins.mjs';
|
|
30
30
|
import { dupeCopiesOf } from '../lib/avoidedimports.mjs';
|
|
@@ -159,7 +159,7 @@ export function loadKnowledge(root) {
|
|
|
159
159
|
// without one (lib/charts.mjs)
|
|
160
160
|
// installed kit code is the kit's own door, never a precedent (profiles/index)
|
|
161
161
|
const installed = installedDirs(P);
|
|
162
|
-
const ownCode = (f) => !
|
|
162
|
+
const ownCode = (f) => !installedFile(P, f);
|
|
163
163
|
const charts = chartSystemOf(files, read, { own: ownCode });
|
|
164
164
|
const gaps = designGaps({ charts, tokenFile: tokens.tokenFile ?? null });
|
|
165
165
|
|
|
@@ -206,6 +206,9 @@ export function loadKnowledge(root) {
|
|
|
206
206
|
// draws (lib/guard-api.mjs): only a shadcn kit has installed code, and a
|
|
207
207
|
// registry's published folders are its own work
|
|
208
208
|
installedDirs: P.isShadcn && !P.isRegistry ? installed : [],
|
|
209
|
+
// the facts installedFile reads (profiles/installed.mjs): a component of
|
|
210
|
+
// the team's own kept in the catalogue folder is judged like own code
|
|
211
|
+
installedFrom: installedFrom(P),
|
|
209
212
|
// the kit the product is built on, and the Tailwind theme it names its
|
|
210
213
|
// colours in: null when the repo is neither (see profiles/)
|
|
211
214
|
kit,
|
|
@@ -24,6 +24,7 @@ import { join, basename, dirname } from 'node:path';
|
|
|
24
24
|
import shadcn from './shadcn.mjs';
|
|
25
25
|
import tailwind, { readTailwindTheme } from './tailwind.mjs';
|
|
26
26
|
import { decidePalette } from '../lib/palette.mjs';
|
|
27
|
+
import { installedFile } from './installed.mjs';
|
|
27
28
|
import mui from './mui.mjs';
|
|
28
29
|
import mantine from './mantine.mjs';
|
|
29
30
|
import chakra from './chakra.mjs';
|
|
@@ -89,6 +90,13 @@ export function decideProfile(profile, components, files, root = null) {
|
|
|
89
90
|
// under a shadcn kit with no rows, or nothing. The report's tile keeps
|
|
90
91
|
// its own gates (harvest/index.mjs) and no score reads this.
|
|
91
92
|
profile.palette = decidePalette(profile, profileOf(profile), () => readTailwindTheme({ ...profile }, ctx)?.facts ?? null);
|
|
93
|
+
// shadcn in utility-class mode (components.json: cssVariables false) paints
|
|
94
|
+
// with Tailwind classes by design. A theme of the repo's own beside it
|
|
95
|
+
// (rybbit retunes the whole neutral scale) is what the rules file points
|
|
96
|
+
// the agent at. Kept as facts: no score and no check reads it (9.5.0).
|
|
97
|
+
if (profile.shadcn && profile.designSystem?.cssVariables === false) {
|
|
98
|
+
profile.shadcn.ownTheme = readTailwindTheme({ ...profile }, ctx)?.facts ?? null;
|
|
99
|
+
}
|
|
92
100
|
// A shadcn repo that PUBLISHES a registry is a registry: the fourth kind.
|
|
93
101
|
// Every count still reads the shadcn facts (release (a): zero score change);
|
|
94
102
|
// the kind, the receipt and the header line say what it is.
|
|
@@ -194,14 +202,31 @@ export function installedDirs(P) {
|
|
|
194
202
|
return [...(P?.uiDirs ?? []), ...(sc.registryDirs ?? []), ...(sc.blockFiles ?? [])];
|
|
195
203
|
}
|
|
196
204
|
const underAny = (file, dirs) => dirs.some((d) => file === d || file.startsWith(`${d}/`));
|
|
205
|
+
// Which files in those folders are installed, file by file (9.5.0): a
|
|
206
|
+
// component of the team's own kept in the catalogue folder is the team's.
|
|
207
|
+
// The rule lives in profiles/installed.mjs.
|
|
208
|
+
export { isShadcnFile, doorFile, installedFile, installedFrom } from './installed.mjs';
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The installed-code test for one reading, as a function of a file path.
|
|
212
|
+
* `none` is set when the repo has no installed folders at all, so a caller
|
|
213
|
+
* can skip the split.
|
|
214
|
+
*/
|
|
215
|
+
export function installedOf(P) {
|
|
216
|
+
const fn = (file) => installedFile(P, file);
|
|
217
|
+
fn.none = installedDirs(P).length === 0;
|
|
218
|
+
return fn;
|
|
219
|
+
}
|
|
197
220
|
|
|
198
221
|
/**
|
|
199
222
|
* Split bracket-value entries ({value, count, files:[{file,count}]}) into the
|
|
200
223
|
* team's own uses and installed uses. Own entries keep only own files with
|
|
201
224
|
* counts re-summed; installed is a flat count with its top values.
|
|
225
|
+
* `installed` is installedOf(P), or (older callers) a list of folders.
|
|
202
226
|
*/
|
|
203
|
-
export function splitArbitrary(entries,
|
|
204
|
-
|
|
227
|
+
export function splitArbitrary(entries, installed) {
|
|
228
|
+
const isInstalled = typeof installed === 'function' ? installed : (file) => underAny(file, installed);
|
|
229
|
+
if (typeof installed === 'function' ? installed.none : !installed.length) return { own: entries, installed: { uses: 0, values: [] } };
|
|
205
230
|
const own = [];
|
|
206
231
|
const installedValues = new Map();
|
|
207
232
|
let installedUses = 0;
|
|
@@ -211,8 +236,8 @@ export function splitArbitrary(entries, dirs) {
|
|
|
211
236
|
// as the team's own, so ai-chatbot's installed badge put one of shadcn's
|
|
212
237
|
// [3px] values in the headline (2026-09-30)
|
|
213
238
|
const all = e.every ?? e.files ?? [];
|
|
214
|
-
const ownFiles = all.filter((f) => !
|
|
215
|
-
const instCount = all.filter((f) =>
|
|
239
|
+
const ownFiles = all.filter((f) => !isInstalled(f.file)).sort((a, b) => b.count - a.count).slice(0, 5);
|
|
240
|
+
const instCount = all.filter((f) => isInstalled(f.file)).reduce((s, f) => s + f.count, 0);
|
|
216
241
|
if (instCount) { installedUses += instCount; installedValues.set(e.value, (installedValues.get(e.value) ?? 0) + instCount); }
|
|
217
242
|
// own is the remainder of the entry's total, never a sum of the files
|
|
218
243
|
// that happened to be listed
|
|
@@ -247,16 +272,16 @@ export function decideFresh(profile, components, files, tokens, root = null) {
|
|
|
247
272
|
const P = profileOf(profile);
|
|
248
273
|
if (!P.isShadcn || !profile.shadcn) return;
|
|
249
274
|
const sc = profile.shadcn;
|
|
250
|
-
const
|
|
251
|
-
const own = (files.code ?? []).filter((f) => /\.[jt]sx?$/.test(f) && !
|
|
275
|
+
const installed = installedOf(P);
|
|
276
|
+
const own = (files.code ?? []).filter((f) => /\.[jt]sx?$/.test(f) && !installed(f));
|
|
252
277
|
// build config at the root (next.config, vite.config, tailwind.config) is scaffold too
|
|
253
278
|
const stem = (f) => f.split('/').pop().replace(/\.[jt]sx?$/, '').replace(/^[\w-]+\.config$/, 'app');
|
|
254
|
-
const ownComponents = (components ?? []).filter((c) => !c.isPage && !
|
|
255
|
-
const ownArbitrary = splitArbitrary(tokens?.tailwind?.arbitrary ?? [],
|
|
256
|
-
const ownInline = (tokens?.inlineStyles?.files ?? []).filter((f) => !
|
|
279
|
+
const ownComponents = (components ?? []).filter((c) => !c.isPage && !installed(c.file));
|
|
280
|
+
const ownArbitrary = splitArbitrary(tokens?.tailwind?.arbitrary ?? [], installed).own.reduce((s, a) => s + a.count, 0);
|
|
281
|
+
const ownInline = (tokens?.inlineStyles?.files ?? []).filter((f) => !installed(f.file ?? f)).length;
|
|
257
282
|
// the demo page the install command writes shows one Button; a page that
|
|
258
283
|
// composes several doors is a screen someone built
|
|
259
|
-
const ownPages = (components ?? []).filter((c) => c.isPage && !
|
|
284
|
+
const ownPages = (components ?? []).filter((c) => c.isPage && !installed(c.file)).map((c) => c.file);
|
|
260
285
|
const doorImports = (f) => {
|
|
261
286
|
if (!root) return 0;
|
|
262
287
|
let src = '';
|
|
@@ -290,7 +315,7 @@ export function ownSpacing(h) {
|
|
|
290
315
|
const css = h.tokens?.spacing ?? [];
|
|
291
316
|
const tw = (h.tokens?.tailwind?.spacing ?? []).filter((v) => v.value.startsWith('['));
|
|
292
317
|
if (!P.isShadcn) return { css, tw, installed: { uses: 0, values: [] } };
|
|
293
|
-
const
|
|
294
|
-
const a = splitArbitrary(css,
|
|
318
|
+
const installed = installedOf(P);
|
|
319
|
+
const a = splitArbitrary(css, installed), b = splitArbitrary(tw, installed);
|
|
295
320
|
return { css: a.own, tw: b.own, installed: { uses: a.installed.uses + b.installed.uses, values: [...a.installed.values, ...b.installed.values].sort((x, y) => y.count - x.count) } };
|
|
296
321
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installed code on a shadcn repo: which files the team did not write.
|
|
3
|
+
*
|
|
4
|
+
* Until 9.5.0 every file in the catalogue folder (components/ui, or wherever
|
|
5
|
+
* components.json points) counted as installed: its bracket values were
|
|
6
|
+
* called shadcn's, its palette classes were not counted, and the guard left
|
|
7
|
+
* it alone. Probed on the fleet (2026-10-01): 33 of 44 shadcn product repos
|
|
8
|
+
* keep components of their own in that folder, 1,014 of 2,709 files, with
|
|
9
|
+
* 1,285 palette classes nothing counted. formbricks keeps 191 of its own
|
|
10
|
+
* there, and the report called their values shadcn's.
|
|
11
|
+
*
|
|
12
|
+
* The rule, in one place for the report, the live checks and the guard:
|
|
13
|
+
* - a third-party registry folder (ai-elements, magicui) is installed
|
|
14
|
+
* - a kit block (app-sidebar, login-form) is installed
|
|
15
|
+
* - a file in the catalogue folder is installed only when it is one of
|
|
16
|
+
* shadcn's components by name, however the team spells it (Nango keeps
|
|
17
|
+
* Avatar.tsx and Checkbox.tsx); anything else there is the team's
|
|
18
|
+
*
|
|
19
|
+
* The name list is shadcn's own (CATALOGUE, BLOCK_COMPONENTS in
|
|
20
|
+
* shadcn-data.mjs), checked on 2026-10-01 against every component name
|
|
21
|
+
* shadcn has shipped, current and historical. A third-party component
|
|
22
|
+
* installed straight into the catalogue folder, rather than a folder of its
|
|
23
|
+
* own, reads as the team's: nothing on disk tells the two apart.
|
|
24
|
+
*/
|
|
25
|
+
import { basename, dirname } from 'node:path';
|
|
26
|
+
import { CATALOGUE, BLOCK_COMPONENTS } from './shadcn-data.mjs';
|
|
27
|
+
|
|
28
|
+
const kebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/_/g, '-').toLowerCase();
|
|
29
|
+
|
|
30
|
+
/** The component a file holds, by shadcn's naming: index files take their folder's name. */
|
|
31
|
+
function componentName(file) {
|
|
32
|
+
const stem = basename(file).replace(/\.[cm]?[jt]sx?$/, '');
|
|
33
|
+
return kebab(stem === 'index' ? basename(dirname(file)) : stem);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Is this one of shadcn's own files: a catalogue component or a kit block, by name? */
|
|
37
|
+
export function isShadcnFile(file) {
|
|
38
|
+
const n = componentName(file);
|
|
39
|
+
return CATALOGUE.has(n) || BLOCK_COMPONENTS.has(n);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const prefixUnder = (file, dirs) => (dirs ?? []).some((d) => file === d || file.startsWith(`${d}/`));
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Is `file` a kit door: one of shadcn's own components in the catalogue
|
|
46
|
+
* folder, or a kit block? Editing a door is the intended use, so the paint
|
|
47
|
+
* checks leave doors alone.
|
|
48
|
+
* @param from { uiDirs, shadcn: { blockFiles } } (a profileOf() reading, or the
|
|
49
|
+
* plain `installedFrom` object the knowledge and the doorway carry)
|
|
50
|
+
* @param under how a file is matched to a folder; the guard passes its own,
|
|
51
|
+
* which also matches when it runs in a subdirectory
|
|
52
|
+
*/
|
|
53
|
+
export function doorFile(from, file, under = prefixUnder) {
|
|
54
|
+
if (!from || !file) return false;
|
|
55
|
+
if (under(file, from.shadcn?.blockFiles)) return true;
|
|
56
|
+
return under(file, from.uiDirs) && isShadcnFile(file);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Is `file` installed code: a door (above), or a file in a third-party registry folder? */
|
|
60
|
+
export function installedFile(from, file, under = prefixUnder) {
|
|
61
|
+
if (!from || !file) return false;
|
|
62
|
+
return under(file, from.shadcn?.registryDirs) || doorFile(from, file, under);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The plain facts installedFile and doorFile read, for a profileOf() reading:
|
|
67
|
+
* what the knowledge and the guard doorway carry. Null when nothing is
|
|
68
|
+
* installed (no shadcn kit, or a registry, whose published folders are its
|
|
69
|
+
* own work).
|
|
70
|
+
*/
|
|
71
|
+
export function installedFrom(P) {
|
|
72
|
+
if (!P?.isShadcn || P.isRegistry) return null;
|
|
73
|
+
return { uiDirs: [...(P.uiDirs ?? [])], shadcn: { blockFiles: [...(P.shadcn?.blockFiles ?? [])], registryDirs: [...(P.shadcn?.registryDirs ?? [])] } };
|
|
74
|
+
}
|
|
@@ -9,7 +9,8 @@ import { distinctTypefaces } from '../lib/typefaces.mjs';
|
|
|
9
9
|
import { nearColorPairs } from './../lib/nearpairs.mjs';
|
|
10
10
|
import { neverImportedComponents } from '../lib/neverimported.mjs';
|
|
11
11
|
import { VERSION } from '../lib/version.mjs';
|
|
12
|
-
import { profileOf,
|
|
12
|
+
import { profileOf, installedOf, splitArbitrary, ownSpacing } from '../profiles/index.mjs';
|
|
13
|
+
import { themeClassFor } from '../lib/palette.mjs';
|
|
13
14
|
import { rulesOn as lintRulesOn, describeRule as lintDescribe } from '../profiles/shadcn-lint.mjs';
|
|
14
15
|
|
|
15
16
|
export function rulesMarkdown(h, opts = {}) {
|
|
@@ -157,7 +158,7 @@ if (neverImported.length >= 3) {
|
|
|
157
158
|
// On a kit the values inside installed folders are shadcn's own: the
|
|
158
159
|
// counts here are the team's, the shadcn section explains the rest.
|
|
159
160
|
const P0 = profileOf(h);
|
|
160
|
-
const arbitrary = P0.isShadcn ? splitArbitrary(t.tailwind?.arbitrary ?? [],
|
|
161
|
+
const arbitrary = P0.isShadcn ? splitArbitrary(t.tailwind?.arbitrary ?? [], installedOf(P0)).own : (t.tailwind?.arbitrary ?? []);
|
|
161
162
|
const arbCount = arbitrary.reduce((s, a) => s + a.count, 0);
|
|
162
163
|
const offScale = ownSpacing(h).css.length;
|
|
163
164
|
if (t.tailwind?.spacing?.length || offScale || arbCount) {
|
|
@@ -205,10 +206,21 @@ if (neverImported.length >= 3) {
|
|
|
205
206
|
const paint = sc.paint ?? null;
|
|
206
207
|
section('shadcn: the components and the theme');
|
|
207
208
|
if (P.isRegistry) rule(`This repo publishes a shadcn registry${P.registry?.publishes ? ` (${[['components', P.registry.publishes.components], ['blocks', P.registry.publishes.blocks], ['styles', P.registry.publishes.styles]].filter(([, v]) => v).map(([k, v]) => `${v} ${k}`).join(', ')})` : ''}. A palette colour, a bracket value or a hand-written dark: colour written here ships into every repo that installs it. Hold published code to the theme variables and the scale harder than app code, and keep demos and examples out of published files.`);
|
|
208
|
-
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
209
|
+
const styleNote = sc.kit?.style ? ` (style \`${sc.kit.style}\`${sc.kit.baseColor ? `, base colour ${sc.kit.baseColor}` : ''})` : '';
|
|
210
|
+
if (sc.kit?.cssVariables === false) {
|
|
211
|
+
// Utility-class mode (9.5.0, words approved by Greg 2026-10-01): the
|
|
212
|
+
// components paint with Tailwind classes by design, so "use the theme
|
|
213
|
+
// variables, never a palette colour" was wrong advice here.
|
|
214
|
+
rule(`This is a shadcn install${styleNote}.`);
|
|
215
|
+
rule(utilityModeRule(sc));
|
|
216
|
+
const op = sc.ownTheme ? sc.ownThemePaint : null;
|
|
217
|
+
if (op?.uses) lines.push(` (${op.uses} palette colour${op.uses === 1 ? '' : 's'} the theme does not own already sit in own code, ${op.samples.slice(0, 3).map((s) => `\`${s.value}\` ×${s.count}`).join(', ')}; do not add to them.)`);
|
|
218
|
+
} else {
|
|
219
|
+
rule(`This is a shadcn install${styleNote}. The theme is a set of CSS variables${sheetFile ? ` in \`${sheetFile}\`` : ''}: background, foreground, primary, muted, border and the rest, each with a light and a dark value. Change a colour there, never in a component.`);
|
|
220
|
+
if ((sc.sheet?.shadcnPresent ?? 0) >= 5) rule('Use the semantic classes the theme gives you (`bg-background`, `text-muted-foreground`, `border-border`), never a palette colour like `bg-blue-500` or `text-gray-600`, and never a hand-written `dark:` colour. The variables already carry both modes.');
|
|
221
|
+
else rule(`${sheetFile ? `\`${sheetFile}\` defines` : 'The theme file defines'} none of shadcn's colour variables, so \`bg-background\` and \`text-muted-foreground\` have nothing behind them here. Until the theme variables are adopted, stay with the palette classes the surrounding file already uses; do not introduce semantic classes with no variable behind them, and do not add a second palette.`);
|
|
222
|
+
if (paint?.tin?.uses) lines.push(` (${paint.tin.uses} palette colour${paint.tin.uses === 1 ? '' : 's'} already sit in own code, ${paint.tin.samples.slice(0, 3).map((s) => `\`${s.value}\` ×${s.count}`).join(', ')}; do not add to them.)`);
|
|
223
|
+
}
|
|
212
224
|
rule('Before adding classes to a shadcn component, use one of its variants (`variant="outline"`, `size="sm"`). `className` on a shadcn component is for layout only: width, margin, position. Never colour, never typography.');
|
|
213
225
|
if (paint?.typo?.uses) lines.push(` (${paint.typo.uses} shadcn component${paint.typo.uses === 1 ? '' : 's'} get a text size or weight through className, like ${paint.typo.samples.slice(0, 2).map((s) => `\`${s.value}\``).join(' and ')}; add a size variant instead.)`);
|
|
214
226
|
if (paint?.doors?.uses) lines.push(` (${paint.doors.uses} shadcn component${paint.doors.uses === 1 ? '' : 's'} already recoloured through className, like ${paint.doors.samples.slice(0, 2).map((s) => `\`${s.value}\``).join(' and ')}; do not add to them.)`);
|
|
@@ -257,3 +269,56 @@ if (neverImported.length >= 3) {
|
|
|
257
269
|
const ruleCount = text.split('\n').filter((l) => l.startsWith('- ')).length;
|
|
258
270
|
return { text, ruleCount };
|
|
259
271
|
}
|
|
272
|
+
|
|
273
|
+
// ---------- shadcn in utility-class mode (words approved by Greg, 2026-10-01) ----------
|
|
274
|
+
const andList = (xs) => (xs.length <= 1 ? xs.join('') : `${xs.slice(0, -1).join(', ')} and ${xs.at(-1)}`);
|
|
275
|
+
// Tailwind names the theme gives new values, as ranges: neutral-50 to neutral-950
|
|
276
|
+
function retunedRanges(retuned) {
|
|
277
|
+
const fam = new Map();
|
|
278
|
+
for (const n of retuned) {
|
|
279
|
+
const m = /^(.*)-(\d+)$/.exec(n);
|
|
280
|
+
if (!m) { fam.set(n, null); continue; }
|
|
281
|
+
if (!fam.has(m[1])) fam.set(m[1], []);
|
|
282
|
+
fam.get(m[1]).push(Number(m[2]));
|
|
283
|
+
}
|
|
284
|
+
return [...fam].map(([f, steps]) => {
|
|
285
|
+
if (!steps) return f;
|
|
286
|
+
steps.sort((a, b) => a - b);
|
|
287
|
+
return steps.length === 1 ? `${f}-${steps[0]}` : `${f}-${steps[0]} to ${f}-${steps.at(-1)}`;
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
// the theme's own names worth naming: the ones a palette colour gets swapped
|
|
291
|
+
// for (destructive, accent), and the chart colours as a group
|
|
292
|
+
const NAMED_RE = /^(destructive|danger|error|negative|success|positive|warning|caution|info|accent|brand|highlight)$/;
|
|
293
|
+
function themeWords(names, retuned) {
|
|
294
|
+
const tuned = new Set(retuned);
|
|
295
|
+
const base = [...new Set(names.filter((n) => !tuned.has(n)).map((n) => n.replace(/-\d+$/, '')))];
|
|
296
|
+
const words = base.filter((n) => NAMED_RE.test(n));
|
|
297
|
+
const chart = base.some((n) => /^chart(-|$)/.test(n));
|
|
298
|
+
const picked = words.length ? words : base.filter((n) => !/(^|-)foreground$|^chart(-|$)/.test(n)).slice(0, 3);
|
|
299
|
+
return chart ? [...picked, 'the chart colours'] : picked;
|
|
300
|
+
}
|
|
301
|
+
function utilityModeRule(sc) {
|
|
302
|
+
const head = 'shadcn is installed without CSS variables here (components.json: cssVariables false), so the components paint with';
|
|
303
|
+
const own = sc.ownTheme;
|
|
304
|
+
if (!own) {
|
|
305
|
+
const shades = sc.doorShades ?? [];
|
|
306
|
+
// the install's base colour first (zinc on a zinc install), so the example is the everyday shade, not the destructive red
|
|
307
|
+
const base = sc.kit?.baseColor ? `-${sc.kit.baseColor}-` : null;
|
|
308
|
+
const first = (util) => (base && shades.find((s) => s.value.startsWith(util) && s.value.includes(base))) ?? shades.find((s) => s.value.startsWith(util));
|
|
309
|
+
const pick = [first('bg-'), first('text-')].filter(Boolean);
|
|
310
|
+
return `${head} Tailwind's palette classes by design. Stay with the shades they already use${pick.length ? ` (${pick.map((s) => `\`${s.value}\``).join(', ')})` : ''}. Never a hex, an rgb() or a bracket colour.`;
|
|
311
|
+
}
|
|
312
|
+
const ranges = retunedRanges(own.retuned ?? []);
|
|
313
|
+
const words = themeWords(own.names ?? [], own.retuned ?? []);
|
|
314
|
+
const gives = ranges.length ? `gives ${andList(ranges)} this repo's own values` : '';
|
|
315
|
+
const namesPart = words.length ? `names ${andList(words)}` : '';
|
|
316
|
+
const theme = gives || namesPart ? ` The theme file, \`${own.file}\`, ${[gives, namesPart].filter(Boolean).join(', and ')}. Use those classes.` : '';
|
|
317
|
+
const samples = sc.ownThemePaint?.samples ?? [];
|
|
318
|
+
const example = (samples.find((s) => !s.value.includes(':')) ?? samples[0])?.value ?? null;
|
|
319
|
+
const swap = example ? themeClassFor(example, own.names ?? [], own.values ?? {}) : null;
|
|
320
|
+
const never = example
|
|
321
|
+
? ` Never a palette colour the theme does not own, such as \`${example}\`; ${swap ? `use \`${swap}\`, or add` : 'add'} the colour to the theme once.`
|
|
322
|
+
: ' Never a palette colour the theme does not own; add the colour to the theme once.';
|
|
323
|
+
return `${head} Tailwind classes.${theme}${never}`;
|
|
324
|
+
}
|