@webjsdev/ui 0.3.11 → 0.3.13

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 CHANGED
@@ -64,7 +64,9 @@ so on. Follow that block and the markup is fully accessible.
64
64
 
65
65
  Nothing to install. `@webjsdev/ui` is a hard dependency of `@webjsdev/cli`,
66
66
  so a global webjs install already includes it. Apps scaffolded with
67
- `webjs create` also have it pre-listed in `devDependencies`.
67
+ `webjs create` reach it the same way. The scaffold deliberately does not pin
68
+ it, because the kit copies source into your project rather than being
69
+ imported at runtime, so the CLI resolves it from its own install.
68
70
 
69
71
  ```sh
70
72
  webjs ui init
@@ -105,7 +107,8 @@ configured alias). Resolves transitive deps via `registryDependencies` and
105
107
  auto-installs npm deps like `@floating-ui/dom` for popover-style components. For
106
108
  a Tier-1 class-helper component it copies the helpers plus a lean header and a
107
109
  one-line pointer, and leaves the worked structural example OUT of the file (get
108
- it on demand with `webjsui view <name>`). It also self-heals the theme tokens if
110
+ it on demand with `npx @webjsdev/ui view <name>`, which is what the pointer it
111
+ leaves behind says too). It also self-heals the theme tokens if
109
112
  they are missing.
110
113
 
111
114
  Resolution is LOCAL-FIRST: `init` / `add` / `list` / `view` read the registry
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/ui",
3
- "version": "0.3.11",
3
+ "version": "0.3.13",
4
4
  "type": "module",
5
5
  "description": "The AI-first component library for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Class-helper functions for visuals, custom elements only where state matters, source-copied into your repo so you own it.",
6
6
  "bin": {
@@ -10,8 +10,10 @@ fetches. There is no build step, no generated output, no `prestart` hook.
10
10
 
11
11
  The composer and the routes moved out of this package with #1099, when the
12
12
  gallery merged into the marketing site. The old `ui.webjs.dev/registry/**`
13
- URLs still resolve, permanently redirecting to the new ones, because
14
- already-published CLI versions fetch from them.
13
+ URLs now redirect to the gallery page rather than to their new equivalents,
14
+ so they no longer serve registry JSON. That breaks `webjsui add` on the
15
+ published 0.3.1 through 0.3.8, which hardcode them; 0.3.9 onward resolves
16
+ local-first and never fetches them.
15
17
 
16
18
  ## Layout
17
19
 
@@ -9,14 +9,16 @@
9
9
  * NativeSelect wrapper → nativeSelectWrapperClass()
10
10
  * NativeSelect chevron → nativeSelectIconClass()
11
11
  * NativeSelect option → bare <option> (or nativeSelectOptionClass()
12
- * for explicit overrides; the installed
12
+ * for explicit overrides, since the theme
13
13
  * stylesheet sets Canvas / CanvasText on
14
14
  * every <option> automatically)
15
15
  *
16
- * Importing this module installs a stylesheet that forces Canvas /
17
- * CanvasText on every <option> inside the wrapper so the dropdown reads
18
- * in both light and dark themes regardless of OS preference; advanced
19
- * overrides use `nativeSelectOptionClass()` / `nativeSelectOptGroupClass()`.
16
+ * The theme stylesheet forces Canvas / CanvasText on every <option> so the
17
+ * dropdown reads in both light and dark themes regardless of OS preference.
18
+ * It arrives with the design tokens (`npx @webjsdev/ui init`, or `add`, which
19
+ * writes the block when it is missing), so it is in the first paint and works
20
+ * with JavaScript off. Advanced overrides use `nativeSelectOptionClass()` and
21
+ * `nativeSelectOptGroupClass()`.
20
22
  *
21
23
  * Design tokens used: --input, --background, --primary, --primary-foreground,
22
24
  * --muted-foreground, --ring, --destructive.
@@ -58,61 +60,17 @@ import { cn } from '../lib/utils.ts';
58
60
 
59
61
  export type NativeSelectSize = 'default' | 'sm';
60
62
 
61
- // Auto-apply Canvas/CanvasText to every <option> on the page. Without
62
- // this, an <option> with no explicit bg paints transparent on top of
63
- // the browser-popup background; in dark mode (when color-scheme: dark
64
- // is set on <html>) Chrome's popup is dark, the option's transparent
65
- // bg lets the popup colour through, and the inherited text colour
66
- // from the <select> matches that dark popup: the option disappears,
67
- // only the focused/selected one stays visible because the browser
68
- // overlays its own highlight on it.
63
+ // The <option> / <optgroup> colour rule is NOT injected from here. It lives in
64
+ // the theme stylesheet the kit installs, so it is in the first paint, works
65
+ // with JavaScript off, and does not make this module client-effecting (a
66
+ // module-scope call pins every page that imports it, #1320). See the
67
+ // `select option, select optgroup` rule in the theme block.
69
68
  //
70
- // Original selector required the option to be inside a
71
- // `.group/native-select` wrapper, on the assumption every user would
72
- // follow the documented Usage block above. But it's easy to write a
73
- // bare <select class=${nativeSelectClass()}> without the wrapper
74
- // (legitimate when you don't need the chevron icon: the popover and
75
- // hover-card docs examples both do this), in which case the rule
76
- // never matched and the dropdown reverted to invisible-options.
77
- // Broadening to `select option, select optgroup` makes the fix work
78
- // everywhere the user has imported native-select, with no required
79
- // wrapper. The Canvas/CanvasText pair is a safe default: they ARE
80
- // the system colours the browser would have painted anyway when no
81
- // rule applied; we just stop relying on inheritance to pull through.
82
- // Selector specificity is 0,0,2 (two elements), so any user who
83
- // genuinely needs custom <option> colours can override with a single
84
- // class anywhere in their cascade (e.g. `.my-select option { ... }`
85
- // at 0,1,2 wins).
86
- //
87
- // `nativeSelectOptionClass()` and `nativeSelectOptGroupClass()` stay
88
- // exported for users who want to opt into the same colours via the
89
- // class helper instead of the global rule. They emit the same
90
- // `bg-[Canvas] text-[CanvasText]` Tailwind utilities: redundant if
91
- // this stylesheet is installed, but harmless and matches the broader
92
- // shadcn convention of "every part has a class helper".
93
- const STYLES = `
94
- select option,
95
- select optgroup {
96
- background-color: Canvas;
97
- color: CanvasText;
98
- }
99
- `;
100
-
101
- let installed = false;
102
- export function installNativeSelectStyles(): void {
103
- if (installed || typeof document === 'undefined') return;
104
- if (document.getElementById('ui-native-select-styles')) {
105
- installed = true;
106
- return;
107
- }
108
- const style = document.createElement('style');
109
- style.id = 'ui-native-select-styles';
110
- style.textContent = STYLES;
111
- document.head.appendChild(style);
112
- installed = true;
113
- }
114
-
115
- if (typeof document !== 'undefined') installNativeSelectStyles();
69
+ // `nativeSelectOptionClass()` and `nativeSelectOptGroupClass()` stay exported
70
+ // for users who want to opt into the same colours via a class helper. They
71
+ // emit the same `bg-[Canvas] text-[CanvasText]` utilities: redundant when the
72
+ // theme block is present, harmless, and they match the broader shadcn
73
+ // convention that every part has a class helper.
116
74
 
117
75
  export const nativeSelectWrapperClass = (): string =>
118
76
  'group/native-select relative w-fit has-[select:disabled]:opacity-50';
@@ -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, text colour, padding, margin, width, height, border,
9
- * rounded, opacity, display).
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:
@@ -48,52 +50,106 @@ function walk(value: ClassValue, out: string[]): void {
48
50
  // IMPORTANT: text-size (text-sm, text-xs, text-base, text-lg, …) and
49
51
  // text-color (text-primary, text-foreground, …) are DIFFERENT properties
50
52
  // and must be in different groups. Same for bg-size vs bg-color etc.
51
- const GROUPS: Array<[RegExp, string]> = [
52
- [/^p-/, 'p'], [/^px-/, 'px'], [/^py-/, 'py'], [/^pt-/, 'pt'], [/^pr-/, 'pr'], [/^pb-/, 'pb'], [/^pl-/, 'pl'],
53
- [/^m-/, 'm'], [/^mx-/, 'mx'], [/^my-/, 'my'], [/^mt-/, 'mt'], [/^mr-/, 'mr'], [/^mb-/, 'mb'], [/^ml-/, 'ml'],
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'],
59
- [/^bg-(linear|gradient|conic|radial|none)/, 'bg-image'],
60
- [/^bg-(no-repeat|repeat|repeat-x|repeat-y|repeat-round|repeat-space)$/, 'bg-repeat'],
61
- [/^bg-(fixed|local|scroll)$/, 'bg-attach'],
62
- [/^bg-(auto|cover|contain)$/, 'bg-size'],
63
- [/^bg-(bottom|center|left|right|top)$/, 'bg-position'],
64
- [/^bg-/, 'bg-color'],
65
- // Font size: explicit list of Tailwind size scale.
66
- [/^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$/, 'text-size'],
67
- // Text color: anything else starting with text- that isn't alignment / wrap / overflow.
68
- [/^text-(?!align-|left$|right$|center$|justify$|start$|end$|wrap$|nowrap$|balance$|pretty$|clip$|ellipsis$|xs$|sm$|base$|lg$|xl$|\d?xl$)/, 'text-color'],
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(),
75
- [/^rounded(-[a-z]+)?$/, 'rounded'],
76
- [/^rounded-/, 'rounded'],
77
- [/^opacity-/, 'opacity'],
78
- [/^font-(thin|light|normal|medium|semibold|bold|black|extralight|extrabold)$/, 'font-weight'],
79
- [/^shadow(-|$)/, 'shadow'],
80
- [/^z-/, 'z'],
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'],
96
- ];
53
+ // Built on FIRST USE, not at module load. A module-scope `...borderGroups()`
54
+ // spread is a real top-level call, so the elision analyser reads this module as
55
+ // client-effecting and every page that reaches `cn` on a component-free path
56
+ // ships whole instead of being elided (#1320).
57
+ let _groups: Array<[RegExp, string]> | undefined;
58
+ function GROUPS(): Array<[RegExp, string]> {
59
+ return (_groups ??= [
60
+ [/^p-/, 'p'], [/^px-/, 'px'], [/^py-/, 'py'], [/^pt-/, 'pt'], [/^pr-/, 'pr'], [/^pb-/, 'pb'], [/^pl-/, 'pl'],
61
+ [/^m-/, 'm'], [/^mx-/, 'mx'], [/^my-/, 'my'], [/^mt-/, 'mt'], [/^mr-/, 'mr'], [/^mb-/, 'mb'], [/^ml-/, 'ml'],
62
+ [/^w-/, 'w'], [/^h-/, 'h'], [/^size-/, 'size'],
63
+ // A `bg-[url(…)]` / `bg-[linear-gradient(…)]` background image is classified
64
+ // by its FUNCTION, since it carries no type hint to classify it by. The
65
+ // hinted forms are handled centrally, in `hintedGroup`.
66
+ [/^bg-\[(url\(|linear-gradient|radial-gradient|conic-gradient)/, 'bg-image'],
67
+ [/^bg-(linear|gradient|conic|radial|none)/, 'bg-image'],
68
+ [/^bg-(no-repeat|repeat|repeat-x|repeat-y|repeat-round|repeat-space)$/, 'bg-repeat'],
69
+ [/^bg-(fixed|local|scroll)$/, 'bg-attach'],
70
+ [/^bg-(auto|cover|contain)$/, 'bg-size'],
71
+ [/^bg-size-/, 'bg-size'],
72
+ // Two entries rather than one alternation: the first covers the live v4
73
+ // compounds (`bg-top-left`), the second the bare keywords plus the v4.1
74
+ // deprecated reversed compounds (`bg-left-top`), which tailwind-merge still
75
+ // carries. An unmatched `bg-*` token falls into the colour catch-all below and
76
+ // evicts a real colour, so admitting a dead spelling is the safe direction.
77
+ [/^bg-(top|bottom)(-(left|right))?$/, 'bg-position'],
78
+ [/^bg-(left|right|center)(-(top|bottom))?$/, 'bg-position'],
79
+ [/^bg-position-/, 'bg-position'],
80
+ // Clip, origin, and blend mode are three more properties under the same
81
+ // prefix. Each sat in `bg-color` before, so `bg-clip-text` evicted a real
82
+ // background colour (the gradient-text idiom lost its clip silently).
83
+ [/^bg-clip-(border|padding|content|text)$/, 'bg-clip'],
84
+ [/^bg-origin-(border|padding|content)$/, 'bg-origin'],
85
+ [/^bg-blend-(normal|multiply|screen|overlay|darken|lighten|color-dodge|color-burn|hard-light|soft-light|difference|exclusion|hue|saturation|color|luminosity)$/, 'bg-blend'],
86
+ [/^bg-/, 'bg-color'],
87
+ // text-shadow is its own property, and its size scale and its colour are two
88
+ // properties again. All three entries precede the text- patterns below, which
89
+ // is what keeps a `text-shadow-*` token out of `text-size` and `text-color`.
90
+ [/^text-shadow(-(2xs|xs|sm|md|lg|none))?(\/([\d.]+|\[[^\]]*\]))?$/, 'text-shadow'],
91
+ [/^text-shadow-(\[(inset|-|\.|\d|var\()|\(--)/, 'text-shadow'],
92
+ [/^text-shadow-/, 'text-shadow-color'],
93
+ // Font size: explicit list of Tailwind size scale.
94
+ [/^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$/, 'text-size'],
95
+ // Alignment, wrapping, and overflow are three more properties under the same
96
+ // prefix. Each was previously excluded from text-color by a lookahead and then
97
+ // matched nothing at all, so two alignments never collapsed.
98
+ [/^text-(left|center|right|justify|start|end)$/, 'text-align'],
99
+ [/^text-(wrap|nowrap|balance|pretty)$/, 'text-wrap'],
100
+ [/^text-(ellipsis|clip)$/, 'text-overflow'],
101
+ // Text color: anything else under the prefix. The specific groups above are
102
+ // the whole carve-out, so no negative lookahead is needed here as well.
103
+ [/^text-/, 'text-color'],
104
+ // Border sub-properties that are neither a width nor a colour. These come
105
+ // FIRST so the width / colour classifier below never sees them.
106
+ [/^border-(collapse|separate)$/, 'border-collapse'],
107
+ [/^border-spacing(-[xy])?-/, 'border-spacing'],
108
+ [/^border-(solid|dashed|dotted|double|hidden|none)$/, 'border-style'],
109
+ ...borderGroups(),
110
+ [/^rounded(-[a-z]+)?$/, 'rounded'],
111
+ [/^rounded-/, 'rounded'],
112
+ [/^opacity-/, 'opacity'],
113
+ [/^font-(thin|light|normal|medium|semibold|bold|black|extralight|extrabold)$/, 'font-weight'],
114
+ // Box-shadow SIZE and box-shadow COLOUR are two properties (`box-shadow` and
115
+ // `--tw-shadow-color`), so they need two groups. `shadow-none` is a size,
116
+ // `shadow-inherit` / `shadow-initial` are colours, and Tailwind accepts an
117
+ // alpha modifier on a size as well as on a colour, so `shadow-lg/25` has to
118
+ // stay on the size side. An unhinted arbitrary value is a SIZE when it opens
119
+ // with `inset`, a sign, a dot, or a digit (a shadow offset list) and also
120
+ // when it is a bare `var()` or the `(--x)` variable shorthand: Tailwind
121
+ // itself resolves an ambiguous arbitrary shadow to `box-shadow` unless the
122
+ // value is provably a colour, and `shadow-[var(--shadow-glow)]` is the normal
123
+ // way to write a design-token shadow. This is the one place the
124
+ // `borderGroups()` convention inverts, because a bare `border-[var(--x)]` is
125
+ // far more often a colour while a bare `shadow-[var(--x)]` is far more often
126
+ // a shadow. The size entries must precede the colour catch-all, or every
127
+ // size lands in the colour group and the bug inverts rather than being fixed.
128
+ [/^shadow(-(2xs|xs|sm|md|lg|xl|2xl|inner|none))?(\/([\d.]+|\[[^\]]*\]))?$/, 'shadow'],
129
+ [/^shadow-(\[(inset|-|\.|\d|var\()|\(--)/, 'shadow'],
130
+ // A bare name the size scale does not list reads as a colour, because
131
+ // `shadow-primary` is overwhelmingly more common than a `@theme`-extended
132
+ // `--shadow-card`. A project that adds a custom shadow NAME is the residual
133
+ // gap, and the docs say so rather than claiming the split is total.
134
+ [/^shadow-/, 'shadow-color'],
135
+ [/^z-/, 'z'],
136
+ // A bare `flex` / `grid` is a DISPLAY value, not a member of the flex / grid
137
+ // sub-property groups below, so it must never dedupe against them: an element
138
+ // can be both a flex container and a flex child (`class="flex flex-1"`), and
139
+ // collapsing the two silently drops `display:flex`. It still belongs to a
140
+ // group of its own, alongside every other display keyword, so a repeated one
141
+ // collapses and `cn('hidden', open && 'flex')` resolves to one display.
142
+ [/^(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'],
143
+ // Each sub-utility below gets the group of the real CSS property it sets, so
144
+ // none of them collapses against the display value or against each other.
145
+ [/^flex-(row|row-reverse|col|col-reverse)$/, 'flex-direction'],
146
+ [/^flex-(wrap|wrap-reverse|nowrap)$/, 'flex-wrap'],
147
+ [/^flex-(\d+|auto|initial|none|\[[^\]]*\])$/, 'flex'],
148
+ [/^grid-cols-/, 'grid-cols'],
149
+ [/^grid-rows-/, 'grid-rows'],
150
+ [/^grid-flow-/, 'grid-flow'],
151
+ ]);
152
+ }
97
153
 
98
154
  /**
99
155
  * Border WIDTH and border COLOUR share the `border-` prefix but are different
@@ -106,9 +162,11 @@ const GROUPS: Array<[RegExp, string]> = [
106
162
  * Classification, after an optional side / axis segment: a bare or numeric
107
163
  * value is a width (`border`, `border-2`, `border-t-4`), an arbitrary value
108
164
  * 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
165
+ * `border-[length:var(--w)]`, and the v4 paren spelling
166
+ * `border-(length:--w)`), and everything else is a colour
110
167
  * (`border-primary`, `border-red-500/50`, `border-[#fff]`). An ambiguous
111
- * `border-[var(--x)]` falls to colour, which is the far more common intent.
168
+ * `border-[var(--x)]`, or its paren spelling `border-(--x)`, falls to colour,
169
+ * which is the far more common intent.
112
170
  *
113
171
  * Each side is its own group so a per-side utility only overrides its own side,
114
172
  * and the shorthand / axis subsumption is declared in CONFLICTS below, exactly
@@ -118,7 +176,7 @@ const GROUPS: Array<[RegExp, string]> = [
118
176
  * the writing direction, so an axis override there would be a guess.
119
177
  */
120
178
  function borderGroups(): Array<[RegExp, string]> {
121
- const width = '(\\d+(\\.\\d+)?|\\[(length:|\\d|\\.)[^\\]]*\\])';
179
+ const width = '(\\d+(\\.\\d+)?|\\[(length:|\\d|\\.)[^\\]]*\\]|\\(length:[^)]*\\))';
122
180
  const out: Array<[RegExp, string]> = [];
123
181
  // Sides before the bare form: `border-t-primary` must match the `t` colour
124
182
  // group, not be swallowed by the side-less `^border-` colour pattern.
@@ -136,7 +194,8 @@ function borderGroups(): Array<[RegExp, string]> {
136
194
 
137
195
  /**
138
196
  * 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
197
+ * `text-[length:14px]`, `shadow-[color:red]`, and in the v4 paren shorthand
198
+ * `bg-(image:--g)`, `shadow-(color:--x)`. The hint exists precisely because
140
199
  * the utility prefix is ambiguous, so the PREFIX cannot decide the group and
141
200
  * the hint must. Letting a hinted value fall into the prefix's default group
142
201
  * evicts a class that sets an entirely different property (`shadow-[color:red]`
@@ -155,7 +214,13 @@ function borderGroups(): Array<[RegExp, string]> {
155
214
  * while `bg-` and `text-` were correct.
156
215
  *
157
216
  * These reach the matcher at all only because `variantPrefix` splits on the
158
- * last colon OUTSIDE brackets; see the comment there.
217
+ * last colon OUTSIDE brackets AND parentheses; see the comment there.
218
+ *
219
+ * Both spellings of one hint produce the identical `<prefix>:<hint>` key, so
220
+ * the map needs no paren-specific entries. The closing delimiter is
221
+ * deliberately not validated: the hint names the CSS property, and how the
222
+ * value terminates cannot change which property that is (the bracket branch
223
+ * has never validated its own close either).
159
224
  */
160
225
  const HINTED_GROUPS: Record<string, string> = {
161
226
  'bg:color': '',
@@ -165,6 +230,12 @@ const HINTED_GROUPS: Record<string, string> = {
165
230
  'bg:length': 'bg-size',
166
231
  'text:color': '',
167
232
  'text:length': 'text-size',
233
+ // Both shadow prefixes carry a colour that is a DIFFERENT property from the
234
+ // prefix's own default (the size), so the hint has a named group to point at
235
+ // and must, or `shadow-[color:red]` keeps an isolated bucket and stops
236
+ // deduping against `shadow-red-500`, which sets the identical property.
237
+ 'shadow:color': 'shadow-color',
238
+ 'text-shadow:color': 'text-shadow-color',
168
239
  };
169
240
 
170
241
  /**
@@ -172,7 +243,7 @@ const HINTED_GROUPS: Record<string, string> = {
172
243
  * should decide it, `undefined` when the token carries no hint at all.
173
244
  */
174
245
  function hintedGroup(bare: string): string | null | undefined {
175
- const m = /^([a-z][a-z-]*)-\[([a-z][a-z-]*):/.exec(bare);
246
+ const m = /^([a-z][a-z-]*)-[\[(]([a-z][a-z-]*):/.exec(bare);
176
247
  if (!m) return undefined;
177
248
  const prefix = m[1];
178
249
  const hint = m[2];
@@ -228,7 +299,7 @@ function dedupeUtilities(input: string): string {
228
299
  const hinted = hintedGroup(bare);
229
300
  let gk: string | null = hinted ?? null;
230
301
  if (hinted === undefined || hinted === null) {
231
- for (const [re, g] of GROUPS) {
302
+ for (const [re, g] of GROUPS()) {
232
303
  if (re.test(bare)) { gk = g; break; }
233
304
  }
234
305
  }
@@ -248,19 +319,32 @@ function dedupeUtilities(input: string): string {
248
319
 
249
320
  function variantPrefix(token: string): string {
250
321
  // 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)]`).
322
+ // conflict within the same variant set. The split is the last colon at top
323
+ // level, meaning outside BOTH square brackets and parentheses, because an
324
+ // arbitrary value can contain a colon of its own in either delimiter
325
+ // (`border-[length:2px]`, `bg-[url(https://x/y.png)]`, `text-[color:var(--c)]`,
326
+ // and the v4 paren spelling `shadow-(color:--x)`, `bg-(image:--g)`).
254
327
  // 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;
328
+ // like `2px]` or `--x)`, which matches nothing, so the utility silently
329
+ // stops deduping against its own property.
330
+ //
331
+ // TWO counters, both required to be zero, rather than one shared counter.
332
+ // Tailwind's own top-level splitter (`segment()`, in the upstream
333
+ // tailwindlabs/tailwindcss repo at packages/tailwindcss/src/utils/segment.ts,
334
+ // NOT a path in this repo) is a MATCHED-PAIR stack, so a
335
+ // stray closer never cancels a live opener of the other kind. A single
336
+ // shared counter would let `)` cancel `[` and read `x-[y)-z:w` as having a
337
+ // top-level colon, which is the fragment bug above wearing a different hat.
338
+ let bracketDepth = 0;
339
+ let parenDepth = 0;
258
340
  let i = -1;
259
341
  for (let n = 0; n < token.length; n += 1) {
260
342
  const c = token[n];
261
- if (c === '[') depth += 1;
262
- else if (c === ']') depth -= 1;
263
- else if (c === ':' && depth === 0) i = n;
343
+ if (c === '[') bracketDepth += 1;
344
+ else if (c === ']') bracketDepth -= 1;
345
+ else if (c === '(') parenDepth += 1;
346
+ else if (c === ')') parenDepth -= 1;
347
+ else if (c === ':' && bracketDepth === 0 && parenDepth === 0) i = n;
264
348
  }
265
349
  return i === -1 ? '' : token.slice(0, i + 1);
266
350
  }
@@ -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 - component sources, themes, lib. Read on demand by @webjsdev/ui-website to compose registry JSON at request time."
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
  }
@@ -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 - verbatim contents of `themes/index.css`
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) {
@@ -149,4 +149,17 @@
149
149
  outline: 2px solid color-mix(in oklab, var(--color-ring) 50%, transparent);
150
150
  outline-offset: 2px;
151
151
  }
152
+ /* Native <select> options paint transparent over the browser popup when no
153
+ rule applies, so in dark mode Chrome's dark popup plus the inherited text
154
+ colour makes every unselected option disappear. Canvas / CanvasText are
155
+ the system colours the browser would have painted anyway, so this stops
156
+ relying on inheritance to pull them through. Two ELEMENT selectors
157
+ (specificity 0,0,2) and no wrapper requirement on purpose: a bare
158
+ <select class=${nativeSelectClass()}> with no wrapper is legitimate, and
159
+ any single class overrides this. */
160
+ select option,
161
+ select optgroup {
162
+ background-color: Canvas;
163
+ color: CanvasText;
164
+ }
152
165
  }
@@ -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 webjsui init')} first.`);
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 webjsui add button')} or ${logger.cyan('npx webjsui list')}.`);
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
 
@@ -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 `webjsui init` first.');
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('webjsui add <name> -o')} to overwrite.`);
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
  });
@@ -15,7 +15,7 @@ export const info = new Command()
15
15
 
16
16
  logger.info(`${logger.bold('cwd')} ${cwd}`);
17
17
  logger.info(`${logger.bold('Registry')} ${DEFAULT_REGISTRY_URL}`);
18
- logger.info(`${logger.bold('Config')} ${config ? 'components.json ✔' : 'components.json ✖ (run `webjsui init`)'}`);
18
+ logger.info(`${logger.bold('Config')} ${config ? 'components.json ✔' : 'components.json ✖ (run `npx @webjsdev/ui init`)'}`);
19
19
  if (config) {
20
20
  logger.info(`${logger.bold('Base color')} ${config.tailwind.baseColor}`);
21
21
  logger.info(`${logger.bold('Tailwind CSS')} ${config.tailwind.css}`);
@@ -162,7 +162,7 @@ export const init = new Command()
162
162
  logger.break();
163
163
  logger.success('Done.');
164
164
  logger.info('');
165
- logger.info(`Add components with: ${logger.cyan('npx webjsui add button card dialog')}`);
165
+ logger.info(`Add components with: ${logger.cyan('npx @webjsdev/ui add button card dialog')}`);
166
166
  });
167
167
 
168
168
  /**
@@ -79,10 +79,13 @@ export function extractExample(src) {
79
79
 
80
80
  /**
81
81
  * The one-line pointer left in place of a stripped example. Uses the EXPLICIT
82
- * `npx @webjsdev/ui view` form (not the bare `webjsui` bin name, which npx would
83
- * resolve to an unrelated `webjsui` package when it is not a direct dep, e.g. in
84
- * a scaffolded app where `@webjsdev/ui` is only transitive). Works in a WebJs
85
- * app and a standalone project alike.
82
+ * `npx @webjsdev/ui view` form, not the bare `webjsui` bin name. `webjsui` is a
83
+ * bin declared INSIDE `@webjsdev/ui` and is not a published package name (the
84
+ * registry 404s on it), so `npx webjsui` resolves through a local
85
+ * `node_modules/.bin` link or not at all. This pointer is written into a file
86
+ * the user keeps, and it cannot know whether the project reading it later has
87
+ * the kit in its tree. The explicit form resolves either way. See invariant 8
88
+ * in this package's AGENTS.md for the rule every printed hint follows (#1264).
86
89
  */
87
90
  export function pointerLine(name) {
88
91
  return `Full usage example: npx @webjsdev/ui view ${name} (or the MCP tool: ui ${name})`;
@@ -6,12 +6,12 @@ import { loadRegistryItem, loadRegistryIndex } from './local.js';
6
6
  * exact URL: it is the one registry whose sources ship inside the package.
7
7
  *
8
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.
9
+ * the marketing site (#1099). That old URL no longer serves the registry: it
10
+ * now redirects to the gallery PAGE, so a fetch of it follows the redirect and
11
+ * gets HTML. Versions 0.3.1 through 0.3.8 hardcode it and hit the network, so
12
+ * `webjsui add` is broken on those and cannot be corrected after publish.
13
+ * Since 0.3.9 the kit resolves LOCAL-first, which is why the break stops
14
+ * there: `add` / `list` / `view` never reach the network on this URL.
15
15
  */
16
16
  export const HOSTED_REGISTRY_URL = 'https://webjs.dev/ui/registry';
17
17
 
@@ -55,7 +55,7 @@ export async function getRegistryItem(name, registryUrl) {
55
55
  const item = loadRegistryItem(name);
56
56
  if (!item) {
57
57
  throw new Error(
58
- `Unknown registry item "${name}". Run \`webjsui list\` to see the available components.`,
58
+ `Unknown registry item "${name}". Run \`npx @webjsdev/ui list\` to see the available components.`,
59
59
  );
60
60
  }
61
61
  return item;
@@ -8,7 +8,7 @@
8
8
  * item shape the hosted registry serves, so a local read and a network fetch
9
9
  * are interchangeable for every consumer.
10
10
  *
11
- * It is the plain-JS twin of the ui-website composer
11
+ * It is the plain-JS twin of the website gallery 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`.