@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,347 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog"
5
+ import { ChevronLeft, ChevronRight, Images, Play, Rotate3d, X, ZoomIn, ZoomOut } from "lucide-react"
6
+
7
+ import { Button } from "./button"
8
+ import { cn } from "../lib/cn"
9
+
10
+ // ─── Types ────────────────────────────────────────────────────────────────────
11
+
12
+ export type MediaKind = "image" | "video" | "walkthrough"
13
+
14
+ /** A single item in a media gallery. Images render in an <img>; video and
15
+ * walkthrough render in an HTML5 <video> (a walkthrough is a labelled video:
16
+ * point `src` at a clip or a 360°/tour capture). */
17
+ export interface MediaItem {
18
+ type: MediaKind
19
+ /** Image URL, or video file URL for video/walkthrough. */
20
+ src: string
21
+ /** Poster frame for video/walkthrough (an image URL). */
22
+ poster?: string
23
+ /** Short label shown on the tile + in the lightbox (e.g. "Walkthrough"). */
24
+ label?: string
25
+ }
26
+
27
+ const clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n))
28
+ const MIN_ZOOM = 1
29
+ const MAX_ZOOM = 5
30
+
31
+ /** Poster/thumbnail source for any item - the image itself, or the video poster. */
32
+ const tileSrc = (m: MediaItem) => (m.type === "image" ? m.src : (m.poster ?? ""))
33
+
34
+ // ─── MediaGallery ───────────────────────────────────────────────────────────
35
+
36
+ export interface MediaGalleryProps {
37
+ /** Ordered media. The first item is the hero; the rest fill the thumbnails. */
38
+ media: MediaItem[]
39
+ /** Used for alt text and the lightbox's accessible title. */
40
+ title?: string
41
+ /** Thumbnails shown beside the hero (defaults to 4). */
42
+ thumbnailCount?: number
43
+ className?: string
44
+ }
45
+
46
+ /**
47
+ * A responsive media gallery: a hero + thumbnail grid that opens a fullscreen
48
+ * lightbox with a keyboard-navigable slideshow, image zoom/pan, and inline
49
+ * video/walkthrough playback. Framework-agnostic. Pass any `MediaItem[]`.
50
+ *
51
+ * @example
52
+ * <MediaGallery media={[{ type: "image", src: "/a.jpg" }, …]} title="Unit 2304" />
53
+ */
54
+ export function MediaGallery({ media, title = "", thumbnailCount = 4, className }: MediaGalleryProps) {
55
+ const [openIndex, setOpenIndex] = React.useState<number | null>(null)
56
+
57
+ const hero = media[0]
58
+ if (!hero) return null
59
+
60
+ const thumbs = media.slice(1, 1 + thumbnailCount)
61
+ const hiddenCount = media.length - (1 + thumbs.length)
62
+ const walkthroughIndex = media.findIndex((m) => m.type === "walkthrough")
63
+
64
+ return (
65
+ <>
66
+ <div className={cn("ax:grid ax:gap-xs ax:sm:grid-cols-4 ax:sm:grid-rows-2", className)}>
67
+ {/* Hero */}
68
+ <div className="ax:group ax:relative ax:aspect-[3/2] ax:overflow-hidden ax:rounded-md ax:bg-muted ax:sm:col-span-2 ax:sm:row-span-2 ax:sm:aspect-auto">
69
+ {/* eslint-disable-next-line @next/next/no-img-element */}
70
+ <img
71
+ src={tileSrc(hero)}
72
+ alt={title}
73
+ className="ax:h-full ax:w-full ax:object-cover ax:transition-transform ax:duration-300 ax:group-hover:scale-[1.03]"
74
+ />
75
+ <button
76
+ type="button"
77
+ aria-label="Open gallery"
78
+ onClick={() => setOpenIndex(0)}
79
+ className="ax:absolute ax:inset-0 ax:z-0"
80
+ />
81
+ {walkthroughIndex >= 0 ? (
82
+ <Button
83
+ size="sm"
84
+ onClick={() => setOpenIndex(walkthroughIndex)}
85
+ className="ax:absolute ax:bottom-3 ax:left-3 ax:z-10 ax:bg-background/90 ax:text-foreground ax:backdrop-blur-sm ax:hover:bg-background"
86
+ >
87
+ <Rotate3d className="ax:size-4" /> Walkthrough
88
+ </Button>
89
+ ) : null}
90
+ </div>
91
+
92
+ {/* Thumbnails */}
93
+ {thumbs.map((m, i) => {
94
+ const mediaIndex = i + 1
95
+ const showMore = i === thumbs.length - 1 && hiddenCount > 0
96
+ return (
97
+ <button
98
+ key={mediaIndex}
99
+ type="button"
100
+ aria-label={showMore ? `View all ${media.length} items` : `Open item ${mediaIndex + 1}`}
101
+ onClick={() => setOpenIndex(mediaIndex)}
102
+ className="ax:group ax:relative ax:hidden ax:aspect-[3/2] ax:overflow-hidden ax:rounded-md ax:bg-muted ax:sm:block"
103
+ >
104
+ {/* eslint-disable-next-line @next/next/no-img-element */}
105
+ <img
106
+ src={tileSrc(m)}
107
+ alt={`${title} - ${mediaIndex + 1}`}
108
+ className="ax:h-full ax:w-full ax:object-cover ax:transition-transform ax:duration-300 ax:group-hover:scale-[1.03]"
109
+ />
110
+ {m.type !== "image" && !showMore ? (
111
+ <span className="ax:absolute ax:inset-0 ax:grid ax:place-items-center ax:bg-black/25">
112
+ <span className="ax:grid ax:size-10 ax:place-items-center ax:rounded-full ax:bg-background/90">
113
+ <Play className="ax:size-5 ax:fill-foreground ax:text-foreground" />
114
+ </span>
115
+ </span>
116
+ ) : null}
117
+ {showMore ? (
118
+ <span className="ax:absolute ax:inset-0 ax:grid ax:place-items-center ax:bg-black/55 ax:text-white">
119
+ <span className="ax:inline-flex ax:items-center ax:gap-1.5 ax:t-label-sm">
120
+ <Images className="ax:size-4" /> +{hiddenCount} Photos
121
+ </span>
122
+ </span>
123
+ ) : null}
124
+ </button>
125
+ )
126
+ })}
127
+ </div>
128
+
129
+ {openIndex !== null ? (
130
+ <Lightbox
131
+ media={media}
132
+ index={openIndex}
133
+ onIndexChange={setOpenIndex}
134
+ onClose={() => setOpenIndex(null)}
135
+ title={title}
136
+ />
137
+ ) : null}
138
+ </>
139
+ )
140
+ }
141
+
142
+ // ─── Lightbox ─────────────────────────────────────────────────────────────────
143
+
144
+ export interface LightboxProps {
145
+ media: MediaItem[]
146
+ index: number
147
+ onIndexChange: (index: number) => void
148
+ onClose: () => void
149
+ title?: string
150
+ }
151
+
152
+ /**
153
+ * Fullscreen media viewer: keyboard-navigable slideshow (←/→, Esc), image
154
+ * zoom/pan, and inline playback for video/walkthroughs. Built on the Radix
155
+ * Dialog primitive (focus trap, scroll lock) with a dark immersive surface.
156
+ * Usually driven by `MediaGallery`, but exported for standalone use.
157
+ */
158
+ export function Lightbox({ media, index, onIndexChange, onClose, title = "" }: LightboxProps) {
159
+ const count = media.length
160
+ const go = React.useCallback(
161
+ (delta: number) => onIndexChange((index + delta + count) % count),
162
+ [index, count, onIndexChange],
163
+ )
164
+ const current = media[index]
165
+
166
+ return (
167
+ <DialogPrimitive.Root open onOpenChange={(open) => !open && onClose()}>
168
+ <DialogPrimitive.Portal>
169
+ <DialogPrimitive.Overlay className="ax:fixed ax:inset-0 ax:z-[1200] ax:bg-black/90 ax:backdrop-blur-sm ax:data-[state=open]:animate-in ax:data-[state=open]:fade-in-0" />
170
+ <DialogPrimitive.Content
171
+ aria-describedby={undefined}
172
+ onKeyDown={(e) => {
173
+ if (e.key === "ArrowRight") go(1)
174
+ else if (e.key === "ArrowLeft") go(-1)
175
+ }}
176
+ className="ax:fixed ax:inset-0 ax:z-[1200] ax:flex ax:flex-col ax:text-white ax:outline-none ax:data-[state=open]:animate-in ax:data-[state=open]:fade-in-0"
177
+ >
178
+ <DialogPrimitive.Title className="ax:sr-only">{title || "Media gallery"}</DialogPrimitive.Title>
179
+
180
+ {/* Top bar */}
181
+ <div className="ax:flex ax:shrink-0 ax:items-center ax:justify-between ax:gap-md ax:px-md ax:py-sm">
182
+ <span className="ax:t-label-sm ax:text-white/80">
183
+ {index + 1} / {count}
184
+ {current?.label ? <span className="ax:ml-2 ax:text-white/60">· {current.label}</span> : null}
185
+ </span>
186
+ <DialogPrimitive.Close asChild>
187
+ <IconButton label="Close">
188
+ <X className="ax:size-5" />
189
+ </IconButton>
190
+ </DialogPrimitive.Close>
191
+ </div>
192
+
193
+ {/* Stage */}
194
+ <div className="ax:relative ax:flex ax:min-h-0 ax:flex-1 ax:items-center ax:justify-center">
195
+ {count > 1 ? (
196
+ <IconButton label="Previous" onClick={() => go(-1)} className="ax:absolute ax:left-md ax:top-1/2 ax:z-10 ax:-translate-y-1/2">
197
+ <ChevronLeft className="ax:size-6" />
198
+ </IconButton>
199
+ ) : null}
200
+
201
+ {current?.type === "image" ? (
202
+ <ZoomableImage key={index} src={current.src} alt={`${title} - ${index + 1} of ${count}`} />
203
+ ) : current ? (
204
+ <video
205
+ key={index}
206
+ src={current.src}
207
+ poster={current.poster}
208
+ controls
209
+ autoPlay
210
+ playsInline
211
+ className="ax:max-h-full ax:max-w-full"
212
+ />
213
+ ) : null}
214
+
215
+ {count > 1 ? (
216
+ <IconButton label="Next" onClick={() => go(1)} className="ax:absolute ax:right-md ax:top-1/2 ax:z-10 ax:-translate-y-1/2">
217
+ <ChevronRight className="ax:size-6" />
218
+ </IconButton>
219
+ ) : null}
220
+ </div>
221
+
222
+ {/* Thumbnail strip */}
223
+ <div className="ax:shrink-0 ax:overflow-x-auto ax:px-md ax:py-md">
224
+ <div className="ax:mx-auto ax:flex ax:w-max ax:gap-xs">
225
+ {media.map((m, i) => (
226
+ <button
227
+ key={i}
228
+ type="button"
229
+ aria-label={`Go to item ${i + 1}`}
230
+ aria-current={i === index}
231
+ onClick={() => onIndexChange(i)}
232
+ className={cn(
233
+ "ax:relative ax:h-14 ax:w-20 ax:shrink-0 ax:overflow-hidden ax:rounded-sm ax:ring-offset-2 ax:ring-offset-black ax:transition",
234
+ i === index ? "ax:ring-2 ax:ring-white" : "ax:opacity-60 ax:hover:opacity-100",
235
+ )}
236
+ >
237
+ {/* eslint-disable-next-line @next/next/no-img-element */}
238
+ <img src={tileSrc(m)} alt="" className="ax:h-full ax:w-full ax:object-cover" draggable={false} />
239
+ {m.type !== "image" ? (
240
+ <span className="ax:absolute ax:inset-0 ax:grid ax:place-items-center ax:bg-black/30">
241
+ <Play className="ax:size-4 ax:fill-white ax:text-white" />
242
+ </span>
243
+ ) : null}
244
+ </button>
245
+ ))}
246
+ </div>
247
+ </div>
248
+ </DialogPrimitive.Content>
249
+ </DialogPrimitive.Portal>
250
+ </DialogPrimitive.Root>
251
+ )
252
+ }
253
+
254
+ // ─── Zoomable image ─────────────────────────────────────────────────────────
255
+
256
+ function ZoomableImage({ src, alt }: { src: string; alt: string }) {
257
+ const [scale, setScale] = React.useState(1)
258
+ const [pos, setPos] = React.useState({ x: 0, y: 0 })
259
+ const dragging = React.useRef(false)
260
+ const last = React.useRef({ x: 0, y: 0 })
261
+
262
+ const reset = () => {
263
+ setScale(1)
264
+ setPos({ x: 0, y: 0 })
265
+ }
266
+ // Panning offset is only meaningful while zoomed in.
267
+ React.useEffect(() => {
268
+ if (scale === 1) setPos({ x: 0, y: 0 })
269
+ }, [scale])
270
+
271
+ const zoomBy = (factor: number) => setScale((s) => clamp(+(s * factor).toFixed(3), MIN_ZOOM, MAX_ZOOM))
272
+
273
+ const onPointerDown = (e: React.PointerEvent<HTMLImageElement>) => {
274
+ if (scale <= 1) return
275
+ dragging.current = true
276
+ last.current = { x: e.clientX, y: e.clientY }
277
+ e.currentTarget.setPointerCapture(e.pointerId)
278
+ }
279
+ const onPointerMove = (e: React.PointerEvent<HTMLImageElement>) => {
280
+ if (!dragging.current) return
281
+ setPos((p) => ({ x: p.x + (e.clientX - last.current.x), y: p.y + (e.clientY - last.current.y) }))
282
+ last.current = { x: e.clientX, y: e.clientY }
283
+ }
284
+ const endDrag = () => {
285
+ dragging.current = false
286
+ }
287
+
288
+ return (
289
+ <div
290
+ className="ax:relative ax:flex ax:h-full ax:w-full ax:items-center ax:justify-center ax:overflow-hidden"
291
+ onWheel={(e) => zoomBy(e.deltaY < 0 ? 1.15 : 1 / 1.15)}
292
+ >
293
+ {/* eslint-disable-next-line @next/next/no-img-element */}
294
+ <img
295
+ src={src}
296
+ alt={alt}
297
+ draggable={false}
298
+ onDoubleClick={() => (scale > 1 ? reset() : setScale(2.5))}
299
+ onPointerDown={onPointerDown}
300
+ onPointerMove={onPointerMove}
301
+ onPointerUp={endDrag}
302
+ onPointerCancel={endDrag}
303
+ className={cn(
304
+ "ax:max-h-full ax:max-w-full ax:select-none ax:object-contain",
305
+ !dragging.current && "ax:transition-transform",
306
+ scale > 1 ? "ax:cursor-grab ax:active:cursor-grabbing" : "ax:cursor-zoom-in",
307
+ )}
308
+ style={{ transform: `translate(${pos.x}px, ${pos.y}px) scale(${scale})` }}
309
+ />
310
+
311
+ <div className="ax:absolute ax:bottom-4 ax:right-4 ax:flex ax:gap-xs">
312
+ <IconButton label="Zoom out" onClick={() => zoomBy(1 / 1.3)} disabled={scale <= MIN_ZOOM}>
313
+ <ZoomOut className="ax:size-5" />
314
+ </IconButton>
315
+ <IconButton label="Zoom in" onClick={() => zoomBy(1.3)} disabled={scale >= MAX_ZOOM}>
316
+ <ZoomIn className="ax:size-5" />
317
+ </IconButton>
318
+ </div>
319
+ </div>
320
+ )
321
+ }
322
+
323
+ // ─── Dark-surface icon button ───────────────────────────────────────────────
324
+
325
+ function IconButton({
326
+ label,
327
+ className,
328
+ children,
329
+ ...props
330
+ }: React.ButtonHTMLAttributes<HTMLButtonElement> & { label: string }) {
331
+ return (
332
+ <button
333
+ type="button"
334
+ aria-label={label}
335
+ title={label}
336
+ className={cn(
337
+ "ax:grid ax:size-10 ax:place-items-center ax:rounded-full ax:bg-white/10 ax:text-white ax:backdrop-blur-sm ax:transition",
338
+ "ax:hover:bg-white/20 ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-white",
339
+ "ax:disabled:pointer-events-none ax:disabled:opacity-40",
340
+ className,
341
+ )}
342
+ {...props}
343
+ >
344
+ {children}
345
+ </button>
346
+ )
347
+ }
@@ -0,0 +1,45 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { MoreHorizontal } from "lucide-react"
4
+ import { MessageAction, MessageActions, type MessageRating } from "./message-actions"
5
+ import { TooltipProvider } from "./tooltip"
6
+
7
+ const ANSWER = "Yas Acres townhouses with a private garden start at AED 4.2M."
8
+
9
+ const meta: Meta<typeof MessageActions> = {
10
+ title: "Components/MessageActions",
11
+ component: MessageActions,
12
+ tags: ["autodocs"],
13
+ decorators: [(Story) => <TooltipProvider><Story /></TooltipProvider>],
14
+ argTypes: { onRate: { control: false }, onRegenerate: { control: false }, onCopy: { control: false } },
15
+ args: { copyText: ANSWER },
16
+ }
17
+
18
+ export default meta
19
+ type Story = StoryObj<typeof MessageActions>
20
+
21
+ function RatingDemo(props: React.ComponentProps<typeof MessageActions>) {
22
+ const [rating, setRating] = React.useState<MessageRating | null>(null)
23
+ return (
24
+ <MessageActions
25
+ {...props}
26
+ rating={rating}
27
+ onRate={(next) => setRating((current) => (current === next ? null : next))}
28
+ onRegenerate={() => undefined}
29
+ />
30
+ )
31
+ }
32
+
33
+ export const Default: Story = { render: (args) => <RatingDemo {...args} /> }
34
+
35
+ export const CopyOnly: Story = {}
36
+
37
+ export const Regenerating: Story = { render: (args) => <RatingDemo {...args} regenerating /> }
38
+
39
+ export const WithExtraAction: Story = {
40
+ render: (args) => (
41
+ <RatingDemo {...args} role="toolbar" aria-label="Answer actions">
42
+ <MessageAction label="More actions"><MoreHorizontal /></MessageAction>
43
+ </RatingDemo>
44
+ ),
45
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import * as React from "react"
3
+ import { renderToStaticMarkup } from "react-dom/server"
4
+ import { MessageActions } from "./message-actions"
5
+ import { TooltipProvider } from "./tooltip"
6
+
7
+ const render = (node: React.ReactNode) => renderToStaticMarkup(<TooltipProvider>{node}</TooltipProvider>)
8
+
9
+ describe("MessageActions", () => {
10
+ it("shows only the actions it has handlers or text for", () => {
11
+ const html = render(<MessageActions copyText="Answer" />)
12
+ expect(html).toContain('aria-label="Copy"')
13
+ expect(html).not.toContain("Good response")
14
+ expect(html).not.toContain("Regenerate")
15
+ })
16
+
17
+ it("marks the current rating as pressed and the other as not", () => {
18
+ const html = render(<MessageActions rating="up" onRate={() => undefined} />)
19
+ expect(html).toMatch(/aria-label="Good response" aria-pressed="true"/)
20
+ expect(html).toMatch(/aria-label="Bad response" aria-pressed="false"/)
21
+ })
22
+
23
+ it("disables Regenerate while regenerating", () => {
24
+ const html = render(<MessageActions onRegenerate={() => undefined} regenerating />)
25
+ expect(html).toMatch(/<button[^>]*disabled=""[^>]*aria-label="Regenerate"|<button[^>]*aria-label="Regenerate"[^>]*disabled=""/)
26
+ })
27
+
28
+ it("hides until hover or focus only when asked", () => {
29
+ expect(render(<MessageActions copyText="Answer" />)).not.toContain("ax:opacity-0")
30
+ expect(render(<MessageActions copyText="Answer" revealOnHover />)).toContain("ax:group-hover/message:opacity-100")
31
+ })
32
+ })
@@ -0,0 +1,119 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Check, Copy, RotateCcw, ThumbsDown, ThumbsUp } from "lucide-react"
5
+ import { cn } from "../lib/cn"
6
+ import { Button, type ButtonProps } from "./button"
7
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"
8
+
9
+ export type MessageRating = "up" | "down"
10
+
11
+ export interface MessageActionProps extends Omit<ButtonProps, "variant" | "size" | "asChild"> {
12
+ /** Accessible name and tooltip text. */
13
+ label: string
14
+ /** Renders as a toggle: sets `aria-pressed` and tints the icon. */
15
+ pressed?: boolean
16
+ }
17
+
18
+ /** A round icon-only ghost button with a tooltip, sized for the action row under a message. */
19
+ export const MessageAction = React.forwardRef<HTMLButtonElement, MessageActionProps>(
20
+ ({ label, pressed, className, ...props }, ref) => (
21
+ <Tooltip>
22
+ <TooltipTrigger asChild>
23
+ <Button
24
+ ref={ref}
25
+ variant="ghost"
26
+ size="icon-sm"
27
+ aria-label={label}
28
+ aria-pressed={pressed}
29
+ className={cn("ax:rounded-full ax:text-muted-foreground ax:hover:text-foreground", pressed && "ax:text-primary-foreground", className)}
30
+ {...props}
31
+ />
32
+ </TooltipTrigger>
33
+ <TooltipContent>{label}</TooltipContent>
34
+ </Tooltip>
35
+ ),
36
+ )
37
+ MessageAction.displayName = "MessageAction"
38
+
39
+ const COPIED_MS = 1500
40
+
41
+ export interface MessageActionsProps extends React.HTMLAttributes<HTMLDivElement> {
42
+ /** Text the Copy action writes to the clipboard. Omit to hide Copy. */
43
+ copyText?: string
44
+ onCopy?: () => void
45
+ rating?: MessageRating | null
46
+ /** Called with the clicked rating; the caller decides whether a second click clears it. Omit to hide rating. */
47
+ onRate?: (rating: MessageRating) => void
48
+ /** Omit to hide Regenerate. */
49
+ onRegenerate?: () => void
50
+ /** Spins the Regenerate icon and disables it. */
51
+ regenerating?: boolean
52
+ regenerateDisabled?: boolean
53
+ /**
54
+ * Hides the row until the message is hovered or the row holds focus. Needs a
55
+ * `group/message` ancestor (ChatMessage sets one); touch screens always show it.
56
+ */
57
+ revealOnHover?: boolean
58
+ }
59
+
60
+ /**
61
+ * MessageActions - the row of icon actions under an assistant message: copy,
62
+ * rate up or down, and regenerate. Extra actions (a version pager, a More
63
+ * menu) go in `children`, after the built-in ones.
64
+ *
65
+ * @example
66
+ * <MessageActions copyText={answer} rating={rating} onRate={setRating} onRegenerate={regenerate} />
67
+ */
68
+ export const MessageActions = React.forwardRef<HTMLDivElement, MessageActionsProps>(
69
+ ({ copyText, onCopy, rating = null, onRate, onRegenerate, regenerating, regenerateDisabled, revealOnHover, className, children, ...props }, ref) => {
70
+ const [copied, setCopied] = React.useState(false)
71
+ const timer = React.useRef<number | undefined>(undefined)
72
+ React.useEffect(() => () => window.clearTimeout(timer.current), [])
73
+
74
+ function copy() {
75
+ if (copyText === undefined) return
76
+ void navigator.clipboard?.writeText(copyText)
77
+ setCopied(true)
78
+ window.clearTimeout(timer.current)
79
+ timer.current = window.setTimeout(() => setCopied(false), COPIED_MS)
80
+ onCopy?.()
81
+ }
82
+
83
+ return (
84
+ <div
85
+ ref={ref}
86
+ data-slot="message-actions"
87
+ className={cn(
88
+ "ax:-ms-xs ax:flex ax:flex-wrap ax:items-center",
89
+ revealOnHover && "ax:opacity-0 ax:transition-opacity ax:duration-150 ax:focus-within:opacity-100 ax:group-hover/message:opacity-100 ax:pointer-coarse:opacity-100 ax:motion-reduce:transition-none",
90
+ className,
91
+ )}
92
+ {...props}
93
+ >
94
+ {copyText !== undefined && (
95
+ <MessageAction label={copied ? "Copied" : "Copy"} onClick={copy}>
96
+ {copied ? <Check /> : <Copy />}
97
+ </MessageAction>
98
+ )}
99
+ {onRate && (
100
+ <>
101
+ <MessageAction label="Good response" pressed={rating === "up"} onClick={() => onRate("up")}>
102
+ <ThumbsUp className={cn(rating === "up" && "ax:fill-current")} />
103
+ </MessageAction>
104
+ <MessageAction label="Bad response" pressed={rating === "down"} onClick={() => onRate("down")}>
105
+ <ThumbsDown className={cn(rating === "down" && "ax:fill-current")} />
106
+ </MessageAction>
107
+ </>
108
+ )}
109
+ {onRegenerate && (
110
+ <MessageAction label="Regenerate" disabled={regenerating || regenerateDisabled} onClick={onRegenerate}>
111
+ <RotateCcw className={cn(regenerating && "ax:animate-spin ax:motion-reduce:animate-none")} />
112
+ </MessageAction>
113
+ )}
114
+ {children}
115
+ </div>
116
+ )
117
+ },
118
+ )
119
+ MessageActions.displayName = "MessageActions"
@@ -0,0 +1,58 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { Bookmark, FilePen, Home, Radio, Settings } from "lucide-react"
4
+ import { NavSidebar } from "./nav-sidebar"
5
+
6
+ const meta: Meta<typeof NavSidebar> = {
7
+ title: "Components/NavSidebar",
8
+ component: NavSidebar,
9
+ tags: ["autodocs"],
10
+ }
11
+
12
+ export default meta
13
+ type Story = StoryObj<typeof NavSidebar>
14
+
15
+ export const Default: Story = {
16
+ render: () => (
17
+ <div className="ax:h-[320px] ax:border ax:border-border ax:rounded-md ax:overflow-hidden">
18
+ <NavSidebar
19
+ currentPath="/"
20
+ brand={<span className="ax:t-label-sm ax:font-medium">Adres</span>}
21
+ sections={[
22
+ {
23
+ items: [
24
+ { label: "Home", href: "/", icon: <Home className="ax:size-4" /> },
25
+ { label: "Settings", href: "/settings", icon: <Settings className="ax:size-4" /> },
26
+ ],
27
+ },
28
+ ]}
29
+ />
30
+ </div>
31
+ ),
32
+ }
33
+
34
+ /** Sections with labels and trailing counts; a count of 0 is hidden. */
35
+ export const WithCounts: Story = {
36
+ render: () => (
37
+ <div className="ax:h-[420px] ax:border ax:border-border ax:rounded-md ax:overflow-hidden">
38
+ <NavSidebar
39
+ currentPath="/saved-properties"
40
+ brand={<span className="ax:t-label-sm ax:font-medium">Adres</span>}
41
+ sections={[
42
+ { items: [{ label: "Dashboard", href: "/dashboard", icon: <Home className="ax:size-4" /> }] },
43
+ {
44
+ label: "Discover",
45
+ items: [{ label: "Saved Properties", href: "/saved-properties", icon: <Bookmark className="ax:size-4" />, count: 3 }],
46
+ },
47
+ {
48
+ label: "My Properties",
49
+ items: [
50
+ { label: "Drafts", href: "/my-properties/drafts", icon: <FilePen className="ax:size-4" />, count: 4 },
51
+ { label: "Live", href: "/my-properties/live", icon: <Radio className="ax:size-4" />, count: 0 },
52
+ ],
53
+ },
54
+ ]}
55
+ />
56
+ </div>
57
+ ),
58
+ }
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import * as React from "react"
3
+ import { renderToStaticMarkup } from "react-dom/server"
4
+ import { NavSidebar, type NavSection } from "./nav-sidebar"
5
+
6
+ const SECTIONS: NavSection[] = [
7
+ {
8
+ items: [
9
+ { href: "/saved", label: "Saved Properties", icon: <svg />, count: 3 },
10
+ { href: "/drafts", label: "Drafts", icon: <svg />, count: 0 },
11
+ { href: "/deals", label: "Deals", icon: <svg />, count: 5, children: [{ href: "/deals/open", label: "Open", count: 2 }] },
12
+ ],
13
+ },
14
+ ]
15
+
16
+ describe("NavSidebar counts", () => {
17
+ it("renders a top-level item's count with an accessible label", () => {
18
+ const html = renderToStaticMarkup(<NavSidebar sections={SECTIONS} currentPath="/saved" />)
19
+ expect(html).toContain('aria-label="3 items"')
20
+ })
21
+
22
+ it("hides a zero count", () => {
23
+ const html = renderToStaticMarkup(<NavSidebar sections={SECTIONS} currentPath="/saved" />)
24
+ expect(html).not.toContain('aria-label="0 items"')
25
+ })
26
+
27
+ it("leaves the trailing space to the chevron on items with children", () => {
28
+ const html = renderToStaticMarkup(<NavSidebar sections={SECTIONS} currentPath="/saved" />)
29
+ expect(html).not.toContain('aria-label="5 items"')
30
+ })
31
+
32
+ it("still renders sub-item counts when the branch is open", () => {
33
+ const html = renderToStaticMarkup(<NavSidebar sections={SECTIONS} currentPath="/deals/open" />)
34
+ expect(html).toContain('aria-label="2 items"')
35
+ })
36
+
37
+ it("drops counts from collapsed icon-only rows", () => {
38
+ const html = renderToStaticMarkup(<NavSidebar sections={SECTIONS} currentPath="/saved" collapsed />)
39
+ expect(html).not.toContain('aria-label="3 items"')
40
+ })
41
+ })
42
+
43
+ describe("NavSidebar collapsed", () => {
44
+ it("names icon-only rows with their label and count", () => {
45
+ const html = renderToStaticMarkup(<NavSidebar sections={SECTIONS} currentPath="/saved" collapsed />)
46
+ expect(html).toContain('aria-label="Saved Properties, 3 items"')
47
+ expect(html).toContain('aria-label="Drafts"')
48
+ })
49
+
50
+ it("does not add an aria-label while expanded, where the visible label names the row", () => {
51
+ const html = renderToStaticMarkup(<NavSidebar sections={SECTIONS} currentPath="/saved" />)
52
+ expect(html).not.toContain('aria-label="Saved Properties')
53
+ })
54
+ })