@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,96 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cva, type VariantProps } from "class-variance-authority"
5
+ import { cn } from "../lib/cn"
6
+
7
+ // ─── Types ────────────────────────────────────────────────────────────────────
8
+
9
+ export type SkeletonVariant = "pulse" | "shimmer" | "none"
10
+
11
+ // ─── Variants ─────────────────────────────────────────────────────────────────
12
+
13
+ const skeletonVariants = cva(
14
+ "ax:rounded-md ax:bg-muted",
15
+ {
16
+ variants: {
17
+ variant: {
18
+ // Tailwind's built-in pulse: opacity oscillation. Quiet, ubiquitous.
19
+ pulse: "ax:animate-pulse ax:motion-reduce:animate-none",
20
+ // Sliding gradient overlay. Feels smoother on larger placeholder blocks
21
+ // (hero images, card previews). Uses an `::after` pseudo-element so the
22
+ // base bg-muted stays visible underneath.
23
+ shimmer: cn(
24
+ "ax:relative ax:overflow-hidden",
25
+ "ax:after:absolute ax:after:inset-0",
26
+ "ax:after:bg-gradient-to-r ax:after:from-transparent ax:after:via-foreground/[0.08] ax:after:to-transparent",
27
+ "ax:after:animate-skeleton-shimmer",
28
+ "ax:motion-reduce:after:animate-none",
29
+ ),
30
+ // Static placeholder with no animation. Use when motion is distracting
31
+ // (e.g. inside an already-animated parent) or for visual snapshots.
32
+ none: "",
33
+ },
34
+ },
35
+ defaultVariants: { variant: "pulse" },
36
+ }
37
+ )
38
+
39
+ // ─── Skeleton ─────────────────────────────────────────────────────────────────
40
+
41
+ export interface SkeletonProps
42
+ extends React.HTMLAttributes<HTMLDivElement>,
43
+ VariantProps<typeof skeletonVariants> {}
44
+
45
+ export const Skeleton = React.forwardRef<HTMLDivElement, SkeletonProps>(
46
+ ({ className, variant, ...props }, ref) => (
47
+ <div
48
+ ref={ref}
49
+ aria-hidden="true"
50
+ className={cn(skeletonVariants({ variant }), className)}
51
+ {...props}
52
+ />
53
+ )
54
+ )
55
+ Skeleton.displayName = "Skeleton"
56
+
57
+ // ─── SkeletonText ─────────────────────────────────────────────────────────────
58
+ //
59
+ // Stacked text-line placeholders with naturally varied widths: the recurring
60
+ // "3 paragraph lines" pattern in one component. The last line is always
61
+ // noticeably shorter to mimic a paragraph ending mid-line.
62
+
63
+ const TEXT_WIDTHS = ["ax:w-full", "ax:w-11/12", "ax:w-10/12", "ax:w-9/12"]
64
+
65
+ export interface SkeletonTextProps extends React.HTMLAttributes<HTMLDivElement> {
66
+ /** Number of text lines to render. Default 3. */
67
+ lines?: number
68
+ /** Animation variant - applied to every line. */
69
+ variant?: SkeletonVariant
70
+ /** Line height - applied to every line. Default `h-3`. */
71
+ lineClassName?: string
72
+ }
73
+
74
+ export const SkeletonText = React.forwardRef<HTMLDivElement, SkeletonTextProps>(
75
+ ({ className, lines = 3, variant, lineClassName, ...props }, ref) => (
76
+ <div
77
+ ref={ref}
78
+ aria-hidden="true"
79
+ className={cn("ax:space-y-2", className)}
80
+ {...props}
81
+ >
82
+ {Array.from({ length: lines }).map((_, i) => {
83
+ const isLast = i === lines - 1
84
+ const width = isLast ? "ax:w-2/3" : TEXT_WIDTHS[i % TEXT_WIDTHS.length]
85
+ return (
86
+ <Skeleton
87
+ key={i}
88
+ variant={variant}
89
+ className={cn("ax:h-3", width, lineClassName)}
90
+ />
91
+ )
92
+ })}
93
+ </div>
94
+ )
95
+ )
96
+ SkeletonText.displayName = "SkeletonText"
@@ -0,0 +1,20 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { Slider } from "./slider"
4
+
5
+ const meta: Meta<typeof Slider> = {
6
+ title: "Components/Slider",
7
+ component: Slider,
8
+ tags: ["autodocs"],
9
+ args: {
10
+ defaultValue: [50],
11
+ max: 100,
12
+ step: 1,
13
+ className: "ax:w-64",
14
+ },
15
+ }
16
+
17
+ export default meta
18
+ type Story = StoryObj<typeof Slider>
19
+
20
+ export const Default: Story = {}
@@ -0,0 +1,176 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as SliderPrimitive from "@radix-ui/react-slider"
5
+ import { cva, type VariantProps } from "class-variance-authority"
6
+ import { cn } from "../lib/cn"
7
+
8
+ export type SliderSize = "sm" | "md" | "lg"
9
+ export type SliderValueLabel = "always" | "active" | false
10
+
11
+ const trackVariants = cva(
12
+ "ax:relative ax:flex ax:w-full ax:touch-none ax:select-none ax:items-center",
13
+ {
14
+ variants: {
15
+ size: {
16
+ sm: "ax:h-4",
17
+ md: "ax:h-5",
18
+ lg: "ax:h-6",
19
+ },
20
+ },
21
+ defaultVariants: { size: "md" },
22
+ }
23
+ )
24
+
25
+ const TRACK_HEIGHT: Record<SliderSize, string> = {
26
+ sm: "ax:h-1",
27
+ md: "ax:h-1.5",
28
+ lg: "ax:h-2",
29
+ }
30
+
31
+ const THUMB_SIZE: Record<SliderSize, string> = {
32
+ sm: "ax:size-3",
33
+ md: "ax:size-4",
34
+ lg: "ax:size-5",
35
+ }
36
+
37
+ // How far above the thumb the value-label pill sits. Increases with thumb size
38
+ // so the gap between thumb and label stays visually consistent.
39
+ const LABEL_OFFSET: Record<SliderSize, string> = {
40
+ sm: "ax:-top-6",
41
+ md: "ax:-top-7",
42
+ lg: "ax:-top-8",
43
+ }
44
+
45
+ export interface SliderProps
46
+ extends React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>,
47
+ VariantProps<typeof trackVariants> {
48
+ /** Show the current value in a small pill above each thumb.
49
+ * - `"always"` - pill is always visible
50
+ * - `"active"` - pill appears on hover / focus / drag, hidden otherwise
51
+ * - `false` (default) - no pill */
52
+ valueLabel?: SliderValueLabel
53
+ /** Format the value rendered in the label (e.g. add a "%" suffix, currency).
54
+ * Default: the raw number. */
55
+ formatValue?: (value: number) => React.ReactNode
56
+ /** Render tick marks along the track.
57
+ * - `true` - auto-generate at every `step` between `min` and `max`
58
+ * (auto-generation is skipped when it would produce more than 21 marks
59
+ * - for fine sliders, provide an explicit array instead)
60
+ * - `number[]` - render marks at exactly the provided positions */
61
+ marks?: boolean | number[]
62
+ }
63
+
64
+ export const Slider = React.forwardRef<
65
+ React.ElementRef<typeof SliderPrimitive.Root>,
66
+ SliderProps
67
+ >(
68
+ (
69
+ {
70
+ className,
71
+ size,
72
+ valueLabel = false,
73
+ formatValue,
74
+ marks = false,
75
+ value,
76
+ defaultValue,
77
+ onValueChange,
78
+ min = 0,
79
+ max = 100,
80
+ step = 1,
81
+ ...props
82
+ },
83
+ ref
84
+ ) => {
85
+ const resolvedSize = size ?? "md"
86
+
87
+ // Track the live value so the per-thumb label has something to render in
88
+ // uncontrolled mode. In controlled mode the parent's `value` wins; in
89
+ // uncontrolled mode the internal mirror is updated via onValueChange.
90
+ const [internalValue, setInternalValue] = React.useState<number[]>(
91
+ () => value ?? defaultValue ?? [min]
92
+ )
93
+ const currentValue = value ?? internalValue
94
+ const handleValueChange = (v: number[]) => {
95
+ setInternalValue(v)
96
+ onValueChange?.(v)
97
+ }
98
+
99
+ const thumbCount = currentValue.length
100
+
101
+ // Compute mark positions. When `marks === true`, generate one per step;
102
+ // bail out when the resulting count would be unreasonable (fine sliders).
103
+ const markPositions: number[] = React.useMemo(() => {
104
+ if (!marks) return []
105
+ if (Array.isArray(marks)) return marks
106
+ const out: number[] = []
107
+ for (let v = min; v <= max; v += step) out.push(v)
108
+ return out.length <= 21 ? out : []
109
+ }, [marks, min, max, step])
110
+
111
+ return (
112
+ <SliderPrimitive.Root
113
+ ref={ref}
114
+ value={value}
115
+ defaultValue={defaultValue}
116
+ onValueChange={handleValueChange}
117
+ min={min}
118
+ max={max}
119
+ step={step}
120
+ className={cn(trackVariants({ size }), className)}
121
+ {...props}
122
+ >
123
+ <SliderPrimitive.Track
124
+ className={cn(
125
+ "ax:relative ax:w-full ax:grow ax:overflow-hidden ax:rounded-full ax:bg-muted",
126
+ TRACK_HEIGHT[resolvedSize]
127
+ )}
128
+ >
129
+ <SliderPrimitive.Range className="ax:absolute ax:h-full ax:bg-primary" />
130
+ {markPositions.map((m) => (
131
+ <span
132
+ key={m}
133
+ aria-hidden="true"
134
+ style={{ left: `${((m - min) / (max - min)) * 100}%` }}
135
+ className="ax:absolute ax:top-1/2 ax:-translate-x-1/2 ax:-translate-y-1/2 ax:size-1 ax:rounded-full ax:bg-foreground/40 ax:pointer-events-none"
136
+ />
137
+ ))}
138
+ </SliderPrimitive.Track>
139
+
140
+ {Array.from({ length: thumbCount }).map((_, i) => {
141
+ const v = currentValue[i] ?? min
142
+ return (
143
+ <SliderPrimitive.Thumb
144
+ key={i}
145
+ className={cn(
146
+ "ax:group ax:relative ax:block ax:rounded-full ax:border-2 ax:border-primary ax:bg-background ax:cursor-grab ax:active:cursor-grabbing",
147
+ "ax:transition-colors ax:transition-snappy",
148
+ "ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring ax:focus-visible:ring-offset-2 ax:focus-visible:ring-offset-background",
149
+ "ax:disabled:pointer-events-none ax:disabled:opacity-50",
150
+ THUMB_SIZE[resolvedSize]
151
+ )}
152
+ >
153
+ {valueLabel && (
154
+ <span
155
+ aria-hidden="true"
156
+ className={cn(
157
+ "ax:absolute ax:left-1/2 ax:-translate-x-1/2 ax:px-1.5 ax:py-0.5 ax:rounded ax:text-xs ax:font-medium ax:tabular-nums ax:whitespace-nowrap ax:pointer-events-none",
158
+ "ax:bg-foreground ax:text-background",
159
+ LABEL_OFFSET[resolvedSize],
160
+ valueLabel === "active" && [
161
+ "ax:opacity-0 ax:transition-opacity",
162
+ "ax:group-hover:opacity-100 ax:group-focus-visible:opacity-100 ax:group-active:opacity-100",
163
+ ]
164
+ )}
165
+ >
166
+ {formatValue ? formatValue(v) : v}
167
+ </span>
168
+ )}
169
+ </SliderPrimitive.Thumb>
170
+ )
171
+ })}
172
+ </SliderPrimitive.Root>
173
+ )
174
+ }
175
+ )
176
+ Slider.displayName = SliderPrimitive.Root.displayName
@@ -0,0 +1,30 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { Slideshow, SlideshowSlide } from "./slideshow"
4
+
5
+ const meta: Meta<typeof Slideshow> = {
6
+ title: "Components/Slideshow",
7
+ component: Slideshow,
8
+ tags: ["autodocs"],
9
+ }
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof Slideshow>
13
+
14
+ const slides = [
15
+ { src: "https://picsum.photos/800/400?random=10", alt: "Slide 1" },
16
+ { src: "https://picsum.photos/800/400?random=11", alt: "Slide 2" },
17
+ { src: "https://picsum.photos/800/400?random=12", alt: "Slide 3" },
18
+ ]
19
+
20
+ export const Default: Story = {
21
+ render: () => (
22
+ <Slideshow className="ax:max-w-xl">
23
+ {slides.map((slide) => (
24
+ <SlideshowSlide key={slide.src}>
25
+ <img src={slide.src} alt={slide.alt} className="ax:w-full ax:rounded-md ax:object-cover ax:aspect-[2/1]" />
26
+ </SlideshowSlide>
27
+ ))}
28
+ </Slideshow>
29
+ ),
30
+ }
@@ -0,0 +1,291 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { motion, AnimatePresence } from "motion/react"
5
+ import { ChevronLeft, ChevronRight, Pause, Play } from "lucide-react"
6
+ import { cn } from "../lib/cn"
7
+ import { useReducedMotion } from "../hooks/use-reduced-motion"
8
+ import { transitions } from "../lib/motion"
9
+
10
+ // ─── Types ────────────────────────────────────────────────────────────────────
11
+
12
+ export type SlideshowVariant = "slide" | "fade"
13
+
14
+ // ─── Slide ────────────────────────────────────────────────────────────────────
15
+
16
+ export interface SlideshowSlideProps extends React.HTMLAttributes<HTMLDivElement> {}
17
+
18
+ export const SlideshowSlide = React.forwardRef<HTMLDivElement, SlideshowSlideProps>(
19
+ ({ className, children, ...props }, ref) => (
20
+ <div ref={ref} className={cn("ax:w-full ax:select-none", className)} {...props}>
21
+ {children}
22
+ </div>
23
+ )
24
+ )
25
+ SlideshowSlide.displayName = "SlideshowSlide"
26
+
27
+ // ─── Root ─────────────────────────────────────────────────────────────────────
28
+
29
+ export interface SlideshowProps extends React.HTMLAttributes<HTMLDivElement> {
30
+ /** Transition style - "slide" (default) or "fade". */
31
+ variant?: SlideshowVariant
32
+ /** Auto-advance interval in ms. Pauses on hover / focus / explicit toggle.
33
+ * Pass `false` to disable (default). When enabled, a visible pause button
34
+ * appears in the top-right corner. */
35
+ autoplay?: number | false
36
+ /** Wrap around at the edges (default true). */
37
+ loop?: boolean
38
+ /** Show prev/next arrow buttons (default true). */
39
+ showArrows?: boolean
40
+ /** Show dot indicators (default true). */
41
+ showDots?: boolean
42
+ /** Controlled active index. */
43
+ index?: number
44
+ /** Callback fired when the active slide changes. */
45
+ onIndexChange?: (index: number) => void
46
+ }
47
+
48
+ export const Slideshow = React.forwardRef<HTMLDivElement, SlideshowProps>(
49
+ (
50
+ {
51
+ className,
52
+ children,
53
+ variant = "slide",
54
+ autoplay = false,
55
+ loop = true,
56
+ showArrows = true,
57
+ showDots = true,
58
+ index: controlledIndex,
59
+ onIndexChange,
60
+ ...props
61
+ },
62
+ ref
63
+ ) => {
64
+ const reduce = useReducedMotion()
65
+ const slides = React.Children.toArray(children)
66
+ const count = slides.length
67
+
68
+ // [activeIndex, direction] - direction: 1 = forward, -1 = backward.
69
+ const [[activeIndex, direction], setSlide] = React.useState<[number, number]>([
70
+ controlledIndex ?? 0,
71
+ 1,
72
+ ])
73
+
74
+ // Pause state. Two sources combine:
75
+ // - `transientPause` - set while hover/focus is inside the carousel
76
+ // (per WAI-ARIA carousel guidance: must pause on pointer + keyboard)
77
+ // - `userPaused` - toggled by the explicit pause/play button
78
+ // (per WCAG 2.2.2: pause control for auto-moving content)
79
+ // Effective pause = either source set.
80
+ const [transientPause, setTransientPause] = React.useState(false)
81
+ const [userPaused, setUserPaused] = React.useState(false)
82
+ const isPaused = transientPause || userPaused
83
+
84
+ // Sync with controlled index when it changes externally.
85
+ React.useEffect(() => {
86
+ if (controlledIndex === undefined) return
87
+ setSlide(([prev]) => [controlledIndex, controlledIndex >= prev ? 1 : -1])
88
+ }, [controlledIndex])
89
+
90
+ const go = React.useCallback(
91
+ (next: number) => {
92
+ setSlide(([prev]) => {
93
+ const clamped = loop
94
+ ? ((next % count) + count) % count
95
+ : Math.min(Math.max(next, 0), count - 1)
96
+ onIndexChange?.(clamped)
97
+ return [clamped, next >= prev ? 1 : -1]
98
+ })
99
+ },
100
+ [count, loop, onIndexChange]
101
+ )
102
+
103
+ // Autoplay - interval cleared/restarted whenever pause state changes.
104
+ React.useEffect(() => {
105
+ if (!autoplay || count < 2 || isPaused) return
106
+ const id = setInterval(() => go(activeIndex + 1), autoplay as number)
107
+ return () => clearInterval(id)
108
+ }, [autoplay, activeIndex, count, go, isPaused])
109
+
110
+ const handleKeyDown = (e: React.KeyboardEvent) => {
111
+ if (e.key === "ArrowLeft") go(activeIndex - 1)
112
+ if (e.key === "ArrowRight") go(activeIndex + 1)
113
+ }
114
+
115
+ // Slide variants - direction-aware horizontal translate.
116
+ const slideVariants = {
117
+ enter: (dir: number) => ({ x: reduce ? 0 : `${dir * 100}%`, opacity: reduce ? 0 : 1 }),
118
+ center: { x: "0%", opacity: 1 },
119
+ exit: (dir: number) => ({ x: reduce ? 0 : `${dir * -100}%`, opacity: reduce ? 0 : 1 }),
120
+ }
121
+
122
+ const fadeVariants = {
123
+ enter: { opacity: 0 },
124
+ center: { opacity: 1 },
125
+ exit: { opacity: 0 },
126
+ }
127
+
128
+ const canGoPrev = loop || activeIndex > 0
129
+ const canGoNext = loop || activeIndex < count - 1
130
+
131
+ // Autoplay progress duration in seconds, used by the active-dot fill.
132
+ const autoplaySec = autoplay ? (autoplay as number) / 1000 : 0
133
+
134
+ return (
135
+ <div
136
+ ref={ref}
137
+ role="region"
138
+ aria-roledescription="carousel"
139
+ tabIndex={0}
140
+ onKeyDown={handleKeyDown}
141
+ onMouseEnter={() => setTransientPause(true)}
142
+ onMouseLeave={() => setTransientPause(false)}
143
+ onFocus={() => setTransientPause(true)}
144
+ onBlur={() => setTransientPause(false)}
145
+ className={cn(
146
+ "ax:relative ax:overflow-hidden ax:rounded-md",
147
+ "ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring ax:focus-visible:ring-offset-2 ax:focus-visible:ring-offset-background",
148
+ className
149
+ )}
150
+ {...props}
151
+ >
152
+ {/* Viewport - only the active slide is rendered */}
153
+ <div aria-live="polite" aria-atomic="true">
154
+ <AnimatePresence initial={false} custom={direction} mode="popLayout">
155
+ <motion.div
156
+ key={activeIndex}
157
+ custom={direction}
158
+ role="group"
159
+ aria-roledescription="slide"
160
+ aria-label={`${activeIndex + 1} of ${count}`}
161
+ variants={variant === "fade" ? fadeVariants : slideVariants}
162
+ initial="enter"
163
+ animate="center"
164
+ exit="exit"
165
+ transition={
166
+ variant === "fade"
167
+ ? (reduce ? { duration: 0 } : transitions.smooth)
168
+ : transitions.spring
169
+ }
170
+ drag={count > 1 ? "x" : false}
171
+ dragConstraints={{ left: 0, right: 0 }}
172
+ dragElastic={0.12}
173
+ onDragEnd={(_, { offset }) => {
174
+ if (offset.x < -50) go(activeIndex + 1)
175
+ if (offset.x > 50) go(activeIndex - 1)
176
+ }}
177
+ className="ax:w-full"
178
+ >
179
+ {slides[activeIndex]}
180
+ </motion.div>
181
+ </AnimatePresence>
182
+ </div>
183
+
184
+ {/* Arrow controls */}
185
+ {showArrows && count > 1 && (
186
+ <>
187
+ <button
188
+ onClick={() => go(activeIndex - 1)}
189
+ disabled={!canGoPrev}
190
+ aria-label="Previous slide"
191
+ className={cn(
192
+ "ax:absolute ax:left-3 ax:top-1/2 ax:-translate-y-1/2 ax:z-10",
193
+ "ax:size-8 ax:flex ax:items-center ax:justify-center",
194
+ "ax:rounded-full ax:bg-background/80 ax:backdrop-blur-sm ax:border ax:border-border ax:shadow-sm",
195
+ "ax:text-foreground ax:hover:bg-background ax:transition-colors ax:cursor-pointer",
196
+ "ax:disabled:opacity-40 ax:disabled:cursor-not-allowed"
197
+ )}
198
+ >
199
+ <ChevronLeft className="ax:size-4" />
200
+ </button>
201
+
202
+ <button
203
+ onClick={() => go(activeIndex + 1)}
204
+ disabled={!canGoNext}
205
+ aria-label="Next slide"
206
+ className={cn(
207
+ "ax:absolute ax:right-3 ax:top-1/2 ax:-translate-y-1/2 ax:z-10",
208
+ "ax:size-8 ax:flex ax:items-center ax:justify-center",
209
+ "ax:rounded-full ax:bg-background/80 ax:backdrop-blur-sm ax:border ax:border-border ax:shadow-sm",
210
+ "ax:text-foreground ax:hover:bg-background ax:transition-colors ax:cursor-pointer",
211
+ "ax:disabled:opacity-40 ax:disabled:cursor-not-allowed"
212
+ )}
213
+ >
214
+ <ChevronRight className="ax:size-4" />
215
+ </button>
216
+ </>
217
+ )}
218
+
219
+ {/* Pause/play button - only when autoplay is on. WCAG 2.2.2 requires
220
+ * a control for auto-moving content longer than 5s; we offer one
221
+ * for any autoplay interval. */}
222
+ {autoplay && count > 1 && (
223
+ <button
224
+ onClick={() => setUserPaused((p) => !p)}
225
+ aria-label={userPaused ? "Play slideshow" : "Pause slideshow"}
226
+ aria-pressed={userPaused}
227
+ className={cn(
228
+ "ax:absolute ax:top-3 ax:right-3 ax:z-10",
229
+ "ax:size-8 ax:flex ax:items-center ax:justify-center",
230
+ "ax:rounded-full ax:bg-background/80 ax:backdrop-blur-sm ax:border ax:border-border ax:shadow-sm",
231
+ "ax:text-foreground ax:hover:bg-background ax:transition-colors ax:cursor-pointer"
232
+ )}
233
+ >
234
+ {userPaused ? <Play className="ax:size-3.5" /> : <Pause className="ax:size-3.5" />}
235
+ </button>
236
+ )}
237
+
238
+ {/* Dot indicators */}
239
+ {showDots && count > 1 && (
240
+ <div
241
+ role="tablist"
242
+ aria-label="Slides"
243
+ className="ax:absolute ax:bottom-3 ax:left-0 ax:right-0 ax:flex ax:justify-center ax:items-center ax:gap-1.5 ax:z-10"
244
+ >
245
+ {Array.from({ length: count }).map((_, i) => {
246
+ const isActive = i === activeIndex
247
+ return (
248
+ <button
249
+ key={i}
250
+ role="tab"
251
+ aria-selected={isActive}
252
+ aria-label={`Slide ${i + 1}`}
253
+ onClick={() => go(i)}
254
+ className={cn(
255
+ "ax:relative ax:overflow-hidden ax:rounded-full ax:transition-all ax:transition-snappy ax:cursor-pointer",
256
+ isActive
257
+ ? "ax:w-4 ax:h-1.5 ax:bg-foreground/20"
258
+ : "ax:size-1.5 ax:bg-foreground/30 ax:hover:bg-foreground/50"
259
+ )}
260
+ >
261
+ {isActive && (
262
+ autoplay && !isPaused ? (
263
+ // Progress fill - animates 0→100% width over the autoplay
264
+ // interval, re-keys (= restarts) on every slide change.
265
+ <motion.span
266
+ key={`progress-${activeIndex}`}
267
+ aria-hidden="true"
268
+ className="ax:absolute ax:inset-y-0 ax:left-0 ax:bg-primary ax:rounded-full ax:origin-left"
269
+ initial={{ scaleX: 0 }}
270
+ animate={{ scaleX: 1 }}
271
+ transition={{ duration: autoplaySec, ease: "linear" }}
272
+ style={{ width: "100%" }}
273
+ />
274
+ ) : (
275
+ // Static full fill when autoplay is off or paused.
276
+ <span
277
+ aria-hidden="true"
278
+ className="ax:absolute ax:inset-0 ax:bg-primary ax:rounded-full"
279
+ />
280
+ )
281
+ )}
282
+ </button>
283
+ )
284
+ })}
285
+ </div>
286
+ )}
287
+ </div>
288
+ )
289
+ }
290
+ )
291
+ Slideshow.displayName = "Slideshow"
@@ -0,0 +1,32 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { Source, SourceContent, SourceTrigger } from "./source"
4
+ import { Text } from "./text"
5
+
6
+ const meta: Meta<typeof Source> = {
7
+ title: "Components/Source",
8
+ component: Source,
9
+ tags: ["autodocs"],
10
+ decorators: [(Story) => <div style={{ maxWidth: 560, paddingBottom: 160 }}><Story /></div>],
11
+ }
12
+
13
+ export default meta
14
+ type Story = StoryObj<typeof Source>
15
+
16
+ export const InText: Story = {
17
+ render: () => (
18
+ <Text as="p" size="2">
19
+ Yas Acres townhouses with a private garden start at AED 4.2M
20
+ <Source href="https://www.propertyfinder.ae/en/search?c=1&l=yas-island">
21
+ <SourceTrigger favicon />
22
+ <SourceContent title="Townhouses for sale in Yas Island" description="42 townhouses under AED 5M, 11 with a private garden." />
23
+ </Source>
24
+ , below the AED 4.6M median sale
25
+ <Source href="https://www.dari.ae/en/market-insights">
26
+ <SourceTrigger label={2} />
27
+ <SourceContent title="Abu Dhabi market insights" description="Registered sales by community over the last 12 months." />
28
+ </Source>
29
+ .
30
+ </Text>
31
+ ),
32
+ }
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import * as React from "react"
3
+ import { renderToStaticMarkup } from "react-dom/server"
4
+ import { Source, SourceTrigger, sourceDomain } from "./source"
5
+
6
+ const HREF = "https://www.propertyfinder.ae/en/yas-island"
7
+
8
+ describe("sourceDomain", () => {
9
+ it("strips the protocol, path and www", () => {
10
+ expect(sourceDomain(HREF)).toBe("propertyfinder.ae")
11
+ })
12
+
13
+ it("falls back to the input when it is not a URL", () => {
14
+ expect(sourceDomain("internal report")).toBe("internal report")
15
+ })
16
+ })
17
+
18
+ describe("SourceTrigger", () => {
19
+ it("is a link to the source, labelled with its domain", () => {
20
+ const html = renderToStaticMarkup(<Source href={HREF}><SourceTrigger /></Source>)
21
+ expect(html).toContain(`href="${HREF}"`)
22
+ expect(html).toContain('rel="noopener noreferrer"')
23
+ expect(html).toContain(">propertyfinder.ae<")
24
+ expect(html).not.toContain("aria-label")
25
+ })
26
+
27
+ it("names a custom label with the domain so a bare number still makes sense", () => {
28
+ const html = renderToStaticMarkup(<Source href={HREF}><SourceTrigger label={2} /></Source>)
29
+ expect(html).toContain('aria-label="Source 2, propertyfinder.ae"')
30
+ })
31
+
32
+ it("uses a given favicon and falls back to a link icon", () => {
33
+ expect(renderToStaticMarkup(<Source href={HREF}><SourceTrigger favicon="/pf.png" /></Source>)).toContain('src="/pf.png"')
34
+ expect(renderToStaticMarkup(<Source href={HREF}><SourceTrigger favicon /></Source>)).toContain("lucide-link2")
35
+ })
36
+
37
+ it("throws outside Source", () => {
38
+ expect(() => renderToStaticMarkup(<SourceTrigger />)).toThrow(/Source/)
39
+ })
40
+ })