@adres/mgda-ui 0.0.0-stage → 0.0.2

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 (325) hide show
  1. package/brand-knowledge.json +451 -0
  2. package/knowledge/accordion.md +135 -0
  3. package/knowledge/activity-rings.md +123 -0
  4. package/knowledge/alert.md +184 -0
  5. package/knowledge/answer-actions.md +217 -0
  6. package/knowledge/aspect-ratio.md +82 -0
  7. package/knowledge/avatar.md +158 -0
  8. package/knowledge/badge.md +172 -0
  9. package/knowledge/bar-graph.md +167 -0
  10. package/knowledge/bento-grid.md +115 -0
  11. package/knowledge/blockquote.md +153 -0
  12. package/knowledge/box-list.md +177 -0
  13. package/knowledge/bullet-list.md +128 -0
  14. package/knowledge/button-group.md +179 -0
  15. package/knowledge/button.md +231 -0
  16. package/knowledge/calendar.md +168 -0
  17. package/knowledge/card.md +321 -0
  18. package/knowledge/cards-nav.md +170 -0
  19. package/knowledge/chain-of-thought.md +164 -0
  20. package/knowledge/chat-container.md +135 -0
  21. package/knowledge/chat-message.md +224 -0
  22. package/knowledge/checkbox-card.md +172 -0
  23. package/knowledge/checkbox.md +111 -0
  24. package/knowledge/citations.md +241 -0
  25. package/knowledge/combobox.md +265 -0
  26. package/knowledge/countdown-pill.md +153 -0
  27. package/knowledge/cta.md +118 -0
  28. package/knowledge/data-list.md +173 -0
  29. package/knowledge/data-table.md +220 -0
  30. package/knowledge/dialog.md +249 -0
  31. package/knowledge/drawer.md +236 -0
  32. package/knowledge/dropdown-menu.md +238 -0
  33. package/knowledge/dropzone.md +153 -0
  34. package/knowledge/email-chips-input.md +161 -0
  35. package/knowledge/empty-state.md +106 -0
  36. package/knowledge/entity-header.md +166 -0
  37. package/knowledge/feature-grid.md +120 -0
  38. package/knowledge/feedback-bar.md +127 -0
  39. package/knowledge/footer.md +116 -0
  40. package/knowledge/heading.md +189 -0
  41. package/knowledge/hero.md +361 -0
  42. package/knowledge/input-button.md +187 -0
  43. package/knowledge/input-group.md +185 -0
  44. package/knowledge/input.md +256 -0
  45. package/knowledge/inset.md +177 -0
  46. package/knowledge/kpi-card.md +206 -0
  47. package/knowledge/list-nav.md +181 -0
  48. package/knowledge/logo-cloud.md +102 -0
  49. package/knowledge/media-gallery.md +158 -0
  50. package/knowledge/message-actions.md +175 -0
  51. package/knowledge/nav-sidebar.md +225 -0
  52. package/knowledge/number-stepper.md +159 -0
  53. package/knowledge/page-header.md +109 -0
  54. package/knowledge/pagination.md +225 -0
  55. package/knowledge/popover.md +159 -0
  56. package/knowledge/pricing.md +132 -0
  57. package/knowledge/progress.md +156 -0
  58. package/knowledge/prompt-input.md +337 -0
  59. package/knowledge/prompt-suggestions.md +168 -0
  60. package/knowledge/quota-bar.md +160 -0
  61. package/knowledge/quote.md +116 -0
  62. package/knowledge/radio-card.md +172 -0
  63. package/knowledge/radio-group.md +135 -0
  64. package/knowledge/reasoning.md +142 -0
  65. package/knowledge/search.md +131 -0
  66. package/knowledge/section-nav.md +162 -0
  67. package/knowledge/segmented-control.md +181 -0
  68. package/knowledge/select-button.md +205 -0
  69. package/knowledge/select.md +210 -0
  70. package/knowledge/separator.md +75 -0
  71. package/knowledge/shimmer-text.md +140 -0
  72. package/knowledge/sidebar.md +227 -0
  73. package/knowledge/skeleton.md +142 -0
  74. package/knowledge/slider.md +133 -0
  75. package/knowledge/slideshow.md +159 -0
  76. package/knowledge/source.md +156 -0
  77. package/knowledge/spinner.md +120 -0
  78. package/knowledge/stat-strip.md +128 -0
  79. package/knowledge/stats.md +117 -0
  80. package/knowledge/step-indicator.md +157 -0
  81. package/knowledge/switch.md +88 -0
  82. package/knowledge/table.md +190 -0
  83. package/knowledge/tabs.md +169 -0
  84. package/knowledge/testimonials.md +115 -0
  85. package/knowledge/text-link.md +192 -0
  86. package/knowledge/text.md +194 -0
  87. package/knowledge/thinking-bar.md +115 -0
  88. package/knowledge/thinking-steps.md +140 -0
  89. package/knowledge/toaster.md +127 -0
  90. package/knowledge/toggle.md +113 -0
  91. package/knowledge/tool-call-card.md +241 -0
  92. package/knowledge/tooltip.md +120 -0
  93. package/knowledge/top-nav.md +138 -0
  94. package/knowledge.json +19854 -0
  95. package/package.json +94 -4
  96. package/src/components/accordion.stories.tsx +46 -0
  97. package/src/components/accordion.tsx +74 -0
  98. package/src/components/activity-rings.stories.tsx +73 -0
  99. package/src/components/activity-rings.tsx +121 -0
  100. package/src/components/alert.stories.tsx +43 -0
  101. package/src/components/alert.tsx +132 -0
  102. package/src/components/answer-actions.stories.tsx +54 -0
  103. package/src/components/answer-actions.test.tsx +37 -0
  104. package/src/components/answer-actions.tsx +313 -0
  105. package/src/components/aspect-ratio.stories.tsx +32 -0
  106. package/src/components/aspect-ratio.tsx +5 -0
  107. package/src/components/avatar.stories.tsx +40 -0
  108. package/src/components/avatar.tsx +154 -0
  109. package/src/components/badge.stories.tsx +27 -0
  110. package/src/components/badge.tsx +63 -0
  111. package/src/components/bar-graph.stories.tsx +28 -0
  112. package/src/components/bar-graph.tsx +204 -0
  113. package/src/components/bento-grid.stories.tsx +23 -0
  114. package/src/components/bento-grid.tsx +67 -0
  115. package/src/components/blockquote.stories.tsx +28 -0
  116. package/src/components/blockquote.tsx +88 -0
  117. package/src/components/box-list.stories.tsx +25 -0
  118. package/src/components/box-list.tsx +108 -0
  119. package/src/components/bullet-list.stories.tsx +22 -0
  120. package/src/components/bullet-list.tsx +101 -0
  121. package/src/components/button-group.stories.tsx +31 -0
  122. package/src/components/button-group.tsx +156 -0
  123. package/src/components/button.stories.tsx +66 -0
  124. package/src/components/button.test.ts +17 -0
  125. package/src/components/button.tsx +139 -0
  126. package/src/components/calendar.stories.tsx +25 -0
  127. package/src/components/calendar.tsx +367 -0
  128. package/src/components/card.stories.tsx +44 -0
  129. package/src/components/card.tsx +392 -0
  130. package/src/components/cards-nav.stories.tsx +35 -0
  131. package/src/components/cards-nav.tsx +115 -0
  132. package/src/components/chain-of-thought.stories.tsx +67 -0
  133. package/src/components/chain-of-thought.test.tsx +50 -0
  134. package/src/components/chain-of-thought.tsx +170 -0
  135. package/src/components/chat-container.stories.tsx +47 -0
  136. package/src/components/chat-container.test.tsx +49 -0
  137. package/src/components/chat-container.tsx +222 -0
  138. package/src/components/chat-message.stories.tsx +52 -0
  139. package/src/components/chat-message.test.tsx +36 -0
  140. package/src/components/chat-message.tsx +163 -0
  141. package/src/components/checkbox-card.stories.tsx +27 -0
  142. package/src/components/checkbox-card.tsx +200 -0
  143. package/src/components/checkbox.stories.tsx +26 -0
  144. package/src/components/checkbox.tsx +61 -0
  145. package/src/components/citations.stories.tsx +64 -0
  146. package/src/components/citations.test.tsx +61 -0
  147. package/src/components/citations.tsx +220 -0
  148. package/src/components/combobox.stories.tsx +184 -0
  149. package/src/components/combobox.test.tsx +82 -0
  150. package/src/components/combobox.tsx +407 -0
  151. package/src/components/countdown-pill.stories.tsx +24 -0
  152. package/src/components/countdown-pill.tsx +129 -0
  153. package/src/components/cta.stories.tsx +29 -0
  154. package/src/components/cta.tsx +52 -0
  155. package/src/components/data-list.stories.tsx +50 -0
  156. package/src/components/data-list.test.tsx +35 -0
  157. package/src/components/data-list.tsx +181 -0
  158. package/src/components/data-table.stories.tsx +29 -0
  159. package/src/components/data-table.tsx +200 -0
  160. package/src/components/dialog.stories.tsx +42 -0
  161. package/src/components/dialog.tsx +200 -0
  162. package/src/components/drawer.stories.tsx +48 -0
  163. package/src/components/drawer.tsx +385 -0
  164. package/src/components/dropdown-menu.stories.tsx +41 -0
  165. package/src/components/dropdown-menu.tsx +252 -0
  166. package/src/components/dropzone.stories.tsx +19 -0
  167. package/src/components/dropzone.tsx +250 -0
  168. package/src/components/email-chips-input.stories.tsx +135 -0
  169. package/src/components/email-chips-input.tsx +210 -0
  170. package/src/components/empty-state.stories.tsx +22 -0
  171. package/src/components/empty-state.tsx +35 -0
  172. package/src/components/entity-header.stories.tsx +43 -0
  173. package/src/components/entity-header.test.tsx +35 -0
  174. package/src/components/entity-header.tsx +92 -0
  175. package/src/components/feature-grid.stories.tsx +25 -0
  176. package/src/components/feature-grid.tsx +87 -0
  177. package/src/components/feedback-bar.stories.tsx +25 -0
  178. package/src/components/feedback-bar.test.tsx +24 -0
  179. package/src/components/feedback-bar.tsx +62 -0
  180. package/src/components/footer.stories.tsx +23 -0
  181. package/src/components/footer.tsx +78 -0
  182. package/src/components/heading.stories.tsx +45 -0
  183. package/src/components/heading.test.tsx +22 -0
  184. package/src/components/heading.tsx +77 -0
  185. package/src/components/hero.stories.tsx +38 -0
  186. package/src/components/hero.tsx +498 -0
  187. package/src/components/input-button.stories.tsx +19 -0
  188. package/src/components/input-button.tsx +112 -0
  189. package/src/components/input-group.stories.tsx +38 -0
  190. package/src/components/input-group.tsx +157 -0
  191. package/src/components/input.stories.tsx +31 -0
  192. package/src/components/input.tsx +284 -0
  193. package/src/components/inset.stories.tsx +29 -0
  194. package/src/components/inset.tsx +70 -0
  195. package/src/components/kpi-card.stories.tsx +29 -0
  196. package/src/components/kpi-card.tsx +284 -0
  197. package/src/components/list-nav.stories.tsx +22 -0
  198. package/src/components/list-nav.tsx +145 -0
  199. package/src/components/logo-cloud.stories.tsx +23 -0
  200. package/src/components/logo-cloud.tsx +42 -0
  201. package/src/components/media-gallery.stories.tsx +25 -0
  202. package/src/components/media-gallery.tsx +347 -0
  203. package/src/components/message-actions.stories.tsx +45 -0
  204. package/src/components/message-actions.test.tsx +32 -0
  205. package/src/components/message-actions.tsx +119 -0
  206. package/src/components/nav-sidebar.stories.tsx +58 -0
  207. package/src/components/nav-sidebar.test.tsx +54 -0
  208. package/src/components/nav-sidebar.tsx +462 -0
  209. package/src/components/number-stepper.stories.tsx +19 -0
  210. package/src/components/number-stepper.tsx +158 -0
  211. package/src/components/page-header.stories.tsx +25 -0
  212. package/src/components/page-header.tsx +64 -0
  213. package/src/components/pagination.stories.tsx +110 -0
  214. package/src/components/pagination.test.tsx +46 -0
  215. package/src/components/pagination.tsx +144 -0
  216. package/src/components/popover.stories.tsx +26 -0
  217. package/src/components/popover.tsx +62 -0
  218. package/src/components/pricing.stories.tsx +42 -0
  219. package/src/components/pricing.tsx +130 -0
  220. package/src/components/progress.stories.tsx +22 -0
  221. package/src/components/progress.tsx +189 -0
  222. package/src/components/prompt-input.stories.tsx +62 -0
  223. package/src/components/prompt-input.test.tsx +106 -0
  224. package/src/components/prompt-input.tsx +343 -0
  225. package/src/components/prompt-suggestions.stories.tsx +53 -0
  226. package/src/components/prompt-suggestions.test.tsx +37 -0
  227. package/src/components/prompt-suggestions.tsx +122 -0
  228. package/src/components/quota-bar.stories.tsx +19 -0
  229. package/src/components/quota-bar.tsx +191 -0
  230. package/src/components/quote.stories.tsx +18 -0
  231. package/src/components/quote.tsx +43 -0
  232. package/src/components/radio-card.stories.tsx +27 -0
  233. package/src/components/radio-card.tsx +225 -0
  234. package/src/components/radio-group.stories.tsx +32 -0
  235. package/src/components/radio-group.tsx +68 -0
  236. package/src/components/reasoning.stories.tsx +50 -0
  237. package/src/components/reasoning.test.tsx +48 -0
  238. package/src/components/reasoning.tsx +143 -0
  239. package/src/components/search.stories.tsx +18 -0
  240. package/src/components/search.tsx +133 -0
  241. package/src/components/section-nav.stories.tsx +59 -0
  242. package/src/components/section-nav.test.tsx +36 -0
  243. package/src/components/section-nav.tsx +146 -0
  244. package/src/components/segmented-control.stories.tsx +22 -0
  245. package/src/components/segmented-control.tsx +301 -0
  246. package/src/components/select-button.stories.tsx +27 -0
  247. package/src/components/select-button.tsx +174 -0
  248. package/src/components/select.stories.tsx +27 -0
  249. package/src/components/select.tsx +198 -0
  250. package/src/components/separator.stories.tsx +22 -0
  251. package/src/components/separator.tsx +23 -0
  252. package/src/components/shimmer-text.stories.tsx +23 -0
  253. package/src/components/shimmer-text.test.tsx +35 -0
  254. package/src/components/shimmer-text.tsx +43 -0
  255. package/src/components/sidebar.stories.tsx +25 -0
  256. package/src/components/sidebar.tsx +234 -0
  257. package/src/components/skeleton.stories.tsx +22 -0
  258. package/src/components/skeleton.tsx +96 -0
  259. package/src/components/slider.stories.tsx +20 -0
  260. package/src/components/slider.tsx +176 -0
  261. package/src/components/slideshow.stories.tsx +30 -0
  262. package/src/components/slideshow.tsx +291 -0
  263. package/src/components/source.stories.tsx +32 -0
  264. package/src/components/source.test.tsx +40 -0
  265. package/src/components/source.tsx +161 -0
  266. package/src/components/spinner.stories.tsx +19 -0
  267. package/src/components/spinner.tsx +76 -0
  268. package/src/components/stat-strip.stories.tsx +36 -0
  269. package/src/components/stat-strip.test.tsx +25 -0
  270. package/src/components/stat-strip.tsx +85 -0
  271. package/src/components/stats.stories.tsx +24 -0
  272. package/src/components/stats.tsx +83 -0
  273. package/src/components/step-indicator.stories.tsx +18 -0
  274. package/src/components/step-indicator.test.tsx +18 -0
  275. package/src/components/step-indicator.tsx +286 -0
  276. package/src/components/switch.stories.tsx +26 -0
  277. package/src/components/switch.tsx +32 -0
  278. package/src/components/table.stories.tsx +38 -0
  279. package/src/components/table.tsx +220 -0
  280. package/src/components/tabs.stories.tsx +25 -0
  281. package/src/components/tabs.tsx +276 -0
  282. package/src/components/testimonials.stories.tsx +22 -0
  283. package/src/components/testimonials.tsx +109 -0
  284. package/src/components/text-link.stories.tsx +47 -0
  285. package/src/components/text-link.test.tsx +18 -0
  286. package/src/components/text-link.tsx +88 -0
  287. package/src/components/text.stories.tsx +49 -0
  288. package/src/components/text.tsx +105 -0
  289. package/src/components/thinking-bar.stories.tsx +19 -0
  290. package/src/components/thinking-bar.test.tsx +27 -0
  291. package/src/components/thinking-bar.tsx +64 -0
  292. package/src/components/thinking-steps.stories.tsx +44 -0
  293. package/src/components/thinking-steps.test.tsx +42 -0
  294. package/src/components/thinking-steps.tsx +92 -0
  295. package/src/components/toaster.stories.tsx +80 -0
  296. package/src/components/toaster.tsx +36 -0
  297. package/src/components/toggle.stories.tsx +21 -0
  298. package/src/components/toggle.tsx +103 -0
  299. package/src/components/tool-call-card.stories.tsx +97 -0
  300. package/src/components/tool-call-card.test.tsx +88 -0
  301. package/src/components/tool-call-card.tsx +231 -0
  302. package/src/components/tooltip.stories.tsx +25 -0
  303. package/src/components/tooltip.tsx +43 -0
  304. package/src/components/top-nav.stories.tsx +20 -0
  305. package/src/components/top-nav.tsx +76 -0
  306. package/src/docs/Colors.stories.tsx +192 -0
  307. package/src/docs/GettingStarted.mdx +72 -0
  308. package/src/docs/Mcp.mdx +52 -0
  309. package/src/docs/Typography.stories.tsx +114 -0
  310. package/src/hooks/use-in-view.ts +32 -0
  311. package/src/hooks/use-reduced-motion.ts +26 -0
  312. package/src/index.ts +343 -0
  313. package/src/lib/cn.test.ts +67 -0
  314. package/src/lib/cn.ts +81 -0
  315. package/src/lib/motion.ts +109 -0
  316. package/src/lib/prefix-utilities.test.ts +56 -0
  317. package/src/lib/prefix-utilities.ts +50 -0
  318. package/src/styles/base.css +489 -0
  319. package/src/styles/motion.css +69 -0
  320. package/src/styles/overrides.css +44 -0
  321. package/src/styles/typography.css +173 -0
  322. package/src/theme/theme.css +391 -0
  323. package/src/theme/tokens.ts +29 -0
  324. package/src/theme/types.ts +26 -0
  325. package/README.md +0 -3
@@ -0,0 +1,367 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ChevronLeft, ChevronRight } from "lucide-react"
5
+ import { cn } from "../lib/cn"
6
+
7
+ // ─── Types ──────────────────────────────────────────────────────────────────────
8
+
9
+ export type CalendarMode = "single" | "range"
10
+
11
+ /** A start/end pair. `to` is undefined while the user is mid-selection. */
12
+ export interface DateRange {
13
+ from: Date | undefined
14
+ to: Date | undefined
15
+ }
16
+
17
+ export type CalendarValue = Date | DateRange | undefined
18
+
19
+ export interface CalendarProps
20
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "defaultValue" | "onChange"> {
21
+ /** Selection mode. `single` picks one date; `range` picks a start→end span. */
22
+ mode?: CalendarMode
23
+ /** Controlled selection. `Date | undefined` in single mode, `DateRange` in range mode. */
24
+ value?: CalendarValue
25
+ /** Uncontrolled initial selection. */
26
+ defaultValue?: CalendarValue
27
+ onValueChange?: (value: CalendarValue) => void
28
+ /** Controlled displayed month (any day within it). */
29
+ month?: Date
30
+ defaultMonth?: Date
31
+ onMonthChange?: (month: Date) => void
32
+ /** Selectable bounds (inclusive). */
33
+ min?: Date
34
+ max?: Date
35
+ /** Return true to disable a specific day (evaluated after min/max). */
36
+ isDateDisabled?: (date: Date) => boolean
37
+ /** 0 = Sunday … 6 = Saturday. Defaults to the locale's first weekday (Mon fallback). */
38
+ weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6
39
+ /** BCP-47 locale for month/weekday names + first weekday. Defaults to the runtime locale. */
40
+ locale?: string
41
+ /** Render leading/trailing days from adjacent months (muted). Default true. */
42
+ showOutsideDays?: boolean
43
+ }
44
+
45
+ // ─── Pure date helpers (local time, no dependency) ───────────────────────────────
46
+
47
+ const startOfDay = (d: Date) => { const x = new Date(d); x.setHours(0, 0, 0, 0); return x }
48
+ const startOfMonth = (d: Date) => { const x = new Date(d); x.setDate(1); x.setHours(0, 0, 0, 0); return x }
49
+ const addDays = (d: Date, n: number) => { const x = new Date(d); x.setDate(x.getDate() + n); return x }
50
+ const addMonths = (d: Date, n: number) => { const x = startOfMonth(d); x.setMonth(x.getMonth() + n); return x }
51
+ const isSameDay = (a?: Date, b?: Date) =>
52
+ !!a && !!b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
53
+ const isSameMonth = (a: Date, b: Date) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth()
54
+ /** a < b < c, comparing whole days. */
55
+ const isBetween = (d: Date, a: Date, b: Date) => {
56
+ const t = startOfDay(d).getTime()
57
+ const lo = Math.min(startOfDay(a).getTime(), startOfDay(b).getTime())
58
+ const hi = Math.max(startOfDay(a).getTime(), startOfDay(b).getTime())
59
+ return t > lo && t < hi
60
+ }
61
+
62
+ /** Locale's first weekday (1=Mon…7=Sun via `weekInfo`) mapped to 0=Sun…6=Sat. Mon fallback. */
63
+ function localeWeekStart(locale: string): number {
64
+ try {
65
+ const loc = new Intl.Locale(locale)
66
+ // `weekInfo` is a getter in newer engines, a property in others.
67
+ const info = (loc as unknown as { weekInfo?: { firstDay?: number }; getWeekInfo?: () => { firstDay?: number } })
68
+ const first = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
69
+ if (first) return first % 7 // 7 (Sun) → 0
70
+ } catch { /* Intl.Locale/weekInfo unsupported */ }
71
+ return 1 // Monday
72
+ }
73
+
74
+ /** The 6×7 grid of days for `month`, starting on `weekStartsOn`. */
75
+ function monthMatrix(month: Date, weekStartsOn: number): Date[] {
76
+ const first = startOfMonth(month)
77
+ const lead = (first.getDay() - weekStartsOn + 7) % 7
78
+ const start = addDays(first, -lead)
79
+ return Array.from({ length: 42 }, (_, i) => addDays(start, i))
80
+ }
81
+
82
+ const isRange = (v: CalendarValue): v is DateRange =>
83
+ !!v && typeof v === "object" && !(v instanceof Date)
84
+
85
+ // ─── Component ───────────────────────────────────────────────────────────────────
86
+
87
+ const CELL = "ax:size-9" // 36px tap target
88
+ const WEEKDAYS_REF = new Date(2021, 7, 1) // a Sunday, anchor for weekday name generation
89
+
90
+ export const Calendar = React.forwardRef<HTMLDivElement, CalendarProps>(
91
+ (
92
+ {
93
+ mode = "single",
94
+ value: valueProp,
95
+ defaultValue,
96
+ onValueChange,
97
+ month: monthProp,
98
+ defaultMonth,
99
+ onMonthChange,
100
+ min,
101
+ max,
102
+ isDateDisabled,
103
+ weekStartsOn,
104
+ locale,
105
+ showOutsideDays = true,
106
+ className,
107
+ ...props
108
+ },
109
+ ref,
110
+ ) => {
111
+ const today = React.useMemo(() => startOfDay(new Date()), [])
112
+ // Resolve the browser locale only AFTER mount. SSR and the first client render
113
+ // must use a fixed locale ("en"); otherwise Intl weekday names + first weekday
114
+ // differ between server (Node default) and client (browser) and break hydration.
115
+ const [clientLocale, setClientLocale] = React.useState<string>()
116
+ React.useEffect(() => {
117
+ if (!locale && typeof navigator !== "undefined") setClientLocale(navigator.language)
118
+ }, [locale])
119
+ const activeLocale = locale ?? clientLocale ?? "en"
120
+
121
+ const derivedWeekStart = React.useMemo(() => localeWeekStart(activeLocale), [activeLocale])
122
+ const weekStart = weekStartsOn ?? derivedWeekStart
123
+
124
+ // Controlled/uncontrolled selection.
125
+ const [valueState, setValueState] = React.useState<CalendarValue>(defaultValue)
126
+ const value = valueProp !== undefined ? valueProp : valueState
127
+ const setValue = (next: CalendarValue) => {
128
+ if (valueProp === undefined) setValueState(next)
129
+ onValueChange?.(next)
130
+ }
131
+
132
+ // First selected date, used to seed the displayed month + focus.
133
+ const anchor = isRange(value) ? value.from : (value as Date | undefined)
134
+
135
+ // Controlled/uncontrolled displayed month.
136
+ const [monthState, setMonthState] = React.useState<Date>(
137
+ () => startOfMonth(defaultMonth ?? anchor ?? today),
138
+ )
139
+ const month = monthProp ? startOfMonth(monthProp) : monthState
140
+ const setMonth = (next: Date) => {
141
+ const m = startOfMonth(next)
142
+ if (!monthProp) setMonthState(m)
143
+ onMonthChange?.(m)
144
+ }
145
+
146
+ // Roving focus target (a day inside the displayed month).
147
+ const [focusedDate, setFocusedDate] = React.useState<Date>(
148
+ () => (anchor && isSameMonth(anchor, month) ? startOfDay(anchor)
149
+ : isSameMonth(today, month) ? today
150
+ : startOfMonth(month)),
151
+ )
152
+ // Keep the focus target inside the visible month when the month changes.
153
+ React.useEffect(() => {
154
+ if (!isSameMonth(focusedDate, month)) {
155
+ const day = Math.min(focusedDate.getDate(), new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate())
156
+ setFocusedDate(new Date(month.getFullYear(), month.getMonth(), day))
157
+ }
158
+ }, [month]) // eslint-disable-line react-hooks/exhaustive-deps
159
+
160
+ // Move DOM focus to the focused cell only after keyboard navigation.
161
+ const focusBtnRef = React.useRef<HTMLButtonElement>(null)
162
+ const shouldFocus = React.useRef(false)
163
+ React.useEffect(() => {
164
+ if (shouldFocus.current) { focusBtnRef.current?.focus(); shouldFocus.current = false }
165
+ }, [focusedDate])
166
+
167
+ // RTL: read the actual resolved direction so arrow keys stay intuitive.
168
+ const rootRef = React.useRef<HTMLDivElement>(null)
169
+ const isRtl = () => rootRef.current ? getComputedStyle(rootRef.current).direction === "rtl" : false
170
+
171
+ const dayDisabled = React.useCallback((d: Date) => {
172
+ if (min && startOfDay(d) < startOfDay(min)) return true
173
+ if (max && startOfDay(d) > startOfDay(max)) return true
174
+ return isDateDisabled?.(d) ?? false
175
+ }, [min, max, isDateDisabled])
176
+
177
+ const select = (d: Date) => {
178
+ if (dayDisabled(d)) return
179
+ const day = startOfDay(d)
180
+ if (mode === "range") {
181
+ const r = isRange(value) ? value : { from: undefined, to: undefined }
182
+ let next: DateRange
183
+ if (!r.from || r.to) next = { from: day, to: undefined } // start fresh
184
+ else next = day < r.from ? { from: day, to: r.from } : { from: r.from, to: day } // close the span
185
+ setValue(next)
186
+ } else {
187
+ setValue(day)
188
+ }
189
+ }
190
+
191
+ const captionId = React.useId()
192
+ const monthLabel = React.useMemo(
193
+ () => new Intl.DateTimeFormat(activeLocale, { month: "long", year: "numeric" }).format(month),
194
+ [month, activeLocale],
195
+ )
196
+ // Weekday headers, generated for the active locale + first weekday.
197
+ const weekdays = React.useMemo(() => {
198
+ const short = new Intl.DateTimeFormat(activeLocale, { weekday: "short" })
199
+ const long = new Intl.DateTimeFormat(activeLocale, { weekday: "long" })
200
+ const sundayOffset = WEEKDAYS_REF.getDay() // 0
201
+ return Array.from({ length: 7 }, (_, i) => {
202
+ const d = addDays(WEEKDAYS_REF, (weekStart + i - sundayOffset + 7) % 7)
203
+ return { short: short.format(d), long: long.format(d) }
204
+ })
205
+ }, [activeLocale, weekStart])
206
+ const fullDate = React.useMemo(
207
+ () => new Intl.DateTimeFormat(activeLocale, { weekday: "long", year: "numeric", month: "long", day: "numeric" }),
208
+ [activeLocale],
209
+ )
210
+
211
+ const days = React.useMemo(() => monthMatrix(month, weekStart), [month, weekStart])
212
+
213
+ const prevDisabled = !!min && startOfMonth(min) > addMonths(month, -1)
214
+ const nextDisabled = !!max && startOfMonth(max) < addMonths(month, 1)
215
+
216
+ const onKeyDown = (e: React.KeyboardEvent) => {
217
+ const rtl = isRtl()
218
+ let next: Date | null = null
219
+ switch (e.key) {
220
+ case "ArrowLeft": next = addDays(focusedDate, rtl ? 1 : -1); break
221
+ case "ArrowRight": next = addDays(focusedDate, rtl ? -1 : 1); break
222
+ case "ArrowUp": next = addDays(focusedDate, -7); break
223
+ case "ArrowDown": next = addDays(focusedDate, 7); break
224
+ case "Home": next = addDays(focusedDate, -((focusedDate.getDay() - weekStart + 7) % 7)); break
225
+ case "End": next = addDays(focusedDate, 6 - ((focusedDate.getDay() - weekStart + 7) % 7)); break
226
+ case "PageUp": next = addMonths(focusedDate, e.shiftKey ? -12 : -1); break
227
+ case "PageDown": next = addMonths(focusedDate, e.shiftKey ? 12 : 1); break
228
+ case "Enter":
229
+ case " ": e.preventDefault(); select(focusedDate); return
230
+ default: return
231
+ }
232
+ e.preventDefault()
233
+ const clamped = startOfDay(next)
234
+ shouldFocus.current = true
235
+ setFocusedDate(clamped)
236
+ if (!isSameMonth(clamped, month)) setMonth(clamped)
237
+ }
238
+
239
+ return (
240
+ <div
241
+ ref={(node) => {
242
+ rootRef.current = node
243
+ if (typeof ref === "function") ref(node)
244
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node
245
+ }}
246
+ className={cn("ax:inline-block ax:select-none ax:p-sm", className)}
247
+ {...props}
248
+ >
249
+ {/* Caption + month navigation */}
250
+ <div className="ax:flex ax:items-center ax:justify-between ax:gap-sm ax:pb-sm">
251
+ <NavButton label="Previous month" disabled={prevDisabled} onClick={() => setMonth(addMonths(month, -1))}>
252
+ <ChevronLeft className="ax:size-4 ax:rtl:-scale-x-100" />
253
+ </NavButton>
254
+ <div id={captionId} aria-live="polite" className="ax:t-label-md ax:text-foreground-strong">
255
+ {monthLabel}
256
+ </div>
257
+ <NavButton label="Next month" disabled={nextDisabled} onClick={() => setMonth(addMonths(month, 1))}>
258
+ <ChevronRight className="ax:size-4 ax:rtl:-scale-x-100" />
259
+ </NavButton>
260
+ </div>
261
+
262
+ {/* Day grid */}
263
+ <div role="grid" aria-labelledby={captionId} className="ax:w-max">
264
+ <div role="row" className="ax:flex">
265
+ {weekdays.map((w) => (
266
+ <div
267
+ key={w.long}
268
+ role="columnheader"
269
+ aria-label={w.long}
270
+ className={cn(CELL, "ax:flex ax:items-center ax:justify-center ax:t-label-2xs ax:text-muted-foreground")}
271
+ >
272
+ {w.short}
273
+ </div>
274
+ ))}
275
+ </div>
276
+
277
+ {/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
278
+ {Array.from({ length: 6 }, (_, w) => (
279
+ <div role="row" key={w} className="ax:flex">
280
+ {days.slice(w * 7, w * 7 + 7).map((day) => {
281
+ const outside = !isSameMonth(day, month)
282
+ if (outside && !showOutsideDays) return <div key={+day} role="gridcell" className={CELL} />
283
+
284
+ const disabled = dayDisabled(day)
285
+ const isToday = isSameDay(day, today)
286
+ const range = isRange(value) ? value : undefined
287
+ const selected =
288
+ mode === "range"
289
+ ? isSameDay(day, range?.from) || isSameDay(day, range?.to)
290
+ : isSameDay(day, value as Date | undefined)
291
+ const inRange = !!range?.from && !!range?.to && isBetween(day, range.from, range.to)
292
+ const isRangeStart = mode === "range" && isSameDay(day, range?.from) && !!range?.to
293
+ const isRangeEnd = mode === "range" && isSameDay(day, range?.to)
294
+ const isFocusTarget = isSameDay(day, focusedDate)
295
+
296
+ return (
297
+ <div
298
+ key={+day}
299
+ role="gridcell"
300
+ aria-selected={selected || undefined}
301
+ className={cn(
302
+ CELL,
303
+ "ax:relative ax:flex ax:items-center ax:justify-center",
304
+ // Continuous range highlight lives on the cell so it butts up edge-to-edge.
305
+ (inRange || isRangeStart || isRangeEnd) && "ax:bg-primary-weakest",
306
+ isRangeStart && "ax:rounded-s-xs",
307
+ isRangeEnd && "ax:rounded-e-xs",
308
+ )}
309
+ >
310
+ <button
311
+ ref={isFocusTarget ? focusBtnRef : undefined}
312
+ type="button"
313
+ tabIndex={isFocusTarget ? 0 : -1}
314
+ disabled={disabled}
315
+ aria-label={fullDate.format(day)}
316
+ aria-current={isToday ? "date" : undefined}
317
+ onClick={() => { setFocusedDate(startOfDay(day)); if (outside) setMonth(day); select(day) }}
318
+ onKeyDown={onKeyDown}
319
+ className={cn(
320
+ "ax:flex ax:size-8 ax:items-center ax:justify-center ax:rounded-xs ax:t-body-sm",
321
+ "ax:transition-colors ax:transition-snappy",
322
+ "ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring ax:focus-visible:ring-offset-1 ax:focus-visible:ring-offset-background",
323
+ "ax:disabled:pointer-events-none ax:disabled:opacity-40",
324
+ selected
325
+ ? "ax:bg-primary-weakest ax:text-primary ax:font-medium ax:hover:bg-primary-weakest"
326
+ : cn(
327
+ "ax:hover:bg-muted",
328
+ outside ? "ax:text-muted-foreground/70" : "ax:text-foreground",
329
+ isToday && "ax:font-semibold ax:text-primary",
330
+ ),
331
+ )}
332
+ >
333
+ {day.getDate()}
334
+ </button>
335
+ </div>
336
+ )
337
+ })}
338
+ </div>
339
+ ))}
340
+ </div>
341
+ </div>
342
+ )
343
+ },
344
+ )
345
+ Calendar.displayName = "Calendar"
346
+
347
+ // Icon-only month-nav control - plain button (no Button dep) with the DS focus ring.
348
+ function NavButton({ label, disabled, onClick, children }: {
349
+ label: string; disabled?: boolean; onClick: () => void; children: React.ReactNode
350
+ }) {
351
+ return (
352
+ <button
353
+ type="button"
354
+ aria-label={label}
355
+ disabled={disabled}
356
+ onClick={onClick}
357
+ className={cn(
358
+ "ax:flex ax:size-8 ax:items-center ax:justify-center ax:rounded-md ax:text-muted-foreground",
359
+ "ax:hover:bg-muted ax:hover:text-foreground ax:transition-colors ax:transition-snappy ax:cursor-pointer",
360
+ "ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring ax:focus-visible:ring-offset-1 ax:focus-visible:ring-offset-background",
361
+ "ax:disabled:pointer-events-none ax:disabled:opacity-40",
362
+ )}
363
+ >
364
+ {children}
365
+ </button>
366
+ )
367
+ }
@@ -0,0 +1,44 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./card"
4
+ import { Button } from "./button"
5
+
6
+ const meta: Meta<typeof Card> = {
7
+ title: "Components/Card",
8
+ component: Card,
9
+ tags: ["autodocs"],
10
+ }
11
+
12
+ export default meta
13
+ type Story = StoryObj<typeof Card>
14
+
15
+ export const Default: Story = {
16
+ render: () => (
17
+ <Card className="ax:max-w-sm">
18
+ <CardHeader>
19
+ <CardTitle>Project overview</CardTitle>
20
+ <CardDescription>Track progress across your team.</CardDescription>
21
+ </CardHeader>
22
+ <CardContent>
23
+ <p>12 tasks completed this week.</p>
24
+ </CardContent>
25
+ <CardFooter>
26
+ <Button size="sm">View details</Button>
27
+ </CardFooter>
28
+ </Card>
29
+ ),
30
+ }
31
+
32
+ export const Sectioned: Story = {
33
+ render: () => (
34
+ <Card sectioned className="ax:max-w-sm">
35
+ <CardHeader size="lg">
36
+ <CardTitle>Billing</CardTitle>
37
+ <CardDescription>Manage your subscription.</CardDescription>
38
+ </CardHeader>
39
+ <CardContent>
40
+ <p>Next invoice on Sep 1.</p>
41
+ </CardContent>
42
+ </Card>
43
+ ),
44
+ }