@webjsdev/ui 0.3.10 → 0.3.11
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 +21 -13
- package/package.json +3 -3
- package/packages/registry/README.md +14 -10
- package/packages/registry/components/accordion.ts +18 -0
- package/packages/registry/components/alert-dialog.ts +203 -22
- package/packages/registry/components/aspect-ratio.ts +12 -0
- package/packages/registry/components/card.ts +20 -1
- package/packages/registry/components/checkbox.ts +45 -3
- package/packages/registry/components/collapsible.ts +15 -0
- package/packages/registry/components/dialog.ts +205 -22
- package/packages/registry/components/dropdown-menu.ts +390 -22
- package/packages/registry/components/hover-card.ts +162 -4
- package/packages/registry/components/input.ts +34 -2
- package/packages/registry/components/kbd.ts +35 -2
- package/packages/registry/components/label.ts +21 -0
- package/packages/registry/components/native-select.ts +28 -7
- package/packages/registry/components/popover.ts +43 -7
- package/packages/registry/components/radio-group.ts +37 -9
- package/packages/registry/components/sonner.ts +53 -5
- package/packages/registry/components/switch.ts +23 -1
- package/packages/registry/components/tabs.ts +15 -0
- package/packages/registry/components/textarea.ts +21 -2
- package/packages/registry/components/toggle-group.ts +81 -8
- package/packages/registry/components/toggle.ts +82 -3
- package/packages/registry/components/tooltip.ts +57 -0
- package/packages/registry/lib/utils.ts +147 -8
- package/packages/registry/registry.json +1 -1
- package/packages/registry/themes/base-colors.js +1 -1
- package/src/commands/add.js +50 -0
- package/src/commands/info.js +1 -4
- package/src/commands/init.js +126 -24
- package/src/registry/fetcher.js +9 -1
- package/src/registry/local.js +1 -1
- package/src/utils/index.js +0 -1
- package/src/utils/detect-project.js +0 -63
|
@@ -52,6 +52,10 @@ const GROUPS: Array<[RegExp, string]> = [
|
|
|
52
52
|
[/^p-/, 'p'], [/^px-/, 'px'], [/^py-/, 'py'], [/^pt-/, 'pt'], [/^pr-/, 'pr'], [/^pb-/, 'pb'], [/^pl-/, 'pl'],
|
|
53
53
|
[/^m-/, 'm'], [/^mx-/, 'mx'], [/^my-/, 'my'], [/^mt-/, 'mt'], [/^mr-/, 'mr'], [/^mb-/, 'mb'], [/^ml-/, 'ml'],
|
|
54
54
|
[/^w-/, 'w'], [/^h-/, 'h'], [/^size-/, 'size'],
|
|
55
|
+
// A `bg-[url(…)]` / `bg-[linear-gradient(…)]` background image is classified
|
|
56
|
+
// by its FUNCTION, since it carries no type hint to classify it by. The
|
|
57
|
+
// hinted forms are handled centrally, in `hintedGroup`.
|
|
58
|
+
[/^bg-\[(url\(|linear-gradient|radial-gradient|conic-gradient)/, 'bg-image'],
|
|
55
59
|
[/^bg-(linear|gradient|conic|radial|none)/, 'bg-image'],
|
|
56
60
|
[/^bg-(no-repeat|repeat|repeat-x|repeat-y|repeat-round|repeat-space)$/, 'bg-repeat'],
|
|
57
61
|
[/^bg-(fixed|local|scroll)$/, 'bg-attach'],
|
|
@@ -62,17 +66,127 @@ const GROUPS: Array<[RegExp, string]> = [
|
|
|
62
66
|
[/^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$/, 'text-size'],
|
|
63
67
|
// Text color: anything else starting with text- that isn't alignment / wrap / overflow.
|
|
64
68
|
[/^text-(?!align-|left$|right$|center$|justify$|start$|end$|wrap$|nowrap$|balance$|pretty$|clip$|ellipsis$|xs$|sm$|base$|lg$|xl$|\d?xl$)/, 'text-color'],
|
|
65
|
-
|
|
69
|
+
// Border sub-properties that are neither a width nor a colour. These come
|
|
70
|
+
// FIRST so the width / colour classifier below never sees them.
|
|
71
|
+
[/^border-(collapse|separate)$/, 'border-collapse'],
|
|
72
|
+
[/^border-spacing(-[xy])?-/, 'border-spacing'],
|
|
73
|
+
[/^border-(solid|dashed|dotted|double|hidden|none)$/, 'border-style'],
|
|
74
|
+
...borderGroups(),
|
|
66
75
|
[/^rounded(-[a-z]+)?$/, 'rounded'],
|
|
67
76
|
[/^rounded-/, 'rounded'],
|
|
68
77
|
[/^opacity-/, 'opacity'],
|
|
69
78
|
[/^font-(thin|light|normal|medium|semibold|bold|black|extralight|extrabold)$/, 'font-weight'],
|
|
70
79
|
[/^shadow(-|$)/, 'shadow'],
|
|
71
80
|
[/^z-/, 'z'],
|
|
72
|
-
|
|
73
|
-
|
|
81
|
+
// A bare `flex` / `grid` is a DISPLAY value, not a member of the flex / grid
|
|
82
|
+
// sub-property groups below, so it must never dedupe against them: an element
|
|
83
|
+
// can be both a flex container and a flex child (`class="flex flex-1"`), and
|
|
84
|
+
// collapsing the two silently drops `display:flex`. It still belongs to a
|
|
85
|
+
// group of its own, alongside every other display keyword, so a repeated one
|
|
86
|
+
// collapses and `cn('hidden', open && 'flex')` resolves to one display.
|
|
87
|
+
[/^(inline-block|inline-flex|inline-grid|inline-table|inline|block|flex|grid|flow-root|contents|hidden|list-item|table-caption|table-cell|table-column-group|table-column|table-footer-group|table-header-group|table-row-group|table-row|table)$/, 'display'],
|
|
88
|
+
// Each sub-utility below gets the group of the real CSS property it sets, so
|
|
89
|
+
// none of them collapses against the display value or against each other.
|
|
90
|
+
[/^flex-(row|row-reverse|col|col-reverse)$/, 'flex-direction'],
|
|
91
|
+
[/^flex-(wrap|wrap-reverse|nowrap)$/, 'flex-wrap'],
|
|
92
|
+
[/^flex-(\d+|auto|initial|none|\[[^\]]*\])$/, 'flex'],
|
|
93
|
+
[/^grid-cols-/, 'grid-cols'],
|
|
94
|
+
[/^grid-rows-/, 'grid-rows'],
|
|
95
|
+
[/^grid-flow-/, 'grid-flow'],
|
|
74
96
|
];
|
|
75
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Border WIDTH and border COLOUR share the `border-` prefix but are different
|
|
100
|
+
* CSS properties, so they need different groups: `cn('border-2',
|
|
101
|
+
* 'border-primary')` must keep BOTH, while `cn('border-border',
|
|
102
|
+
* 'border-accent')` must keep only the later colour (today the winner is
|
|
103
|
+
* decided by compiled stylesheet order, which silently loses whenever the
|
|
104
|
+
* override sorts alphabetically earlier).
|
|
105
|
+
*
|
|
106
|
+
* Classification, after an optional side / axis segment: a bare or numeric
|
|
107
|
+
* value is a width (`border`, `border-2`, `border-t-4`), an arbitrary value
|
|
108
|
+
* that opens with a digit, a dot, or `length:` is a width (`border-[3px]`,
|
|
109
|
+
* `border-[length:var(--w)]`), and everything else is a colour
|
|
110
|
+
* (`border-primary`, `border-red-500/50`, `border-[#fff]`). An ambiguous
|
|
111
|
+
* `border-[var(--x)]` falls to colour, which is the far more common intent.
|
|
112
|
+
*
|
|
113
|
+
* Each side is its own group so a per-side utility only overrides its own side,
|
|
114
|
+
* and the shorthand / axis subsumption is declared in CONFLICTS below, exactly
|
|
115
|
+
* like padding and margin. The logical inline sides (`border-s-*`,
|
|
116
|
+
* `border-e-*`) get their own groups but are subsumed only by the all-sides
|
|
117
|
+
* shorthand, never by `border-x-*`: which physical side they land on depends on
|
|
118
|
+
* the writing direction, so an axis override there would be a guess.
|
|
119
|
+
*/
|
|
120
|
+
function borderGroups(): Array<[RegExp, string]> {
|
|
121
|
+
const width = '(\\d+(\\.\\d+)?|\\[(length:|\\d|\\.)[^\\]]*\\])';
|
|
122
|
+
const out: Array<[RegExp, string]> = [];
|
|
123
|
+
// Sides before the bare form: `border-t-primary` must match the `t` colour
|
|
124
|
+
// group, not be swallowed by the side-less `^border-` colour pattern.
|
|
125
|
+
const sides = ['x', 'y', 't', 'r', 'b', 'l', 's', 'e', ''];
|
|
126
|
+
for (const side of sides) {
|
|
127
|
+
const seg = side ? `-${side}` : '';
|
|
128
|
+
out.push([new RegExp(`^border${seg}(-${width})?$`), `border-w${seg}`]);
|
|
129
|
+
}
|
|
130
|
+
for (const side of sides) {
|
|
131
|
+
const seg = side ? `-${side}` : '';
|
|
132
|
+
out.push([new RegExp(`^border${seg}-`), `border-color${seg}`]);
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* An arbitrary value may carry a Tailwind TYPE HINT: `bg-[image:var(--g)]`,
|
|
139
|
+
* `text-[length:14px]`, `shadow-[color:red]`. The hint exists precisely because
|
|
140
|
+
* the utility prefix is ambiguous, so the PREFIX cannot decide the group and
|
|
141
|
+
* the hint must. Letting a hinted value fall into the prefix's default group
|
|
142
|
+
* evicts a class that sets an entirely different property (`shadow-[color:red]`
|
|
143
|
+
* would eat `shadow-lg`).
|
|
144
|
+
*
|
|
145
|
+
* Every `<prefix>:<hint>` pair listed here names the group it belongs to; an
|
|
146
|
+
* empty string means the hint names the prefix's OWN default property, so the
|
|
147
|
+
* GROUPS table above is already right for it and matching falls through. A pair
|
|
148
|
+
* that is NOT listed gets a group of its own, so it can only ever collide with
|
|
149
|
+
* the identical hint under the identical prefix, never with the prefix's
|
|
150
|
+
* default. That is the safe direction to fail: an extra class renders, a
|
|
151
|
+
* dropped one does not.
|
|
152
|
+
*
|
|
153
|
+
* This is central rather than per-prefix on purpose. Handling only the prefixes
|
|
154
|
+
* that came to mind is how `shadow-[color:red]` was left evicting `shadow-lg`
|
|
155
|
+
* while `bg-` and `text-` were correct.
|
|
156
|
+
*
|
|
157
|
+
* These reach the matcher at all only because `variantPrefix` splits on the
|
|
158
|
+
* last colon OUTSIDE brackets; see the comment there.
|
|
159
|
+
*/
|
|
160
|
+
const HINTED_GROUPS: Record<string, string> = {
|
|
161
|
+
'bg:color': '',
|
|
162
|
+
'bg:image': 'bg-image',
|
|
163
|
+
'bg:position': 'bg-position',
|
|
164
|
+
'bg:size': 'bg-size',
|
|
165
|
+
'bg:length': 'bg-size',
|
|
166
|
+
'text:color': '',
|
|
167
|
+
'text:length': 'text-size',
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The group for a hinted arbitrary value, `null` when the normal GROUPS table
|
|
172
|
+
* should decide it, `undefined` when the token carries no hint at all.
|
|
173
|
+
*/
|
|
174
|
+
function hintedGroup(bare: string): string | null | undefined {
|
|
175
|
+
const m = /^([a-z][a-z-]*)-\[([a-z][a-z-]*):/.exec(bare);
|
|
176
|
+
if (!m) return undefined;
|
|
177
|
+
const prefix = m[1];
|
|
178
|
+
const hint = m[2];
|
|
179
|
+
const key = `${prefix}:${hint}`;
|
|
180
|
+
if (key in HINTED_GROUPS) return HINTED_GROUPS[key] || null;
|
|
181
|
+
// A border width or colour hint is read by `borderGroups()`'s value parser,
|
|
182
|
+
// which already classifies `border-[length:2px]` as a width and everything
|
|
183
|
+
// else after `border-` as a colour. The side list MUST match the one
|
|
184
|
+
// `borderGroups()` enumerates, logical inline sides included, or the hinted
|
|
185
|
+
// and plain spellings of one utility land in different groups.
|
|
186
|
+
if (/^border(-[xytrbles])?$/.test(prefix) && (hint === 'length' || hint === 'color')) return null;
|
|
187
|
+
return `hint:${key}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
76
190
|
// Directional shorthand conflicts (the tailwind-merge model). A shorthand
|
|
77
191
|
// utility invalidates the axis / side utilities it SUBSUMES, because Tailwind's
|
|
78
192
|
// cascade makes the shorthand win: `p-0` after `px-4 py-2` drops both, but
|
|
@@ -88,6 +202,12 @@ const CONFLICTS: Record<string, string[]> = {
|
|
|
88
202
|
mx: ['ml', 'mr'],
|
|
89
203
|
my: ['mt', 'mb'],
|
|
90
204
|
size: ['w', 'h'],
|
|
205
|
+
'border-w': ['border-w-x', 'border-w-y', 'border-w-t', 'border-w-r', 'border-w-b', 'border-w-l', 'border-w-s', 'border-w-e'],
|
|
206
|
+
'border-w-x': ['border-w-l', 'border-w-r'],
|
|
207
|
+
'border-w-y': ['border-w-t', 'border-w-b'],
|
|
208
|
+
'border-color': ['border-color-x', 'border-color-y', 'border-color-t', 'border-color-r', 'border-color-b', 'border-color-l', 'border-color-s', 'border-color-e'],
|
|
209
|
+
'border-color-x': ['border-color-l', 'border-color-r'],
|
|
210
|
+
'border-color-y': ['border-color-t', 'border-color-b'],
|
|
91
211
|
};
|
|
92
212
|
|
|
93
213
|
function dedupeUtilities(input: string): string {
|
|
@@ -102,9 +222,15 @@ function dedupeUtilities(input: string): string {
|
|
|
102
222
|
// only apply WITHIN the same variant (`px-4 hover:p-0` keeps both).
|
|
103
223
|
const prefix = variantPrefix(token);
|
|
104
224
|
const bare = prefix ? token.slice(prefix.length) : token;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
225
|
+
// A type-hinted arbitrary value is classified by its HINT, which overrides
|
|
226
|
+
// the prefix-keyed table below (`hintedGroup` returns null when the hint
|
|
227
|
+
// names the prefix's own default property and the table is already right).
|
|
228
|
+
const hinted = hintedGroup(bare);
|
|
229
|
+
let gk: string | null = hinted ?? null;
|
|
230
|
+
if (hinted === undefined || hinted === null) {
|
|
231
|
+
for (const [re, g] of GROUPS) {
|
|
232
|
+
if (re.test(bare)) { gk = g; break; }
|
|
233
|
+
}
|
|
108
234
|
}
|
|
109
235
|
if (gk) {
|
|
110
236
|
// Remove earlier survivors in this group AND in every group it subsumes.
|
|
@@ -121,8 +247,21 @@ function dedupeUtilities(input: string): string {
|
|
|
121
247
|
}
|
|
122
248
|
|
|
123
249
|
function variantPrefix(token: string): string {
|
|
124
|
-
//
|
|
125
|
-
|
|
250
|
+
// Capture leading variants like `hover:`, `dark:`, `md:`: overrides only
|
|
251
|
+
// conflict within the same variant set. The split is the last colon OUTSIDE
|
|
252
|
+
// square brackets, because an arbitrary value can contain one of its own
|
|
253
|
+
// (`border-[length:2px]`, `bg-[url(https://x/y.png)]`, `text-[color:var(--c)]`).
|
|
254
|
+
// Splitting on the last colon anywhere hands the group matcher a fragment
|
|
255
|
+
// like `2px]`, which matches nothing, so the utility silently stops
|
|
256
|
+
// deduping against its own property.
|
|
257
|
+
let depth = 0;
|
|
258
|
+
let i = -1;
|
|
259
|
+
for (let n = 0; n < token.length; n += 1) {
|
|
260
|
+
const c = token[n];
|
|
261
|
+
if (c === '[') depth += 1;
|
|
262
|
+
else if (c === ']') depth -= 1;
|
|
263
|
+
else if (c === ':' && depth === 0) i = n;
|
|
264
|
+
}
|
|
126
265
|
return i === -1 ? '' : token.slice(0, i + 1);
|
|
127
266
|
}
|
|
128
267
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* preserved; only hue / chroma shift.
|
|
6
6
|
*
|
|
7
7
|
* Pure ESM data module: imported at request time by the website's registry
|
|
8
|
-
* composer (`
|
|
8
|
+
* composer (`website/modules/ui/queries/registry.server.ts`) to synthesize `theme-<color>` items
|
|
9
9
|
* on demand. No build step, no committed output. Numbers match the values
|
|
10
10
|
* shadcn emits in `apps/v4/public/r/themes/*.json`.
|
|
11
11
|
*/
|
package/src/commands/add.js
CHANGED
|
@@ -74,6 +74,16 @@ async function writeRegistryFile(cwd, config, item, file, opts) {
|
|
|
74
74
|
const target = resolveTarget(cwd, config, item, file);
|
|
75
75
|
ensureDir(dirname(target));
|
|
76
76
|
|
|
77
|
+
// The shared helpers are yours to edit, and since #1129 they live at the
|
|
78
|
+
// `utils` alias, which is the file `webjs create` generates and the one the
|
|
79
|
+
// docs tell you to retune. `--yes` means "do not ask me about overwrites",
|
|
80
|
+
// which is the normal non-interactive invocation, so honouring it here would
|
|
81
|
+
// replace edited source on every `add`. Only an explicit `--overwrite` may.
|
|
82
|
+
if (existsSync(target) && !opts.overwrite && helperTarget(config, item, file)) {
|
|
83
|
+
logger.info(`${relative(cwd, target)} already exists: keeping it.`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
77
87
|
if (existsSync(target) && !opts.overwrite && !opts.yes) {
|
|
78
88
|
const r = await prompts({
|
|
79
89
|
type: 'confirm',
|
|
@@ -168,6 +178,17 @@ function replaceImportSpecifier(content, spec, destAbs, target) {
|
|
|
168
178
|
}
|
|
169
179
|
|
|
170
180
|
function resolveTarget(cwd, config, item, file) {
|
|
181
|
+
// The two shared helpers are pinned to `lib/utils.ts` / `lib/dom.ts` by the
|
|
182
|
+
// registry manifest, but where they actually BELONG is wherever the project's
|
|
183
|
+
// `utils` alias points, because that is the path `rewriteUtilsImport` writes
|
|
184
|
+
// into every component's import. Honour the alias for them, ahead of the
|
|
185
|
+
// manifest target, or `add` writes a second copy at the pinned path that
|
|
186
|
+
// nothing imports while the components resolve to the aliased one.
|
|
187
|
+
// The dom helper is the utils file's SIBLING, the same relationship the
|
|
188
|
+
// rewrite assumes, so the two cannot disagree.
|
|
189
|
+
const helper = helperTarget(config, item, file);
|
|
190
|
+
if (helper) return helper;
|
|
191
|
+
|
|
171
192
|
// explicit `target` wins
|
|
172
193
|
if (file.target) return join(cwd, file.target);
|
|
173
194
|
|
|
@@ -188,6 +209,35 @@ function resolveTarget(cwd, config, item, file) {
|
|
|
188
209
|
}
|
|
189
210
|
}
|
|
190
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Absolute target for the `lib-utils` / `lib-dom` registry items, derived from
|
|
214
|
+
* the project's configured `utils` alias, or null for anything else.
|
|
215
|
+
*
|
|
216
|
+
* Kept as the single place that answers "where do the shared helpers live",
|
|
217
|
+
* so `add`'s WRITE and {@link rewriteUtilsImport}'s REWRITE read the same
|
|
218
|
+
* answer. `init` writes the same two paths (see `init.js` `writeLibUtils`).
|
|
219
|
+
*
|
|
220
|
+
* @param {{ resolvedPaths?: { utils?: string } }} config parsed components.json
|
|
221
|
+
* @param {{ name?: string }} [item] the registry item
|
|
222
|
+
* @param {{ path?: string }} [file] the file within that item
|
|
223
|
+
* @returns {string | null}
|
|
224
|
+
*/
|
|
225
|
+
function helperTarget(config, item, file) {
|
|
226
|
+
const utilsAbs = config?.resolvedPaths?.utils;
|
|
227
|
+
if (!utilsAbs || !item) return null;
|
|
228
|
+
// Matched per FILE, not per item. `rewriteUtilsImport` always retargets
|
|
229
|
+
// component imports to `resolvedPaths.utils`, so the ONE file those imports
|
|
230
|
+
// resolve to has to be written there or it is an orphan. Any other file a
|
|
231
|
+
// custom registry ships under the same item is not that file, so it keeps
|
|
232
|
+
// its manifest target: routing them all here would collapse them onto one
|
|
233
|
+
// path and lose every file but the last.
|
|
234
|
+
if (!file) return null;
|
|
235
|
+
const name = basename(file.path || '');
|
|
236
|
+
if (item.name === 'lib-utils' && name === 'utils.ts') return utilsAbs;
|
|
237
|
+
if (item.name === 'lib-dom' && name === 'dom.ts') return join(dirname(utilsAbs), 'dom.ts');
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
191
241
|
function relative(cwd, p) {
|
|
192
242
|
return p.startsWith(cwd) ? p.slice(cwd.length + 1) : p;
|
|
193
243
|
}
|
package/src/commands/info.js
CHANGED
|
@@ -2,20 +2,17 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { getConfig } from '../utils/get-config.js';
|
|
5
|
-
import { detectProject } from '../utils/detect-project.js';
|
|
6
5
|
import { DEFAULT_REGISTRY_URL } from '../registry/fetcher.js';
|
|
7
6
|
import { logger } from '../utils/logger.js';
|
|
8
7
|
|
|
9
8
|
export const info = new Command()
|
|
10
9
|
.name('info')
|
|
11
|
-
.description('Print project diagnostics (config,
|
|
10
|
+
.description('Print project diagnostics (config, registry)')
|
|
12
11
|
.option('-c, --cwd <cwd>', 'the working directory', process.cwd())
|
|
13
12
|
.action((opts) => {
|
|
14
13
|
const cwd = opts.cwd;
|
|
15
|
-
const { type } = detectProject(cwd);
|
|
16
14
|
const config = getConfig(cwd);
|
|
17
15
|
|
|
18
|
-
logger.info(`${logger.bold('Project')} ${type}`);
|
|
19
16
|
logger.info(`${logger.bold('cwd')} ${cwd}`);
|
|
20
17
|
logger.info(`${logger.bold('Registry')} ${DEFAULT_REGISTRY_URL}`);
|
|
21
18
|
logger.info(`${logger.bold('Config')} ${config ? 'components.json ✔' : 'components.json ✖ (run `webjsui init`)'}`);
|
package/src/commands/init.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import prompts from 'prompts';
|
|
5
|
-
import { defaultsForProject } from '../utils/detect-project.js';
|
|
6
5
|
import { writeConfig, CONFIG_FILE } from '../utils/get-config.js';
|
|
7
6
|
import { logger } from '../utils/logger.js';
|
|
8
7
|
import { getRegistryItem, DEFAULT_REGISTRY_URL } from '../registry/fetcher.js';
|
|
@@ -10,22 +9,96 @@ import { ensureTheme } from '../utils/theme.js';
|
|
|
10
9
|
|
|
11
10
|
const BASE_COLORS = ['neutral', 'stone', 'zinc', 'mauve', 'olive', 'mist', 'taupe'];
|
|
12
11
|
|
|
12
|
+
/**
|
|
13
|
+
* The stylesheet `init` defaults to. It is `styles/globals.css`, NOT
|
|
14
|
+
* `app/globals.css`, because in WebJs `app/` is routing-only, so a
|
|
15
|
+
* non-routing stylesheet lives outside it.
|
|
16
|
+
*
|
|
17
|
+
* Exported so `webjs create` can be checked against it rather than against a
|
|
18
|
+
* hand-copied literal (`test/scaffolds/scaffold-ui-integration.test.js`).
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_TAILWIND_CSS = 'styles/globals.css';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The alias map `init` writes into `components.json`. These are the paths a
|
|
24
|
+
* WebJs app uses, and they match byte for byte what `webjs create` scaffolds
|
|
25
|
+
* (see `packages/cli/lib/create.js`), so `init` on a bare app and a freshly
|
|
26
|
+
* scaffolded one land on the same layout.
|
|
27
|
+
*
|
|
28
|
+
* `utils` is `lib/utils/cn`, not `lib/utils`: `get-config.js` appends `'.ts'`
|
|
29
|
+
* when resolving the alias, so this resolves to `lib/utils/cn.ts`. Writing
|
|
30
|
+
* the helper into the `lib/utils/` DIRECTORY is what lets the client-only
|
|
31
|
+
* `onBeforeCache()` helper sit beside it at `lib/utils/dom.ts` (#819), which
|
|
32
|
+
* is where `add` rewrites the registry's `'../lib/dom.ts'` import to point.
|
|
33
|
+
*
|
|
34
|
+
* Exported for the same drift check as {@link DEFAULT_TAILWIND_CSS}. The two
|
|
35
|
+
* generators used to disagree here (init said `lib/utils`, the scaffold said
|
|
36
|
+
* `lib/utils/cn`) with only a comment claiming they matched, so the agreement
|
|
37
|
+
* is asserted now rather than described.
|
|
38
|
+
*/
|
|
39
|
+
export const DEFAULT_ALIASES = {
|
|
40
|
+
components: 'components',
|
|
41
|
+
utils: 'lib/utils/cn',
|
|
42
|
+
ui: 'components/ui',
|
|
43
|
+
lib: 'lib',
|
|
44
|
+
};
|
|
45
|
+
|
|
13
46
|
export const init = new Command()
|
|
14
47
|
.name('init')
|
|
15
|
-
.description('Initialize @webjsdev/ui in a project: writes components.json, theme CSS, lib/utils')
|
|
48
|
+
.description('Initialize @webjsdev/ui in a project: writes components.json, theme CSS, lib/utils/cn.ts')
|
|
16
49
|
.option('-c, --cwd <cwd>', 'the working directory', process.cwd())
|
|
17
50
|
.option('-y, --yes', 'skip confirmation prompts', false)
|
|
18
51
|
.option('--base-color <color>', `base color (${BASE_COLORS.join('|')})`)
|
|
19
52
|
.option('--css <path>', 'path to the project Tailwind CSS file')
|
|
53
|
+
.option('-o, --overwrite', 'replace an existing alias map and existing helper files', false)
|
|
20
54
|
.option('--registry <url>', 'registry base URL', DEFAULT_REGISTRY_URL)
|
|
21
55
|
.action(async (opts) => {
|
|
22
56
|
const cwd = opts.cwd;
|
|
23
|
-
|
|
57
|
+
|
|
58
|
+
// Re-running `init` on a project that already has one must not RELOCATE it.
|
|
59
|
+
// The alias map decides where cn() lives and what every already-added
|
|
60
|
+
// component imports, and `tailwind.css` decides which stylesheet the app
|
|
61
|
+
// actually builds, so adopting the current defaults over an existing
|
|
62
|
+
// project's values would strand those imports and append the tokens to a
|
|
63
|
+
// file nothing compiles. Keep what the project declared; `--overwrite`
|
|
64
|
+
// opts into replacing it.
|
|
65
|
+
//
|
|
66
|
+
// Read the RAW json rather than going through `getConfig`: its schema is
|
|
67
|
+
// `.strict()`, so a config carrying any unknown key (a shadcn-shaped one
|
|
68
|
+
// with `rsc` / `tsx`, or a hand-added field) throws, and treating that as
|
|
69
|
+
// "no config" is exactly what would relocate a real project and then
|
|
70
|
+
// overwrite the file. Only a file we cannot parse AT ALL is a refusal,
|
|
71
|
+
// because silently replacing unreadable config is the same data loss.
|
|
72
|
+
const cfgPath = join(cwd, CONFIG_FILE);
|
|
73
|
+
/** @type {any} */
|
|
74
|
+
let existing = null;
|
|
75
|
+
if (existsSync(cfgPath)) {
|
|
76
|
+
try {
|
|
77
|
+
existing = JSON.parse(readFileSync(cfgPath, 'utf8'));
|
|
78
|
+
} catch {
|
|
79
|
+
if (!opts.overwrite) {
|
|
80
|
+
logger.error(`${CONFIG_FILE} exists but is not valid JSON, so its settings cannot be preserved.`);
|
|
81
|
+
logger.info('Fix or delete it, or re-run with --overwrite to replace it.');
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const keep = existing && !opts.overwrite;
|
|
87
|
+
if (keep) {
|
|
88
|
+
logger.info(`${CONFIG_FILE} already exists: keeping its settings (use --overwrite to reset them).`);
|
|
89
|
+
}
|
|
90
|
+
// Layer over the defaults rather than replacing them: the schema gives
|
|
91
|
+
// several alias keys defaults, so a hand-written map may legitimately omit
|
|
92
|
+
// one, and reading it raw (above) skips that default-filling. Taking the
|
|
93
|
+
// map wholesale left `utils` undefined and crashed on the write.
|
|
94
|
+
const aliases = { ...DEFAULT_ALIASES, ...((keep && existing.aliases) || null) };
|
|
95
|
+
const initialCss = (keep && existing.tailwind && existing.tailwind.css) || DEFAULT_TAILWIND_CSS;
|
|
96
|
+
const initialBaseColor = (keep && existing.tailwind && existing.tailwind.baseColor) || 'neutral';
|
|
24
97
|
|
|
25
98
|
/** @type {{ baseColor: string, css: string }} */
|
|
26
99
|
let answers = {
|
|
27
|
-
baseColor: opts.baseColor ||
|
|
28
|
-
css: opts.css ||
|
|
100
|
+
baseColor: opts.baseColor || initialBaseColor,
|
|
101
|
+
css: opts.css || initialCss,
|
|
29
102
|
};
|
|
30
103
|
|
|
31
104
|
if (!opts.yes) {
|
|
@@ -36,13 +109,13 @@ export const init = new Command()
|
|
|
36
109
|
name: 'baseColor',
|
|
37
110
|
message: 'Base color?',
|
|
38
111
|
choices: BASE_COLORS.map((c) => ({ title: c, value: c })),
|
|
39
|
-
initial: 0,
|
|
112
|
+
initial: Math.max(0, BASE_COLORS.indexOf(initialBaseColor)),
|
|
40
113
|
},
|
|
41
114
|
{
|
|
42
115
|
type: opts.css ? null : 'text',
|
|
43
116
|
name: 'css',
|
|
44
117
|
message: 'Tailwind CSS file path?',
|
|
45
|
-
initial:
|
|
118
|
+
initial: initialCss,
|
|
46
119
|
},
|
|
47
120
|
],
|
|
48
121
|
{ onCancel: () => process.exit(1) },
|
|
@@ -50,23 +123,29 @@ export const init = new Command()
|
|
|
50
123
|
answers = { ...answers, ...r };
|
|
51
124
|
}
|
|
52
125
|
|
|
126
|
+
// Built from KNOWN keys only. The schema is `.strict()`, so carrying an
|
|
127
|
+
// unrecognised key through here would write a file that `getConfig` (and
|
|
128
|
+
// therefore `add` / `diff` / `info`) throws on. Preserving a project's
|
|
129
|
+
// settings means its VALUES, not arbitrary extra fields.
|
|
53
130
|
const config = {
|
|
54
131
|
$schema: 'https://ui.webjs.dev/schema.json',
|
|
55
|
-
style: 'default',
|
|
132
|
+
style: (keep && existing.style) || 'default',
|
|
56
133
|
tailwind: {
|
|
57
134
|
css: answers.css,
|
|
58
135
|
baseColor: answers.baseColor,
|
|
59
|
-
cssVariables:
|
|
136
|
+
cssVariables: keep && existing.tailwind && existing.tailwind.cssVariables !== undefined
|
|
137
|
+
? existing.tailwind.cssVariables
|
|
138
|
+
: true,
|
|
60
139
|
},
|
|
61
|
-
aliases
|
|
62
|
-
iconLibrary: 'lucide',
|
|
140
|
+
aliases,
|
|
141
|
+
iconLibrary: (keep && existing.iconLibrary) || 'lucide',
|
|
63
142
|
};
|
|
64
143
|
|
|
65
144
|
writeConfig(cwd, config);
|
|
66
145
|
logger.success(`Wrote ${CONFIG_FILE}`);
|
|
67
146
|
|
|
68
147
|
// Pull lib/utils + the chosen theme from the registry and write them in.
|
|
69
|
-
await writeLibUtils(cwd,
|
|
148
|
+
await writeLibUtils(cwd, aliases.utils, opts.registry, opts.overwrite);
|
|
70
149
|
|
|
71
150
|
// The theme tokens are what the class helpers render against. A silent
|
|
72
151
|
// failure here (the old behaviour) left an unstyled install with a clean
|
|
@@ -86,30 +165,53 @@ export const init = new Command()
|
|
|
86
165
|
logger.info(`Add components with: ${logger.cyan('npx webjsui add button card dialog')}`);
|
|
87
166
|
});
|
|
88
167
|
|
|
89
|
-
|
|
90
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Write the two shared helpers, WITHOUT clobbering a copy the project already
|
|
170
|
+
* has. The kit is source-copied and you-own-it, so `lib/utils/cn.ts` is a file
|
|
171
|
+
* users are told to edit ("change one helper to retune the entire app"), and
|
|
172
|
+
* `webjs create` puts its own copy there. An unguarded write was harmless while
|
|
173
|
+
* init targeted `lib/utils.ts`, a path nothing else owned, but the moment it
|
|
174
|
+
* writes where the scaffold and the user's edits live it becomes silent data
|
|
175
|
+
* loss, and re-running init is the documented fix for an unstyled install. So
|
|
176
|
+
* an existing file is kept unless `--overwrite` says otherwise. `add` keeps
|
|
177
|
+
* the same two files on the same terms (its `--yes` skips prompts but does not
|
|
178
|
+
* license replacing them), and the theme is idempotent, so every path into a
|
|
179
|
+
* project leaves an edited helper alone.
|
|
180
|
+
*/
|
|
181
|
+
async function writeLibUtils(cwd, utilsAlias, registryUrl, overwrite) {
|
|
182
|
+
// The `utils` alias omits the extension ("lib/utils/cn"), matching how
|
|
183
|
+
// get-config.js resolves it, so append '.ts' to get the target path.
|
|
91
184
|
const utilsRel = utilsAlias.replace(/^@\//, '') + '.ts';
|
|
92
185
|
const utilsTarget = join(cwd, utilsRel);
|
|
93
186
|
try {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
187
|
+
if (existsSync(utilsTarget) && !overwrite) {
|
|
188
|
+
logger.info(`${utilsRel} already exists: keeping it.`);
|
|
189
|
+
} else {
|
|
190
|
+
const item = await getRegistryItem('lib-utils', registryUrl);
|
|
191
|
+
if (item.files) {
|
|
192
|
+
for (const f of item.files) {
|
|
193
|
+
ensureDir(dirname(utilsTarget));
|
|
194
|
+
writeFileSync(utilsTarget, f.content || '', 'utf8');
|
|
195
|
+
logger.success(`Wrote ${utilsRel}`);
|
|
196
|
+
}
|
|
100
197
|
}
|
|
101
198
|
}
|
|
102
199
|
} catch (e) {
|
|
103
|
-
logger.warn(`Could not fetch lib-utils from registry (${e.message}). You may need to write
|
|
200
|
+
logger.warn(`Could not fetch lib-utils from registry (${e.message}). You may need to write ${utilsRel} manually.`);
|
|
104
201
|
}
|
|
105
202
|
|
|
106
203
|
// The onBeforeCache() DOM helper lives in a SEPARATE module (#819) because
|
|
107
204
|
// it references `document`, so keeping it out of cn()'s file prevents the
|
|
108
205
|
// elision analyzer pinning every page that imports cn to the browser.
|
|
109
206
|
// Overlay components import it from `../lib/dom.ts`, so write it as a
|
|
110
|
-
// sibling of the utils file
|
|
207
|
+
// sibling of the utils file, which lands it at lib/utils/dom.ts next to
|
|
208
|
+
// cn.ts. `add` reads that same adjacency when it rewrites the import.
|
|
111
209
|
const domTarget = join(dirname(utilsTarget), 'dom.ts');
|
|
112
210
|
try {
|
|
211
|
+
if (existsSync(domTarget) && !overwrite) {
|
|
212
|
+
logger.info(`${relative(cwd, domTarget)} already exists: keeping it.`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
113
215
|
const item = await getRegistryItem('lib-dom', registryUrl);
|
|
114
216
|
if (!item.files) return;
|
|
115
217
|
for (const f of item.files) {
|
|
@@ -118,7 +220,7 @@ async function writeLibUtils(cwd, utilsAlias, registryUrl) {
|
|
|
118
220
|
logger.success(`Wrote ${relative(cwd, domTarget)}`);
|
|
119
221
|
}
|
|
120
222
|
} catch (e) {
|
|
121
|
-
logger.warn(`Could not fetch lib-dom from registry (${e.message}). You may need to write
|
|
223
|
+
logger.warn(`Could not fetch lib-dom from registry (${e.message}). You may need to write ${relative(cwd, domTarget)} manually.`);
|
|
122
224
|
}
|
|
123
225
|
}
|
|
124
226
|
|
package/src/registry/fetcher.js
CHANGED
|
@@ -4,8 +4,16 @@ import { loadRegistryItem, loadRegistryIndex } from './local.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* The canonical hosted registry. Local-first resolution applies ONLY for this
|
|
6
6
|
* exact URL: it is the one registry whose sources ship inside the package.
|
|
7
|
+
*
|
|
8
|
+
* This moved from `https://ui.webjs.dev/registry` when the gallery merged into
|
|
9
|
+
* the marketing site (#1099). The OLD URL still works and must keep working
|
|
10
|
+
* forever, because every already-published version of this package carries it
|
|
11
|
+
* and cannot be corrected: `ui.webjs.dev` now permanently redirects here, and
|
|
12
|
+
* `fetch` follows redirects by default (verified against the real published
|
|
13
|
+
* 0.3.1 and 0.3.8 tarballs before the move). Pointing new releases straight at
|
|
14
|
+
* the destination just saves them the hop.
|
|
7
15
|
*/
|
|
8
|
-
export const HOSTED_REGISTRY_URL = 'https://
|
|
16
|
+
export const HOSTED_REGISTRY_URL = 'https://webjs.dev/ui/registry';
|
|
9
17
|
|
|
10
18
|
/**
|
|
11
19
|
* Default registry URL. A `REGISTRY_URL` env var points at a CUSTOM registry,
|
package/src/registry/local.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* are interchangeable for every consumer.
|
|
10
10
|
*
|
|
11
11
|
* It is the plain-JS twin of the ui-website composer
|
|
12
|
-
* (`
|
|
12
|
+
* (`website/modules/ui/queries/registry.server.ts`): both read
|
|
13
13
|
* `packages/registry/registry.json` + the `components/*.ts` sources and
|
|
14
14
|
* synthesize the 6 non-neutral base-colour themes from `themes/base-colors.js`.
|
|
15
15
|
* Keeping the CLI on this module (rather than always hitting the network) is
|
package/src/utils/index.js
CHANGED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Detect the host project type so we can pick sensible defaults for paths.
|
|
6
|
-
*
|
|
7
|
-
* @param {string} cwd
|
|
8
|
-
* @returns {{ type: 'webjs' | 'next' | 'vite' | 'astro' | 'plain', meta: any }}
|
|
9
|
-
*/
|
|
10
|
-
export function detectProject(cwd = process.cwd()) {
|
|
11
|
-
const pkgPath = join(cwd, 'package.json');
|
|
12
|
-
if (!existsSync(pkgPath)) return { type: 'plain', meta: {} };
|
|
13
|
-
|
|
14
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
15
|
-
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
16
|
-
|
|
17
|
-
if (deps['@webjsdev/server'] || deps['@webjsdev/cli'] || existsSync(join(cwd, 'app', 'layout.ts')) || existsSync(join(cwd, 'app', 'layout.js'))) {
|
|
18
|
-
return { type: 'webjs', meta: { pkg } };
|
|
19
|
-
}
|
|
20
|
-
if (deps['next']) return { type: 'next', meta: { pkg } };
|
|
21
|
-
if (deps['vite'] || deps['@vitejs/plugin-react'] || deps['@vitejs/plugin-vue']) return { type: 'vite', meta: { pkg } };
|
|
22
|
-
if (deps['astro']) return { type: 'astro', meta: { pkg } };
|
|
23
|
-
|
|
24
|
-
return { type: 'plain', meta: { pkg } };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Pick default `aliases` + `tailwind.css` path based on the detected project.
|
|
29
|
-
*
|
|
30
|
-
* @param {string} cwd
|
|
31
|
-
*/
|
|
32
|
-
export function defaultsForProject(cwd = process.cwd()) {
|
|
33
|
-
const { type } = detectProject(cwd);
|
|
34
|
-
switch (type) {
|
|
35
|
-
case 'webjs':
|
|
36
|
-
return {
|
|
37
|
-
// styles/globals.css, NOT app/globals.css: in webjs, app/ is routing-only,
|
|
38
|
-
// so a non-routing stylesheet lives outside it.
|
|
39
|
-
tailwindCss: 'styles/globals.css',
|
|
40
|
-
aliases: { components: 'components', utils: 'lib/utils', ui: 'components/ui', lib: 'lib' },
|
|
41
|
-
};
|
|
42
|
-
case 'next':
|
|
43
|
-
return {
|
|
44
|
-
tailwindCss: 'app/globals.css',
|
|
45
|
-
aliases: { components: '@/components', utils: '@/lib/utils', ui: '@/components/ui', lib: '@/lib' },
|
|
46
|
-
};
|
|
47
|
-
case 'vite':
|
|
48
|
-
return {
|
|
49
|
-
tailwindCss: 'src/index.css',
|
|
50
|
-
aliases: { components: 'src/components', utils: 'src/lib/utils', ui: 'src/components/ui', lib: 'src/lib' },
|
|
51
|
-
};
|
|
52
|
-
case 'astro':
|
|
53
|
-
return {
|
|
54
|
-
tailwindCss: 'src/styles/globals.css',
|
|
55
|
-
aliases: { components: 'src/components', utils: 'src/lib/utils', ui: 'src/components/ui', lib: 'src/lib' },
|
|
56
|
-
};
|
|
57
|
-
default:
|
|
58
|
-
return {
|
|
59
|
-
tailwindCss: 'styles/globals.css',
|
|
60
|
-
aliases: { components: 'components', utils: 'lib/utils', ui: 'components/ui', lib: 'lib' },
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
}
|