@webjsdev/ui 0.3.3 → 0.3.5

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.3",
3
+ "version": "0.3.5",
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": {
@@ -64,6 +64,7 @@
64
64
  * Design tokens used: --background, --border, --muted-foreground.
65
65
  */
66
66
  import { WebComponent, html } from '@webjsdev/core';
67
+ import { ref, createRef } from '@webjsdev/core/directives';
67
68
  import { buttonClass, type ButtonVariant, type ButtonSize } from './button.ts';
68
69
 
69
70
  export const alertDialogContentClass = (): string =>
@@ -228,18 +229,23 @@ export class UiAlertDialogContent extends WebComponent {
228
229
  };
229
230
  declare size: 'default' | 'sm';
230
231
 
232
+ // ref to the own-rendered native <dialog>. render() creates it, so a
233
+ // ref() binding is the lit-idiomatic handle (no querySelector against
234
+ // a string selector into the component's own output).
235
+ #dialog = createRef<HTMLDialogElement>();
236
+
231
237
  constructor() {
232
238
  super();
233
239
  this.size = 'default';
234
240
  }
235
241
 
236
242
  showModal(): void {
237
- const native = this.querySelector<HTMLDialogElement>('dialog[data-slot="alert-dialog-native"]');
243
+ const native = this.#dialog.value;
238
244
  if (native && !native.open) native.showModal();
239
245
  }
240
246
 
241
247
  close(): void {
242
- const native = this.querySelector<HTMLDialogElement>('dialog[data-slot="alert-dialog-native"]');
248
+ const native = this.#dialog.value;
243
249
  if (native?.open) native.close();
244
250
  }
245
251
 
@@ -248,6 +254,7 @@ export class UiAlertDialogContent extends WebComponent {
248
254
  return html`<dialog
249
255
  data-slot="alert-dialog-native"
250
256
  class=${NATIVE_DIALOG_CLASS}
257
+ ${ref(this.#dialog)}
251
258
  @cancel=${this._onNativeCancel}
252
259
  @close=${this._onNativeClose}
253
260
  ><div
@@ -64,6 +64,7 @@
64
64
  * Design tokens used: --background, --border, --muted-foreground.
65
65
  */
66
66
  import { WebComponent, html, unsafeHTML } from '@webjsdev/core';
67
+ import { ref, createRef } from '@webjsdev/core/directives';
67
68
  import { buttonClass } from './button.ts';
68
69
 
69
70
  // --------------------------------------------------------------------------
@@ -262,18 +263,23 @@ export class UiDialogContent extends WebComponent {
262
263
  };
263
264
  declare showCloseButton: string;
264
265
 
266
+ // ref to the own-rendered native <dialog>. render() creates it, so a
267
+ // ref() binding is the lit-idiomatic handle (no querySelector against
268
+ // a string selector into the component's own output).
269
+ #dialog = createRef<HTMLDialogElement>();
270
+
265
271
  constructor() {
266
272
  super();
267
273
  this.showCloseButton = 'true';
268
274
  }
269
275
 
270
276
  showModal(): void {
271
- const native = this.querySelector<HTMLDialogElement>('dialog[data-slot="dialog-native"]');
277
+ const native = this.#dialog.value;
272
278
  if (native && !native.open) native.showModal();
273
279
  }
274
280
 
275
281
  close(): void {
276
- const native = this.querySelector<HTMLDialogElement>('dialog[data-slot="dialog-native"]');
282
+ const native = this.#dialog.value;
277
283
  if (native?.open) native.close();
278
284
  }
279
285
 
@@ -283,6 +289,7 @@ export class UiDialogContent extends WebComponent {
283
289
  return html`<dialog
284
290
  data-slot="dialog-native"
285
291
  class=${NATIVE_DIALOG_CLASS}
292
+ ${ref(this.#dialog)}
286
293
  @close=${this._onNativeClose}
287
294
  @click=${this._onNativeBackdropClick}
288
295
  ><div
@@ -74,7 +74,7 @@
74
74
  * Design tokens used: --popover, --popover-foreground, --accent,
75
75
  * --accent-foreground, --destructive, --muted-foreground, --border.
76
76
  */
77
- import { WebComponent, html, unsafeHTML } from '@webjsdev/core';
77
+ import { WebComponent, html, unsafeHTML, signal } from '@webjsdev/core';
78
78
  import { positionFloating, type PopoverSide, type PopoverAlign } from './popover.ts';
79
79
 
80
80
  // --------------------------------------------------------------------------
@@ -348,6 +348,12 @@ export class UiDropdownMenuItem extends WebComponent {
348
348
  declare variant: 'default' | 'destructive';
349
349
  declare inset: boolean;
350
350
 
351
+ // Keyboard / pointer highlight state for the own-rendered menuitem. A
352
+ // local signal bound with ?data-highlighted keeps the highlight in the
353
+ // declarative template instead of an imperative setAttribute on
354
+ // e.currentTarget (the lit-idiomatic form).
355
+ #highlighted = signal(false);
356
+
351
357
  constructor() {
352
358
  super();
353
359
  this.variant = 'default';
@@ -361,6 +367,7 @@ export class UiDropdownMenuItem extends WebComponent {
361
367
  tabindex="-1"
362
368
  data-variant=${this.variant}
363
369
  ?data-inset=${this.inset}
370
+ ?data-highlighted=${this.#highlighted.get()}
364
371
  class=${dropdownMenuItemClass()}
365
372
  @click=${this._onClick}
366
373
  @pointerenter=${this._onPointerEnter}
@@ -381,12 +388,12 @@ export class UiDropdownMenuItem extends WebComponent {
381
388
  el.focus();
382
389
  };
383
390
 
384
- _onFocus = (e: Event): void => {
385
- (e.currentTarget as HTMLElement).setAttribute('data-highlighted', '');
391
+ _onFocus = (): void => {
392
+ this.#highlighted.set(true);
386
393
  };
387
394
 
388
- _onBlur = (e: Event): void => {
389
- (e.currentTarget as HTMLElement).removeAttribute('data-highlighted');
395
+ _onBlur = (): void => {
396
+ this.#highlighted.set(false);
390
397
  };
391
398
  }
392
399
  UiDropdownMenuItem.register('ui-dropdown-menu-item');
@@ -55,8 +55,14 @@ export const hoverCardContentClass = (): string =>
55
55
  export class UiHoverCard extends WebComponent {
56
56
  static properties = {
57
57
  open: { type: Boolean, reflect: true },
58
+ openDelay: { type: Number },
59
+ closeDelay: { type: Number },
58
60
  };
59
61
  declare open: boolean;
62
+ // `openDelay` / `closeDelay` ride the `open-delay` / `close-delay`
63
+ // attributes (shadcn parity), read as typed props.
64
+ declare openDelay: number;
65
+ declare closeDelay: number;
60
66
 
61
67
  _showTimer: number | undefined;
62
68
  _hideTimer: number | undefined;
@@ -64,6 +70,8 @@ export class UiHoverCard extends WebComponent {
64
70
  constructor() {
65
71
  super();
66
72
  this.open = false;
73
+ this.openDelay = 700;
74
+ this.closeDelay = 300;
67
75
  }
68
76
 
69
77
  // Back-compat getter.
@@ -71,14 +79,12 @@ export class UiHoverCard extends WebComponent {
71
79
 
72
80
  show(): void {
73
81
  clearTimeout(this._hideTimer);
74
- const delay = Number(this.getAttribute('open-delay') ?? 700);
75
- this._showTimer = window.setTimeout(() => { this.open = true; }, delay);
82
+ this._showTimer = window.setTimeout(() => { this.open = true; }, this.openDelay);
76
83
  }
77
84
 
78
85
  hide(): void {
79
86
  clearTimeout(this._showTimer);
80
- const delay = Number(this.getAttribute('close-delay') ?? 300);
81
- this._hideTimer = window.setTimeout(() => { this.open = false; }, delay);
87
+ this._hideTimer = window.setTimeout(() => { this.open = false; }, this.closeDelay);
82
88
  }
83
89
 
84
90
  render() {
@@ -177,9 +177,9 @@ export class UiTabsTrigger extends WebComponent {
177
177
  this.value = '';
178
178
  }
179
179
 
180
- // render() runs server-side too; linkedom doesn't implement closest()
181
- // on custom elements. Return null during SSR; the client re-renders
182
- // with the parent reference after hydration.
180
+ // render() runs server-side too. webjs resolves closest() at SSR against
181
+ // the enclosing-element ancestor chain, so the active tab is marked in the
182
+ // first paint (no hydration flash). The typeof guard stays defensive.
183
183
  get _tabs(): UiTabs | null {
184
184
  if (typeof this.closest !== 'function') return null;
185
185
  return this.closest('ui-tabs') as UiTabs | null;
@@ -173,9 +173,9 @@ export class UiToggleGroupItem extends WebComponent {
173
173
  this.pressed = false;
174
174
  }
175
175
 
176
- // render() runs server-side too. linkedom doesn't implement closest()
177
- // on custom elements, so guard it; the client re-renders with the
178
- // real parent reference after hydration.
176
+ // render() runs server-side too. webjs resolves closest() at SSR against
177
+ // the enclosing-element ancestor chain, so the pressed item is marked in
178
+ // the first paint (no hydration flash). The typeof guard stays defensive.
179
179
  get _group(): UiToggleGroup | null {
180
180
  if (typeof this.closest !== 'function') return null;
181
181
  return this.closest('ui-toggle-group') as UiToggleGroup | null;
@@ -64,8 +64,14 @@ let lastTooltipHideAt = 0;
64
64
  export class UiTooltip extends WebComponent {
65
65
  static properties = {
66
66
  open: { type: Boolean, reflect: true },
67
+ delayDuration: { type: Number },
68
+ skipDelayDuration: { type: Number },
67
69
  };
68
70
  declare open: boolean;
71
+ // `delayDuration` / `skipDelayDuration` ride the `delay-duration` /
72
+ // `skip-delay-duration` attributes (shadcn parity), read as typed props.
73
+ declare delayDuration: number;
74
+ declare skipDelayDuration: number;
69
75
 
70
76
  _showTimer: number | undefined;
71
77
  _hideTimer: number | undefined;
@@ -73,6 +79,8 @@ export class UiTooltip extends WebComponent {
73
79
  constructor() {
74
80
  super();
75
81
  this.open = false;
82
+ this.delayDuration = 700;
83
+ this.skipDelayDuration = 300;
76
84
  }
77
85
 
78
86
  // Back-compat getter for tests + external code that read `el.isOpen`
@@ -82,8 +90,8 @@ export class UiTooltip extends WebComponent {
82
90
  show(): void {
83
91
  clearTimeout(this._showTimer);
84
92
  clearTimeout(this._hideTimer);
85
- const delay = Number(this.getAttribute('delay-duration') ?? 700);
86
- const skipDelay = Number(this.getAttribute('skip-delay-duration') ?? 300);
93
+ const delay = this.delayDuration;
94
+ const skipDelay = this.skipDelayDuration;
87
95
  const sinceLastHide = Date.now() - lastTooltipHideAt;
88
96
  if (lastTooltipHideAt > 0 && sinceLastHide < skipDelay) {
89
97
  this.open = true;