@webjsdev/ui 0.3.7 → 0.3.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/ui",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "type": "module",
5
5
  "description": "An AI-first component library - class-helper functions for visuals, custom elements only where state matters. Source-copied into your repo, you own it. Works with any Tailwind v4 project.",
6
6
  "bin": {
@@ -65,7 +65,8 @@
65
65
  */
66
66
  import { WebComponent, html, prop } from '@webjsdev/core';
67
67
  import { ref, createRef } from '@webjsdev/core/directives';
68
- import { ensureId, onBeforeCache } from '../lib/utils.ts';
68
+ import { ensureId } from '../lib/utils.ts';
69
+ import { onBeforeCache } from '../lib/dom.ts';
69
70
  import { buttonClass, type ButtonVariant, type ButtonSize } from './button.ts';
70
71
 
71
72
  export const alertDialogContentClass = (): string =>
@@ -65,7 +65,8 @@
65
65
  */
66
66
  import { WebComponent, html, unsafeHTML, prop } from '@webjsdev/core';
67
67
  import { ref, createRef } from '@webjsdev/core/directives';
68
- import { ensureId, onBeforeCache } from '../lib/utils.ts';
68
+ import { ensureId } from '../lib/utils.ts';
69
+ import { onBeforeCache } from '../lib/dom.ts';
69
70
  import { buttonClass } from './button.ts';
70
71
 
71
72
  // Wires a dialog panel's accessible name + description to its title /
@@ -78,7 +78,8 @@
78
78
  * --accent-foreground, --destructive, --muted-foreground, --border.
79
79
  */
80
80
  import { WebComponent, html, unsafeHTML, signal, prop } from '@webjsdev/core';
81
- import { ensureId, onBeforeCache } from '../lib/utils.ts';
81
+ import { ensureId } from '../lib/utils.ts';
82
+ import { onBeforeCache } from '../lib/dom.ts';
82
83
  import { positionFloating, type PopoverSide, type PopoverAlign } from './popover.ts';
83
84
 
84
85
  // --------------------------------------------------------------------------
@@ -39,7 +39,8 @@
39
39
  * Design tokens used: --popover, --popover-foreground, --border.
40
40
  */
41
41
  import { WebComponent, html, prop } from '@webjsdev/core';
42
- import { ensureId, onBeforeCache } from '../lib/utils.ts';
42
+ import { ensureId } from '../lib/utils.ts';
43
+ import { onBeforeCache } from '../lib/dom.ts';
43
44
  import { positionFloating, type PopoverSide, type PopoverAlign } from './popover.ts';
44
45
 
45
46
  // `fixed m-0` opts out of the UA `[popover]` auto-centering margin so
@@ -44,7 +44,7 @@
44
44
  * Design tokens used: --popover, --popover-foreground, --border, --radius.
45
45
  */
46
46
  import { WebComponent, html, repeat, unsafeHTML, signal, prop } from '@webjsdev/core';
47
- import { onBeforeCache } from '../lib/utils.ts';
47
+ import { onBeforeCache } from '../lib/dom.ts';
48
48
 
49
49
  type ToastType = 'default' | 'success' | 'error' | 'info' | 'warning' | 'loading';
50
50
 
@@ -41,7 +41,8 @@
41
41
  * Design tokens used: --foreground, --background.
42
42
  */
43
43
  import { WebComponent, html, prop } from '@webjsdev/core';
44
- import { ensureId, onBeforeCache } from '../lib/utils.ts';
44
+ import { ensureId } from '../lib/utils.ts';
45
+ import { onBeforeCache } from '../lib/dom.ts';
45
46
  import { positionFloating, type PopoverSide, type PopoverAlign } from './popover.ts';
46
47
 
47
48
  // UA `[popover]` defaults paint a bordered, padded panel centered with
@@ -0,0 +1,18 @@
1
+ // Client-only DOM helpers. Kept SEPARATE from lib/utils.ts because these
2
+ // reference `document`: any module that references a client global is treated
3
+ // as client-effecting by the elision analyzer, so bundling this with the pure
4
+ // `cn()` would pin every page that imports `cn` to the browser (#819). Import
5
+ // these ONLY from components (which ship anyway), never from a page/layout.
6
+
7
+ /**
8
+ * Run `reset` just before the webjs client router snapshots the page into its
9
+ * back/forward cache (the `webjs:before-cache` event). Transient overlays use
10
+ * this to close themselves so a restored snapshot is clean. SSR-safe (a no-op
11
+ * when there is no `document`). Wire it in `connectedCallback` and call the
12
+ * returned disposer in `disconnectedCallback` so it never leaks across navs.
13
+ */
14
+ export function onBeforeCache(reset: () => void): () => void {
15
+ if (typeof document === 'undefined') return () => {};
16
+ document.addEventListener('webjs:before-cache', reset);
17
+ return () => document.removeEventListener('webjs:before-cache', reset);
18
+ }
@@ -127,95 +127,12 @@ export function ensureId(el: { id: string }, prefix = 'ui'): string {
127
127
  return el.id;
128
128
  }
129
129
 
130
- // ---------------------------------------------------------------------------
131
- // Custom-element base: SSR-safe. In the browser `Base = HTMLElement`. On
132
- // the server (Node, during SSR) `HTMLElement` is undefined; we substitute
133
- // a stub class so that `class Foo extends Base { … }` doesn't throw at
134
- // module-load time. The stub's methods are never actually called server-side
135
- // because connectedCallback/lifecycle hooks only run when the element is
136
- // inserted into a live DOM, which doesn't happen during webjs SSR.
137
- // ---------------------------------------------------------------------------
138
-
139
- const HasHTMLElement = typeof HTMLElement !== 'undefined';
140
-
141
- class ServerHTMLElementStub {
142
- // Minimal surface so attribute reads/writes inside synchronous code paths
143
- // that DO execute during SSR (e.g. attribute reflection) don't throw.
144
- _ssrAttrs: Record<string, string> = {};
145
- getAttribute(name: string): string | null {
146
- return this._ssrAttrs[name] ?? null;
147
- }
148
- setAttribute(name: string, value: string): void {
149
- this._ssrAttrs[name] = String(value);
150
- }
151
- hasAttribute(name: string): boolean {
152
- return name in this._ssrAttrs;
153
- }
154
- removeAttribute(name: string): void {
155
- delete this._ssrAttrs[name];
156
- }
157
- toggleAttribute(name: string, force?: boolean): boolean {
158
- const want = force === undefined ? !this.hasAttribute(name) : force;
159
- if (want) this.setAttribute(name, '');
160
- else this.removeAttribute(name);
161
- return want;
162
- }
163
- addEventListener(): void {}
164
- removeEventListener(): void {}
165
- dispatchEvent(): boolean {
166
- return true;
167
- }
168
- // Tree-walk APIs no-op into null: components that call them server-side
169
- // simply see "no children / no siblings", which is acceptable for SSR.
170
- closest(): null {
171
- return null;
172
- }
173
- querySelector(): null {
174
- return null;
175
- }
176
- querySelectorAll(): never[] {
177
- return [];
178
- }
179
- focus(): void {}
180
- blur(): void {}
181
- contains(): boolean {
182
- return false;
183
- }
184
- insertBefore<T>(node: T): T {
185
- return node;
186
- }
187
- appendChild<T>(node: T): T {
188
- return node;
189
- }
190
- replaceChildren(): void {}
191
- get firstChild(): null {
192
- return null;
193
- }
194
- get classList() {
195
- return { add: () => {}, remove: () => {}, toggle: () => false, contains: () => false };
196
- }
197
- get className(): string {
198
- return this._ssrAttrs.class ?? '';
199
- }
200
- set className(v: string) {
201
- this._ssrAttrs.class = v;
202
- }
203
- get style(): Record<string, string> {
204
- return {};
205
- }
206
- }
207
-
208
- /** SSR-safe base class: `HTMLElement` in browser, a thin stub in Node. */
209
- export const Base: typeof HTMLElement = (HasHTMLElement
210
- ? HTMLElement
211
- : (ServerHTMLElementStub as unknown as typeof HTMLElement)) as typeof HTMLElement;
212
-
213
- /** Register a custom element. No-op on server (no `customElements` global). */
214
- export function defineElement(name: string, cls: CustomElementConstructor): void {
215
- if (typeof customElements === 'undefined') return;
216
- if (customElements.get(name)) return; // already defined (HMR / double-import)
217
- customElements.define(name, cls);
218
- }
130
+ // NOTE: the old `HTMLElement`-era `Base` / `defineElement` / SSR-stub helpers
131
+ // were removed. The ui registry components extend `WebComponent` from
132
+ // `@webjsdev/core` now, so those helpers were dead code, and referencing
133
+ // `HTMLElement` / `customElements` at module scope marked this util (and any
134
+ // page that imports `cn`) as client-effecting. Everything below is pure or
135
+ // SSR-safe, so importing `cn` no longer pins a page to the browser (#819).
219
136
 
220
137
  // ---------------------------------------------------------------------------
221
138
  // Layout helpers: encode the design-system rhythm (spacing between label /
@@ -266,19 +183,3 @@ export const helpClass = () => 'text-xs text-muted-foreground';
266
183
 
267
184
  /** Validation error text: replaces hint when the field is invalid. */
268
185
  export const errorClass = () => 'text-sm font-medium text-destructive';
269
-
270
- /**
271
- * Run `reset` just before the webjs client router snapshots the page into its
272
- * back/forward cache (the `webjs:before-cache` event). Transient overlays use
273
- * this to close themselves so a restored snapshot is clean rather than, say, a
274
- * hover-card frozen open after Back then Forward (#766). Wire it in
275
- * `connectedCallback` and call the returned disposer in `disconnectedCallback`
276
- * so the listener never leaks across soft navigations.
277
- *
278
- * SSR-safe: a no-op when there is no `document` (server / no client router).
279
- */
280
- export function onBeforeCache(reset: () => void): () => void {
281
- if (typeof document === 'undefined') return () => {};
282
- document.addEventListener('webjs:before-cache', reset);
283
- return () => document.removeEventListener('webjs:before-cache', reset);
284
- }
@@ -15,6 +15,18 @@
15
15
  }
16
16
  ]
17
17
  },
18
+ {
19
+ "name": "lib-dom",
20
+ "type": "registry:lib",
21
+ "description": "onBeforeCache() client-router cache hook",
22
+ "files": [
23
+ {
24
+ "path": "lib/dom.ts",
25
+ "type": "registry:lib",
26
+ "target": "lib/dom.ts"
27
+ }
28
+ ]
29
+ },
18
30
  {
19
31
  "name": "theme-neutral",
20
32
  "type": "registry:theme",
@@ -59,7 +71,8 @@
59
71
  "type": "registry:ui",
60
72
  "registryDependencies": [
61
73
  "button",
62
- "dialog"
74
+ "dialog",
75
+ "lib-dom"
63
76
  ],
64
77
  "dependencies": [
65
78
  "@webjsdev/core"
@@ -189,7 +202,8 @@
189
202
  "type": "registry:ui",
190
203
  "registryDependencies": [
191
204
  "lib-utils",
192
- "button"
205
+ "button",
206
+ "lib-dom"
193
207
  ],
194
208
  "dependencies": [
195
209
  "@webjsdev/core"
@@ -205,7 +219,8 @@
205
219
  "name": "dropdown-menu",
206
220
  "type": "registry:ui",
207
221
  "registryDependencies": [
208
- "popover"
222
+ "popover",
223
+ "lib-dom"
209
224
  ],
210
225
  "dependencies": [
211
226
  "@webjsdev/core"
@@ -221,7 +236,8 @@
221
236
  "name": "hover-card",
222
237
  "type": "registry:ui",
223
238
  "registryDependencies": [
224
- "popover"
239
+ "popover",
240
+ "lib-dom"
225
241
  ],
226
242
  "dependencies": [
227
243
  "@webjsdev/core"
@@ -381,6 +397,9 @@
381
397
  {
382
398
  "name": "sonner",
383
399
  "type": "registry:ui",
400
+ "registryDependencies": [
401
+ "lib-dom"
402
+ ],
384
403
  "dependencies": [
385
404
  "@webjsdev/core"
386
405
  ],
@@ -476,7 +495,8 @@
476
495
  "name": "tooltip",
477
496
  "type": "registry:ui",
478
497
  "registryDependencies": [
479
- "popover"
498
+ "popover",
499
+ "lib-dom"
480
500
  ],
481
501
  "dependencies": [
482
502
  "@webjsdev/core"
@@ -76,31 +76,51 @@ async function writeRegistryFile(cwd, config, item, file, opts) {
76
76
  }
77
77
 
78
78
  /**
79
- * Rewrite the registry-relative `'../lib/utils.ts'` import to the path
80
- * that resolves correctly from the file's target location to the user's
81
- * cn() helper.
79
+ * Rewrite the registry-relative `'../lib/utils.ts'` and `'../lib/dom.ts'`
80
+ * imports to the paths that resolve correctly from the file's target
81
+ * location to the user's cn() helper and onBeforeCache() DOM helper.
82
82
  *
83
83
  * The registry source assumes its own layout (`<registry>/components/<x>.ts`
84
- * imports `'../lib/utils.ts'`). When that file lands in the user's
85
- * components/ui/<x>.ts, the literal `'../lib/utils.ts'` resolves to
86
- * `components/lib/utils.ts`, which doesn't exist. We compute the actual
87
- * relative path from the target directory to `config.resolvedPaths.utils`
88
- * (an absolute path the user has already configured via components.json's
89
- * aliases.utils) and substitute it in.
84
+ * imports `'../lib/utils.ts'` / `'../lib/dom.ts'`). When that file lands in
85
+ * the user's components/ui/<x>.ts, the literal `'../lib/utils.ts'` resolves
86
+ * to `components/lib/utils.ts`, which doesn't exist. We compute the actual
87
+ * relative path from the target directory to the user's configured helper
88
+ * (`config.resolvedPaths.utils`, from components.json's aliases.utils) and
89
+ * substitute it in. The DOM helper is written as a SIBLING of the utils
90
+ * file (the scaffold puts it at `lib/utils/dom.ts`, next to `cn.ts`), so its
91
+ * absolute target is `dom.ts` in the utils file's directory. sonner imports
92
+ * ONLY the DOM helper, so both specifiers are handled independently and
93
+ * neither presence gates the other.
90
94
  *
91
95
  * @param {string} content raw file content from the registry
92
96
  * @param {string} target absolute path where the file will be written
93
97
  * @param {{ resolvedPaths: { utils: string } }} config parsed components.json
94
98
  */
95
99
  export function rewriteUtilsImport(content, target, config) {
96
- if (!content.includes('../lib/utils.ts')) return content;
97
100
  const utilsAbs = config?.resolvedPaths?.utils;
98
101
  if (!utilsAbs) return content;
99
- let rel = relPath(dirname(target), utilsAbs).split(/[\\/]/).join('/');
102
+ let out = replaceImportSpecifier(content, '../lib/utils.ts', utilsAbs, target);
103
+ const domAbs = join(dirname(utilsAbs), 'dom.ts');
104
+ out = replaceImportSpecifier(out, '../lib/dom.ts', domAbs, target);
105
+ return out;
106
+ }
107
+
108
+ /**
109
+ * Replace a single registry-relative import specifier with the relative path
110
+ * from `target`'s directory to `destAbs`. No-op when `spec` is absent.
111
+ *
112
+ * @param {string} content
113
+ * @param {string} spec the registry-relative specifier to replace
114
+ * @param {string} destAbs absolute path of the destination file
115
+ * @param {string} target absolute path where the importing file is written
116
+ */
117
+ function replaceImportSpecifier(content, spec, destAbs, target) {
118
+ if (!content.includes(spec)) return content;
119
+ let rel = relPath(dirname(target), destAbs).split(/[\\/]/).join('/');
100
120
  if (!rel.startsWith('.')) rel = './' + rel;
101
121
  return content
102
- .replaceAll("'../lib/utils.ts'", `'${rel}'`)
103
- .replaceAll('"../lib/utils.ts"', `"${rel}"`);
122
+ .replaceAll(`'${spec}'`, `'${rel}'`)
123
+ .replaceAll(`"${spec}"`, `"${rel}"`);
104
124
  }
105
125
 
106
126
  function resolveTarget(cwd, config, item, file) {
@@ -75,21 +75,45 @@ export const init = new Command()
75
75
  });
76
76
 
77
77
  async function writeLibUtils(cwd, utilsAlias, registryUrl) {
78
+ // `utils` alias points at e.g. "lib/utils" so we write to lib/utils.ts
79
+ const utilsRel = utilsAlias.replace(/^@\//, '') + '.ts';
80
+ const utilsTarget = join(cwd, utilsRel);
78
81
  try {
79
82
  const item = await fetchRegistryItem('lib-utils', registryUrl);
83
+ if (item.files) {
84
+ for (const f of item.files) {
85
+ ensureDir(dirname(utilsTarget));
86
+ writeFileSync(utilsTarget, f.content || '', 'utf8');
87
+ logger.success(`Wrote ${utilsAlias}.ts`);
88
+ }
89
+ }
90
+ } catch (e) {
91
+ logger.warn(`Could not fetch lib-utils from registry (${e.message}). You may need to write lib/utils.ts manually.`);
92
+ }
93
+
94
+ // The onBeforeCache() DOM helper lives in a SEPARATE module (#819) because
95
+ // it references `document`, so keeping it out of cn()'s file prevents the
96
+ // elision analyzer pinning every page that imports cn to the browser.
97
+ // Overlay components import it from `../lib/dom.ts`, so write it as a
98
+ // sibling of the utils file (e.g. lib/utils/dom.ts next to cn.ts).
99
+ const domTarget = join(dirname(utilsTarget), 'dom.ts');
100
+ try {
101
+ const item = await fetchRegistryItem('lib-dom', registryUrl);
80
102
  if (!item.files) return;
81
103
  for (const f of item.files) {
82
- // `utils` alias points at e.g. "lib/utils" → we write to lib/utils.ts
83
- const target = join(cwd, utilsAlias.replace(/^@\//, '') + '.ts');
84
- ensureDir(dirname(target));
85
- writeFileSync(target, f.content || '', 'utf8');
86
- logger.success(`Wrote ${utilsAlias}.ts`);
104
+ ensureDir(dirname(domTarget));
105
+ writeFileSync(domTarget, f.content || '', 'utf8');
106
+ logger.success(`Wrote ${relative(cwd, domTarget)}`);
87
107
  }
88
108
  } catch (e) {
89
- logger.warn(`Could not fetch lib-utils from registry (${e.message}). You may need to write lib/utils.ts manually.`);
109
+ logger.warn(`Could not fetch lib-dom from registry (${e.message}). You may need to write lib/dom.ts manually.`);
90
110
  }
91
111
  }
92
112
 
113
+ function relative(cwd, p) {
114
+ return p.startsWith(cwd) ? p.slice(cwd.length + 1) : p;
115
+ }
116
+
93
117
  async function writeTheme(cwd, baseColor, cssPath, registryUrl) {
94
118
  try {
95
119
  const item = await fetchRegistryItem(`theme-${baseColor}`, registryUrl);
@@ -34,7 +34,9 @@ export function defaultsForProject(cwd = process.cwd()) {
34
34
  switch (type) {
35
35
  case 'webjs':
36
36
  return {
37
- tailwindCss: 'app/globals.css',
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',
38
40
  aliases: { components: 'components', utils: 'lib/utils', ui: 'components/ui', lib: 'lib' },
39
41
  };
40
42
  case 'next':