@adres/mgda-ui 0.0.0-stage → 0.0.1

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,161 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Link2 } from "lucide-react"
5
+ import { cn } from "../lib/cn"
6
+ import { Popover, PopoverAnchor, PopoverContent } from "./popover"
7
+ import { Text } from "./text"
8
+
9
+ const OPEN_DELAY_MS = 150
10
+ /** Long enough to move the pointer from the pill into the preview. */
11
+ const CLOSE_DELAY_MS = 120
12
+
13
+ /** "zillow.com" from a URL; the input itself when it is not a valid URL. */
14
+ export function sourceDomain(href: string) {
15
+ try {
16
+ return new URL(href).hostname.replace(/^www\./, "")
17
+ } catch {
18
+ return href
19
+ }
20
+ }
21
+
22
+ interface SourceContextValue {
23
+ href: string
24
+ domain: string
25
+ open: boolean
26
+ show: () => void
27
+ hide: () => void
28
+ }
29
+
30
+ const SourceContext = React.createContext<SourceContextValue | null>(null)
31
+
32
+ function useSource(part: string) {
33
+ const context = React.useContext(SourceContext)
34
+ if (!context) throw new Error(`${part} must be used inside Source`)
35
+ return context
36
+ }
37
+
38
+ export interface SourceProps {
39
+ href: string
40
+ children: React.ReactNode
41
+ }
42
+
43
+ /**
44
+ * Source - a link to where an answer came from: a small pill labelled with
45
+ * the domain, previewing the page on hover and keyboard focus. Works from a
46
+ * URL alone, so it suits web search results.
47
+ *
48
+ * @example
49
+ * <Source href="https://www.propertyfinder.ae/en/yas-island">
50
+ * <SourceTrigger />
51
+ * <SourceContent title="Yas Island townhouses" description="42 listings under AED 5M" />
52
+ * </Source>
53
+ */
54
+ export function Source({ href, children }: SourceProps) {
55
+ const [open, setOpen] = React.useState(false)
56
+ const timer = React.useRef<number | undefined>(undefined)
57
+
58
+ const schedule = React.useCallback((next: boolean, delay: number) => {
59
+ window.clearTimeout(timer.current)
60
+ timer.current = window.setTimeout(() => setOpen(next), delay)
61
+ }, [])
62
+ const show = React.useCallback(() => schedule(true, OPEN_DELAY_MS), [schedule])
63
+ const hide = React.useCallback(() => schedule(false, CLOSE_DELAY_MS), [schedule])
64
+ React.useEffect(() => () => window.clearTimeout(timer.current), [])
65
+
66
+ const value = React.useMemo(() => ({ href, domain: sourceDomain(href), open, show, hide }), [href, open, show, hide])
67
+
68
+ return (
69
+ <SourceContext.Provider value={value}>
70
+ <Popover open={open} onOpenChange={setOpen}>{children}</Popover>
71
+ </SourceContext.Provider>
72
+ )
73
+ }
74
+
75
+ function Favicon({ src, className }: { src?: string; className?: string }) {
76
+ const [failed, setFailed] = React.useState(false)
77
+ if (!src || failed) return <Link2 className={className} aria-hidden />
78
+ // eslint-disable-next-line @next/next/no-img-element -- framework-agnostic package
79
+ return <img src={src} alt="" className={cn("ax:rounded-full", className)} onError={() => setFailed(true)} />
80
+ }
81
+
82
+ export interface SourceTriggerProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
83
+ /** Replaces the domain, e.g. a citation number. */
84
+ label?: React.ReactNode
85
+ /** Shows a favicon before the label. Pass an image URL to use it; otherwise a link icon. */
86
+ favicon?: string | boolean
87
+ }
88
+
89
+ /** The pill in running text. It is a real link to the source. */
90
+ export const SourceTrigger = React.forwardRef<HTMLAnchorElement, SourceTriggerProps>(
91
+ ({ label, favicon, className, onMouseEnter, onMouseLeave, onFocus, onBlur, ...props }, ref) => {
92
+ const { href, domain, show, hide } = useSource("SourceTrigger")
93
+ const custom = label !== undefined
94
+ return (
95
+ <PopoverAnchor asChild>
96
+ <a
97
+ ref={ref}
98
+ href={href}
99
+ target="_blank"
100
+ rel="noopener noreferrer"
101
+ data-slot="source-trigger"
102
+ aria-label={custom ? `Source ${typeof label === "string" || typeof label === "number" ? `${label}, ` : ""}${domain}` : undefined}
103
+ onMouseEnter={(event) => { onMouseEnter?.(event); show() }}
104
+ onMouseLeave={(event) => { onMouseLeave?.(event); hide() }}
105
+ onFocus={(event) => { onFocus?.(event); show() }}
106
+ onBlur={(event) => { onBlur?.(event); hide() }}
107
+ className={cn(
108
+ "ax:mx-0.5 ax:inline-flex ax:h-5 ax:max-w-32 ax:items-center ax:gap-1 ax:overflow-hidden ax:rounded-full ax:bg-muted ax:align-text-top",
109
+ "ax:text-xs ax:tabular-nums ax:text-muted-foreground ax:no-underline ax:transition-colors",
110
+ "ax:hover:bg-muted-foreground/20 ax:hover:text-foreground ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring",
111
+ favicon ? "ax:ps-1 ax:pe-2" : "ax:px-1.5",
112
+ className,
113
+ )}
114
+ {...props}
115
+ >
116
+ {favicon && <Favicon src={typeof favicon === "string" ? favicon : undefined} className="ax:size-3.5 ax:shrink-0" />}
117
+ <span className="ax:truncate">{custom ? label : domain}</span>
118
+ </a>
119
+ </PopoverAnchor>
120
+ )
121
+ },
122
+ )
123
+ SourceTrigger.displayName = "SourceTrigger"
124
+
125
+ export interface SourceContentProps {
126
+ title: string
127
+ description?: string
128
+ /** Image URL for the site's icon; a link icon otherwise. */
129
+ favicon?: string
130
+ className?: string
131
+ }
132
+
133
+ /** The hover preview: domain, title and description, all one link to the source. */
134
+ export function SourceContent({ title, description, favicon, className }: SourceContentProps) {
135
+ const { href, domain, show, hide } = useSource("SourceContent")
136
+ return (
137
+ <PopoverContent
138
+ align="start"
139
+ className={cn("ax:w-80 ax:p-0 ax:shadow-xs", className)}
140
+ onOpenAutoFocus={(event) => event.preventDefault()}
141
+ onCloseAutoFocus={(event) => event.preventDefault()}
142
+ onMouseEnter={show}
143
+ onMouseLeave={hide}
144
+ >
145
+ <a
146
+ href={href}
147
+ target="_blank"
148
+ rel="noopener noreferrer"
149
+ tabIndex={-1}
150
+ className="ax:flex ax:flex-col ax:gap-xs ax:rounded-[inherit] ax:p-sm ax:no-underline ax:transition-colors ax:hover:bg-muted/50"
151
+ >
152
+ <span className="ax:flex ax:items-center ax:gap-1.5 ax:text-muted-foreground">
153
+ <Favicon src={favicon} className="ax:size-4 ax:shrink-0" />
154
+ <Text size="1" color="muted" truncate>{domain}</Text>
155
+ </span>
156
+ <Text as="span" size="2" weight="medium" className="ax:line-clamp-2">{title}</Text>
157
+ {description && <Text as="span" size="2" color="muted" className="ax:line-clamp-2">{description}</Text>}
158
+ </a>
159
+ </PopoverContent>
160
+ )
161
+ }
@@ -0,0 +1,19 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { Spinner } from "./spinner"
4
+
5
+ const meta: Meta<typeof Spinner> = {
6
+ title: "Components/Spinner",
7
+ component: Spinner,
8
+ tags: ["autodocs"],
9
+ args: { size: "md" },
10
+ }
11
+
12
+ export default meta
13
+ type Story = StoryObj<typeof Spinner>
14
+
15
+ export const Default: Story = {}
16
+
17
+ export const Large: Story = {
18
+ args: { size: "lg" },
19
+ }
@@ -0,0 +1,76 @@
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
+ export type SpinnerSize = "sm" | "md" | "lg" | "xl"
8
+
9
+ const spinnerVariants = cva(
10
+ cn(
11
+ "ax:animate-spin ax:text-primary",
12
+ // Slow the spin to a 3-second cycle when the OS requests reduced motion.
13
+ // We don't stop it: a stationary spinner is just a static circle and no
14
+ // longer reads as "loading". A slow rotation preserves the semantic.
15
+ "ax:motion-reduce:[animation-duration:3s]"
16
+ ),
17
+ {
18
+ variants: {
19
+ size: {
20
+ sm: "ax:size-3.5",
21
+ md: "ax:size-4",
22
+ lg: "ax:size-5",
23
+ xl: "ax:size-6",
24
+ },
25
+ },
26
+ defaultVariants: { size: "md" },
27
+ }
28
+ )
29
+
30
+ export interface SpinnerProps
31
+ extends React.SVGAttributes<SVGSVGElement>,
32
+ VariantProps<typeof spinnerVariants> {
33
+ /** Accessible label announced by screen readers via the live region.
34
+ * Defaults to `"Loading"`. Use this for i18n or to be more specific
35
+ * ("Saving", "Searching", "Fetching listings"). Ignored when
36
+ * `decorative` is true. */
37
+ label?: string
38
+ /** Hide from assistive tech (`aria-hidden`, drops `role` and `aria-label`).
39
+ * Use when a sibling element already announces the loading state - e.g.
40
+ * inside a button with visible "Saving…" text - to avoid duplicate
41
+ * screen-reader announcements. */
42
+ decorative?: boolean
43
+ }
44
+
45
+ export const Spinner = React.forwardRef<SVGSVGElement, SpinnerProps>(
46
+ ({ className, size, label, decorative = false, ...props }, ref) => {
47
+ const ariaProps = decorative
48
+ ? { "aria-hidden": true as const }
49
+ : { role: "status" as const, "aria-label": label ?? "Loading" }
50
+ return (
51
+ <svg
52
+ ref={ref}
53
+ viewBox="0 0 24 24"
54
+ fill="none"
55
+ {...ariaProps}
56
+ className={cn(spinnerVariants({ size }), className)}
57
+ {...props}
58
+ >
59
+ <circle
60
+ cx="12"
61
+ cy="12"
62
+ r="10"
63
+ strokeWidth="2.5"
64
+ className="ax:stroke-current ax:opacity-20"
65
+ />
66
+ <path
67
+ d="M22 12a10 10 0 0 0-10-10"
68
+ strokeWidth="2.5"
69
+ strokeLinecap="round"
70
+ className="ax:stroke-current"
71
+ />
72
+ </svg>
73
+ )
74
+ }
75
+ )
76
+ Spinner.displayName = "Spinner"
@@ -0,0 +1,36 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { Handshake, ShoppingBag, TrendingUp, Users } from "lucide-react"
4
+ import { StatStrip } from "./stat-strip"
5
+
6
+ const ITEMS = [
7
+ { label: "Sold", value: "1,086", icon: <ShoppingBag aria-hidden /> },
8
+ { label: "On-Market", value: "1,854", icon: <TrendingUp aria-hidden /> },
9
+ { label: "Deals", value: "2,451", icon: <Handshake aria-hidden /> },
10
+ { label: "Active brokers", value: "275", icon: <Users aria-hidden /> },
11
+ ]
12
+
13
+ const meta: Meta<typeof StatStrip> = {
14
+ title: "Components/StatStrip",
15
+ component: StatStrip,
16
+ tags: ["autodocs"],
17
+ argTypes: { tone: { control: "select", options: ["neutral", "primary", "secondary"] } },
18
+ args: { label: "Madhmoun MLS global stats", items: ITEMS, tone: "secondary" },
19
+ }
20
+
21
+ export default meta
22
+ type Story = StoryObj<typeof StatStrip>
23
+
24
+ export const Default: Story = {}
25
+
26
+ export const Tones: Story = {
27
+ render: (args) => (
28
+ <div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
29
+ <StatStrip {...args} tone="neutral" />
30
+ <StatStrip {...args} tone="primary" />
31
+ <StatStrip {...args} tone="secondary" />
32
+ </div>
33
+ ),
34
+ }
35
+
36
+ export const WithoutLabel: Story = { args: { label: undefined, tone: "neutral" } }
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import * as React from "react"
3
+ import { renderToStaticMarkup } from "react-dom/server"
4
+ import { StatStrip } from "./stat-strip"
5
+
6
+ const ITEMS = [{ label: "Sold", value: "1,086" }, { label: "Deals", value: "2,451" }]
7
+
8
+ describe("StatStrip", () => {
9
+ it("pairs each label with its value in a definition list", () => {
10
+ const html = renderToStaticMarkup(<StatStrip items={ITEMS} />)
11
+ expect(html).toMatch(/<dl[^>]*>.*<dt[^>]*>Sold<\/dt><dd[^>]*>1,086<\/dd>.*<\/dl>/)
12
+ })
13
+
14
+ it("names the list with its label when one is given", () => {
15
+ const html = renderToStaticMarkup(<StatStrip label="Global stats" items={ITEMS} />)
16
+ const id = html.match(/<p id="([^"]+)"/)?.[1]
17
+ expect(id).toBeTruthy()
18
+ expect(html).toContain(`aria-labelledby="${id}"`)
19
+ })
20
+
21
+ it("applies the tone", () => {
22
+ expect(renderToStaticMarkup(<StatStrip tone="secondary" items={ITEMS} />)).toContain("ax:bg-secondary-weakest")
23
+ expect(renderToStaticMarkup(<StatStrip items={ITEMS} />)).toContain("ax:bg-muted")
24
+ })
25
+ })
@@ -0,0 +1,85 @@
1
+ import * as React from "react"
2
+ import { tv, type VariantProps } from "tailwind-variants"
3
+ import { cn } from "../lib/cn"
4
+
5
+ export type StatStripTone = "neutral" | "primary" | "secondary"
6
+
7
+ export interface StatStripItem {
8
+ label: string
9
+ value: React.ReactNode
10
+ /** Small leading icon. Mark it decorative; the label names the figure. */
11
+ icon?: React.ReactNode
12
+ }
13
+
14
+ /**
15
+ * Tones use the `weakest` fills with `-foreground` text, which the emphasis
16
+ * aliases keep legible in light and dark without `dark:` overrides.
17
+ */
18
+ export const statStripVariants = tv({
19
+ slots: {
20
+ root: "ax:flex ax:flex-col ax:gap-xs ax:rounded-md ax:border ax:px-md ax:py-xs ax:sm:flex-row ax:sm:items-center ax:sm:gap-md",
21
+ label: "ax:t-label-xs ax:shrink-0",
22
+ list: "ax:grid ax:grid-cols-2 ax:gap-x-md ax:gap-y-xs ax:sm:flex ax:sm:flex-wrap ax:sm:gap-x-md",
23
+ // Labels and values never break; in a narrow two-column cell the value wraps under its label instead.
24
+ item: "ax:flex ax:min-w-0 ax:flex-wrap ax:items-center ax:gap-x-xs ax:sm:border-s ax:sm:ps-md ax:[&_svg]:size-3.5 ax:[&_svg]:shrink-0",
25
+ },
26
+ variants: {
27
+ tone: {
28
+ neutral: {
29
+ root: "ax:border-border ax:bg-muted ax:text-foreground",
30
+ item: "ax:sm:border-border",
31
+ },
32
+ primary: {
33
+ root: "ax:border-primary-weaker ax:bg-primary-weakest ax:text-primary-foreground",
34
+ item: "ax:sm:border-primary-weaker",
35
+ },
36
+ secondary: {
37
+ root: "ax:border-secondary-weaker ax:bg-secondary-weakest ax:text-secondary-foreground",
38
+ item: "ax:sm:border-secondary-weaker",
39
+ },
40
+ },
41
+ },
42
+ defaultVariants: { tone: "neutral" },
43
+ }, { twMerge: false })
44
+
45
+ export interface StatStripProps
46
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "children">,
47
+ VariantProps<typeof statStripVariants> {
48
+ /** Leading caption naming the set, e.g. "Madhmoun MLS global stats". */
49
+ label?: React.ReactNode
50
+ items: StatStripItem[]
51
+ }
52
+
53
+ /**
54
+ * StatStrip - a compact, single-line bar of headline figures for the top of a
55
+ * dashboard. Renders a definition list so each label is tied to its value;
56
+ * collapses to two columns below `sm`.
57
+ *
58
+ * @example
59
+ * <StatStrip
60
+ * tone="secondary"
61
+ * label="Madhmoun MLS global stats"
62
+ * items={[{ label: "Sold", value: "1,086" }, { label: "Deals", value: "2,451" }]}
63
+ * />
64
+ */
65
+ export const StatStrip = React.forwardRef<HTMLDivElement, StatStripProps>(
66
+ ({ className, tone, label, items, ...props }, ref) => {
67
+ const styles = statStripVariants({ tone })
68
+ const labelId = React.useId()
69
+ return (
70
+ <div ref={ref} data-slot="stat-strip" className={cn(styles.root(), className)} {...props}>
71
+ {label && <p id={labelId} className={styles.label()}>{label}</p>}
72
+ <dl className={styles.list()} aria-labelledby={label ? labelId : undefined}>
73
+ {items.map((item) => (
74
+ <div key={item.label} className={styles.item()}>
75
+ {item.icon}
76
+ <dt className="ax:t-label-xs ax:font-normal ax:whitespace-nowrap">{item.label}</dt>
77
+ <dd className="ax:t-label-xs ax:font-bold ax:whitespace-nowrap ax:tabular-nums">{item.value}</dd>
78
+ </div>
79
+ ))}
80
+ </dl>
81
+ </div>
82
+ )
83
+ },
84
+ )
85
+ StatStrip.displayName = "StatStrip"
@@ -0,0 +1,24 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { Stats } from "./stats"
4
+
5
+ const meta: Meta<typeof Stats> = {
6
+ title: "Components/Stats",
7
+ component: Stats,
8
+ tags: ["autodocs"],
9
+ args: {
10
+ eyebrow: "By the numbers",
11
+ title: "Trusted worldwide",
12
+ stats: [
13
+ { value: "10k+", label: "Teams" },
14
+ { value: "99.9%", label: "Uptime" },
15
+ { value: "50+", label: "Countries" },
16
+ { value: "4.9", label: "Rating" },
17
+ ],
18
+ },
19
+ }
20
+
21
+ export default meta
22
+ type Story = StoryObj<typeof Stats>
23
+
24
+ export const Default: Story = {}
@@ -0,0 +1,83 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { motion } from "motion/react"
5
+ import { cn } from "../lib/cn"
6
+ import { useReducedMotion } from "../hooks/use-reduced-motion"
7
+ import { stagger, variants, viewport as defaultViewport } from "../lib/motion"
8
+
9
+ export interface Stat {
10
+ value: React.ReactNode
11
+ label: string
12
+ description?: string
13
+ valueClassName?: string
14
+ }
15
+
16
+ export interface StatsProps extends Omit<React.HTMLAttributes<HTMLElement>, "title"> {
17
+ eyebrow?: React.ReactNode
18
+ title?: React.ReactNode
19
+ description?: React.ReactNode
20
+ stats: Stat[]
21
+ }
22
+
23
+ export function Stats({ className, eyebrow, title, description, stats, ...props }: StatsProps) {
24
+ const reduce = useReducedMotion()
25
+
26
+ return (
27
+ <section className={cn("ax:w-full ax:px-xl ax:md:px-3xl ax:py-5xl ax:md:py-6xl", className)} {...props}>
28
+ <div className="ax:mx-auto ax:max-w-420">
29
+ {(eyebrow || title || description) && (
30
+ <div className="ax:max-w-2xl ax:mb-4xl ax:space-y-sm">
31
+ {eyebrow && (
32
+ <span className="ax:inline-block ax:t-overline ax:text-primary">
33
+ {eyebrow}
34
+ </span>
35
+ )}
36
+ {title && (
37
+ <h2 className="ax:t-display-md ax:md:t-display-lg ax:text-foreground ax:[text-wrap:balance]">
38
+ {title}
39
+ </h2>
40
+ )}
41
+ {description && (
42
+ <p className="ax:t-body-md ax:text-muted-foreground ax:[text-wrap:pretty]">{description}</p>
43
+ )}
44
+ </div>
45
+ )}
46
+
47
+ <motion.dl
48
+ className={cn(
49
+ "ax:grid ax:gap-px ax:bg-border ax:rounded-xl ax:overflow-hidden ax:border ax:border-border",
50
+ stats.length === 2 && "ax:grid-cols-1 ax:sm:grid-cols-2",
51
+ stats.length === 3 && "ax:grid-cols-1 ax:sm:grid-cols-3",
52
+ stats.length === 4 && "ax:grid-cols-2 ax:lg:grid-cols-4",
53
+ stats.length > 4 && "ax:grid-cols-2 ax:md:grid-cols-3 ax:lg:grid-cols-4",
54
+ )}
55
+ {...(reduce ? {} : {
56
+ variants: stagger(),
57
+ initial: "hidden",
58
+ whileInView: "visible",
59
+ viewport: defaultViewport,
60
+ })}
61
+ >
62
+ {stats.map((stat, i) => (
63
+ <motion.div
64
+ key={i}
65
+ {...(reduce ? {} : { variants: variants.fadeUp })}
66
+ className="ax:flex ax:flex-col ax:gap-2xs ax:bg-card ax:px-xl ax:py-2xl"
67
+ >
68
+ <dt className="ax:t-overline ax:text-muted-foreground">
69
+ {stat.label}
70
+ </dt>
71
+ <dd className={cn("ax:t-display-lg ax:text-foreground ax:tabular-nums", stat.valueClassName)}>
72
+ {stat.value}
73
+ </dd>
74
+ {stat.description && (
75
+ <p className="ax:mt-2xs ax:t-caption ax:text-muted-foreground">{stat.description}</p>
76
+ )}
77
+ </motion.div>
78
+ ))}
79
+ </motion.dl>
80
+ </div>
81
+ </section>
82
+ )
83
+ }
@@ -0,0 +1,18 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { StepIndicator } from "./step-indicator"
4
+
5
+ const meta: Meta<typeof StepIndicator> = {
6
+ title: "Components/StepIndicator",
7
+ component: StepIndicator,
8
+ tags: ["autodocs"],
9
+ args: {
10
+ steps: [{ label: "Account" }, { label: "Profile" }, { label: "Review" }],
11
+ currentStep: 1,
12
+ },
13
+ }
14
+
15
+ export default meta
16
+ type Story = StoryObj<typeof StepIndicator>
17
+
18
+ export const Default: Story = {}
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import * as React from "react"
3
+ import { renderToStaticMarkup } from "react-dom/server"
4
+ import { StepIndicator } from "./step-indicator"
5
+
6
+ describe("StepIndicator horizontal labels", () => {
7
+ it("anchors the first, middle, and last labels to the circle", () => {
8
+ const html = renderToStaticMarkup(
9
+ <StepIndicator
10
+ currentStep={1}
11
+ steps={[{ label: "Account" }, { label: "Profile" }, { label: "Review" }]}
12
+ />,
13
+ )
14
+ expect(html).toContain("ax:start-0")
15
+ expect(html).toContain("ax:start-1/2 ax:-translate-x-1/2 ax:rtl:translate-x-1/2")
16
+ expect(html).toContain("ax:end-0")
17
+ })
18
+ })