@shadow-js/ui 0.0.0-stage → 1.0.0
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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +598 -2
- package/dist/cli.mjs +2293 -0
- package/dist/dev/cli/schema.d.ts +73 -0
- package/dist/dev/index.d.ts +6 -0
- package/dist/dev/index.js +5 -0
- package/dist/dev/locales/ar.d.ts +141 -0
- package/dist/dev/locales/ar.js +128 -0
- package/dist/dev/locales/ar.js.map +1 -0
- package/dist/dev/locales/en.d.ts +141 -0
- package/dist/dev/locales/en.js +128 -0
- package/dist/dev/locales/en.js.map +1 -0
- package/dist/dev/registry/utils/utils.d.ts +29 -0
- package/dist/dev/registry/utils/utils.js +27 -0
- package/dist/dev/registry/utils/utils.js.map +1 -0
- package/dist/dev/theme/theme-logic.d.ts +187 -0
- package/dist/dev/theme/theme-logic.js +184 -0
- package/dist/dev/theme/theme-logic.js.map +1 -0
- package/dist/dev/theme/theme-provider.d.ts +90 -0
- package/dist/dev/theme/theme-provider.js +150 -0
- package/dist/dev/theme/theme-provider.js.map +1 -0
- package/dist/prod/index.js +1 -0
- package/dist/prod/locales/ar.js +2 -0
- package/dist/prod/locales/ar.js.map +1 -0
- package/dist/prod/locales/en.js +2 -0
- package/dist/prod/locales/en.js.map +1 -0
- package/dist/prod/registry/utils/utils.js +2 -0
- package/dist/prod/registry/utils/utils.js.map +1 -0
- package/dist/prod/theme/theme-logic.js +2 -0
- package/dist/prod/theme/theme-logic.js.map +1 -0
- package/dist/prod/theme/theme-provider.js +2 -0
- package/dist/prod/theme/theme-provider.js.map +1 -0
- package/package.json +102 -4
- package/registry/accordion/accordion.tsx +210 -0
- package/registry/accordion/item.json +4 -0
- package/registry/alert/alert.tsx +200 -0
- package/registry/alert/item.json +4 -0
- package/registry/alert-dialog/alert-dialog.tsx +530 -0
- package/registry/alert-dialog/item.json +4 -0
- package/registry/aspect-ratio/aspect-ratio.tsx +71 -0
- package/registry/aspect-ratio/item.json +4 -0
- package/registry/attachment/attachment.tsx +459 -0
- package/registry/attachment/item.json +4 -0
- package/registry/avatar/avatar.tsx +385 -0
- package/registry/avatar/item.json +4 -0
- package/registry/badge/badge.tsx +108 -0
- package/registry/badge/item.json +4 -0
- package/registry/breadcrumb/breadcrumb.tsx +354 -0
- package/registry/breadcrumb/item.json +4 -0
- package/registry/bubble/bubble.tsx +260 -0
- package/registry/bubble/item.json +4 -0
- package/registry/button/button.tsx +125 -0
- package/registry/button/item.json +4 -0
- package/registry/button-group/button-group.tsx +189 -0
- package/registry/button-group/item.json +4 -0
- package/registry/calendar/calendar.tsx +348 -0
- package/registry/calendar/item.json +4 -0
- package/registry/card/card.tsx +311 -0
- package/registry/card/item.json +4 -0
- package/registry/carousel/carousel.tsx +851 -0
- package/registry/carousel/item.json +4 -0
- package/registry/chart/chart.tsx +1956 -0
- package/registry/chart/item.json +4 -0
- package/registry/chart-scale/chart-scale.ts +670 -0
- package/registry/chart-scale/item.json +5 -0
- package/registry/checkbox/checkbox.tsx +79 -0
- package/registry/checkbox/item.json +4 -0
- package/registry/code-block/code-block.tsx +470 -0
- package/registry/code-block/item.json +4 -0
- package/registry/collapsible/collapsible.tsx +117 -0
- package/registry/collapsible/item.json +4 -0
- package/registry/combobox/combobox.tsx +977 -0
- package/registry/combobox/item.json +4 -0
- package/registry/command/command.tsx +757 -0
- package/registry/command/item.json +4 -0
- package/registry/context-menu/context-menu.tsx +697 -0
- package/registry/context-menu/item.json +4 -0
- package/registry/data-table/data-table.tsx +437 -0
- package/registry/data-table/item.json +4 -0
- package/registry/date-picker/date-picker.tsx +349 -0
- package/registry/date-picker/item.json +4 -0
- package/registry/dialog/dialog.tsx +412 -0
- package/registry/dialog/item.json +4 -0
- package/registry/direction/direction.tsx +88 -0
- package/registry/direction/item.json +4 -0
- package/registry/drawer/drawer.tsx +787 -0
- package/registry/drawer/item.json +4 -0
- package/registry/dropdown-menu/dropdown-menu.tsx +692 -0
- package/registry/dropdown-menu/item.json +4 -0
- package/registry/empty/empty.tsx +240 -0
- package/registry/empty/item.json +4 -0
- package/registry/field/field.tsx +624 -0
- package/registry/field/item.json +4 -0
- package/registry/form/form.tsx +1112 -0
- package/registry/form/item.json +4 -0
- package/registry/hover-card/hover-card.tsx +144 -0
- package/registry/hover-card/item.json +4 -0
- package/registry/icons/icons.tsx +1691 -0
- package/registry/icons/item.json +4 -0
- package/registry/input/input.tsx +43 -0
- package/registry/input/item.json +4 -0
- package/registry/input-group/input-group.tsx +315 -0
- package/registry/input-group/item.json +4 -0
- package/registry/input-otp/input-otp.tsx +329 -0
- package/registry/input-otp/item.json +4 -0
- package/registry/item/item.json +4 -0
- package/registry/item/item.tsx +439 -0
- package/registry/kbd/item.json +4 -0
- package/registry/kbd/kbd.tsx +65 -0
- package/registry/label/item.json +4 -0
- package/registry/label/label.tsx +60 -0
- package/registry/marker/item.json +4 -0
- package/registry/marker/marker.tsx +175 -0
- package/registry/menubar/item.json +4 -0
- package/registry/menubar/menubar.tsx +694 -0
- package/registry/message/item.json +4 -0
- package/registry/message/message.tsx +280 -0
- package/registry/message-scroller/item.json +4 -0
- package/registry/message-scroller/message-scroller.tsx +262 -0
- package/registry/native-select/item.json +4 -0
- package/registry/native-select/native-select.tsx +113 -0
- package/registry/navigation-menu/item.json +4 -0
- package/registry/navigation-menu/navigation-menu.tsx +362 -0
- package/registry/pagination/item.json +4 -0
- package/registry/pagination/pagination.tsx +460 -0
- package/registry/password-input/item.json +5 -0
- package/registry/password-input/password-input.tsx +494 -0
- package/registry/popover/item.json +4 -0
- package/registry/popover/popover.tsx +284 -0
- package/registry/progress/item.json +4 -0
- package/registry/progress/progress.tsx +190 -0
- package/registry/questionnaire/item.json +4 -0
- package/registry/questionnaire/questionnaire.tsx +569 -0
- package/registry/radio-group/item.json +4 -0
- package/registry/radio-group/radio-group.tsx +101 -0
- package/registry/resizable/item.json +4 -0
- package/registry/resizable/resizable.tsx +455 -0
- package/registry/scroll-area/item.json +4 -0
- package/registry/scroll-area/scroll-area.tsx +149 -0
- package/registry/select/item.json +4 -0
- package/registry/select/select.tsx +571 -0
- package/registry/separator/item.json +4 -0
- package/registry/separator/separator.tsx +52 -0
- package/registry/sheet/item.json +4 -0
- package/registry/sheet/sheet.tsx +476 -0
- package/registry/sidebar/item.json +4 -0
- package/registry/sidebar/sidebar.tsx +1870 -0
- package/registry/skeleton/item.json +4 -0
- package/registry/skeleton/skeleton.tsx +47 -0
- package/registry/slider/item.json +4 -0
- package/registry/slider/slider.tsx +149 -0
- package/registry/spinner/item.json +4 -0
- package/registry/spinner/spinner.tsx +51 -0
- package/registry/switch/item.json +4 -0
- package/registry/switch/switch.tsx +93 -0
- package/registry/table/item.json +4 -0
- package/registry/table/table.tsx +373 -0
- package/registry/tabs/item.json +4 -0
- package/registry/tabs/tabs.tsx +209 -0
- package/registry/textarea/item.json +4 -0
- package/registry/textarea/textarea.tsx +51 -0
- package/registry/toast/item.json +4 -0
- package/registry/toast/toast.tsx +949 -0
- package/registry/toggle/item.json +4 -0
- package/registry/toggle/toggle.tsx +110 -0
- package/registry/toggle-group/item.json +4 -0
- package/registry/toggle-group/toggle-group.tsx +189 -0
- package/registry/tooltip/item.json +4 -0
- package/registry/tooltip/tooltip.tsx +169 -0
- package/registry/trace/item.json +4 -0
- package/registry/trace/trace.tsx +356 -0
- package/registry/ui-strings/item.json +5 -0
- package/registry/ui-strings/ui-strings.tsx +454 -0
- package/registry/use-highlighted-code/item.json +5 -0
- package/registry/use-highlighted-code/use-highlighted-code.tsx +145 -0
- package/registry/use-mobile/item.json +5 -0
- package/registry/use-mobile/use-mobile.ts +46 -0
- package/registry/utils/item.json +5 -0
- package/registry/utils/utils.ts +38 -0
- package/registry.json +3274 -0
- package/styles/animations.css +321 -0
- package/styles/theme-core.css +55 -0
- package/styles/theme.css +12 -0
- package/styles/themes/gray.css +69 -0
- package/styles/themes/neutral.css +69 -0
- package/styles/themes/slate.css +69 -0
- package/styles/themes/stone.css +69 -0
- package/styles/themes/zinc.css +69 -0
- package/styles/typeset.css +251 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { splitProps } from "@shadow-js/core";
|
|
2
|
+
import { Polymorphic, type PolymorphicProps, type PolymorphicTarget } from "@shadow-js/primitives";
|
|
3
|
+
import type { JSX } from "@shadow-js/web";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
import { type ClassValue, cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The class list of a button for each `variant` and `size`. Use it to style a link or any other
|
|
9
|
+
* element like a button.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* import { buttonVariants } from "@/components/ui/button";
|
|
14
|
+
*
|
|
15
|
+
* export function DocsLink() {
|
|
16
|
+
* return (
|
|
17
|
+
* <a class={buttonVariants({ variant: "outline", size: "sm" })} href="/docs">
|
|
18
|
+
* Docs
|
|
19
|
+
* </a>
|
|
20
|
+
* );
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
* @since 1.0.0
|
|
24
|
+
* @see {@link Button}
|
|
25
|
+
*/
|
|
26
|
+
const buttonVariants = cva(
|
|
27
|
+
"inline-flex shrink-0 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-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 forced-colors:disabled:opacity-100 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-transparent",
|
|
28
|
+
{
|
|
29
|
+
variants: {
|
|
30
|
+
variant: {
|
|
31
|
+
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
|
32
|
+
destructive:
|
|
33
|
+
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
|
|
34
|
+
outline:
|
|
35
|
+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
|
36
|
+
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
|
37
|
+
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
38
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
39
|
+
},
|
|
40
|
+
size: {
|
|
41
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
42
|
+
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
43
|
+
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
|
44
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
45
|
+
icon: "size-9",
|
|
46
|
+
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
|
47
|
+
"icon-sm": "size-8",
|
|
48
|
+
"icon-lg": "size-10",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
defaultVariants: {
|
|
52
|
+
variant: "default",
|
|
53
|
+
size: "default",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
/** The props as the implementation sees them: the typed own props plus whatever the target takes. */
|
|
59
|
+
type Loose = VariantProps<typeof buttonVariants> & {
|
|
60
|
+
readonly as?: PolymorphicTarget;
|
|
61
|
+
readonly class?: ClassValue;
|
|
62
|
+
readonly [prop: string]: unknown;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Props of {@link Button}: every attribute of the chosen element plus `variant` and `size`. The
|
|
67
|
+
* `variant` is `"default"` (the default), `"destructive"`, `"outline"`, `"secondary"`, `"ghost"` or
|
|
68
|
+
* `"link"`. The `size` is `"default"` (the default), `"xs"`, `"sm"`, `"lg"`, `"icon"`, `"icon-xs"`,
|
|
69
|
+
* `"icon-sm"` or `"icon-lg"`. The `as` prop picks the element (a `button` by default).
|
|
70
|
+
* @since 1.0.0
|
|
71
|
+
* @see {@link Button}
|
|
72
|
+
*/
|
|
73
|
+
export type ButtonProps<T extends PolymorphicTarget = "button"> = PolymorphicProps<
|
|
74
|
+
"button",
|
|
75
|
+
VariantProps<typeof buttonVariants>,
|
|
76
|
+
T
|
|
77
|
+
>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A button. Pick the look with `variant` and the height with `size`. Use `as` to render another
|
|
81
|
+
* element, for example a link: `<Button as="a" href="/docs">Docs</Button>`.
|
|
82
|
+
*
|
|
83
|
+
* Keyboard and ARIA: the default element is a native `button`, so Enter and Space activate it and
|
|
84
|
+
* it takes focus with Tab. `disabled` is forwarded to the element, which then ignores clicks. The
|
|
85
|
+
* button's accessible name is its text, so an icon-only button (`size="icon"`) needs an
|
|
86
|
+
* `aria-label`. The component adds no ARIA attributes of its own.
|
|
87
|
+
*
|
|
88
|
+
* Compared with shadcn/ui: `as` replaces `asChild`, `class` replaces `className`, and the element
|
|
89
|
+
* carries `data-slot="button"`. The props are typed from `@shadow-js/primitives`.
|
|
90
|
+
*
|
|
91
|
+
* @param props - The button props: `variant`, `size`, `as`, `children`, and the attributes of the
|
|
92
|
+
* element (for example `onClick` and `disabled`).
|
|
93
|
+
* @returns The button element.
|
|
94
|
+
* @example
|
|
95
|
+
* ```tsx
|
|
96
|
+
* import { Button } from "@/components/ui/button";
|
|
97
|
+
*
|
|
98
|
+
* export function SaveButton() {
|
|
99
|
+
* const save = (): void => {
|
|
100
|
+
* console.log("saved");
|
|
101
|
+
* };
|
|
102
|
+
* return (
|
|
103
|
+
* <Button variant="outline" size="sm" onClick={save}>
|
|
104
|
+
* Save
|
|
105
|
+
* </Button>
|
|
106
|
+
* );
|
|
107
|
+
* }
|
|
108
|
+
* ```
|
|
109
|
+
* @since 1.0.0
|
|
110
|
+
* @see {@link ButtonProps}
|
|
111
|
+
* @see {@link buttonVariants}
|
|
112
|
+
*/
|
|
113
|
+
function Button<T extends PolymorphicTarget = "button">(props: ButtonProps<T>): JSX.Element {
|
|
114
|
+
const [local, rest] = splitProps(props as unknown as Loose, ["as", "class", "variant", "size"]);
|
|
115
|
+
return (
|
|
116
|
+
<Polymorphic
|
|
117
|
+
as={local.as ?? "button"}
|
|
118
|
+
data-slot="button"
|
|
119
|
+
class={cn(buttonVariants({ variant: local.variant, size: local.size }), local.class)}
|
|
120
|
+
{...rest}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { splitProps } from "@shadow-js/core";
|
|
2
|
+
import { Polymorphic, type PolymorphicProps, type PolymorphicTarget } from "@shadow-js/primitives";
|
|
3
|
+
import type { ComponentProps, JSX } from "@shadow-js/web";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
import { type ClassValue, cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
const groupVariants = cva(
|
|
8
|
+
"flex w-fit items-stretch [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
orientation: {
|
|
12
|
+
horizontal:
|
|
13
|
+
"[&>*:not(:first-child)]:rounded-s-none [&>*:not(:first-child)]:border-s-0 [&>*:not(:last-child)]:rounded-e-none",
|
|
14
|
+
vertical:
|
|
15
|
+
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: { orientation: "horizontal" },
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Props of {@link ButtonGroup}: the attributes of a `div` plus `orientation`. The `orientation` is
|
|
24
|
+
* `"horizontal"` (the default) or `"vertical"`.
|
|
25
|
+
* @since 1.0.0
|
|
26
|
+
* @see {@link ButtonGroup}
|
|
27
|
+
*/
|
|
28
|
+
export type ButtonGroupProps = ComponentProps<"div"> & VariantProps<typeof groupVariants>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Buttons (or inputs) that sit together as one control: the inner corners are square and the
|
|
32
|
+
* borders meet. Horizontal by default; `orientation="vertical"` stacks them.
|
|
33
|
+
*
|
|
34
|
+
* Keyboard and ARIA: the group is a `div` with `role="group"`. It adds no keys, so each button
|
|
35
|
+
* keeps its own keys (Tab moves between them). Give the group an `aria-label` that names the
|
|
36
|
+
* controls, because a group without a name is announced without one. A focused child is raised
|
|
37
|
+
* above its neighbors so its focus ring shows.
|
|
38
|
+
*
|
|
39
|
+
* Compared with shadcn/ui: `class` replaces `className`, and the element carries
|
|
40
|
+
* `data-slot="button-group"`.
|
|
41
|
+
*
|
|
42
|
+
* @param props - The group props: `orientation`, `children`, and the `div` attributes.
|
|
43
|
+
* @returns The group element.
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* import { Button } from "@/components/ui/button";
|
|
47
|
+
* import { ButtonGroup } from "@/components/ui/button-group";
|
|
48
|
+
*
|
|
49
|
+
* export function Alignment() {
|
|
50
|
+
* return (
|
|
51
|
+
* <ButtonGroup aria-label="Text alignment">
|
|
52
|
+
* <Button variant="outline">Left</Button>
|
|
53
|
+
* <Button variant="outline">Center</Button>
|
|
54
|
+
* <Button variant="outline">Right</Button>
|
|
55
|
+
* </ButtonGroup>
|
|
56
|
+
* );
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
* @since 1.0.0
|
|
60
|
+
* @see {@link ButtonGroupProps}
|
|
61
|
+
* @see {@link ButtonGroupSeparator}
|
|
62
|
+
* @see {@link ButtonGroupText}
|
|
63
|
+
*/
|
|
64
|
+
function ButtonGroup(props: ButtonGroupProps): JSX.Element {
|
|
65
|
+
const [local, rest] = splitProps(props, ["class", "orientation"]);
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
role="group"
|
|
69
|
+
data-slot="button-group"
|
|
70
|
+
data-orientation={local.orientation ?? "horizontal"}
|
|
71
|
+
class={cn(groupVariants({ orientation: local.orientation }), local.class)}
|
|
72
|
+
{...rest}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A divider between two buttons of a group. It is a vertical separator by default, so assistive
|
|
79
|
+
* technology announces it as a separator; `orientation="horizontal"` draws a line between the
|
|
80
|
+
* buttons of a vertical group.
|
|
81
|
+
*
|
|
82
|
+
* Keyboard and ARIA: it has `role="separator"`, `aria-orientation` set to its orientation, and no
|
|
83
|
+
* keys.
|
|
84
|
+
*
|
|
85
|
+
* @param props - The separator props: `orientation` (`"vertical"` by default) and the `div`
|
|
86
|
+
* attributes.
|
|
87
|
+
* @returns The separator element.
|
|
88
|
+
* @example
|
|
89
|
+
* ```tsx
|
|
90
|
+
* import { Button } from "@/components/ui/button";
|
|
91
|
+
* import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group";
|
|
92
|
+
*
|
|
93
|
+
* export function Split() {
|
|
94
|
+
* return (
|
|
95
|
+
* <ButtonGroup aria-label="Save options">
|
|
96
|
+
* <Button variant="outline">Save</Button>
|
|
97
|
+
* <ButtonGroupSeparator />
|
|
98
|
+
* <Button variant="outline">More</Button>
|
|
99
|
+
* </ButtonGroup>
|
|
100
|
+
* );
|
|
101
|
+
* }
|
|
102
|
+
* ```
|
|
103
|
+
* @since 1.0.0
|
|
104
|
+
* @see {@link ButtonGroup}
|
|
105
|
+
*/
|
|
106
|
+
function ButtonGroupSeparator(
|
|
107
|
+
props: ComponentProps<"div"> & { readonly orientation?: "horizontal" | "vertical" | undefined },
|
|
108
|
+
): JSX.Element {
|
|
109
|
+
const [local, rest] = splitProps(props, ["class", "orientation"]);
|
|
110
|
+
const orientation = (): "horizontal" | "vertical" => local.orientation ?? "vertical";
|
|
111
|
+
return (
|
|
112
|
+
<div
|
|
113
|
+
role="separator"
|
|
114
|
+
aria-orientation={orientation()}
|
|
115
|
+
data-orientation={orientation()}
|
|
116
|
+
data-slot="button-group-separator"
|
|
117
|
+
class={cn(
|
|
118
|
+
"relative !m-0 bg-input",
|
|
119
|
+
orientation() === "horizontal" ? "h-px w-full" : "w-px self-stretch",
|
|
120
|
+
local.class,
|
|
121
|
+
)}
|
|
122
|
+
{...rest}
|
|
123
|
+
/>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Props of {@link ButtonGroupText}: the attributes of the rendered element, and `as` to change it.
|
|
129
|
+
* @since 1.0.0
|
|
130
|
+
* @see {@link ButtonGroupText}
|
|
131
|
+
*/
|
|
132
|
+
export type ButtonGroupTextProps<T extends PolymorphicTarget = "div"> = PolymorphicProps<
|
|
133
|
+
"div",
|
|
134
|
+
object,
|
|
135
|
+
T
|
|
136
|
+
>;
|
|
137
|
+
|
|
138
|
+
/** The props as the implementation sees them: the typed own props plus whatever the target takes. */
|
|
139
|
+
type TextLoose = {
|
|
140
|
+
/** The element to render instead of a `div`. */
|
|
141
|
+
readonly as?: PolymorphicTarget;
|
|
142
|
+
/** Classes added to the text's own classes (merged with tailwind-merge). */
|
|
143
|
+
readonly class?: ClassValue;
|
|
144
|
+
readonly [prop: string]: unknown;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Text inside a group, styled like a button without being one. Use it for a label such as "Sort
|
|
149
|
+
* by" next to the buttons. It has no role and no keys. `as` renders another element (a link or a
|
|
150
|
+
* label), where shadcn uses `render`.
|
|
151
|
+
*
|
|
152
|
+
* @param props - The text props: `as` (`"div"` by default), `children`, and the attributes of the
|
|
153
|
+
* rendered element.
|
|
154
|
+
* @returns The text element.
|
|
155
|
+
* @example
|
|
156
|
+
* ```tsx
|
|
157
|
+
* import { Button } from "@/components/ui/button";
|
|
158
|
+
* import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group";
|
|
159
|
+
*
|
|
160
|
+
* export function SortControl() {
|
|
161
|
+
* return (
|
|
162
|
+
* <ButtonGroup aria-label="Sort">
|
|
163
|
+
* <ButtonGroupText>Sort by</ButtonGroupText>
|
|
164
|
+
* <Button variant="outline">Date</Button>
|
|
165
|
+
* </ButtonGroup>
|
|
166
|
+
* );
|
|
167
|
+
* }
|
|
168
|
+
* ```
|
|
169
|
+
* @since 1.0.0
|
|
170
|
+
* @see {@link ButtonGroup}
|
|
171
|
+
*/
|
|
172
|
+
function ButtonGroupText<T extends PolymorphicTarget = "div">(
|
|
173
|
+
props: ButtonGroupTextProps<T>,
|
|
174
|
+
): JSX.Element {
|
|
175
|
+
const [local, rest] = splitProps(props as unknown as TextLoose, ["as", "class"]);
|
|
176
|
+
return (
|
|
177
|
+
<Polymorphic
|
|
178
|
+
as={local.as ?? "div"}
|
|
179
|
+
data-slot="button-group-text"
|
|
180
|
+
class={cn(
|
|
181
|
+
"flex items-center gap-2 rounded-md border bg-muted px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
|
182
|
+
local.class,
|
|
183
|
+
)}
|
|
184
|
+
{...rest}
|
|
185
|
+
/>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export { ButtonGroup, ButtonGroupSeparator, ButtonGroupText };
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { splitProps } from "@shadow-js/core";
|
|
2
|
+
import {
|
|
3
|
+
type CalendarDate,
|
|
4
|
+
Calendar as CalendarPrimitive,
|
|
5
|
+
type CalendarProps as CalendarPrimitiveProps,
|
|
6
|
+
type CalendarValue,
|
|
7
|
+
useCalendar,
|
|
8
|
+
} from "@shadow-js/primitives";
|
|
9
|
+
import type { JSX } from "@shadow-js/web";
|
|
10
|
+
import { Button } from "@/components/ui/button";
|
|
11
|
+
import { ChevronLeftIcon, ChevronRightIcon } from "@/components/ui/icons";
|
|
12
|
+
import { useUiStrings } from "@/lib/ui-strings";
|
|
13
|
+
import { cn } from "@/lib/utils";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Props of {@link Calendar}: every prop of the primitive (`mode`, `value`, `onValueChange`, `min`,
|
|
17
|
+
* `max`, `disabled`, `locale`, `weekStartsOn`, `defaultMonth`, `today`, `dir`, `timeZone`), plus the props this
|
|
18
|
+
* component adds: `captionLayout`, `yearRange` and `labels`. With `captionLayout="dropdown"` a month
|
|
19
|
+
* select and a year select replace the caption.
|
|
20
|
+
* @since 1.0.0
|
|
21
|
+
* @see {@link Calendar}
|
|
22
|
+
*/
|
|
23
|
+
export type CalendarProps = CalendarPrimitiveProps & {
|
|
24
|
+
/**
|
|
25
|
+
* `"label"` shows the month name in the caption. `"dropdown"` shows a month select and a year
|
|
26
|
+
* select instead.
|
|
27
|
+
* @default "label"
|
|
28
|
+
*/
|
|
29
|
+
readonly captionLayout?: "label" | "dropdown" | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* How many years on either side of the shown year the year select lists.
|
|
32
|
+
* @default 10
|
|
33
|
+
*/
|
|
34
|
+
readonly yearRange?: number | undefined;
|
|
35
|
+
/** The names of the controls, for a language other than English. See {@link CalendarLabels}. */
|
|
36
|
+
readonly labels?: CalendarLabels | undefined;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The names of the calendar's controls, for a language other than English. Each name is optional,
|
|
41
|
+
* and the English default is used for any name you leave out.
|
|
42
|
+
* @since 1.0.0
|
|
43
|
+
* @see {@link CalendarProps}
|
|
44
|
+
*/
|
|
45
|
+
export interface CalendarLabels {
|
|
46
|
+
/**
|
|
47
|
+
* The accessible name of the previous-month button.
|
|
48
|
+
* @default "Previous month"
|
|
49
|
+
*/
|
|
50
|
+
readonly previousMonth?: string | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* The accessible name of the next-month button.
|
|
53
|
+
* @default "Next month"
|
|
54
|
+
*/
|
|
55
|
+
readonly nextMonth?: string | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* The accessible name of the month select in the dropdown layout.
|
|
58
|
+
* @default "Month"
|
|
59
|
+
*/
|
|
60
|
+
readonly month?: string | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* The accessible name of the year select in the dropdown layout.
|
|
63
|
+
* @default "Year"
|
|
64
|
+
*/
|
|
65
|
+
readonly year?: string | undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The month names of a locale, from `Intl` (January first). @internal */
|
|
69
|
+
function monthNames(locale: string): string[] {
|
|
70
|
+
const format = new Intl.DateTimeFormat(locale, { month: "long", timeZone: "UTC" });
|
|
71
|
+
return Array.from({ length: 12 }, (_, index) =>
|
|
72
|
+
format.format(new Date(Date.UTC(2000, index, 15))),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The previous and next month buttons and the caption (or the selects). @internal */
|
|
77
|
+
function CalendarHeader(props: {
|
|
78
|
+
readonly layout: "label" | "dropdown";
|
|
79
|
+
readonly yearRange: number;
|
|
80
|
+
readonly labels: CalendarLabels | undefined;
|
|
81
|
+
}): JSX.Element {
|
|
82
|
+
const calendar = useCalendar("Calendar");
|
|
83
|
+
const t = useUiStrings();
|
|
84
|
+
const shown = calendar.month;
|
|
85
|
+
const months = monthNames(calendar.locale);
|
|
86
|
+
const years = (): number[] => {
|
|
87
|
+
const center = shown().year;
|
|
88
|
+
return Array.from(
|
|
89
|
+
{ length: props.yearRange * 2 + 1 },
|
|
90
|
+
(_, index) => center - props.yearRange + index,
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
const jumpTo = (year: number, month: number): void => {
|
|
94
|
+
const current = shown();
|
|
95
|
+
calendar.navigate((year - current.year) * 12 + (month - current.month));
|
|
96
|
+
};
|
|
97
|
+
return (
|
|
98
|
+
<div data-slot="calendar-header" class="relative flex items-center justify-center pt-1">
|
|
99
|
+
<Button
|
|
100
|
+
data-slot="calendar-previous"
|
|
101
|
+
variant="outline"
|
|
102
|
+
size="icon"
|
|
103
|
+
class="absolute start-1 size-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
|
104
|
+
aria-label={props.labels?.previousMonth ?? t("calendar.previousMonth", "Previous month")}
|
|
105
|
+
onClick={() => calendar.navigate(-1)}
|
|
106
|
+
disabled={!calendar.canGoPrevious()}
|
|
107
|
+
>
|
|
108
|
+
<ChevronLeftIcon class="rtl:-scale-x-100" />
|
|
109
|
+
</Button>
|
|
110
|
+
{props.layout === "dropdown" ? (
|
|
111
|
+
<div data-slot="calendar-dropdowns" class="flex items-center gap-1 text-sm font-medium">
|
|
112
|
+
<select
|
|
113
|
+
aria-label={props.labels?.month ?? t("calendar.month", "Month")}
|
|
114
|
+
data-slot="calendar-month-select"
|
|
115
|
+
class="h-7 rounded-md border border-input bg-transparent px-2 text-sm"
|
|
116
|
+
value={String(shown().month)}
|
|
117
|
+
onChange={(event: Event) =>
|
|
118
|
+
jumpTo(shown().year, Number((event.currentTarget as HTMLSelectElement).value))
|
|
119
|
+
}
|
|
120
|
+
>
|
|
121
|
+
{months.map((name, index) => (
|
|
122
|
+
<option value={index + 1}>{name}</option>
|
|
123
|
+
))}
|
|
124
|
+
</select>
|
|
125
|
+
<select
|
|
126
|
+
aria-label={props.labels?.year ?? t("calendar.year", "Year")}
|
|
127
|
+
data-slot="calendar-year-select"
|
|
128
|
+
class="h-7 rounded-md border border-input bg-transparent px-2 text-sm"
|
|
129
|
+
value={String(shown().year)}
|
|
130
|
+
onChange={(event: Event) =>
|
|
131
|
+
jumpTo(Number((event.currentTarget as HTMLSelectElement).value), shown().month)
|
|
132
|
+
}
|
|
133
|
+
>
|
|
134
|
+
{years().map((year) => (
|
|
135
|
+
<option value={year}>{year}</option>
|
|
136
|
+
))}
|
|
137
|
+
</select>
|
|
138
|
+
<div
|
|
139
|
+
id={calendar.captionId()}
|
|
140
|
+
aria-live="polite"
|
|
141
|
+
data-slot="calendar-caption"
|
|
142
|
+
class="sr-only"
|
|
143
|
+
>
|
|
144
|
+
{calendar.caption()}
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
) : (
|
|
148
|
+
<div
|
|
149
|
+
id={calendar.captionId()}
|
|
150
|
+
aria-live="polite"
|
|
151
|
+
data-slot="calendar-caption"
|
|
152
|
+
class="text-sm font-medium"
|
|
153
|
+
>
|
|
154
|
+
{calendar.caption()}
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
<Button
|
|
158
|
+
data-slot="calendar-next"
|
|
159
|
+
variant="outline"
|
|
160
|
+
size="icon"
|
|
161
|
+
class="absolute end-1 size-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
|
162
|
+
aria-label={props.labels?.nextMonth ?? t("calendar.nextMonth", "Next month")}
|
|
163
|
+
onClick={() => calendar.navigate(1)}
|
|
164
|
+
disabled={!calendar.canGoNext()}
|
|
165
|
+
>
|
|
166
|
+
<ChevronRightIcon class="rtl:-scale-x-100" />
|
|
167
|
+
</Button>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The weekday header and the weeks, as a grid. @internal */
|
|
173
|
+
function CalendarGrid(): JSX.Element {
|
|
174
|
+
const calendar = useCalendar("Calendar");
|
|
175
|
+
return (
|
|
176
|
+
<table
|
|
177
|
+
// biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: a calendar grid is the WAI-ARIA grid pattern on a real table
|
|
178
|
+
role="grid"
|
|
179
|
+
data-slot="calendar-grid"
|
|
180
|
+
aria-labelledby={calendar.captionId()}
|
|
181
|
+
aria-multiselectable={calendar.mode === "single" ? undefined : true}
|
|
182
|
+
class="w-full border-collapse"
|
|
183
|
+
>
|
|
184
|
+
<thead>
|
|
185
|
+
<tr>
|
|
186
|
+
{calendar.weekdays().map((label) => (
|
|
187
|
+
<th
|
|
188
|
+
scope="col"
|
|
189
|
+
abbr={label.long}
|
|
190
|
+
class="w-8 rounded-md text-[0.8rem] font-normal text-muted-foreground"
|
|
191
|
+
>
|
|
192
|
+
{label.short}
|
|
193
|
+
</th>
|
|
194
|
+
))}
|
|
195
|
+
</tr>
|
|
196
|
+
</thead>
|
|
197
|
+
<tbody>
|
|
198
|
+
{calendar.weeks().map((week) => (
|
|
199
|
+
<tr role="row" class="mt-2">
|
|
200
|
+
{week.map((day: CalendarDate) => (
|
|
201
|
+
<CalendarDayCell date={day} />
|
|
202
|
+
))}
|
|
203
|
+
</tr>
|
|
204
|
+
))}
|
|
205
|
+
</tbody>
|
|
206
|
+
</table>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* One day of the grid: the primitive's `gridcell` with this kit's look. It shows the day number and
|
|
212
|
+
* styles the selection, the range, today, the days of another month and the focus ring. It is used
|
|
213
|
+
* by {@link Calendar} for every day; use it directly only when you build your own grid.
|
|
214
|
+
*
|
|
215
|
+
* Keyboard and ARIA: the focused day is the only one in the tab order. Enter and Space select it,
|
|
216
|
+
* and the arrow keys, PageUp and PageDown, and Home and End move the focus. It carries
|
|
217
|
+
* `aria-selected`, `aria-disabled` and `aria-current="date"` for today.
|
|
218
|
+
*
|
|
219
|
+
* @param props - The day props: `date`, a `{ year, month, day }` value.
|
|
220
|
+
* @returns The day cell element.
|
|
221
|
+
* @example
|
|
222
|
+
* ```tsx
|
|
223
|
+
* import { CalendarDayCell } from "@/components/ui/calendar";
|
|
224
|
+
*
|
|
225
|
+
* export function March5() {
|
|
226
|
+
* return <CalendarDayCell date={{ year: 2026, month: 3, day: 5 }} />;
|
|
227
|
+
* }
|
|
228
|
+
* ```
|
|
229
|
+
* @since 1.0.0
|
|
230
|
+
* @see {@link Calendar}
|
|
231
|
+
*/
|
|
232
|
+
function CalendarDayCell(props: { readonly date: CalendarDate }): JSX.Element {
|
|
233
|
+
return (
|
|
234
|
+
<CalendarPrimitive.Day
|
|
235
|
+
date={props.date}
|
|
236
|
+
data-slot="calendar-day"
|
|
237
|
+
class={cn(
|
|
238
|
+
"relative size-8 p-0 text-center text-sm select-none outline-none",
|
|
239
|
+
"[&>span]:flex [&>span]:size-8 [&>span]:items-center [&>span]:justify-center [&>span]:rounded-md [&>span]:font-normal",
|
|
240
|
+
"data-[outside]:text-muted-foreground data-[disabled]:opacity-50 data-[today]:bg-accent data-[today]:text-accent-foreground",
|
|
241
|
+
"data-[range=middle]:bg-accent data-[range=middle]:text-accent-foreground",
|
|
242
|
+
"data-[selected]:bg-primary data-[selected]:text-primary-foreground",
|
|
243
|
+
"focus-visible:z-10 focus-visible:ring-[3px] focus-visible:ring-ring/50 outline-none focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-transparent",
|
|
244
|
+
"forced-colors:data-[selected]:bg-[Highlight] forced-colors:data-[selected]:text-[HighlightText]",
|
|
245
|
+
)}
|
|
246
|
+
>
|
|
247
|
+
<span>{props.date.day}</span>
|
|
248
|
+
</CalendarPrimitive.Day>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* A month of days to pick from: one day, several days, or a range (`mode`, default `"single"`).
|
|
254
|
+
* It has month navigation, or a month and a year select with `captionLayout="dropdown"`, and it
|
|
255
|
+
* follows the locale's names and week start. There is no date library: the dates are plain
|
|
256
|
+
* `{ year, month, day }` values.
|
|
257
|
+
*
|
|
258
|
+
* Keyboard and ARIA: the days form a `role="grid"`, named by the caption in the default label
|
|
259
|
+
* layout. Only the focused day is in the tab order. Left and Right move one day (swapped in
|
|
260
|
+
* right-to-left layouts), Up and Down move a week, PageUp and PageDown move a month (with Shift, a
|
|
261
|
+
* year), and Home and End move to the first and last day of the week. Enter and Space select the
|
|
262
|
+
* focused day. Each day is a `gridcell` with `aria-selected` and `aria-disabled`. The caption is
|
|
263
|
+
* `aria-live="polite"`, so a month change is announced. The previous and next buttons are named
|
|
264
|
+
* "Previous month" and "Next month" unless you pass `labels`. A `mode` other than `"single"` sets
|
|
265
|
+
* `aria-multiselectable` on the grid.
|
|
266
|
+
*
|
|
267
|
+
* Compared with shadcn/ui: the selection is `value` and `onValueChange` (not `selected` and
|
|
268
|
+
* `onSelect`), and the dates are plain `{ year, month, day }` objects, not `Date` values. `class`
|
|
269
|
+
* replaces `className`, and `labels` is a ShadowJS prop for the control names.
|
|
270
|
+
*
|
|
271
|
+
* @param props - The calendar props: see {@link CalendarProps}.
|
|
272
|
+
* @returns The calendar element.
|
|
273
|
+
* @example
|
|
274
|
+
* ```tsx
|
|
275
|
+
* import { Calendar } from "@/components/ui/calendar";
|
|
276
|
+
*
|
|
277
|
+
* export function Picker() {
|
|
278
|
+
* return (
|
|
279
|
+
* <Calendar
|
|
280
|
+
* mode="range"
|
|
281
|
+
* defaultMonth={{ year: 2026, month: 3 }}
|
|
282
|
+
* captionLayout="dropdown"
|
|
283
|
+
* />
|
|
284
|
+
* );
|
|
285
|
+
* }
|
|
286
|
+
* ```
|
|
287
|
+
* @since 1.0.0
|
|
288
|
+
* @see {@link CalendarProps}
|
|
289
|
+
* @see {@link CalendarDayCell}
|
|
290
|
+
* @see {@link isCalendarDay}
|
|
291
|
+
*/
|
|
292
|
+
function Calendar(props: CalendarProps): JSX.Element {
|
|
293
|
+
const [local, rest] = splitProps(props as CalendarProps & { readonly class?: unknown }, [
|
|
294
|
+
"class",
|
|
295
|
+
"captionLayout",
|
|
296
|
+
"yearRange",
|
|
297
|
+
"labels",
|
|
298
|
+
]);
|
|
299
|
+
return (
|
|
300
|
+
<CalendarPrimitive
|
|
301
|
+
data-slot="calendar"
|
|
302
|
+
class={cn("group/calendar bg-background p-3", local.class as string | undefined)}
|
|
303
|
+
{...rest}
|
|
304
|
+
>
|
|
305
|
+
<CalendarHeader
|
|
306
|
+
layout={(local.captionLayout as "label" | "dropdown" | undefined) ?? "label"}
|
|
307
|
+
yearRange={(local.yearRange as number | undefined) ?? 10}
|
|
308
|
+
labels={local.labels as CalendarLabels | undefined}
|
|
309
|
+
/>
|
|
310
|
+
<div class="mt-4">
|
|
311
|
+
<CalendarGrid />
|
|
312
|
+
</div>
|
|
313
|
+
</CalendarPrimitive>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Narrows a {@link CalendarValue} to a single day. Use it in `onValueChange` of a single-day
|
|
319
|
+
* calendar, where the value is one `{ year, month, day }` date.
|
|
320
|
+
*
|
|
321
|
+
* @param value - The selection to check.
|
|
322
|
+
* @returns `true` when `value` is one day, so it can be used as a {@link CalendarDate}.
|
|
323
|
+
* @example
|
|
324
|
+
* ```tsx
|
|
325
|
+
* import { useSignal } from "@shadow-js/core";
|
|
326
|
+
* import type { CalendarDate } from "@shadow-js/primitives";
|
|
327
|
+
* import { Calendar, isCalendarDay } from "@/components/ui/calendar";
|
|
328
|
+
*
|
|
329
|
+
* export function DatePick() {
|
|
330
|
+
* const [date, setDate] = useSignal<CalendarDate | undefined>(undefined);
|
|
331
|
+
* return (
|
|
332
|
+
* <Calendar
|
|
333
|
+
* value={date()}
|
|
334
|
+
* onValueChange={(value) => {
|
|
335
|
+
* if (isCalendarDay(value)) setDate(value);
|
|
336
|
+
* }}
|
|
337
|
+
* />
|
|
338
|
+
* );
|
|
339
|
+
* }
|
|
340
|
+
* ```
|
|
341
|
+
* @since 1.0.0
|
|
342
|
+
* @see {@link Calendar}
|
|
343
|
+
*/
|
|
344
|
+
function isCalendarDay(value: CalendarValue): value is CalendarDate {
|
|
345
|
+
return typeof value === "object" && value !== null && "day" in value;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export { Calendar, CalendarDayCell, isCalendarDay };
|