@webjsdev/ui 0.3.10 → 0.3.12
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 +26 -15
- package/package.json +3 -3
- package/packages/registry/README.md +16 -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 +208 -14
- package/packages/registry/package.json +1 -1
- package/packages/registry/registry.json +1 -1
- package/packages/registry/themes/base-colors.js +2 -2
- package/src/commands/add.js +52 -2
- package/src/commands/diff.js +2 -2
- package/src/commands/info.js +2 -5
- package/src/commands/init.js +127 -25
- package/src/registry/example.js +7 -4
- package/src/registry/fetcher.js +10 -2
- package/src/registry/local.js +2 -2
- package/src/utils/index.js +0 -1
- package/src/utils/detect-project.js +0 -63
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* - Concatenates truthy arguments separated by spaces.
|
|
6
6
|
* - Later Tailwind utilities win when they target the same property, mimicking
|
|
7
7
|
* `tailwind-merge`'s behaviour for the cases components actually hit
|
|
8
|
-
* (background colour,
|
|
9
|
-
*
|
|
8
|
+
* (background colour, image, clip, origin, blend mode, position, size,
|
|
9
|
+
* repeat, attachment; text colour, size, alignment, wrapping, overflow;
|
|
10
|
+
* box-shadow and text-shadow, each split into size and colour; padding,
|
|
11
|
+
* margin, width, height, border, rounded, opacity, display).
|
|
10
12
|
*
|
|
11
13
|
* For projects that want the full tailwind-merge behaviour, install
|
|
12
14
|
* `clsx` + `tailwind-merge` and replace this file:
|
|
@@ -52,27 +54,194 @@ const GROUPS: Array<[RegExp, string]> = [
|
|
|
52
54
|
[/^p-/, 'p'], [/^px-/, 'px'], [/^py-/, 'py'], [/^pt-/, 'pt'], [/^pr-/, 'pr'], [/^pb-/, 'pb'], [/^pl-/, 'pl'],
|
|
53
55
|
[/^m-/, 'm'], [/^mx-/, 'mx'], [/^my-/, 'my'], [/^mt-/, 'mt'], [/^mr-/, 'mr'], [/^mb-/, 'mb'], [/^ml-/, 'ml'],
|
|
54
56
|
[/^w-/, 'w'], [/^h-/, 'h'], [/^size-/, 'size'],
|
|
57
|
+
// A `bg-[url(…)]` / `bg-[linear-gradient(…)]` background image is classified
|
|
58
|
+
// by its FUNCTION, since it carries no type hint to classify it by. The
|
|
59
|
+
// hinted forms are handled centrally, in `hintedGroup`.
|
|
60
|
+
[/^bg-\[(url\(|linear-gradient|radial-gradient|conic-gradient)/, 'bg-image'],
|
|
55
61
|
[/^bg-(linear|gradient|conic|radial|none)/, 'bg-image'],
|
|
56
62
|
[/^bg-(no-repeat|repeat|repeat-x|repeat-y|repeat-round|repeat-space)$/, 'bg-repeat'],
|
|
57
63
|
[/^bg-(fixed|local|scroll)$/, 'bg-attach'],
|
|
58
64
|
[/^bg-(auto|cover|contain)$/, 'bg-size'],
|
|
59
|
-
[/^bg-
|
|
65
|
+
[/^bg-size-/, 'bg-size'],
|
|
66
|
+
// Two entries rather than one alternation: the first covers the live v4
|
|
67
|
+
// compounds (`bg-top-left`), the second the bare keywords plus the v4.1
|
|
68
|
+
// deprecated reversed compounds (`bg-left-top`), which tailwind-merge still
|
|
69
|
+
// carries. An unmatched `bg-*` token falls into the colour catch-all below and
|
|
70
|
+
// evicts a real colour, so admitting a dead spelling is the safe direction.
|
|
71
|
+
[/^bg-(top|bottom)(-(left|right))?$/, 'bg-position'],
|
|
72
|
+
[/^bg-(left|right|center)(-(top|bottom))?$/, 'bg-position'],
|
|
73
|
+
[/^bg-position-/, 'bg-position'],
|
|
74
|
+
// Clip, origin, and blend mode are three more properties under the same
|
|
75
|
+
// prefix. Each sat in `bg-color` before, so `bg-clip-text` evicted a real
|
|
76
|
+
// background colour (the gradient-text idiom lost its clip silently).
|
|
77
|
+
[/^bg-clip-(border|padding|content|text)$/, 'bg-clip'],
|
|
78
|
+
[/^bg-origin-(border|padding|content)$/, 'bg-origin'],
|
|
79
|
+
[/^bg-blend-(normal|multiply|screen|overlay|darken|lighten|color-dodge|color-burn|hard-light|soft-light|difference|exclusion|hue|saturation|color|luminosity)$/, 'bg-blend'],
|
|
60
80
|
[/^bg-/, 'bg-color'],
|
|
81
|
+
// text-shadow is its own property, and its size scale and its colour are two
|
|
82
|
+
// properties again. All three entries precede the text- patterns below, which
|
|
83
|
+
// is what keeps a `text-shadow-*` token out of `text-size` and `text-color`.
|
|
84
|
+
[/^text-shadow(-(2xs|xs|sm|md|lg|none))?(\/([\d.]+|\[[^\]]*\]))?$/, 'text-shadow'],
|
|
85
|
+
[/^text-shadow-(\[(inset|-|\.|\d|var\()|\(--)/, 'text-shadow'],
|
|
86
|
+
[/^text-shadow-/, 'text-shadow-color'],
|
|
61
87
|
// Font size: explicit list of Tailwind size scale.
|
|
62
88
|
[/^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$/, 'text-size'],
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
89
|
+
// Alignment, wrapping, and overflow are three more properties under the same
|
|
90
|
+
// prefix. Each was previously excluded from text-color by a lookahead and then
|
|
91
|
+
// matched nothing at all, so two alignments never collapsed.
|
|
92
|
+
[/^text-(left|center|right|justify|start|end)$/, 'text-align'],
|
|
93
|
+
[/^text-(wrap|nowrap|balance|pretty)$/, 'text-wrap'],
|
|
94
|
+
[/^text-(ellipsis|clip)$/, 'text-overflow'],
|
|
95
|
+
// Text color: anything else under the prefix. The specific groups above are
|
|
96
|
+
// the whole carve-out, so no negative lookahead is needed here as well.
|
|
97
|
+
[/^text-/, 'text-color'],
|
|
98
|
+
// Border sub-properties that are neither a width nor a colour. These come
|
|
99
|
+
// FIRST so the width / colour classifier below never sees them.
|
|
100
|
+
[/^border-(collapse|separate)$/, 'border-collapse'],
|
|
101
|
+
[/^border-spacing(-[xy])?-/, 'border-spacing'],
|
|
102
|
+
[/^border-(solid|dashed|dotted|double|hidden|none)$/, 'border-style'],
|
|
103
|
+
...borderGroups(),
|
|
66
104
|
[/^rounded(-[a-z]+)?$/, 'rounded'],
|
|
67
105
|
[/^rounded-/, 'rounded'],
|
|
68
106
|
[/^opacity-/, 'opacity'],
|
|
69
107
|
[/^font-(thin|light|normal|medium|semibold|bold|black|extralight|extrabold)$/, 'font-weight'],
|
|
70
|
-
|
|
108
|
+
// Box-shadow SIZE and box-shadow COLOUR are two properties (`box-shadow` and
|
|
109
|
+
// `--tw-shadow-color`), so they need two groups. `shadow-none` is a size,
|
|
110
|
+
// `shadow-inherit` / `shadow-initial` are colours, and Tailwind accepts an
|
|
111
|
+
// alpha modifier on a size as well as on a colour, so `shadow-lg/25` has to
|
|
112
|
+
// stay on the size side. An unhinted arbitrary value is a SIZE when it opens
|
|
113
|
+
// with `inset`, a sign, a dot, or a digit (a shadow offset list) and also
|
|
114
|
+
// when it is a bare `var()` or the `(--x)` variable shorthand: Tailwind
|
|
115
|
+
// itself resolves an ambiguous arbitrary shadow to `box-shadow` unless the
|
|
116
|
+
// value is provably a colour, and `shadow-[var(--shadow-glow)]` is the normal
|
|
117
|
+
// way to write a design-token shadow. This is the one place the
|
|
118
|
+
// `borderGroups()` convention inverts, because a bare `border-[var(--x)]` is
|
|
119
|
+
// far more often a colour while a bare `shadow-[var(--x)]` is far more often
|
|
120
|
+
// a shadow. The size entries must precede the colour catch-all, or every
|
|
121
|
+
// size lands in the colour group and the bug inverts rather than being fixed.
|
|
122
|
+
[/^shadow(-(2xs|xs|sm|md|lg|xl|2xl|inner|none))?(\/([\d.]+|\[[^\]]*\]))?$/, 'shadow'],
|
|
123
|
+
[/^shadow-(\[(inset|-|\.|\d|var\()|\(--)/, 'shadow'],
|
|
124
|
+
// A bare name the size scale does not list reads as a colour, because
|
|
125
|
+
// `shadow-primary` is overwhelmingly more common than a `@theme`-extended
|
|
126
|
+
// `--shadow-card`. A project that adds a custom shadow NAME is the residual
|
|
127
|
+
// gap, and the docs say so rather than claiming the split is total.
|
|
128
|
+
[/^shadow-/, 'shadow-color'],
|
|
71
129
|
[/^z-/, 'z'],
|
|
72
|
-
|
|
73
|
-
|
|
130
|
+
// A bare `flex` / `grid` is a DISPLAY value, not a member of the flex / grid
|
|
131
|
+
// sub-property groups below, so it must never dedupe against them: an element
|
|
132
|
+
// can be both a flex container and a flex child (`class="flex flex-1"`), and
|
|
133
|
+
// collapsing the two silently drops `display:flex`. It still belongs to a
|
|
134
|
+
// group of its own, alongside every other display keyword, so a repeated one
|
|
135
|
+
// collapses and `cn('hidden', open && 'flex')` resolves to one display.
|
|
136
|
+
[/^(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'],
|
|
137
|
+
// Each sub-utility below gets the group of the real CSS property it sets, so
|
|
138
|
+
// none of them collapses against the display value or against each other.
|
|
139
|
+
[/^flex-(row|row-reverse|col|col-reverse)$/, 'flex-direction'],
|
|
140
|
+
[/^flex-(wrap|wrap-reverse|nowrap)$/, 'flex-wrap'],
|
|
141
|
+
[/^flex-(\d+|auto|initial|none|\[[^\]]*\])$/, 'flex'],
|
|
142
|
+
[/^grid-cols-/, 'grid-cols'],
|
|
143
|
+
[/^grid-rows-/, 'grid-rows'],
|
|
144
|
+
[/^grid-flow-/, 'grid-flow'],
|
|
74
145
|
];
|
|
75
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Border WIDTH and border COLOUR share the `border-` prefix but are different
|
|
149
|
+
* CSS properties, so they need different groups: `cn('border-2',
|
|
150
|
+
* 'border-primary')` must keep BOTH, while `cn('border-border',
|
|
151
|
+
* 'border-accent')` must keep only the later colour (today the winner is
|
|
152
|
+
* decided by compiled stylesheet order, which silently loses whenever the
|
|
153
|
+
* override sorts alphabetically earlier).
|
|
154
|
+
*
|
|
155
|
+
* Classification, after an optional side / axis segment: a bare or numeric
|
|
156
|
+
* value is a width (`border`, `border-2`, `border-t-4`), an arbitrary value
|
|
157
|
+
* that opens with a digit, a dot, or `length:` is a width (`border-[3px]`,
|
|
158
|
+
* `border-[length:var(--w)]`), and everything else is a colour
|
|
159
|
+
* (`border-primary`, `border-red-500/50`, `border-[#fff]`). An ambiguous
|
|
160
|
+
* `border-[var(--x)]` falls to colour, which is the far more common intent.
|
|
161
|
+
*
|
|
162
|
+
* Each side is its own group so a per-side utility only overrides its own side,
|
|
163
|
+
* and the shorthand / axis subsumption is declared in CONFLICTS below, exactly
|
|
164
|
+
* like padding and margin. The logical inline sides (`border-s-*`,
|
|
165
|
+
* `border-e-*`) get their own groups but are subsumed only by the all-sides
|
|
166
|
+
* shorthand, never by `border-x-*`: which physical side they land on depends on
|
|
167
|
+
* the writing direction, so an axis override there would be a guess.
|
|
168
|
+
*/
|
|
169
|
+
function borderGroups(): Array<[RegExp, string]> {
|
|
170
|
+
const width = '(\\d+(\\.\\d+)?|\\[(length:|\\d|\\.)[^\\]]*\\])';
|
|
171
|
+
const out: Array<[RegExp, string]> = [];
|
|
172
|
+
// Sides before the bare form: `border-t-primary` must match the `t` colour
|
|
173
|
+
// group, not be swallowed by the side-less `^border-` colour pattern.
|
|
174
|
+
const sides = ['x', 'y', 't', 'r', 'b', 'l', 's', 'e', ''];
|
|
175
|
+
for (const side of sides) {
|
|
176
|
+
const seg = side ? `-${side}` : '';
|
|
177
|
+
out.push([new RegExp(`^border${seg}(-${width})?$`), `border-w${seg}`]);
|
|
178
|
+
}
|
|
179
|
+
for (const side of sides) {
|
|
180
|
+
const seg = side ? `-${side}` : '';
|
|
181
|
+
out.push([new RegExp(`^border${seg}-`), `border-color${seg}`]);
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* An arbitrary value may carry a Tailwind TYPE HINT: `bg-[image:var(--g)]`,
|
|
188
|
+
* `text-[length:14px]`, `shadow-[color:red]`. The hint exists precisely because
|
|
189
|
+
* the utility prefix is ambiguous, so the PREFIX cannot decide the group and
|
|
190
|
+
* the hint must. Letting a hinted value fall into the prefix's default group
|
|
191
|
+
* evicts a class that sets an entirely different property (`shadow-[color:red]`
|
|
192
|
+
* would eat `shadow-lg`).
|
|
193
|
+
*
|
|
194
|
+
* Every `<prefix>:<hint>` pair listed here names the group it belongs to; an
|
|
195
|
+
* empty string means the hint names the prefix's OWN default property, so the
|
|
196
|
+
* GROUPS table above is already right for it and matching falls through. A pair
|
|
197
|
+
* that is NOT listed gets a group of its own, so it can only ever collide with
|
|
198
|
+
* the identical hint under the identical prefix, never with the prefix's
|
|
199
|
+
* default. That is the safe direction to fail: an extra class renders, a
|
|
200
|
+
* dropped one does not.
|
|
201
|
+
*
|
|
202
|
+
* This is central rather than per-prefix on purpose. Handling only the prefixes
|
|
203
|
+
* that came to mind is how `shadow-[color:red]` was left evicting `shadow-lg`
|
|
204
|
+
* while `bg-` and `text-` were correct.
|
|
205
|
+
*
|
|
206
|
+
* These reach the matcher at all only because `variantPrefix` splits on the
|
|
207
|
+
* last colon OUTSIDE brackets; see the comment there.
|
|
208
|
+
*/
|
|
209
|
+
const HINTED_GROUPS: Record<string, string> = {
|
|
210
|
+
'bg:color': '',
|
|
211
|
+
'bg:image': 'bg-image',
|
|
212
|
+
'bg:position': 'bg-position',
|
|
213
|
+
'bg:size': 'bg-size',
|
|
214
|
+
'bg:length': 'bg-size',
|
|
215
|
+
'text:color': '',
|
|
216
|
+
'text:length': 'text-size',
|
|
217
|
+
// Both shadow prefixes carry a colour that is a DIFFERENT property from the
|
|
218
|
+
// prefix's own default (the size), so the hint has a named group to point at
|
|
219
|
+
// and must, or `shadow-[color:red]` keeps an isolated bucket and stops
|
|
220
|
+
// deduping against `shadow-red-500`, which sets the identical property.
|
|
221
|
+
'shadow:color': 'shadow-color',
|
|
222
|
+
'text-shadow:color': 'text-shadow-color',
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The group for a hinted arbitrary value, `null` when the normal GROUPS table
|
|
227
|
+
* should decide it, `undefined` when the token carries no hint at all.
|
|
228
|
+
*/
|
|
229
|
+
function hintedGroup(bare: string): string | null | undefined {
|
|
230
|
+
const m = /^([a-z][a-z-]*)-\[([a-z][a-z-]*):/.exec(bare);
|
|
231
|
+
if (!m) return undefined;
|
|
232
|
+
const prefix = m[1];
|
|
233
|
+
const hint = m[2];
|
|
234
|
+
const key = `${prefix}:${hint}`;
|
|
235
|
+
if (key in HINTED_GROUPS) return HINTED_GROUPS[key] || null;
|
|
236
|
+
// A border width or colour hint is read by `borderGroups()`'s value parser,
|
|
237
|
+
// which already classifies `border-[length:2px]` as a width and everything
|
|
238
|
+
// else after `border-` as a colour. The side list MUST match the one
|
|
239
|
+
// `borderGroups()` enumerates, logical inline sides included, or the hinted
|
|
240
|
+
// and plain spellings of one utility land in different groups.
|
|
241
|
+
if (/^border(-[xytrbles])?$/.test(prefix) && (hint === 'length' || hint === 'color')) return null;
|
|
242
|
+
return `hint:${key}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
76
245
|
// Directional shorthand conflicts (the tailwind-merge model). A shorthand
|
|
77
246
|
// utility invalidates the axis / side utilities it SUBSUMES, because Tailwind's
|
|
78
247
|
// cascade makes the shorthand win: `p-0` after `px-4 py-2` drops both, but
|
|
@@ -88,6 +257,12 @@ const CONFLICTS: Record<string, string[]> = {
|
|
|
88
257
|
mx: ['ml', 'mr'],
|
|
89
258
|
my: ['mt', 'mb'],
|
|
90
259
|
size: ['w', 'h'],
|
|
260
|
+
'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'],
|
|
261
|
+
'border-w-x': ['border-w-l', 'border-w-r'],
|
|
262
|
+
'border-w-y': ['border-w-t', 'border-w-b'],
|
|
263
|
+
'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'],
|
|
264
|
+
'border-color-x': ['border-color-l', 'border-color-r'],
|
|
265
|
+
'border-color-y': ['border-color-t', 'border-color-b'],
|
|
91
266
|
};
|
|
92
267
|
|
|
93
268
|
function dedupeUtilities(input: string): string {
|
|
@@ -102,9 +277,15 @@ function dedupeUtilities(input: string): string {
|
|
|
102
277
|
// only apply WITHIN the same variant (`px-4 hover:p-0` keeps both).
|
|
103
278
|
const prefix = variantPrefix(token);
|
|
104
279
|
const bare = prefix ? token.slice(prefix.length) : token;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
280
|
+
// A type-hinted arbitrary value is classified by its HINT, which overrides
|
|
281
|
+
// the prefix-keyed table below (`hintedGroup` returns null when the hint
|
|
282
|
+
// names the prefix's own default property and the table is already right).
|
|
283
|
+
const hinted = hintedGroup(bare);
|
|
284
|
+
let gk: string | null = hinted ?? null;
|
|
285
|
+
if (hinted === undefined || hinted === null) {
|
|
286
|
+
for (const [re, g] of GROUPS) {
|
|
287
|
+
if (re.test(bare)) { gk = g; break; }
|
|
288
|
+
}
|
|
108
289
|
}
|
|
109
290
|
if (gk) {
|
|
110
291
|
// Remove earlier survivors in this group AND in every group it subsumes.
|
|
@@ -121,8 +302,21 @@ function dedupeUtilities(input: string): string {
|
|
|
121
302
|
}
|
|
122
303
|
|
|
123
304
|
function variantPrefix(token: string): string {
|
|
124
|
-
//
|
|
125
|
-
|
|
305
|
+
// Capture leading variants like `hover:`, `dark:`, `md:`: overrides only
|
|
306
|
+
// conflict within the same variant set. The split is the last colon OUTSIDE
|
|
307
|
+
// square brackets, because an arbitrary value can contain one of its own
|
|
308
|
+
// (`border-[length:2px]`, `bg-[url(https://x/y.png)]`, `text-[color:var(--c)]`).
|
|
309
|
+
// Splitting on the last colon anywhere hands the group matcher a fragment
|
|
310
|
+
// like `2px]`, which matches nothing, so the utility silently stops
|
|
311
|
+
// deduping against its own property.
|
|
312
|
+
let depth = 0;
|
|
313
|
+
let i = -1;
|
|
314
|
+
for (let n = 0; n < token.length; n += 1) {
|
|
315
|
+
const c = token[n];
|
|
316
|
+
if (c === '[') depth += 1;
|
|
317
|
+
else if (c === ']') depth -= 1;
|
|
318
|
+
else if (c === ':' && depth === 0) i = n;
|
|
319
|
+
}
|
|
126
320
|
return i === -1 ? '' : token.slice(0, i + 1);
|
|
127
321
|
}
|
|
128
322
|
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "Source registry for @webjsdev/ui
|
|
6
|
+
"description": "Source registry for @webjsdev/ui, holding component sources, themes, and lib. Read on demand by the website gallery to compose registry JSON at request time."
|
|
7
7
|
}
|
|
@@ -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
|
*/
|
|
@@ -165,7 +165,7 @@ export const BASE_OVERRIDES = {
|
|
|
165
165
|
* the original file's formatting + comments verbatim: only the values
|
|
166
166
|
* change.
|
|
167
167
|
*
|
|
168
|
-
* @param {string} neutralCss
|
|
168
|
+
* @param {string} neutralCss Verbatim contents of `themes/index.css`
|
|
169
169
|
* @param {{ light: Record<string,string>, dark: Record<string,string> }} overrides
|
|
170
170
|
*/
|
|
171
171
|
export function mergeThemeCss(neutralCss, overrides) {
|
package/src/commands/add.js
CHANGED
|
@@ -25,13 +25,13 @@ export const add = new Command()
|
|
|
25
25
|
const config = getConfig(cwd);
|
|
26
26
|
if (!config) {
|
|
27
27
|
logger.error(`No ${logger.cyan('components.json')} found in ${cwd}.`);
|
|
28
|
-
logger.info(`Run ${logger.cyan('npx
|
|
28
|
+
logger.info(`Run ${logger.cyan('npx @webjsdev/ui init')} first.`);
|
|
29
29
|
process.exit(1);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
if (!components || components.length === 0) {
|
|
33
33
|
logger.error('No components specified.');
|
|
34
|
-
logger.info(`Try ${logger.cyan('npx
|
|
34
|
+
logger.info(`Try ${logger.cyan('npx @webjsdev/ui add button')} or ${logger.cyan('npx @webjsdev/ui list')}.`);
|
|
35
35
|
process.exit(1);
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -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/diff.js
CHANGED
|
@@ -15,7 +15,7 @@ export const diff = new Command()
|
|
|
15
15
|
.action(async (name, opts) => {
|
|
16
16
|
const config = getConfig(opts.cwd);
|
|
17
17
|
if (!config) {
|
|
18
|
-
logger.error('No components.json. Run `
|
|
18
|
+
logger.error('No components.json. Run `npx @webjsdev/ui init` first.');
|
|
19
19
|
process.exit(1);
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -52,5 +52,5 @@ export const diff = new Command()
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
if (changed === 0) logger.success('All local components match the registry.');
|
|
55
|
-
else logger.info(`\n${changed} file${changed === 1 ? '' : 's'} differ. Re-add with ${logger.cyan('
|
|
55
|
+
else logger.info(`\n${changed} file${changed === 1 ? '' : 's'} differ. Re-add with ${logger.cyan('npx @webjsdev/ui add <name> -o')} to overwrite.`);
|
|
56
56
|
});
|
package/src/commands/info.js
CHANGED
|
@@ -2,23 +2,20 @@ 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
|
-
logger.info(`${logger.bold('Config')} ${config ? 'components.json ✔' : 'components.json ✖ (run `
|
|
18
|
+
logger.info(`${logger.bold('Config')} ${config ? 'components.json ✔' : 'components.json ✖ (run `npx @webjsdev/ui init`)'}`);
|
|
22
19
|
if (config) {
|
|
23
20
|
logger.info(`${logger.bold('Base color')} ${config.tailwind.baseColor}`);
|
|
24
21
|
logger.info(`${logger.bold('Tailwind CSS')} ${config.tailwind.css}`);
|