@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.
- package/package.json +4 -3
- package/packages/registry/README.md +35 -0
- package/packages/registry/components/accordion.ts +74 -0
- package/packages/registry/components/alert-dialog.ts +359 -0
- package/packages/registry/components/alert.ts +51 -0
- package/packages/registry/components/aspect-ratio.ts +22 -0
- package/packages/registry/components/avatar.ts +52 -0
- package/packages/registry/components/badge.ts +40 -0
- package/packages/registry/components/breadcrumb.ts +43 -0
- package/packages/registry/components/button.ts +72 -0
- package/packages/registry/components/card.ts +86 -0
- package/packages/registry/components/checkbox.ts +97 -0
- package/packages/registry/components/collapsible.ts +60 -0
- package/packages/registry/components/dialog.ts +378 -0
- package/packages/registry/components/dropdown-menu.ts +607 -0
- package/packages/registry/components/hover-card.ts +175 -0
- package/packages/registry/components/input.ts +36 -0
- package/packages/registry/components/kbd.ts +25 -0
- package/packages/registry/components/label.ts +23 -0
- package/packages/registry/components/native-select.ts +110 -0
- package/packages/registry/components/pagination.ts +45 -0
- package/packages/registry/components/popover.ts +260 -0
- package/packages/registry/components/progress.ts +46 -0
- package/packages/registry/components/radio-group.ts +113 -0
- package/packages/registry/components/separator.ts +30 -0
- package/packages/registry/components/skeleton.ts +16 -0
- package/packages/registry/components/sonner.ts +240 -0
- package/packages/registry/components/switch.ts +52 -0
- package/packages/registry/components/table.ts +58 -0
- package/packages/registry/components/tabs.ts +271 -0
- package/packages/registry/components/textarea.ts +27 -0
- package/packages/registry/components/toggle-group.ts +236 -0
- package/packages/registry/components/toggle.ts +118 -0
- package/packages/registry/components/tooltip.ts +195 -0
- package/packages/registry/lib/utils.ts +241 -0
- package/packages/registry/package.json +7 -0
- package/packages/registry/registry.json +479 -0
- package/packages/registry/themes/base-colors.js +193 -0
- 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';
|