open-ds 0.0.0-stage → 0.1.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.
Files changed (201) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +66 -2
  3. package/assets/schemes/accessible-first.json +3093 -0
  4. package/assets/schemes/brand-expressive.json +3124 -0
  5. package/assets/schemes/content-first.json +3110 -0
  6. package/assets/schemes/dense-data.json +3489 -0
  7. package/assets/schemes/grammar.json +3903 -0
  8. package/assets/schemes/multi-brand.json +4359 -0
  9. package/assets/skills/open-ds/SKILL.md +50 -0
  10. package/assets/skills/open-ds-build/SKILL.md +19 -0
  11. package/assets/skills/open-ds-change/SKILL.md +19 -0
  12. package/assets/skills/open-ds-choose/SKILL.md +18 -0
  13. package/assets/skills/open-ds-review/SKILL.md +22 -0
  14. package/assets/skills/open-ds-write/SKILL.md +13 -0
  15. package/assets/stories/accordion.stories.tsx +46 -0
  16. package/assets/stories/alert-dialog.stories.tsx +65 -0
  17. package/assets/stories/alert.stories.tsx +44 -0
  18. package/assets/stories/aspect-ratio.stories.tsx +37 -0
  19. package/assets/stories/avatar.stories.tsx +42 -0
  20. package/assets/stories/badge.stories.tsx +39 -0
  21. package/assets/stories/breadcrumb.stories.tsx +54 -0
  22. package/assets/stories/button.stories.tsx +46 -0
  23. package/assets/stories/calendar.stories.tsx +30 -0
  24. package/assets/stories/card.stories.tsx +50 -0
  25. package/assets/stories/carousel.stories.tsx +64 -0
  26. package/assets/stories/chart.stories.tsx +61 -0
  27. package/assets/stories/checkbox.stories.tsx +29 -0
  28. package/assets/stories/collapsible.stories.tsx +39 -0
  29. package/assets/stories/combobox.stories.tsx +57 -0
  30. package/assets/stories/command.stories.tsx +60 -0
  31. package/assets/stories/context-menu.stories.tsx +55 -0
  32. package/assets/stories/data-table.stories.tsx +93 -0
  33. package/assets/stories/date-picker.stories.tsx +27 -0
  34. package/assets/stories/dialog.stories.tsx +70 -0
  35. package/assets/stories/drawer.stories.tsx +62 -0
  36. package/assets/stories/dropdown-menu.stories.tsx +65 -0
  37. package/assets/stories/empty.stories.tsx +39 -0
  38. package/assets/stories/field.stories.tsx +42 -0
  39. package/assets/stories/form.stories.tsx +56 -0
  40. package/assets/stories/input-group.stories.tsx +41 -0
  41. package/assets/stories/input-otp.stories.tsx +48 -0
  42. package/assets/stories/input.stories.tsx +32 -0
  43. package/assets/stories/kbd.stories.tsx +30 -0
  44. package/assets/stories/label.stories.tsx +26 -0
  45. package/assets/stories/native-select.stories.tsx +39 -0
  46. package/assets/stories/navigation-menu.stories.tsx +41 -0
  47. package/assets/stories/pagination.stories.tsx +61 -0
  48. package/assets/stories/popover.stories.tsx +37 -0
  49. package/assets/stories/progress.stories.tsx +30 -0
  50. package/assets/stories/radio-group.stories.tsx +45 -0
  51. package/assets/stories/resizable.stories.tsx +54 -0
  52. package/assets/stories/scroll-area.stories.tsx +47 -0
  53. package/assets/stories/select.stories.tsx +57 -0
  54. package/assets/stories/separator.stories.tsx +28 -0
  55. package/assets/stories/sheet.stories.tsx +46 -0
  56. package/assets/stories/sidebar.stories.tsx +62 -0
  57. package/assets/stories/skeleton.stories.tsx +27 -0
  58. package/assets/stories/slider.stories.tsx +44 -0
  59. package/assets/stories/spinner.stories.tsx +25 -0
  60. package/assets/stories/switch.stories.tsx +29 -0
  61. package/assets/stories/table.stories.tsx +97 -0
  62. package/assets/stories/tabs.stories.tsx +55 -0
  63. package/assets/stories/textarea.stories.tsx +28 -0
  64. package/assets/stories/toast.stories.tsx +30 -0
  65. package/assets/stories/toggle-group.stories.tsx +41 -0
  66. package/assets/stories/toggle.stories.tsx +20 -0
  67. package/assets/stories/tooltip.stories.tsx +49 -0
  68. package/assets/tokens/accessible-first/content.json +32 -0
  69. package/assets/tokens/accessible-first/dark.tokens.json +581 -0
  70. package/assets/tokens/accessible-first/light.tokens.json +581 -0
  71. package/assets/tokens/accessible-first/shadcn.css +21 -0
  72. package/assets/tokens/accessible-first/theme.css +101 -0
  73. package/assets/tokens/accessible-first/tokens.css +331 -0
  74. package/assets/tokens/accessible-first/tokens.json +1616 -0
  75. package/assets/tokens/accessible-first/tokens.scoped.css +333 -0
  76. package/assets/tokens/brand-expressive/content.json +32 -0
  77. package/assets/tokens/brand-expressive/dark.tokens.json +585 -0
  78. package/assets/tokens/brand-expressive/light.tokens.json +585 -0
  79. package/assets/tokens/brand-expressive/shadcn.css +21 -0
  80. package/assets/tokens/brand-expressive/theme.css +101 -0
  81. package/assets/tokens/brand-expressive/tokens.css +331 -0
  82. package/assets/tokens/brand-expressive/tokens.json +1651 -0
  83. package/assets/tokens/brand-expressive/tokens.scoped.css +333 -0
  84. package/assets/tokens/content-first/content.json +32 -0
  85. package/assets/tokens/content-first/dark.tokens.json +582 -0
  86. package/assets/tokens/content-first/light.tokens.json +582 -0
  87. package/assets/tokens/content-first/shadcn.css +21 -0
  88. package/assets/tokens/content-first/theme.css +101 -0
  89. package/assets/tokens/content-first/tokens.css +331 -0
  90. package/assets/tokens/content-first/tokens.json +1631 -0
  91. package/assets/tokens/content-first/tokens.scoped.css +333 -0
  92. package/assets/tokens/dense-data/content.json +32 -0
  93. package/assets/tokens/dense-data/dark.tokens.json +580 -0
  94. package/assets/tokens/dense-data/light.tokens.json +580 -0
  95. package/assets/tokens/dense-data/shadcn.css +21 -0
  96. package/assets/tokens/dense-data/theme.css +101 -0
  97. package/assets/tokens/dense-data/tokens.css +331 -0
  98. package/assets/tokens/dense-data/tokens.json +1623 -0
  99. package/assets/tokens/dense-data/tokens.scoped.css +333 -0
  100. package/assets/tokens/index.json +143 -0
  101. package/assets/tokens/multi-brand/aster-dark.tokens.json +625 -0
  102. package/assets/tokens/multi-brand/aster-light.tokens.json +625 -0
  103. package/assets/tokens/multi-brand/birch-dark.tokens.json +625 -0
  104. package/assets/tokens/multi-brand/birch-light.tokens.json +625 -0
  105. package/assets/tokens/multi-brand/content.json +32 -0
  106. package/assets/tokens/multi-brand/coral-dark.tokens.json +625 -0
  107. package/assets/tokens/multi-brand/coral-light.tokens.json +625 -0
  108. package/assets/tokens/multi-brand/shadcn.css +21 -0
  109. package/assets/tokens/multi-brand/theme.css +101 -0
  110. package/assets/tokens/multi-brand/tokens.css +1002 -0
  111. package/assets/tokens/multi-brand/tokens.json +1754 -0
  112. package/assets/tokens/multi-brand/tokens.scoped.css +1022 -0
  113. package/assets/tokens/shadcn-map.json +34 -0
  114. package/assets/ui/components/accordion.tsx +60 -0
  115. package/assets/ui/components/alert-dialog.tsx +171 -0
  116. package/assets/ui/components/alert.tsx +56 -0
  117. package/assets/ui/components/aspect-ratio.tsx +17 -0
  118. package/assets/ui/components/avatar.tsx +96 -0
  119. package/assets/ui/components/badge.tsx +36 -0
  120. package/assets/ui/components/breadcrumb.tsx +99 -0
  121. package/assets/ui/components/button.tsx +53 -0
  122. package/assets/ui/components/calendar.tsx +169 -0
  123. package/assets/ui/components/card.tsx +72 -0
  124. package/assets/ui/components/carousel.tsx +224 -0
  125. package/assets/ui/components/chart.tsx +315 -0
  126. package/assets/ui/components/checkbox.tsx +29 -0
  127. package/assets/ui/components/collapsible.tsx +25 -0
  128. package/assets/ui/components/combobox.tsx +275 -0
  129. package/assets/ui/components/command.tsx +145 -0
  130. package/assets/ui/components/context-menu.tsx +218 -0
  131. package/assets/ui/components/data-table.tsx +307 -0
  132. package/assets/ui/components/date-picker.tsx +79 -0
  133. package/assets/ui/components/dialog.tsx +68 -0
  134. package/assets/ui/components/drawer.tsx +115 -0
  135. package/assets/ui/components/dropdown-menu.tsx +58 -0
  136. package/assets/ui/components/empty.tsx +92 -0
  137. package/assets/ui/components/field.tsx +234 -0
  138. package/assets/ui/components/form.tsx +146 -0
  139. package/assets/ui/components/input-group.tsx +154 -0
  140. package/assets/ui/components/input-otp.tsx +75 -0
  141. package/assets/ui/components/input.tsx +25 -0
  142. package/assets/ui/components/kbd.tsx +30 -0
  143. package/assets/ui/components/label.tsx +22 -0
  144. package/assets/ui/components/native-select.tsx +59 -0
  145. package/assets/ui/components/navigation-menu.tsx +149 -0
  146. package/assets/ui/components/pagination.tsx +108 -0
  147. package/assets/ui/components/popover.tsx +61 -0
  148. package/assets/ui/components/progress.tsx +31 -0
  149. package/assets/ui/components/radio-group.tsx +40 -0
  150. package/assets/ui/components/resizable.tsx +54 -0
  151. package/assets/ui/components/scroll-area.tsx +55 -0
  152. package/assets/ui/components/select.tsx +81 -0
  153. package/assets/ui/components/separator.tsx +35 -0
  154. package/assets/ui/components/sheet.tsx +114 -0
  155. package/assets/ui/components/sidebar.tsx +685 -0
  156. package/assets/ui/components/skeleton.tsx +15 -0
  157. package/assets/ui/components/slider.tsx +70 -0
  158. package/assets/ui/components/spinner.tsx +16 -0
  159. package/assets/ui/components/switch.tsx +32 -0
  160. package/assets/ui/components/table.tsx +78 -0
  161. package/assets/ui/components/tabs.tsx +49 -0
  162. package/assets/ui/components/textarea.tsx +25 -0
  163. package/assets/ui/components/toast.tsx +54 -0
  164. package/assets/ui/components/toggle-group.tsx +88 -0
  165. package/assets/ui/components/toggle.tsx +48 -0
  166. package/assets/ui/components/tooltip.tsx +34 -0
  167. package/assets/ui/lib/use-mobile.ts +20 -0
  168. package/assets/ui/lib/utils.ts +7 -0
  169. package/assets/ui/styles.css +27 -0
  170. package/bin/open-ds.js +6 -0
  171. package/package.json +56 -4
  172. package/src/agents.mjs +150 -0
  173. package/src/cli.mjs +445 -0
  174. package/src/components.mjs +69 -0
  175. package/src/context.mjs +143 -0
  176. package/src/create.mjs +377 -0
  177. package/src/docs.mjs +570 -0
  178. package/src/guidance.mjs +63 -0
  179. package/src/index.mjs +20 -0
  180. package/src/mcp.mjs +660 -0
  181. package/src/readmes.mjs +106 -0
  182. package/src/schemes.mjs +188 -0
  183. package/src/tokens.mjs +127 -0
  184. package/templates/project/.github/workflows/ci.yml +15 -0
  185. package/templates/project/.storybook/component-docs.mdx +26 -0
  186. package/templates/project/.storybook/main.ts +17 -0
  187. package/templates/project/.storybook/manager-head.html +3 -0
  188. package/templates/project/.storybook/manager.ts +37 -0
  189. package/templates/project/.storybook/preview-head.html +3 -0
  190. package/templates/project/.storybook/preview.tsx +82 -0
  191. package/templates/project/docs/docusaurus.config.ts +64 -0
  192. package/templates/project/docs/sidebars.ts +8 -0
  193. package/templates/project/docs/src/css/custom.css +275 -0
  194. package/templates/project/gitignore +6 -0
  195. package/templates/project/src/styles/index.css +16 -0
  196. package/templates/project/src/vite-env.d.ts +7 -0
  197. package/templates/project/stories/docs-blocks.tsx +190 -0
  198. package/templates/project/stories/samples.tsx +102 -0
  199. package/templates/project/stories/usage.tsx +31 -0
  200. package/templates/project/tokens/build.mjs +468 -0
  201. package/templates/project/tsconfig.json +15 -0
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Calendar. A month grid for picking dates.
3
+ *
4
+ * From shadcn/ui (new-york-v4, MIT), carried by the schemes whose inventory names it.
5
+ * Reads the shadcn variables, which theme.css maps onto the scheme's semantic tokens, and the
6
+ * radius, shadow and text scale that shadcn.css maps onto the scheme's tokens. Dark mode comes
7
+ * from the tokens, so no dark: classes.
8
+ */
9
+ "use client";
10
+
11
+ import * as React from "react";
12
+ import { cn } from "../lib/utils";
13
+ import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
14
+ import { DayPicker, getDefaultClassNames, type DayButton } from "react-day-picker";
15
+
16
+ import { Button, buttonVariants } from "./button";
17
+
18
+ function Calendar({
19
+ className,
20
+ classNames,
21
+ showOutsideDays = true,
22
+ captionLayout = "label",
23
+ buttonVariant = "ghost",
24
+ formatters,
25
+ components,
26
+ ...props
27
+ }: React.ComponentProps<typeof DayPicker> & {
28
+ buttonVariant?: React.ComponentProps<typeof Button>["variant"];
29
+ }) {
30
+ const defaultClassNames = getDefaultClassNames();
31
+
32
+ return (
33
+ <DayPicker
34
+ showOutsideDays={showOutsideDays}
35
+ className={cn(
36
+ "group/calendar bg-background p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
37
+ String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
38
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
39
+ className,
40
+ )}
41
+ captionLayout={captionLayout}
42
+ formatters={{
43
+ formatMonthDropdown: (date) => date.toLocaleString("default", { month: "short" }),
44
+ ...formatters,
45
+ }}
46
+ classNames={{
47
+ root: cn("w-fit", defaultClassNames.root),
48
+ months: cn("relative flex flex-col gap-4 md:flex-row", defaultClassNames.months),
49
+ month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
50
+ nav: cn("absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", defaultClassNames.nav),
51
+ button_previous: cn(
52
+ buttonVariants({ variant: buttonVariant }),
53
+ "size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
54
+ defaultClassNames.button_previous,
55
+ ),
56
+ button_next: cn(
57
+ buttonVariants({ variant: buttonVariant }),
58
+ "size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
59
+ defaultClassNames.button_next,
60
+ ),
61
+ month_caption: cn(
62
+ "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
63
+ defaultClassNames.month_caption,
64
+ ),
65
+ dropdowns: cn(
66
+ "flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
67
+ defaultClassNames.dropdowns,
68
+ ),
69
+ dropdown_root: cn(
70
+ "relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50",
71
+ defaultClassNames.dropdown_root,
72
+ ),
73
+ dropdown: cn("absolute inset-0 bg-popover opacity-0", defaultClassNames.dropdown),
74
+ caption_label: cn(
75
+ "font-medium select-none",
76
+ captionLayout === "label"
77
+ ? "text-sm"
78
+ : "flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
79
+ defaultClassNames.caption_label,
80
+ ),
81
+ month_grid: cn("w-full border-collapse", defaultClassNames.month_grid),
82
+ weekdays: cn("flex", defaultClassNames.weekdays),
83
+ weekday: cn(
84
+ "flex-1 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none",
85
+ defaultClassNames.weekday,
86
+ ),
87
+ week: cn("mt-2 flex w-full", defaultClassNames.week),
88
+ week_number_header: cn("w-(--cell-size) select-none", defaultClassNames.week_number_header),
89
+ week_number: cn("text-[0.8rem] text-muted-foreground select-none", defaultClassNames.week_number),
90
+ day: cn(
91
+ "group/day relative aspect-square h-full w-full p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-md",
92
+ props.showWeekNumber
93
+ ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
94
+ : "[&:first-child[data-selected=true]_button]:rounded-l-md",
95
+ defaultClassNames.day,
96
+ ),
97
+ range_start: cn("rounded-l-md bg-accent", defaultClassNames.range_start),
98
+ range_middle: cn("rounded-none", defaultClassNames.range_middle),
99
+ range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
100
+ today: cn(
101
+ "rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none",
102
+ defaultClassNames.today,
103
+ ),
104
+ outside: cn("text-muted-foreground aria-selected:text-muted-foreground", defaultClassNames.outside),
105
+ disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled),
106
+ hidden: cn("invisible", defaultClassNames.hidden),
107
+ ...classNames,
108
+ }}
109
+ components={{
110
+ Root: ({ className, rootRef, ...props }) => {
111
+ return <div data-slot="calendar" ref={rootRef} className={cn(className)} {...props} />;
112
+ },
113
+ Chevron: ({ className, orientation, ...props }) => {
114
+ if (orientation === "left") {
115
+ return <ChevronLeftIcon className={cn("size-4", className)} {...props} />;
116
+ }
117
+
118
+ if (orientation === "right") {
119
+ return <ChevronRightIcon className={cn("size-4", className)} {...props} />;
120
+ }
121
+
122
+ return <ChevronDownIcon className={cn("size-4", className)} {...props} />;
123
+ },
124
+ DayButton: CalendarDayButton,
125
+ WeekNumber: ({ children, ...props }) => {
126
+ return (
127
+ <td {...props}>
128
+ <div className="flex size-(--cell-size) items-center justify-center text-center">{children}</div>
129
+ </td>
130
+ );
131
+ },
132
+ ...components,
133
+ }}
134
+ {...props}
135
+ />
136
+ );
137
+ }
138
+
139
+ function CalendarDayButton({ className, day, modifiers, ...props }: React.ComponentProps<typeof DayButton>) {
140
+ const defaultClassNames = getDefaultClassNames();
141
+
142
+ const ref = React.useRef<HTMLButtonElement>(null);
143
+ React.useEffect(() => {
144
+ if (modifiers.focused) ref.current?.focus();
145
+ }, [modifiers.focused]);
146
+
147
+ return (
148
+ <Button
149
+ ref={ref}
150
+ variant="ghost"
151
+ size="icon"
152
+ data-day={day.date.toLocaleDateString()}
153
+ data-selected-single={
154
+ modifiers.selected && !modifiers.range_start && !modifiers.range_end && !modifiers.range_middle
155
+ }
156
+ data-range-start={modifiers.range_start}
157
+ data-range-end={modifiers.range_end}
158
+ data-range-middle={modifiers.range_middle}
159
+ className={cn(
160
+ "flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground [&>span]:text-xs [&>span]:opacity-70",
161
+ defaultClassNames.day,
162
+ className,
163
+ )}
164
+ {...props}
165
+ />
166
+ );
167
+ }
168
+
169
+ export { Calendar, CalendarDayButton };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Card. Groups content inside a page. Whether it has a shadow is a scheme decision
3
+ * (elevation.roles, elevation.definition): --card-shadow is "no shadow" in schemes that separate with lines.
4
+ *
5
+ * Reads only component keys: --card-*.
6
+ */
7
+ import * as React from "react";
8
+ import { cn } from "../lib/utils";
9
+
10
+ export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
11
+ return (
12
+ <div
13
+ data-slot="card"
14
+ className={cn(
15
+ "flex flex-col gap-[var(--space-gap-default)] p-[var(--card-padding)] rounded-[var(--card-radius)]",
16
+ "bg-[var(--card-background)] text-[var(--color-text-primary)] [box-shadow:var(--card-shadow)]",
17
+ "border-[length:var(--border-width-default)] border-[var(--card-border)]",
18
+ className,
19
+ )}
20
+ {...props}
21
+ />
22
+ );
23
+ }
24
+
25
+ export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
26
+ return (
27
+ <div data-slot="card-header" className={cn("flex flex-col gap-[var(--space-gap-tight)]", className)} {...props} />
28
+ );
29
+ }
30
+
31
+ export function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
32
+ return (
33
+ <h3
34
+ data-slot="card-title"
35
+ className={cn(
36
+ "[font-family:var(--type-heading-font-family)] text-[length:var(--type-heading-font-size)] [font-weight:var(--type-heading-font-weight)] leading-[var(--type-heading-line-height)]",
37
+ className,
38
+ )}
39
+ {...props}
40
+ />
41
+ );
42
+ }
43
+
44
+ export function CardDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
45
+ return (
46
+ <p
47
+ data-slot="card-description"
48
+ className={cn("text-[var(--color-text-secondary)] text-[length:var(--type-body-font-size)]", className)}
49
+ {...props}
50
+ />
51
+ );
52
+ }
53
+
54
+ export function CardContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
55
+ return (
56
+ <div
57
+ data-slot="card-content"
58
+ className={cn("flex flex-col gap-[var(--space-gap-default)]", className)}
59
+ {...props}
60
+ />
61
+ );
62
+ }
63
+
64
+ export function CardFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
65
+ return (
66
+ <div
67
+ data-slot="card-footer"
68
+ className={cn("flex items-center gap-[var(--space-gap-default)]", className)}
69
+ {...props}
70
+ />
71
+ );
72
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Carousel. Slides that move left and right.
3
+ *
4
+ * From shadcn/ui (new-york-v4, MIT), carried by the schemes whose inventory names it.
5
+ * Reads the shadcn variables, which theme.css maps onto the scheme's semantic tokens, and the
6
+ * radius, shadow and text scale that shadcn.css maps onto the scheme's tokens. Dark mode comes
7
+ * from the tokens, so no dark: classes.
8
+ */
9
+ "use client";
10
+
11
+ import * as React from "react";
12
+ import { cn } from "../lib/utils";
13
+ import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
14
+ import { ArrowLeft, ArrowRight } from "lucide-react";
15
+
16
+ import { Button } from "./button";
17
+
18
+ type CarouselApi = UseEmblaCarouselType[1];
19
+ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
20
+ type CarouselOptions = UseCarouselParameters[0];
21
+ type CarouselPlugin = UseCarouselParameters[1];
22
+
23
+ type CarouselProps = {
24
+ opts?: CarouselOptions;
25
+ plugins?: CarouselPlugin;
26
+ orientation?: "horizontal" | "vertical";
27
+ setApi?: (api: CarouselApi) => void;
28
+ };
29
+
30
+ type CarouselContextProps = {
31
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0];
32
+ api: ReturnType<typeof useEmblaCarousel>[1];
33
+ scrollPrev: () => void;
34
+ scrollNext: () => void;
35
+ canScrollPrev: boolean;
36
+ canScrollNext: boolean;
37
+ } & CarouselProps;
38
+
39
+ const CarouselContext = React.createContext<CarouselContextProps | null>(null);
40
+
41
+ function useCarousel() {
42
+ const context = React.useContext(CarouselContext);
43
+
44
+ if (!context) {
45
+ throw new Error("useCarousel must be used within a <Carousel />");
46
+ }
47
+
48
+ return context;
49
+ }
50
+
51
+ function Carousel({
52
+ orientation = "horizontal",
53
+ opts,
54
+ setApi,
55
+ plugins,
56
+ className,
57
+ children,
58
+ ...props
59
+ }: React.ComponentProps<"div"> & CarouselProps) {
60
+ const [carouselRef, api] = useEmblaCarousel(
61
+ {
62
+ ...opts,
63
+ axis: orientation === "horizontal" ? "x" : "y",
64
+ },
65
+ plugins,
66
+ );
67
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
68
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
69
+
70
+ const onSelect = React.useCallback((api: CarouselApi) => {
71
+ if (!api) return;
72
+ setCanScrollPrev(api.canScrollPrev());
73
+ setCanScrollNext(api.canScrollNext());
74
+ }, []);
75
+
76
+ const scrollPrev = React.useCallback(() => {
77
+ api?.scrollPrev();
78
+ }, [api]);
79
+
80
+ const scrollNext = React.useCallback(() => {
81
+ api?.scrollNext();
82
+ }, [api]);
83
+
84
+ const handleKeyDown = React.useCallback(
85
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
86
+ if (event.key === "ArrowLeft") {
87
+ event.preventDefault();
88
+ scrollPrev();
89
+ } else if (event.key === "ArrowRight") {
90
+ event.preventDefault();
91
+ scrollNext();
92
+ }
93
+ },
94
+ [scrollPrev, scrollNext],
95
+ );
96
+
97
+ React.useEffect(() => {
98
+ if (!api || !setApi) return;
99
+ setApi(api);
100
+ }, [api, setApi]);
101
+
102
+ React.useEffect(() => {
103
+ if (!api) return;
104
+ onSelect(api);
105
+ api.on("reInit", onSelect);
106
+ api.on("select", onSelect);
107
+
108
+ return () => {
109
+ api?.off("select", onSelect);
110
+ };
111
+ }, [api, onSelect]);
112
+
113
+ return (
114
+ <CarouselContext.Provider
115
+ value={{
116
+ carouselRef,
117
+ api: api,
118
+ opts,
119
+ orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
120
+ scrollPrev,
121
+ scrollNext,
122
+ canScrollPrev,
123
+ canScrollNext,
124
+ }}
125
+ >
126
+ <div
127
+ onKeyDownCapture={handleKeyDown}
128
+ className={cn("relative", className)}
129
+ role="region"
130
+ aria-roledescription="carousel"
131
+ data-slot="carousel"
132
+ {...props}
133
+ >
134
+ {children}
135
+ </div>
136
+ </CarouselContext.Provider>
137
+ );
138
+ }
139
+
140
+ function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
141
+ const { carouselRef, orientation } = useCarousel();
142
+
143
+ return (
144
+ <div ref={carouselRef} className="overflow-hidden" data-slot="carousel-content">
145
+ <div className={cn("flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", className)} {...props} />
146
+ </div>
147
+ );
148
+ }
149
+
150
+ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
151
+ const { orientation } = useCarousel();
152
+
153
+ return (
154
+ <div
155
+ role="group"
156
+ aria-roledescription="slide"
157
+ data-slot="carousel-item"
158
+ className={cn("min-w-0 shrink-0 grow-0 basis-full", orientation === "horizontal" ? "pl-4" : "pt-4", className)}
159
+ {...props}
160
+ />
161
+ );
162
+ }
163
+
164
+ function CarouselPrevious({
165
+ className,
166
+ variant = "secondary",
167
+ size = "icon",
168
+ ...props
169
+ }: React.ComponentProps<typeof Button>) {
170
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
171
+
172
+ return (
173
+ <Button
174
+ data-slot="carousel-previous"
175
+ variant={variant}
176
+ size={size}
177
+ className={cn(
178
+ "absolute size-8 rounded-full",
179
+ orientation === "horizontal"
180
+ ? "top-1/2 -left-12 -translate-y-1/2"
181
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
182
+ className,
183
+ )}
184
+ disabled={!canScrollPrev}
185
+ onClick={scrollPrev}
186
+ {...props}
187
+ >
188
+ <ArrowLeft />
189
+ <span className="sr-only">Previous slide</span>
190
+ </Button>
191
+ );
192
+ }
193
+
194
+ function CarouselNext({
195
+ className,
196
+ variant = "secondary",
197
+ size = "icon",
198
+ ...props
199
+ }: React.ComponentProps<typeof Button>) {
200
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
201
+
202
+ return (
203
+ <Button
204
+ data-slot="carousel-next"
205
+ variant={variant}
206
+ size={size}
207
+ className={cn(
208
+ "absolute size-8 rounded-full",
209
+ orientation === "horizontal"
210
+ ? "top-1/2 -right-12 -translate-y-1/2"
211
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
212
+ className,
213
+ )}
214
+ disabled={!canScrollNext}
215
+ onClick={scrollNext}
216
+ {...props}
217
+ >
218
+ <ArrowRight />
219
+ <span className="sr-only">Next slide</span>
220
+ </Button>
221
+ );
222
+ }
223
+
224
+ export { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };