@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,43 @@
1
+ /**
2
+ * Breadcrumb: semantic nav with breadcrumb list. Tier-1 class helpers;
3
+ * compose with native `<nav>` + `<ol>` + `<li>` + `<a>` / `<span>`.
4
+ *
5
+ * shadcn parity:
6
+ * Breadcrumb → <nav aria-label="breadcrumb" data-slot="breadcrumb">
7
+ * BreadcrumbList → breadcrumbListClass()
8
+ * BreadcrumbItem → breadcrumbItemClass()
9
+ * BreadcrumbLink → breadcrumbLinkClass()
10
+ * BreadcrumbPage → breadcrumbPageClass() (with aria-current="page")
11
+ * BreadcrumbSeparator → breadcrumbSeparatorClass() (aria-hidden + role="presentation")
12
+ * BreadcrumbEllipsis → breadcrumbEllipsisClass()
13
+ *
14
+ * Usage:
15
+ * <nav aria-label="breadcrumb" data-slot="breadcrumb">
16
+ * <ol class=${breadcrumbListClass()}>
17
+ * <li class=${breadcrumbItemClass()}>
18
+ * <a class=${breadcrumbLinkClass()} href="/">Home</a>
19
+ * </li>
20
+ * <li class=${breadcrumbSeparatorClass()} role="presentation" aria-hidden="true">
21
+ * <svg>›</svg>
22
+ * </li>
23
+ * <li class=${breadcrumbItemClass()}>
24
+ * <span class=${breadcrumbPageClass()} aria-current="page">Posts</span>
25
+ * </li>
26
+ * </ol>
27
+ * </nav>
28
+ *
29
+ * Design tokens used: --muted-foreground, --foreground.
30
+ */
31
+
32
+ export const breadcrumbListClass = (): string =>
33
+ 'flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5';
34
+
35
+ export const breadcrumbItemClass = (): string => 'inline-flex items-center gap-1.5';
36
+
37
+ export const breadcrumbLinkClass = (): string => 'transition-colors hover:text-foreground';
38
+
39
+ export const breadcrumbPageClass = (): string => 'font-normal text-foreground';
40
+
41
+ export const breadcrumbSeparatorClass = (): string => '[&>svg]:size-3.5';
42
+
43
+ export const breadcrumbEllipsisClass = (): string => 'flex size-9 items-center justify-center';
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Button: styled native `<button>`. Tier-1 class helper. Compose with a
3
+ * real `<button>` (or `<a>` for link-styled buttons) so form submission,
4
+ * focus, keyboard activation, and screen-reader semantics all "just work".
5
+ *
6
+ * shadcn parity:
7
+ * Button (variant: default | destructive | outline | secondary | ghost | link)
8
+ * (size: default | xs | sm | lg | icon | icon-xs | icon-sm | icon-lg)
9
+ * → buttonClass({ variant, size })
10
+ *
11
+ * Usage:
12
+ * <button class=${buttonClass()} type="submit">Save</button>
13
+ * <button class=${buttonClass({ variant: 'outline', size: 'sm' })}>Cancel</button>
14
+ * <button class=${buttonClass({ size: 'icon' })} aria-label="Settings">⚙</button>
15
+ * <a class=${buttonClass({ variant: 'link' })} href="/about">About</a>
16
+ *
17
+ * shadcn React's `asChild` (Slot) prop has no equivalent here: just call
18
+ * `buttonClass(...)` and spread the classes onto whatever element you want.
19
+ *
20
+ * Design tokens used: --primary, --primary-foreground, --destructive,
21
+ * --secondary, --secondary-foreground, --accent, --accent-foreground,
22
+ * --background, --input, --ring.
23
+ */
24
+ import { cn } from '../lib/utils.ts';
25
+
26
+ // cursor-pointer is on the BASE so every variant (default, outline,
27
+ // ghost, link, …) gets the right hover affordance. Native <button>
28
+ // defaults to the OS arrow cursor in Chromium and Firefox: fine for
29
+ // native chrome but unusual for app buttons; shadcn's modern Button
30
+ // has long since gravitated toward an explicit cursor-pointer in the
31
+ // real world (see the open issue shadcn-ui/ui#1791). disabled:pointer-
32
+ // events-none below already suppresses cursor on disabled buttons by
33
+ // virtue of the element not receiving pointer events at all.
34
+ const BASE =
35
+ "inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4";
36
+
37
+ const VARIANTS = {
38
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
39
+ destructive:
40
+ 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40',
41
+ outline:
42
+ 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
43
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
44
+ ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
45
+ link: 'text-primary underline-offset-4 hover:underline',
46
+ } as const;
47
+
48
+ const SIZES = {
49
+ default: 'h-9 px-4 py-2 has-[>svg]:px-3',
50
+ xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
51
+ sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
52
+ lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
53
+ icon: 'size-9',
54
+ 'icon-xs': "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
55
+ 'icon-sm': 'size-8',
56
+ 'icon-lg': 'size-10',
57
+ } as const;
58
+
59
+ export type ButtonVariant = keyof typeof VARIANTS;
60
+ export type ButtonSize = keyof typeof SIZES;
61
+
62
+ export interface ButtonClassOptions {
63
+ variant?: ButtonVariant;
64
+ size?: ButtonSize;
65
+ }
66
+
67
+ /** Compose Tailwind classes for a button. Stable shape: object-arg, both optional. */
68
+ export function buttonClass(opts: ButtonClassOptions = {}): string {
69
+ const variant = opts.variant ?? 'default';
70
+ const size = opts.size ?? 'default';
71
+ return cn(BASE, VARIANTS[variant], SIZES[size]);
72
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Card: visual container. Tier-1 class helpers; compose with `<div>`
3
+ * (or any element) for each subpart.
4
+ *
5
+ * shadcn parity:
6
+ * Card (size: default | sm) → cardClass({ size })
7
+ * CardHeader → cardHeaderClass()
8
+ * CardTitle → cardTitleClass()
9
+ * CardDescription → cardDescriptionClass()
10
+ * CardAction → cardActionClass()
11
+ * CardContent → cardContentClass()
12
+ * CardFooter → cardFooterClass()
13
+ *
14
+ * Usage:
15
+ * <div class=${cardClass()}>
16
+ * <div class=${cardHeaderClass()}>
17
+ * <div class=${cardTitleClass()}>Notifications</div>
18
+ * <div class=${cardDescriptionClass()}>You have 3 unread messages.</div>
19
+ * <div class=${cardActionClass()}>
20
+ * <button class=${buttonClass({ variant: 'ghost', size: 'sm' })}>Mark all read</button>
21
+ * </div>
22
+ * </div>
23
+ * <div class=${cardContentClass()}>…</div>
24
+ * <div class=${cardFooterClass()}>
25
+ * <button class=${buttonClass()}>Save</button>
26
+ * </div>
27
+ * </div>
28
+ *
29
+ * Design tokens used: --card, --card-foreground, --muted-foreground, --border.
30
+ */
31
+
32
+ export type CardSize = 'default' | 'sm';
33
+
34
+ /**
35
+ * Card root. shadcn ships `size?: "default" | "sm"` on Card across
36
+ * 14/15 style families (only new-york-v4 omits it). The class string
37
+ * uses `group/card` so the header / title / content / footer helpers
38
+ * can read the parent card's data-size and adjust their own padding
39
+ * + gap.
40
+ *
41
+ * USAGE: pass size to cardClass AND set data-size="<size>" on the
42
+ * same host element so the group-data-[size=...]/card child rules
43
+ * fire. Set `data-slot="card"` for shadcn parity.
44
+ *
45
+ * <div class=${cardClass({ size: 'sm' })} data-slot="card" data-size="sm">
46
+ * <div class=${cardHeaderClass()}>...</div>
47
+ * ...
48
+ * </div>
49
+ *
50
+ * Sizes:
51
+ * default: gap-6 / py-6 (shadcn new-york-v4 default)
52
+ * sm: gap-3 / py-3 (shadcn radix-nova + base-* defaults)
53
+ */
54
+ export const cardClass = (opts: { size?: CardSize } = {}): string => {
55
+ const size = opts.size ?? 'default';
56
+ const base =
57
+ 'group/card flex flex-col rounded-xl border bg-card text-card-foreground shadow-sm';
58
+ return size === 'sm' ? base + ' gap-3 py-3' : base + ' gap-6 py-6';
59
+ };
60
+
61
+ /**
62
+ * Card header: supports an optional `CardAction` slot via grid layout.
63
+ * group-data-[size=sm]/card rules pick up the compact layout when the
64
+ * root card carries data-size="sm".
65
+ */
66
+ export const cardHeaderClass = (): string =>
67
+ '@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 group-data-[size=sm]/card:px-4 group-data-[size=sm]/card:gap-1 group-data-[size=sm]/card:[.border-b]:pb-3';
68
+
69
+ /** Card title: heading text within the header. Smaller when card is data-size="sm". */
70
+ export const cardTitleClass = (): string =>
71
+ 'leading-none font-semibold group-data-[size=sm]/card:text-sm';
72
+
73
+ /** Card description: subdued caption beneath the title. */
74
+ export const cardDescriptionClass = (): string => 'text-sm text-muted-foreground';
75
+
76
+ /** Card action: right-aligned controls inside the header (matches shadcn CardAction). */
77
+ export const cardActionClass = (): string =>
78
+ 'col-start-2 row-span-2 row-start-1 self-start justify-self-end';
79
+
80
+ /** Card content: the main body region. Tighter padding when card is data-size="sm". */
81
+ export const cardContentClass = (): string =>
82
+ 'px-6 group-data-[size=sm]/card:px-4';
83
+
84
+ /** Card footer: trailing controls or actions. Tighter padding when card is data-size="sm". */
85
+ export const cardFooterClass = (): string =>
86
+ 'flex items-center px-6 [.border-t]:pt-6 group-data-[size=sm]/card:px-4 group-data-[size=sm]/card:[.border-t]:pt-3';
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Checkbox: styled native `<input type="checkbox">`. Tier-1 class
3
+ * helper. Uses `appearance: none` + an inline-SVG `background-image` for
4
+ * the checkmark when `:checked`, so it remains a real form control
5
+ * (participates in `<form>` submission, no ElementInternals required).
6
+ *
7
+ * shadcn parity:
8
+ * Checkbox → checkboxClass() (visual: size-4, rounded, primary fill
9
+ * when checked, inset shadow, focus ring;
10
+ * bypasses Radix)
11
+ *
12
+ * Usage:
13
+ * <input type="checkbox" name="terms" id="terms" class=${checkboxClass()}>
14
+ * <label class=${labelClass()} for="terms">I accept the terms</label>
15
+ *
16
+ * Design tokens used: --input, --primary, --primary-foreground, --background,
17
+ * --ring, --destructive.
18
+ */
19
+ import { cn } from '../lib/utils.ts';
20
+
21
+ // Inline SVG checkmark used as background when :checked. Encoded for url().
22
+ //
23
+ // Two variants because shadcn flips `--primary` (and therefore the checked-
24
+ // state box colour) between light + dark: in light mode the box is dark
25
+ // (`oklch(0.205 0 0)`) and the checkmark needs to be light (white); in dark
26
+ // mode the box is light (`oklch(0.922 0 0)`) and the checkmark needs to be
27
+ // dark (black). `currentColor` inside a data:url SVG does NOT inherit from
28
+ // the host element when used as a background-image: that's a long-
29
+ // standing browser limitation: and pseudo-elements (::before/::after) on
30
+ // `<input>` aren't reliable cross-browser, so the simplest correct fix is
31
+ // to ship two SVGs and toggle them via a theme selector.
32
+ const CHECKMARK_LIGHT =
33
+ 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 20 20\' fill=\'white\'><path d=\'M16.704 5.293a1 1 0 010 1.414l-7.001 7a1 1 0 01-1.414 0l-3-3a1 1 0 011.414-1.414L9 11.586l6.29-6.293a1 1 0 011.414 0z\'/></svg>")';
34
+ const CHECKMARK_DARK =
35
+ 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 20 20\' fill=\'black\'><path d=\'M16.704 5.293a1 1 0 010 1.414l-7.001 7a1 1 0 01-1.414 0l-3-3a1 1 0 011.414-1.414L9 11.586l6.29-6.293a1 1 0 011.414 0z\'/></svg>")';
36
+
37
+ const CHECKBOX_CLASS =
38
+ 'peer size-4 shrink-0 appearance-none rounded-[4px] border border-input bg-transparent shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 checked:border-primary checked:bg-primary checked:bg-no-repeat checked:bg-center dark:bg-input/30 dark:aria-invalid:ring-destructive/40';
39
+
40
+ // Inject style once for the checkmark background-image when :checked.
41
+ //
42
+ // Theme selectors are kept in sync with what shadcn's components.json
43
+ // scaffolds for theme switching: explicit `[data-theme='dark']` /
44
+ // `.dark` on `<html>` (set by toggle scripts), AND `prefers-color-
45
+ // scheme: dark` gated by `:not([data-theme='light']):not(.light)` so
46
+ // an explicit-light toggle still wins over the OS preference. Matches
47
+ // the same pattern used elsewhere in the registry.
48
+ const STYLES = `
49
+ input[type="checkbox"][data-slot="checkbox"]:checked {
50
+ background-image: ${CHECKMARK_LIGHT};
51
+ background-size: 80%;
52
+ }
53
+ input[type="checkbox"][data-slot="checkbox"]:indeterminate {
54
+ background-color: var(--primary);
55
+ background-image: linear-gradient(to right, white, white);
56
+ background-size: 60% 2px;
57
+ background-repeat: no-repeat;
58
+ background-position: center;
59
+ }
60
+ @media (prefers-color-scheme: dark) {
61
+ :root:not([data-theme='light']):not(.light) input[type="checkbox"][data-slot="checkbox"]:checked {
62
+ background-image: ${CHECKMARK_DARK};
63
+ }
64
+ :root:not([data-theme='light']):not(.light) input[type="checkbox"][data-slot="checkbox"]:indeterminate {
65
+ background-image: linear-gradient(to right, black, black);
66
+ }
67
+ }
68
+ :root[data-theme='dark'] input[type="checkbox"][data-slot="checkbox"]:checked,
69
+ :root.dark input[type="checkbox"][data-slot="checkbox"]:checked {
70
+ background-image: ${CHECKMARK_DARK};
71
+ }
72
+ :root[data-theme='dark'] input[type="checkbox"][data-slot="checkbox"]:indeterminate,
73
+ :root.dark input[type="checkbox"][data-slot="checkbox"]:indeterminate {
74
+ background-image: linear-gradient(to right, black, black);
75
+ }
76
+ `;
77
+
78
+ let installed = false;
79
+ export function installCheckboxStyles(): void {
80
+ if (installed || typeof document === 'undefined') return;
81
+ if (document.getElementById('ui-checkbox-styles')) {
82
+ installed = true;
83
+ return;
84
+ }
85
+ const style = document.createElement('style');
86
+ style.id = 'ui-checkbox-styles';
87
+ style.textContent = STYLES;
88
+ document.head.appendChild(style);
89
+ installed = true;
90
+ }
91
+
92
+ if (typeof document !== 'undefined') installCheckboxStyles();
93
+
94
+ /** Tailwind classes for a styled native `<input type="checkbox">`. Add `data-slot="checkbox"` for the checkmark style to apply. */
95
+ export function checkboxClass(): string {
96
+ return cn(CHECKBOX_CLASS);
97
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Collapsible: togglable content panel built on native <details>/<summary>.
3
+ *
4
+ * Tier-1 (no custom element). The browser handles open/close state,
5
+ * keyboard activation (Enter / Space on <summary>), focus management,
6
+ * and disclosure-widget accessibility, nothing to ship in JS.
7
+ *
8
+ * shadcn parity:
9
+ * Collapsible → <details class=${collapsibleClass()}>
10
+ * CollapsibleTrigger → <summary class=${collapsibleTriggerClass()}>
11
+ * CollapsibleContent → <div class=${collapsibleContentClass()}>
12
+ *
13
+ * Usage:
14
+ * <details class=${collapsibleClass()}>
15
+ * <summary class=${collapsibleTriggerClass()}>
16
+ * Show details
17
+ * <svg class="size-4 transition-transform group-open:rotate-180">…</svg>
18
+ * </summary>
19
+ * <div class=${collapsibleContentClass()}>
20
+ * Hidden until <summary> is clicked, Enter / Space pressed, or the
21
+ * <details> element's `open` property is set via JS.
22
+ * </div>
23
+ * </details>
24
+ *
25
+ * Initial state: add `open` on <details> to render expanded on first
26
+ * paint. Programmatic toggling: `el.open = true | false`. Migrated from
27
+ * the prior <ui-collapsible> custom element; the trigger class hides
28
+ * the native disclosure marker so callers can render their own chevron.
29
+ *
30
+ * Design tokens used: --border, --ring, --foreground.
31
+ */
32
+
33
+ /**
34
+ * Root: marks the disclosure widget as a `group` so descendants can use
35
+ * Tailwind's `group-open:` variant to react to the `[open]` attribute
36
+ * (which `<details>` sets natively). No visual styling of its own.
37
+ */
38
+ export const collapsibleClass = (): string => 'group';
39
+
40
+ /**
41
+ * Trigger: hides the native ::marker (and the WebKit -details-marker shim)
42
+ * so the disclosure triangle does not appear; callers wrap their own
43
+ * chevron icon and rotate it on open via `group-open:rotate-180`.
44
+ *
45
+ * `disabled: true` returns the visual disabled state. Native <details>
46
+ * has no `disabled` attribute, so for full keyboard prevention add the
47
+ * standard `inert` attribute on the <details> element. shadcn's React
48
+ * `disabled` prop combines both visual and behavior; we split them.
49
+ */
50
+ export const collapsibleTriggerClass = (opts: { disabled?: boolean } = {}): string => {
51
+ const base = 'flex w-full cursor-pointer list-none items-center justify-between gap-2 rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-2 focus-visible:ring-ring/50 marker:hidden [&::-webkit-details-marker]:hidden';
52
+ if (opts.disabled) return `${base} pointer-events-none cursor-not-allowed opacity-50`;
53
+ return base;
54
+ };
55
+
56
+ /**
57
+ * Content: <details> already hides children other than <summary> when
58
+ * not [open], so this is purely typographic spacing. No display rules.
59
+ */
60
+ export const collapsibleContentClass = (): string => 'text-sm';