@webjsdev/ui 0.3.1 → 0.3.3

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.
Files changed (39) hide show
  1. package/package.json +4 -3
  2. package/packages/registry/README.md +35 -0
  3. package/packages/registry/components/accordion.ts +74 -0
  4. package/packages/registry/components/alert-dialog.ts +359 -0
  5. package/packages/registry/components/alert.ts +51 -0
  6. package/packages/registry/components/aspect-ratio.ts +22 -0
  7. package/packages/registry/components/avatar.ts +52 -0
  8. package/packages/registry/components/badge.ts +40 -0
  9. package/packages/registry/components/breadcrumb.ts +43 -0
  10. package/packages/registry/components/button.ts +72 -0
  11. package/packages/registry/components/card.ts +86 -0
  12. package/packages/registry/components/checkbox.ts +97 -0
  13. package/packages/registry/components/collapsible.ts +60 -0
  14. package/packages/registry/components/dialog.ts +378 -0
  15. package/packages/registry/components/dropdown-menu.ts +607 -0
  16. package/packages/registry/components/hover-card.ts +175 -0
  17. package/packages/registry/components/input.ts +36 -0
  18. package/packages/registry/components/kbd.ts +25 -0
  19. package/packages/registry/components/label.ts +23 -0
  20. package/packages/registry/components/native-select.ts +110 -0
  21. package/packages/registry/components/pagination.ts +45 -0
  22. package/packages/registry/components/popover.ts +260 -0
  23. package/packages/registry/components/progress.ts +46 -0
  24. package/packages/registry/components/radio-group.ts +113 -0
  25. package/packages/registry/components/separator.ts +30 -0
  26. package/packages/registry/components/skeleton.ts +16 -0
  27. package/packages/registry/components/sonner.ts +240 -0
  28. package/packages/registry/components/switch.ts +52 -0
  29. package/packages/registry/components/table.ts +58 -0
  30. package/packages/registry/components/tabs.ts +271 -0
  31. package/packages/registry/components/textarea.ts +27 -0
  32. package/packages/registry/components/toggle-group.ts +236 -0
  33. package/packages/registry/components/toggle.ts +118 -0
  34. package/packages/registry/components/tooltip.ts +195 -0
  35. package/packages/registry/lib/utils.ts +241 -0
  36. package/packages/registry/package.json +7 -0
  37. package/packages/registry/registry.json +479 -0
  38. package/packages/registry/themes/base-colors.js +193 -0
  39. package/packages/registry/themes/index.css +141 -0
@@ -0,0 +1,175 @@
1
+ /**
2
+ * HoverCard: popover-like panel triggered by hover with configurable
3
+ * open / close delays. Tier-2. The content uses the native Popover API
4
+ * in `popover="manual"` mode for top-layer rendering; the custom
5
+ * element owns the hover-with-linger state machine and JS positioning.
6
+ *
7
+ * shadcn parity:
8
+ * HoverCard → <ui-hover-card open-delay close-delay>
9
+ * HoverCardTrigger → <ui-hover-card-trigger>
10
+ * HoverCardContent → <ui-hover-card-content side align side-offset align-offset>
11
+ *
12
+ * Usage:
13
+ * <ui-hover-card open-delay="700" close-delay="300">
14
+ * <ui-hover-card-trigger>
15
+ * <a href="/user/vivek">@vivek</a>
16
+ * </ui-hover-card-trigger>
17
+ * <ui-hover-card-content>
18
+ * <div class="flex gap-3">…</div>
19
+ * </ui-hover-card-content>
20
+ * </ui-hover-card>
21
+ *
22
+ * Attributes on <ui-hover-card>:
23
+ * `open`: boolean (reflected). Open state.
24
+ * `open-delay`: ms, default 700. Hover delay before opening.
25
+ * `close-delay`: ms, default 300. Linger delay before closing once
26
+ * cursor leaves trigger + content.
27
+ *
28
+ * Attributes on <ui-hover-card-content>:
29
+ * `side`: "top" | "right" | "bottom" (default) | "left".
30
+ * `align`: "center" (default) | "start" | "end".
31
+ * `side-offset`: number, default 4. Pixels between trigger and content.
32
+ * `align-offset`: number, default 0. Pixels of cross-axis shift.
33
+ *
34
+ * Events: none dispatched at present; observe the reflected `open`
35
+ * attribute from CSS or JS.
36
+ *
37
+ * Programmatic API on <ui-hover-card>: `.show()` · `.hide()`.
38
+ *
39
+ * Design tokens used: --popover, --popover-foreground, --border.
40
+ */
41
+ import { WebComponent, html } from '@webjsdev/core';
42
+ import { positionFloating, type PopoverSide, type PopoverAlign } from './popover.ts';
43
+
44
+ // `fixed m-0` opts out of the UA `[popover]` auto-centering margin so
45
+ // JS-computed top/left from positionFloating lands correctly. shadcn's
46
+ // visual layer sits on top. UA `[popover]:not(:popover-open) {
47
+ // display: none }` handles closed-state hiding.
48
+ export const hoverCardContentClass = (): string =>
49
+ 'fixed z-50 w-64 m-0 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden';
50
+
51
+ // --------------------------------------------------------------------------
52
+ // <ui-hover-card>
53
+ // --------------------------------------------------------------------------
54
+
55
+ export class UiHoverCard extends WebComponent {
56
+ static properties = {
57
+ open: { type: Boolean, reflect: true },
58
+ };
59
+ declare open: boolean;
60
+
61
+ _showTimer: number | undefined;
62
+ _hideTimer: number | undefined;
63
+
64
+ constructor() {
65
+ super();
66
+ this.open = false;
67
+ }
68
+
69
+ // Back-compat getter.
70
+ get isOpen(): boolean { return this.open; }
71
+
72
+ show(): void {
73
+ clearTimeout(this._hideTimer);
74
+ const delay = Number(this.getAttribute('open-delay') ?? 700);
75
+ this._showTimer = window.setTimeout(() => { this.open = true; }, delay);
76
+ }
77
+
78
+ hide(): void {
79
+ clearTimeout(this._showTimer);
80
+ const delay = Number(this.getAttribute('close-delay') ?? 300);
81
+ this._hideTimer = window.setTimeout(() => { this.open = false; }, delay);
82
+ }
83
+
84
+ render() {
85
+ return html`<div
86
+ data-slot="hover-card"
87
+ data-state=${this.open ? 'open' : 'closed'}
88
+ ><slot></slot></div>`;
89
+ }
90
+
91
+ updated(changedProperties: Map<string, unknown>): void {
92
+ if (!changedProperties.has('open')) return;
93
+ if (changedProperties.get('open') === undefined) return;
94
+ // Wait one microtask for <ui-hover-card-content>'s inner [popover]
95
+ // element to commit; we drive its showPopover() / hidePopover().
96
+ queueMicrotask(() => this._syncContent());
97
+ }
98
+
99
+ _syncContent(): void {
100
+ // Same nested-popover pattern as tooltip: <ui-hover-card-content>
101
+ // renders an inner <div popover="manual">; the Popover API lives on
102
+ // that inner div, not the host.
103
+ const popover = this.querySelector<HTMLElement>('ui-hover-card-content [popover]');
104
+ const host = this.querySelector<HTMLElement>('ui-hover-card-content');
105
+ if (!popover || !popover.isConnected) return;
106
+ const p = popover as HTMLElement & {
107
+ showPopover?: () => void;
108
+ hidePopover?: () => void;
109
+ matches: (s: string) => boolean;
110
+ };
111
+ if (typeof p.showPopover !== 'function') return;
112
+ if (this.open) {
113
+ if (!p.matches(':popover-open')) p.showPopover();
114
+ if (host) this._reposition(host, popover);
115
+ } else if (p.matches(':popover-open')) {
116
+ p.hidePopover();
117
+ }
118
+ }
119
+
120
+ _reposition(contentHost: HTMLElement, popover: HTMLElement): void {
121
+ const trigger = this.querySelector<HTMLElement>('ui-hover-card-trigger');
122
+ if (!trigger) return;
123
+ positionFloating(trigger, popover, {
124
+ side: (contentHost.getAttribute('side') ?? 'bottom') as PopoverSide,
125
+ align: (contentHost.getAttribute('align') ?? 'center') as PopoverAlign,
126
+ sideOffset: Number(contentHost.getAttribute('side-offset') ?? 4),
127
+ alignOffset: Number(contentHost.getAttribute('align-offset') ?? 0),
128
+ });
129
+ }
130
+ }
131
+ UiHoverCard.register('ui-hover-card');
132
+
133
+ // --------------------------------------------------------------------------
134
+ // <ui-hover-card-trigger>
135
+ // --------------------------------------------------------------------------
136
+
137
+ export class UiHoverCardTrigger extends WebComponent {
138
+ render() {
139
+ return html`<div
140
+ data-slot="hover-card-trigger"
141
+ @mouseenter=${this._onEnter}
142
+ @mouseleave=${this._onLeave}
143
+ @focusin=${this._onEnter}
144
+ @focusout=${this._onLeave}
145
+ ><slot></slot></div>`;
146
+ }
147
+
148
+ _onEnter = (): void => (this.closest('ui-hover-card') as UiHoverCard | null)?.show();
149
+ _onLeave = (): void => (this.closest('ui-hover-card') as UiHoverCard | null)?.hide();
150
+ }
151
+ UiHoverCardTrigger.register('ui-hover-card-trigger');
152
+
153
+ // --------------------------------------------------------------------------
154
+ // <ui-hover-card-content>
155
+ // The mouseenter/mouseleave handlers keep the card open while the cursor
156
+ // is over the content itself (so it does not close during a brief
157
+ // mouseleave on the trigger if the user is moving toward the card).
158
+ // --------------------------------------------------------------------------
159
+
160
+ export class UiHoverCardContent extends WebComponent {
161
+ render() {
162
+ return html`<div
163
+ data-slot="hover-card-content"
164
+ role="dialog"
165
+ popover="manual"
166
+ class=${hoverCardContentClass()}
167
+ @mouseenter=${this._onEnter}
168
+ @mouseleave=${this._onLeave}
169
+ ><slot></slot></div>`;
170
+ }
171
+
172
+ _onEnter = (): void => (this.closest('ui-hover-card') as UiHoverCard | null)?.show();
173
+ _onLeave = (): void => (this.closest('ui-hover-card') as UiHoverCard | null)?.hide();
174
+ }
175
+ UiHoverCardContent.register('ui-hover-card-content');
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Input: styled native `<input>`. Tier-1 class helper. Works with every
3
+ * input type (text, email, password, number, search, tel, url, date,
4
+ * time, file, color, …). Form submission, autocomplete, browser
5
+ * validation, and password managers all work because it IS the native
6
+ * input.
7
+ *
8
+ * shadcn parity:
9
+ * Input → inputClass()
10
+ *
11
+ * Usage:
12
+ * <input class=${inputClass()} type="email" name="email" id="email" required
13
+ * aria-describedby="email-hint">
14
+ *
15
+ * Pair with `<label class=${labelClass()} for="...">` and a hint paragraph
16
+ * (`<p class=${hintClass()} id="...-hint">`). Wrap all three in
17
+ * `<div class=${fieldClass()}>` for the canonical field rhythm.
18
+ *
19
+ * Design tokens used: --input, --background, --primary, --primary-foreground,
20
+ * --muted-foreground, --foreground, --ring, --destructive.
21
+ */
22
+ import { cn } from '../lib/utils.ts';
23
+
24
+ const INPUT_BASE =
25
+ 'h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30';
26
+
27
+ const INPUT_FOCUS =
28
+ 'focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50';
29
+
30
+ const INPUT_INVALID =
31
+ 'aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40';
32
+
33
+ /** Compose Tailwind classes for a native `<input>`. */
34
+ export function inputClass(): string {
35
+ return cn(INPUT_BASE, INPUT_FOCUS, INPUT_INVALID);
36
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Kbd: keyboard chord display. Tier-1 class helpers; compose with the
3
+ * native `<kbd>` element for correct semantics.
4
+ *
5
+ * shadcn parity:
6
+ * Kbd → kbdClass()
7
+ * KbdGroup → kbdGroupClass()
8
+ *
9
+ * Usage:
10
+ * <kbd class=${kbdClass()}>⌘</kbd>
11
+ * <kbd class=${kbdClass()}>K</kbd>
12
+ *
13
+ * <div class=${kbdGroupClass()}>
14
+ * <kbd class=${kbdClass()}>⌘</kbd>
15
+ * <kbd class=${kbdClass()}>Shift</kbd>
16
+ * <kbd class=${kbdClass()}>P</kbd>
17
+ * </div>
18
+ *
19
+ * Design tokens used: --muted, --muted-foreground, --background.
20
+ */
21
+
22
+ export const kbdClass = (): string =>
23
+ "pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm bg-muted px-1 font-sans text-xs font-medium text-muted-foreground select-none [&_svg:not([class*='size-'])]:size-3 [[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10";
24
+
25
+ export const kbdGroupClass = (): string => 'inline-flex items-center gap-1';
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Label: styled native `<label>`. Tier-1 class helper. Compose with a
3
+ * real `<label for="...">` so click-to-focus, `htmlFor` / `for` linking,
4
+ * and screen-reader association all work natively (no Radix Label needed).
5
+ *
6
+ * shadcn parity:
7
+ * Label → labelClass()
8
+ *
9
+ * Usage:
10
+ * <label class=${labelClass()} for="email">Email</label>
11
+ * <input class=${inputClass()} id="email" name="email" type="email">
12
+ *
13
+ * Disabled-state inheritance: when the label is inside a container with
14
+ * `data-disabled="true"` (the "field" pattern), or next to a peer-disabled
15
+ * control, it dims automatically.
16
+ *
17
+ * Design tokens used: none (typography only).
18
+ */
19
+
20
+ /** Compose Tailwind classes for a native `<label>`. */
21
+ export function labelClass(): string {
22
+ return 'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50';
23
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * NativeSelect: styled native `<select>`. Tier-1 class helpers. Uses
3
+ * `appearance: none` to hide the platform chevron and overlays an SVG
4
+ * chevron on top. Best mobile UX (native picker), full keyboard support,
5
+ * form submission works natively. No JS.
6
+ *
7
+ * shadcn parity:
8
+ * NativeSelect → nativeSelectClass()
9
+ * NativeSelect wrapper → nativeSelectWrapperClass()
10
+ * NativeSelect chevron → nativeSelectIconClass()
11
+ * NativeSelect option → bare <option> (or nativeSelectOptionClass()
12
+ * for explicit overrides; the installed
13
+ * stylesheet sets Canvas / CanvasText on
14
+ * every <option> automatically)
15
+ *
16
+ * Usage:
17
+ * <div class=${nativeSelectWrapperClass()}>
18
+ * <select class=${nativeSelectClass()} name="plan">
19
+ * <option>Basic</option>
20
+ * <option>Pro</option>
21
+ * </select>
22
+ * <!-- chevron icon, decorative -->
23
+ * <svg class="${nativeSelectIconClass()}" aria-hidden="true">…</svg>
24
+ * </div>
25
+ *
26
+ * Importing this module installs a stylesheet that forces Canvas /
27
+ * CanvasText on every <option> inside the wrapper so the dropdown reads
28
+ * in both light and dark themes regardless of OS preference; advanced
29
+ * overrides use `nativeSelectOptionClass()` / `nativeSelectOptGroupClass()`.
30
+ *
31
+ * Design tokens used: --input, --background, --primary, --primary-foreground,
32
+ * --muted-foreground, --ring, --destructive.
33
+ */
34
+ import { cn } from '../lib/utils.ts';
35
+
36
+ export type NativeSelectSize = 'default' | 'sm';
37
+
38
+ // Auto-apply Canvas/CanvasText to every <option> on the page. Without
39
+ // this, an <option> with no explicit bg paints transparent on top of
40
+ // the browser-popup background; in dark mode (when color-scheme: dark
41
+ // is set on <html>) Chrome's popup is dark, the option's transparent
42
+ // bg lets the popup colour through, and the inherited text colour
43
+ // from the <select> matches that dark popup: the option disappears,
44
+ // only the focused/selected one stays visible because the browser
45
+ // overlays its own highlight on it.
46
+ //
47
+ // Original selector required the option to be inside a
48
+ // `.group/native-select` wrapper, on the assumption every user would
49
+ // follow the documented Usage block above. But it's easy to write a
50
+ // bare <select class=${nativeSelectClass()}> without the wrapper
51
+ // (legitimate when you don't need the chevron icon: the popover and
52
+ // hover-card docs examples both do this), in which case the rule
53
+ // never matched and the dropdown reverted to invisible-options.
54
+ // Broadening to `select option, select optgroup` makes the fix work
55
+ // everywhere the user has imported native-select, with no required
56
+ // wrapper. The Canvas/CanvasText pair is a safe default: they ARE
57
+ // the system colours the browser would have painted anyway when no
58
+ // rule applied; we just stop relying on inheritance to pull through.
59
+ // Selector specificity is 0,0,2 (two elements), so any user who
60
+ // genuinely needs custom <option> colours can override with a single
61
+ // class anywhere in their cascade (e.g. `.my-select option { ... }`
62
+ // at 0,1,2 wins).
63
+ //
64
+ // `nativeSelectOptionClass()` and `nativeSelectOptGroupClass()` stay
65
+ // exported for users who want to opt into the same colours via the
66
+ // class helper instead of the global rule. They emit the same
67
+ // `bg-[Canvas] text-[CanvasText]` Tailwind utilities: redundant if
68
+ // this stylesheet is installed, but harmless and matches the broader
69
+ // shadcn convention of "every part has a class helper".
70
+ const STYLES = `
71
+ select option,
72
+ select optgroup {
73
+ background-color: Canvas;
74
+ color: CanvasText;
75
+ }
76
+ `;
77
+
78
+ let installed = false;
79
+ export function installNativeSelectStyles(): void {
80
+ if (installed || typeof document === 'undefined') return;
81
+ if (document.getElementById('ui-native-select-styles')) {
82
+ installed = true;
83
+ return;
84
+ }
85
+ const style = document.createElement('style');
86
+ style.id = 'ui-native-select-styles';
87
+ style.textContent = STYLES;
88
+ document.head.appendChild(style);
89
+ installed = true;
90
+ }
91
+
92
+ if (typeof document !== 'undefined') installNativeSelectStyles();
93
+
94
+ export const nativeSelectWrapperClass = (): string =>
95
+ 'group/native-select relative w-fit has-[select:disabled]:opacity-50';
96
+
97
+ export function nativeSelectClass(): string {
98
+ return cn(
99
+ 'h-9 w-full min-w-0 appearance-none rounded-md border border-input bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed data-[size=sm]:h-8 data-[size=sm]:py-1 dark:bg-input/30 dark:hover:bg-input/50',
100
+ 'focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50',
101
+ 'aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40',
102
+ );
103
+ }
104
+
105
+ export const nativeSelectIconClass = (): string =>
106
+ 'pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 text-muted-foreground opacity-50 select-none';
107
+
108
+ /** Option / optgroup styling: forces themed background even in dark mode. */
109
+ export const nativeSelectOptionClass = (): string => 'bg-[Canvas] text-[CanvasText]';
110
+ export const nativeSelectOptGroupClass = (): string => 'bg-[Canvas] text-[CanvasText]';
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Pagination: page navigation. Tier-1 class helpers; compose with native
3
+ * `<nav>` + `<ul>` + `<li>` + `<a>`. Links route through `buttonClass()`
4
+ * so the visual matches the rest of the button vocabulary exactly.
5
+ *
6
+ * shadcn parity:
7
+ * Pagination → <nav aria-label="pagination" class=${paginationClass()}>
8
+ * PaginationContent → paginationContentClass()
9
+ * PaginationItem → <li> (no helper needed)
10
+ * PaginationLink → paginationLinkClass({ isActive, size })
11
+ * PaginationPrevious → paginationPreviousClass()
12
+ * PaginationNext → paginationNextClass()
13
+ * PaginationEllipsis → paginationEllipsisClass()
14
+ *
15
+ * Usage:
16
+ * <nav role="navigation" aria-label="pagination" class=${paginationClass()}>
17
+ * <ul class=${paginationContentClass()}>
18
+ * <li><a class=${paginationPreviousClass()} href="?page=1">‹ Previous</a></li>
19
+ * <li><a class=${paginationLinkClass({ isActive: false })} href="?page=2">2</a></li>
20
+ * <li><a class=${paginationLinkClass({ isActive: true })} aria-current="page">3</a></li>
21
+ * <li class=${paginationEllipsisClass()} aria-hidden="true">…</li>
22
+ * <li><a class=${paginationNextClass()} href="?page=4">Next ›</a></li>
23
+ * </ul>
24
+ * </nav>
25
+ *
26
+ * Design tokens used: inherited from buttonClass.
27
+ */
28
+ import { cn } from '../lib/utils.ts';
29
+ import { buttonClass, type ButtonSize } from './button.ts';
30
+
31
+ export const paginationClass = (): string => 'mx-auto flex w-full justify-center';
32
+
33
+ export const paginationContentClass = (): string => 'flex flex-row items-center gap-1';
34
+
35
+ export function paginationLinkClass(opts: { isActive?: boolean; size?: ButtonSize } = {}): string {
36
+ return cn(buttonClass({ variant: opts.isActive ? 'outline' : 'ghost', size: opts.size ?? 'icon' }));
37
+ }
38
+
39
+ export const paginationPreviousClass = (): string =>
40
+ cn(buttonClass({ variant: 'ghost', size: 'default' }), 'gap-1 px-2.5 sm:pl-2.5');
41
+
42
+ export const paginationNextClass = (): string =>
43
+ cn(buttonClass({ variant: 'ghost', size: 'default' }), 'gap-1 px-2.5 sm:pr-2.5');
44
+
45
+ export const paginationEllipsisClass = (): string => 'flex size-9 items-center justify-center';