@heyramzi/ui 6.1.0 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/COMPONENTS.md +32 -40
  2. package/README.md +10 -4
  3. package/package.json +31 -29
  4. package/scripts/sync-shadcn.mjs +29 -16
  5. package/scripts/sync-transitions.mjs +11 -11
  6. package/src/ai/ai-button/AiButton.svelte +86 -0
  7. package/src/ai/ai-button/index.ts +4 -0
  8. package/src/ai/confirmation/Confirmation.svelte +8 -8
  9. package/src/ai/confirmation/confirmation-context.svelte.ts +2 -2
  10. package/src/ai/generation-progress/GenerationProgress.svelte +354 -0
  11. package/src/ai/generation-progress/index.ts +7 -0
  12. package/src/ai/index.ts +16 -24
  13. package/src/ai/prompt-input/PromptInput.svelte +19 -1
  14. package/src/ai/prompt-input/PromptInputSubmit.svelte +9 -3
  15. package/src/ai/reasoning/Reasoning.svelte +11 -8
  16. package/src/ambient.d.ts +4 -0
  17. package/src/animations/README.md +17 -17
  18. package/src/animations/index.css +3 -14
  19. package/src/animations/upstream-coverage.test.ts +1 -1
  20. package/src/components/copy/clipboard.test.ts +206 -0
  21. package/src/components/copy/clipboard.ts +49 -0
  22. package/src/components/copy/copied.svelte.ts +39 -0
  23. package/src/components/copy/copy-button.svelte +175 -0
  24. package/src/components/copy/copy-button.test.ts +110 -0
  25. package/src/components/copy/index.ts +3 -0
  26. package/src/components/data-table/data-table.test.ts +134 -0
  27. package/src/components/data-table/data-table.ts +77 -0
  28. package/src/components/data-table/index.ts +20 -0
  29. package/src/components/data-table/infinite-scroll.svelte +69 -0
  30. package/src/components/data-table/pagination.svelte +62 -0
  31. package/src/components/data-table/row-arrow.svelte +28 -0
  32. package/src/components/data-table/row-context-menu.svelte +106 -0
  33. package/src/components/data-table/row-context-menu.test.ts +18 -0
  34. package/src/components/data-table/sortable-head.svelte +56 -0
  35. package/src/components/data-table/table-skeleton.svelte +83 -0
  36. package/src/components/hold-to-confirm/hold-to-confirm.svelte +232 -0
  37. package/src/components/hold-to-confirm/index.ts +1 -0
  38. package/src/components/icon/icon.svelte +23 -0
  39. package/src/components/icon/index.ts +1 -0
  40. package/src/components/language-flag/index.ts +1 -0
  41. package/src/components/language-flag/language-flag.svelte +60 -0
  42. package/src/components/list-page/detail-empty.svelte +50 -0
  43. package/src/components/list-page/detail-header.svelte +107 -0
  44. package/src/components/list-page/detail-panel.svelte +90 -0
  45. package/src/components/list-page/detail-row.svelte +36 -0
  46. package/src/components/list-page/detail-stack.svelte +33 -0
  47. package/src/components/list-page/empty-state.svelte +91 -0
  48. package/src/components/list-page/filter-item.svelte +49 -0
  49. package/src/components/list-page/filter-popover.svelte +88 -0
  50. package/src/components/list-page/filter-section.ts +12 -0
  51. package/src/components/list-page/grouped-list.svelte +116 -0
  52. package/src/components/list-page/index.ts +28 -0
  53. package/src/components/list-page/list-page-shell.svelte +195 -0
  54. package/src/components/list-page/list-table.ts +19 -0
  55. package/src/components/list-page/master-detail.svelte +206 -0
  56. package/src/components/list-page/master-detail.test.ts +37 -0
  57. package/src/components/list-page/notice-bar.svelte +68 -0
  58. package/src/components/list-page/prev-next.svelte +104 -0
  59. package/src/components/list-page/queue-rail.ts +25 -0
  60. package/src/components/list-page/rail-body.svelte +29 -0
  61. package/src/components/list-page/rail-empty.svelte +51 -0
  62. package/src/components/list-page/rail-head.svelte +49 -0
  63. package/src/components/list-page/rail-row.svelte +163 -0
  64. package/src/components/list-page/section-host.ts +24 -0
  65. package/src/components/list-page/section-label.svelte +39 -0
  66. package/src/components/list-page/step.test.ts +21 -0
  67. package/src/components/list-page/step.ts +10 -0
  68. package/src/components/list-page/toolbar-search.svelte +63 -0
  69. package/src/components/list-page/toolbar-trigger.ts +23 -0
  70. package/src/components/list-page/types.ts +1 -0
  71. package/src/components/locale-switch/index.ts +1 -0
  72. package/src/components/locale-switch/locale-switch.svelte +59 -0
  73. package/src/components/mark/index.ts +1 -0
  74. package/src/components/mark/mark.svelte +70 -0
  75. package/src/components/open-in/index.ts +6 -0
  76. package/src/components/open-in/open-in.svelte +106 -0
  77. package/src/components/picker/index.ts +4 -0
  78. package/src/components/picker/mark-picker.svelte +84 -0
  79. package/src/components/picker/picker.svelte +143 -0
  80. package/src/components/picker/trigger.ts +42 -0
  81. package/src/components/picker/types.ts +26 -0
  82. package/src/components/segmented/animated-tabs.svelte +175 -0
  83. package/src/components/segmented/collapse.test.ts +49 -0
  84. package/src/components/segmented/collapse.ts +39 -0
  85. package/src/components/segmented/index.ts +41 -0
  86. package/src/components/segmented/section-runs.svelte +79 -0
  87. package/src/components/segmented/segment.svelte +106 -0
  88. package/src/components/segmented/segmented-filter.svelte +125 -0
  89. package/src/components/segmented/segmented-nav.svelte +82 -0
  90. package/src/components/segmented/segmented-overflow.svelte +85 -0
  91. package/src/components/segmented/segmented-track.svelte +181 -0
  92. package/src/components/segmented/segmented.test.ts +20 -0
  93. package/src/components/segmented/segmented.ts +80 -0
  94. package/src/components/star-rating-input/index.ts +9 -0
  95. package/src/components/star-rating-input/star-rating-input.svelte +256 -0
  96. package/src/components/svg/index.ts +1 -0
  97. package/src/components/svg/svg.svelte +9 -0
  98. package/src/effects/border-beam/BorderBeam.svelte +1 -1
  99. package/src/effects/image-comparison/ImageComparison.css +42 -8
  100. package/src/effects/image-comparison/ImageComparison.svelte +138 -21
  101. package/src/effects/image-comparison/compare.test.ts +17 -0
  102. package/src/effects/image-comparison/compare.ts +5 -0
  103. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +338 -0
  104. package/src/effects/magic-card/MagicCard.css +47 -0
  105. package/src/effects/magic-card/MagicCard.svelte +57 -0
  106. package/src/effects/marquee/Marquee.css +27 -1
  107. package/src/effects/marquee/Marquee.svelte +67 -3
  108. package/src/effects/reveal/index.ts +2 -1
  109. package/src/effects/reveal/observe.test.ts +60 -0
  110. package/src/effects/reveal/observe.ts +33 -1
  111. package/src/effects/reveal/types.ts +7 -0
  112. package/src/effects/theme-toggle/ThemeToggle.css +199 -6
  113. package/src/effects/theme-toggle/ThemeToggle.svelte +51 -26
  114. package/src/gallery.test.ts +52 -0
  115. package/src/index.ts +5 -1
  116. package/src/lib/panel.ts +7 -1
  117. package/src/lib/sliding-pill.ts +68 -0
  118. package/src/lib/theme-transition.ts +41 -13
  119. package/src/lib/tone.ts +48 -0
  120. package/src/shadcn/accordion/accordion-trigger.svelte +15 -6
  121. package/src/shadcn/alert/alert.svelte +3 -1
  122. package/src/shadcn/badge/badge.svelte +8 -7
  123. package/src/shadcn/breadcrumb/breadcrumb-ellipsis.svelte +2 -2
  124. package/src/shadcn/breadcrumb/breadcrumb-separator.svelte +2 -2
  125. package/src/shadcn/calendar/calendar-month-select.svelte +2 -2
  126. package/src/shadcn/calendar/calendar-next-button.svelte +2 -2
  127. package/src/shadcn/calendar/calendar-prev-button.svelte +2 -2
  128. package/src/shadcn/calendar/calendar-year-select.svelte +2 -2
  129. package/src/shadcn/calendar/calendar.svelte +6 -0
  130. package/src/shadcn/code-block/code-block-actions.svelte +23 -0
  131. package/src/shadcn/code-block/code-block-content.svelte +55 -0
  132. package/src/shadcn/code-block/code-block-copy-button.svelte +10 -0
  133. package/src/shadcn/code-block/code-block-footer.svelte +20 -0
  134. package/src/shadcn/code-block/code-block-header.svelte +23 -0
  135. package/src/shadcn/code-block/code-block-line.svelte +71 -0
  136. package/src/shadcn/code-block/code-block-title.svelte +23 -0
  137. package/src/shadcn/code-block/code-block.svelte +47 -0
  138. package/src/shadcn/code-block/index.ts +42 -0
  139. package/src/shadcn/command/index.ts +3 -0
  140. package/src/shadcn/field/index.ts +3 -0
  141. package/src/shadcn/field/simple-field.svelte +71 -0
  142. package/src/shadcn/field/simple-field.test.ts +47 -0
  143. package/src/shadcn/hooks/highlighter.ts +132 -0
  144. package/src/shadcn/hooks/use-highlighted-code.svelte.ts +98 -0
  145. package/src/shadcn/kbd/index.ts +10 -0
  146. package/src/shadcn/kbd/kbd-group.svelte +20 -0
  147. package/src/shadcn/kbd/kbd.svelte +23 -0
  148. package/src/shadcn/message-scroller/index.ts +28 -0
  149. package/src/shadcn/message-scroller/message-scroller-button.svelte +52 -0
  150. package/src/shadcn/message-scroller/message-scroller-content.svelte +17 -0
  151. package/src/shadcn/message-scroller/message-scroller-item.svelte +22 -0
  152. package/src/shadcn/message-scroller/message-scroller-provider.svelte +7 -0
  153. package/src/shadcn/message-scroller/message-scroller-viewport.svelte +20 -0
  154. package/src/shadcn/message-scroller/message-scroller.svelte +20 -0
  155. package/src/shadcn/radio-group/radio-group-item.svelte +3 -2
  156. package/src/shadcn/sheet/sheet-content.svelte +1 -0
  157. package/src/shadcn/sheet/sheet.css +48 -0
  158. package/src/shadcn/sonner/sonner.svelte +7 -10
  159. package/src/shadcn/tabs/tabs-list.svelte +13 -2
  160. package/src/shadcn/tabs/tabs-trigger.svelte +1 -1
  161. package/src/shadcn/toggle-group/toggle-group.svelte +10 -3
  162. package/src/shadcn/trace/index.ts +29 -0
  163. package/src/shadcn/trace/trace-collapsible-content.svelte +29 -0
  164. package/src/shadcn/trace/trace-collapsible-trigger.svelte +29 -0
  165. package/src/shadcn/trace/trace-collapsible.svelte +20 -0
  166. package/src/shadcn/trace/trace-description.svelte +20 -0
  167. package/src/shadcn/trace/trace-icon.svelte +24 -0
  168. package/src/shadcn/trace/trace-line.svelte +31 -0
  169. package/src/shadcn/trace/trace-title.svelte +20 -0
  170. package/src/shadcn/trace/trace.svelte +23 -0
  171. package/src/ai/composer/Composer.css +0 -108
  172. package/src/ai/composer/Composer.svelte +0 -111
  173. package/src/ai/composer/index.ts +0 -10
  174. package/src/ai/conversation/Conversation.svelte +0 -28
  175. package/src/ai/conversation/ConversationContent.svelte +0 -35
  176. package/src/ai/conversation/ConversationEmptyState.svelte +0 -46
  177. package/src/ai/conversation/ConversationScrollButton.svelte +0 -51
  178. package/src/ai/conversation/index.ts +0 -33
  179. package/src/ai/conversation/stick-to-bottom-context.svelte.ts +0 -131
  180. package/src/animations/accordion.css +0 -58
  181. package/src/animations/icon-swap.css +0 -43
  182. package/src/animations/notification-badge.css +0 -81
  183. package/src/animations/panel.css +0 -86
  184. package/src/animations/reasoning-stream.css +0 -49
  185. package/src/animations/skeleton-reveal.css +0 -97
  186. package/src/animations/streaming-text.css +0 -31
  187. package/src/animations/tabs-sliding.css +0 -83
  188. package/src/animations/texts-reveal.css +0 -70
  189. package/src/animations/tooltip.css +0 -72
  190. package/src/animations/view-transition-reveal.ts +0 -74
  191. package/src/charts/animated-value.css +0 -7
  192. package/src/charts/animated-value.svelte +0 -110
  193. package/src/charts/area-chart.css +0 -29
  194. package/src/charts/area-chart.svelte +0 -195
  195. package/src/charts/bar-chart.css +0 -119
  196. package/src/charts/bar-chart.svelte +0 -269
  197. package/src/charts/charts.css +0 -201
  198. package/src/charts/crosshair.svelte +0 -35
  199. package/src/charts/donut-chart.css +0 -56
  200. package/src/charts/donut-chart.svelte +0 -158
  201. package/src/charts/edge-fade.svelte +0 -43
  202. package/src/charts/edge-fade.ts +0 -16
  203. package/src/charts/figure.css +0 -100
  204. package/src/charts/figure.svelte +0 -122
  205. package/src/charts/gauge-chart.css +0 -94
  206. package/src/charts/gauge-chart.svelte +0 -303
  207. package/src/charts/hover-band.test.ts +0 -30
  208. package/src/charts/hover-band.ts +0 -21
  209. package/src/charts/hover-focus.svelte +0 -85
  210. package/src/charts/index.ts +0 -30
  211. package/src/charts/legend.css +0 -131
  212. package/src/charts/legend.svelte +0 -112
  213. package/src/charts/line-chart.css +0 -26
  214. package/src/charts/line-chart.svelte +0 -256
  215. package/src/charts/motion.ts +0 -19
  216. package/src/charts/number-chart.css +0 -168
  217. package/src/charts/number-chart.svelte +0 -177
  218. package/src/charts/primitives/chart-container.svelte +0 -57
  219. package/src/charts/primitives/chart-style.svelte +0 -36
  220. package/src/charts/primitives/chart-tooltip.svelte +0 -161
  221. package/src/charts/primitives/chart-utils.ts +0 -54
  222. package/src/charts/primitives/index.ts +0 -11
  223. package/src/charts/reveal.svelte +0 -61
  224. package/src/charts/ring-chart.css +0 -96
  225. package/src/charts/ring-chart.svelte +0 -158
  226. package/src/charts/sparkline.css +0 -45
  227. package/src/charts/sparkline.svelte +0 -140
  228. package/src/charts/stat-card.css +0 -103
  229. package/src/charts/stat-card.svelte +0 -267
  230. package/src/charts/time-series-chart.css +0 -28
  231. package/src/charts/time-series-chart.svelte +0 -179
  232. package/src/charts/trend-pill.css +0 -38
  233. package/src/charts/trend-pill.svelte +0 -48
  234. package/src/lib/hooks/is-mobile.svelte.ts +0 -11
  235. package/src/lib/utils.ts +0 -45
  236. package/src/showcase/AiSection.svelte +0 -157
  237. package/src/showcase/AnimationsSection.svelte +0 -1658
  238. package/src/showcase/ChartsSection.svelte +0 -458
  239. package/src/showcase/EffectsSection.svelte +0 -152
  240. package/src/showcase/ShadcnSection.svelte +0 -27
  241. package/src/showcase/ShowcaseImport.svelte +0 -126
  242. package/src/showcase/ShowcaseList.svelte +0 -132
  243. package/src/showcase/ShowcaseNav.svelte +0 -247
  244. package/src/showcase/ShowcaseSection.svelte +0 -112
  245. package/src/showcase/ShowcaseShell.svelte +0 -55
  246. package/src/showcase/ShowcaseStage.svelte +0 -342
  247. package/src/showcase/ShowcaseVariant.svelte +0 -109
  248. package/src/showcase/assets/image-reveal/art-1.jpg +0 -0
  249. package/src/showcase/assets/image-reveal/art-2.jpg +0 -0
  250. package/src/showcase/assets/image-reveal/art-3.jpg +0 -0
  251. package/src/showcase/check.svg +0 -1
  252. package/src/showcase/chevron-arms.svg +0 -2
  253. package/src/showcase/chevron-down.svg +0 -1
  254. package/src/showcase/copy.svg +0 -2
  255. package/src/showcase/demos/ai/confirmation.svelte +0 -59
  256. package/src/showcase/demos/ai/conversation.svelte +0 -59
  257. package/src/showcase/demos/ai/loading-state.svelte +0 -19
  258. package/src/showcase/demos/ai/prompt-input.svelte +0 -57
  259. package/src/showcase/demos/ai/reasoning.svelte +0 -19
  260. package/src/showcase/demos/ai/thinking-dots.svelte +0 -15
  261. package/src/showcase/demos/ai/voice-waveform.svelte +0 -24
  262. package/src/showcase/demos/effects/animated-beam.svelte +0 -50
  263. package/src/showcase/demos/effects/animated-group.svelte +0 -47
  264. package/src/showcase/demos/effects/border-beam-default.svelte +0 -20
  265. package/src/showcase/demos/effects/border-beam-line.svelte +0 -20
  266. package/src/showcase/demos/effects/border-beam-mono.svelte +0 -20
  267. package/src/showcase/demos/effects/border-beam-sunset.svelte +0 -20
  268. package/src/showcase/demos/effects/image-comparison.svelte +0 -25
  269. package/src/showcase/demos/effects/image-reveal.svelte +0 -49
  270. package/src/showcase/demos/effects/in-view.svelte +0 -41
  271. package/src/showcase/demos/effects/integrations-hub.svelte +0 -82
  272. package/src/showcase/demos/effects/magnetic.svelte +0 -31
  273. package/src/showcase/demos/effects/marquee.svelte +0 -33
  274. package/src/showcase/demos/effects/progressive-blur.svelte +0 -47
  275. package/src/showcase/demos/effects/spotlight.svelte +0 -40
  276. package/src/showcase/demos/effects/text-effect.svelte +0 -55
  277. package/src/showcase/demos/shadcn/accordion.svelte +0 -45
  278. package/src/showcase/demos/shadcn/alert-dialog.svelte +0 -23
  279. package/src/showcase/demos/shadcn/alert.svelte +0 -30
  280. package/src/showcase/demos/shadcn/attachment.svelte +0 -71
  281. package/src/showcase/demos/shadcn/avatar.svelte +0 -30
  282. package/src/showcase/demos/shadcn/badge.svelte +0 -26
  283. package/src/showcase/demos/shadcn/breadcrumb.svelte +0 -34
  284. package/src/showcase/demos/shadcn/bubble.svelte +0 -36
  285. package/src/showcase/demos/shadcn/button.svelte +0 -11
  286. package/src/showcase/demos/shadcn/calendar.svelte +0 -8
  287. package/src/showcase/demos/shadcn/card.svelte +0 -39
  288. package/src/showcase/demos/shadcn/checkbox.svelte +0 -39
  289. package/src/showcase/demos/shadcn/collapsible.svelte +0 -20
  290. package/src/showcase/demos/shadcn/command.svelte +0 -48
  291. package/src/showcase/demos/shadcn/context-menu.svelte +0 -54
  292. package/src/showcase/demos/shadcn/demo-harness.svelte +0 -12
  293. package/src/showcase/demos/shadcn/demos.test.ts +0 -20
  294. package/src/showcase/demos/shadcn/dialog.svelte +0 -38
  295. package/src/showcase/demos/shadcn/dropdown-menu.svelte +0 -59
  296. package/src/showcase/demos/shadcn/empty.svelte +0 -29
  297. package/src/showcase/demos/shadcn/field.svelte +0 -115
  298. package/src/showcase/demos/shadcn/hover-card.svelte +0 -32
  299. package/src/showcase/demos/shadcn/index.ts +0 -358
  300. package/src/showcase/demos/shadcn/input-otp.svelte +0 -19
  301. package/src/showcase/demos/shadcn/input.svelte +0 -5
  302. package/src/showcase/demos/shadcn/label.svelte +0 -11
  303. package/src/showcase/demos/shadcn/marker.svelte +0 -30
  304. package/src/showcase/demos/shadcn/message.svelte +0 -75
  305. package/src/showcase/demos/shadcn/popover.svelte +0 -36
  306. package/src/showcase/demos/shadcn/progress.svelte +0 -13
  307. package/src/showcase/demos/shadcn/radio-group.svelte +0 -19
  308. package/src/showcase/demos/shadcn/scroll-area.svelte +0 -18
  309. package/src/showcase/demos/shadcn/select.svelte +0 -29
  310. package/src/showcase/demos/shadcn/separator.svelte +0 -12
  311. package/src/showcase/demos/shadcn/sheet.svelte +0 -48
  312. package/src/showcase/demos/shadcn/skeleton.svelte +0 -11
  313. package/src/showcase/demos/shadcn/slider.svelte +0 -7
  314. package/src/showcase/demos/shadcn/sonner.svelte +0 -18
  315. package/src/showcase/demos/shadcn/spinner.svelte +0 -11
  316. package/src/showcase/demos/shadcn/switch.svelte +0 -9
  317. package/src/showcase/demos/shadcn/table.svelte +0 -76
  318. package/src/showcase/demos/shadcn/tabs.svelte +0 -62
  319. package/src/showcase/demos/shadcn/textarea.svelte +0 -5
  320. package/src/showcase/demos/shadcn/toggle-group.svelte +0 -18
  321. package/src/showcase/demos/shadcn/toggle.svelte +0 -14
  322. package/src/showcase/demos/shadcn/tooltip.svelte +0 -13
  323. package/src/showcase/heart.svg +0 -1
  324. package/src/showcase/index.ts +0 -38
  325. package/src/showcase/kit.test.ts +0 -72
  326. package/src/showcase/kit.ts +0 -17
  327. package/src/showcase/plus.svg +0 -1
  328. package/src/showcase/showcase-shell.css +0 -91
  329. package/src/showcase/stacked-section.css +0 -15
  330. package/src/showcase/success-check.svg +0 -1
  331. package/src/showcase/types.ts +0 -29
  332. package/src/svg-raw.d.ts +0 -7
@@ -0,0 +1,163 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import type { Snippet } from "svelte";
6
+ import { getContext } from "svelte";
7
+ import { cn } from "@heyramzi/utils";
8
+ import { MASTER_DETAIL_PANE, type MasterDetailPane } from "./queue-rail.js";
9
+
10
+ // =========================
11
+ // TYPES
12
+ // =========================
13
+ type Props = {
14
+ // One row shape for every queue rail, in a fixed order: identity, preview,
15
+ // facts. Three pages used to draw three different rows.
16
+ title: string;
17
+ selected?: boolean;
18
+ onclick: () => void;
19
+ // A thumbnail or avatar ahead of the title.
20
+ leading?: Snippet;
21
+ // The record's state, at the end of the preview line. Never beside the title,
22
+ // where it competes with the name.
23
+ trailing?: Snippet;
24
+ // One line of the record's own words, under the title.
25
+ preview?: string | null;
26
+ // Dot-separated facts under the preview. Falsy entries drop out.
27
+ meta?: Array<string | null | undefined | false>;
28
+ // Which fact gives up its width first. The others hold their full text, so
29
+ // a date or a count never truncates to nonsense.
30
+ metaTruncate?: "first" | "last";
31
+ // Icons that qualify the facts (a waiting reply, a removed comment).
32
+ metaTrail?: Snippet;
33
+ // Controls that appear on hover, over the row's top-right corner.
34
+ actions?: Snippet;
35
+ // Long records (thread titles) need two lines to be recognisable.
36
+ titleLines?: 1 | 2;
37
+ class?: string;
38
+ };
39
+
40
+ // =========================
41
+ // PROPS
42
+ // =========================
43
+ let {
44
+ title,
45
+ selected = false,
46
+ onclick,
47
+ leading,
48
+ trailing,
49
+ preview,
50
+ meta,
51
+ metaTruncate = "first",
52
+ metaTrail,
53
+ actions,
54
+ titleLines = 1,
55
+ class: className,
56
+ }: Props = $props();
57
+
58
+ // =========================
59
+ // DERIVED
60
+ // =========================
61
+ let facts = $derived((meta ?? []).filter((item): item is string => Boolean(item)));
62
+
63
+ // =========================
64
+ // FUNCTIONS
65
+ // =========================
66
+ // Picking a row moves a phone from list to record. `undefined` outside a
67
+ // `MasterDetail`, where a rail is just a list.
68
+ let row = $state<HTMLDivElement>();
69
+
70
+ // A row picked from a link, a shortcut or the next arrow can sit below the fold.
71
+ $effect(() => {
72
+ if (selected) row?.scrollIntoView({ block: "nearest" });
73
+ });
74
+
75
+ let pane = getContext<MasterDetailPane | undefined>(MASTER_DETAIL_PANE);
76
+
77
+ function select() {
78
+ onclick();
79
+ pane?.open();
80
+ }
81
+ </script>
82
+
83
+ <!-- =========================== -->
84
+ <!-- MARKUP -->
85
+ <!-- =========================== -->
86
+ <!-- The hairline belongs to the row, so grouped and flat rails draw it the same.
87
+ `first:border-t-0` keeps each group's top row clean under its header. -->
88
+ <div
89
+ bind:this={row}
90
+ class={cn(
91
+ // `group/row` too: `CopyHandoff` waits for `group-hover/row`, and a bare
92
+ // `group` left it invisible on every rail.
93
+ "group group/row relative border-t border-border first:border-t-0",
94
+ selected ? "bg-accent" : "hover:bg-muted/50",
95
+ className,
96
+ )}
97
+ >
98
+ <button
99
+ type="button"
100
+ class={cn(
101
+ "t-press flex w-full items-start gap-3 px-4 py-3 text-left any-pointer-coarse:py-3.5",
102
+ "focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:ring-inset",
103
+ // WHY: the hover actions sit over this corner, so the text stops short
104
+ // of them rather than sliding underneath when a title is long.
105
+ actions && "pr-24",
106
+ )}
107
+ onclick={select}
108
+ aria-current={selected ? "page" : undefined}
109
+ >
110
+ {#if leading}
111
+ <span class="shrink-0">{@render leading()}</span>
112
+ {/if}
113
+ <!-- The title owns its column. A chip beside it took a third of the width
114
+ and clamped two-line titles. -->
115
+ <span class="flex min-w-0 flex-1 flex-col gap-1">
116
+ <span
117
+ class={cn(
118
+ "text-sm font-medium leading-snug",
119
+ titleLines === 2 ? "line-clamp-2" : "truncate",
120
+ selected && "text-accent-foreground",
121
+ )}
122
+ >
123
+ {title}
124
+ </span>
125
+ <!-- The state rides the preview, not the facts. Facts are `shrink-0`, so
126
+ at 280px the chip got pushed off the row. Prose can give up width. -->
127
+ {#if preview || trailing}
128
+ <span class="flex min-w-0 items-center text-xs text-muted-foreground">
129
+ {#if preview}
130
+ <span class="truncate">{preview}</span>
131
+ {/if}
132
+ {#if trailing}
133
+ <span class="ml-auto flex shrink-0 items-center gap-1.5 pl-2">{@render trailing()}</span>
134
+ {/if}
135
+ </span>
136
+ {/if}
137
+ {#if facts.length > 0 || metaTrail}
138
+ <span class="flex min-w-0 items-center text-xs text-muted-foreground">
139
+ {#each facts as fact, i (i)}
140
+ {#if i > 0}
141
+ <span aria-hidden="true" class="mx-1.5 shrink-0 opacity-40">&middot;</span>
142
+ {/if}
143
+ {@const flexible =
144
+ facts.length > 1 && i === (metaTruncate === "last" ? facts.length - 1 : 0)}
145
+ <span class={flexible ? "truncate" : "shrink-0"}>{fact}</span>
146
+ {/each}
147
+ {#if metaTrail}
148
+ <span class="ml-1.5 flex shrink-0 items-center gap-1">{@render metaTrail()}</span>
149
+ {/if}
150
+ </span>
151
+ {/if}
152
+ </span>
153
+ </button>
154
+ {#if actions}
155
+ <!-- WHY visible on a coarse pointer: there is no hover on a touchscreen, so
156
+ the row's actions would be unreachable there. -->
157
+ <div
158
+ class="absolute right-2 top-2 hidden items-center gap-1 group-hover:flex group-focus-within:flex any-pointer-coarse:flex"
159
+ >
160
+ {@render actions()}
161
+ </div>
162
+ {/if}
163
+ </div>
@@ -0,0 +1,24 @@
1
+ import { getContext, setContext } from "svelte";
2
+ import type { SectionStrip } from "../segmented/section-runs.svelte";
3
+
4
+ /**
5
+ * What an app shell gives a list page so the page's toolbar can draw the
6
+ * section strip. `strip` must be reactive (a `$state` field), and `claim` tells
7
+ * the shell a toolbar is drawing it and returns the release.
8
+ */
9
+ export interface SectionHost {
10
+ readonly strip: SectionStrip | null;
11
+ claim(): () => void;
12
+ }
13
+
14
+ // WHY context: the shell decides the strip once and ninety pages draw it, so a prop on every page would be ninety edits.
15
+ const KEY = Symbol("list-page.section-host");
16
+
17
+ /** Call once in the layout that owns the section strip. */
18
+ export function setSectionHost(host: SectionHost): void {
19
+ setContext(KEY, host);
20
+ }
21
+
22
+ export function getSectionHost(): SectionHost | undefined {
23
+ return getContext<SectionHost | undefined>(KEY);
24
+ }
@@ -0,0 +1,39 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import type { Snippet } from "svelte";
6
+ import { cn } from "@heyramzi/utils";
7
+ import type { PhosphorIcon } from "../../lib/icon-types.js";
8
+
9
+ // =========================
10
+ // TYPES
11
+ // =========================
12
+ type Props = {
13
+ // The one label style for panel, rail group and sub-section headers. Retyped
14
+ // copies drifted to a different tracking value.
15
+ icon?: PhosphorIcon;
16
+ children: Snippet;
17
+ class?: string;
18
+ };
19
+
20
+ // =========================
21
+ // PROPS
22
+ // =========================
23
+ let { icon: Icon, children, class: className }: Props = $props();
24
+ </script>
25
+
26
+ <!-- =========================== -->
27
+ <!-- MARKUP -->
28
+ <!-- =========================== -->
29
+ <span
30
+ class={cn(
31
+ "flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground",
32
+ className,
33
+ )}
34
+ >
35
+ {#if Icon}
36
+ <Icon class="size-4 text-brand-indigo" aria-hidden="true" />
37
+ {/if}
38
+ {@render children()}
39
+ </span>
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from "vite-plus/test";
2
+ import { stepTo } from "./step.js";
3
+
4
+ describe("stepTo", () => {
5
+ const ids = ["a", "b", "c"];
6
+
7
+ it("steps both ways from the middle", () => {
8
+ expect(stepTo(ids, "b", -1)).toBe("a");
9
+ expect(stepTo(ids, "b", 1)).toBe("c");
10
+ });
11
+
12
+ it("stops at either end", () => {
13
+ expect(stepTo(ids, "a", -1)).toBe(null);
14
+ expect(stepTo(ids, "c", 1)).toBe(null);
15
+ });
16
+
17
+ it("goes nowhere without a listed selection", () => {
18
+ expect(stepTo(ids, null, 1)).toBe(null);
19
+ expect(stepTo(ids, "z", 1)).toBe(null);
20
+ });
21
+ });
@@ -0,0 +1,10 @@
1
+ /** The id `by` rows away from `current`, or null at either end or when `current` isn't listed. */
2
+ export function stepTo(
3
+ ids: readonly string[],
4
+ current: string | null | undefined,
5
+ by: 1 | -1,
6
+ ): string | null {
7
+ const index = current ? ids.indexOf(current) : -1;
8
+ if (index < 0) return null;
9
+ return ids[index + by] ?? null;
10
+ }
@@ -0,0 +1,63 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import { Input } from "../../shadcn/input/index.js";
6
+ import { MagnifyingGlass } from "phosphor-svelte";
7
+ import { cn } from "@heyramzi/utils";
8
+
9
+ // =========================
10
+ // TYPES
11
+ // =========================
12
+ type Props = {
13
+ value: string;
14
+ placeholder?: string;
15
+ size?: "sm" | "default";
16
+ class?: string;
17
+ };
18
+
19
+ // =========================
20
+ // PROPS
21
+ // =========================
22
+ let {
23
+ value = $bindable(""),
24
+ placeholder = "Search...",
25
+ size = "default",
26
+ class: extraClass,
27
+ }: Props = $props();
28
+ </script>
29
+
30
+ <!-- =========================== -->
31
+ <!-- MARKUP -->
32
+ <!-- =========================== -->
33
+ <!-- WHY: leading magnifying glass + a fixed height so every list page's toolbar
34
+ keeps the same rhythm; size="sm" matches the compact filter chips and
35
+ tabs used on dense list pages. -->
36
+ <!-- Drops the caller's desk width once the toolbar row breaks, read from the
37
+ shell's container. Outside the shell the caller's width stands. -->
38
+ <div
39
+ class={cn(
40
+ "relative",
41
+ extraClass,
42
+ "@max-3xl/toolbar:w-auto @max-3xl/toolbar:min-w-0 @max-3xl/toolbar:flex-1"
43
+ )}
44
+ >
45
+ <MagnifyingGlass
46
+ class={cn(
47
+ "pointer-events-none absolute top-1/2 -translate-y-1/2 text-muted-foreground",
48
+ size === "sm" ? "left-2 size-3" : "left-2 size-3",
49
+ )}
50
+ aria-hidden="true"
51
+ />
52
+ <!-- Keyed to the pointer, not the viewport. On an iPad, 12px text makes
53
+ Safari zoom on focus and never zoom back. -->
54
+ <Input
55
+ bind:value
56
+ {placeholder}
57
+ class={cn(
58
+ "w-full",
59
+ size === "sm" ? "h-7 pl-7 pr-2.5 text-xs" : "h-7 pl-7 pr-2.5 text-xs",
60
+ "any-pointer-coarse:h-10 any-pointer-coarse:text-base",
61
+ )}
62
+ />
63
+ </div>
@@ -0,0 +1,23 @@
1
+ import { cn } from "@heyramzi/utils";
2
+
3
+ type ToolbarTriggerOpts = {
4
+ active?: boolean;
5
+ className?: string;
6
+ };
7
+
8
+ // The trigger class for every toolbar control. Coarse pointers get a taller
9
+ // box at the same type size, since the chip is too small for a thumb.
10
+
11
+ // One fixed height, matching the `sm` button and the segmented track. A second
12
+ // size put a 24px filter beside 28px controls on the calendar (14 Oct 2026).
13
+ export function toolbarTriggerClass({
14
+ active = false,
15
+ className,
16
+ }: ToolbarTriggerOpts = {}): string {
17
+ return cn(
18
+ "inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-border font-medium transition-colors active:translate-y-px",
19
+ "h-7 px-2.5 text-xs any-pointer-coarse:h-10 any-pointer-coarse:px-3",
20
+ active ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground",
21
+ className,
22
+ );
23
+ }
@@ -0,0 +1 @@
1
+ export type ViewMode = "list" | "board" | "grid";
@@ -0,0 +1 @@
1
+ export { default as LocaleSwitch } from "./locale-switch.svelte";
@@ -0,0 +1,59 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import { SegmentedFilter, type TabDef } from "../segmented/index.js";
6
+
7
+ // =========================
8
+ // TYPES
9
+ // =========================
10
+ type LocaleOption = {
11
+ value: string;
12
+ label: string;
13
+ flag: string;
14
+ };
15
+
16
+ type Props = {
17
+ // Wraps `SegmentedFilter` so it shares one track with every other strip. It
18
+ // only adds each language's flag.
19
+ value: string;
20
+ onchange: (value: string) => void;
21
+ options?: LocaleOption[];
22
+ /** Names the control for a screen reader. Without it the group is two bare letters. */
23
+ ariaLabel?: string;
24
+ class?: string;
25
+ };
26
+
27
+ // =========================
28
+ // PROPS
29
+ // =========================
30
+ let {
31
+ value,
32
+ onchange,
33
+ options = [
34
+ { value: "en", label: "EN", flag: "/flags/US.svg" },
35
+ { value: "fr", label: "FR", flag: "/flags/FR.svg" },
36
+ ],
37
+ ariaLabel = "Language",
38
+ class: className,
39
+ }: Props = $props();
40
+
41
+ // =========================
42
+ // DERIVED
43
+ // =========================
44
+ let segments = $derived<TabDef<string>[]>(
45
+ options.map((option) => ({ value: option.value, label: option.label, image: option.flag })),
46
+ );
47
+ </script>
48
+
49
+ <!-- =========================== -->
50
+ <!-- MARKUP -->
51
+ <!-- =========================== -->
52
+ <SegmentedFilter
53
+ {value}
54
+ onValueChange={onchange}
55
+ options={segments}
56
+ block={false}
57
+ {ariaLabel}
58
+ class={className}
59
+ />
@@ -0,0 +1 @@
1
+ export { default as Mark } from "./mark.svelte";
@@ -0,0 +1,70 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import { cn } from "@heyramzi/utils";
6
+
7
+ // =========================
8
+ // TYPES
9
+ // =========================
10
+
11
+ /**
12
+ * A brand's mark at glyph size: a kit icon, a channel avatar, a client logo.
13
+ * One renderer, because three places each handled a missing file their own way.
14
+ */
15
+ type Props = {
16
+ /** The file. A missing or broken one falls back to the label's initial. */
17
+ src?: string;
18
+ /** Read for the fallback letter. The mark itself is decorative. */
19
+ label: string;
20
+ /** `circle` crops a photograph to a disc, `square` fits a logo whole. */
21
+ shape?: "square" | "circle";
22
+ class?: string;
23
+ };
24
+
25
+ // =========================
26
+ // PROPS
27
+ // =========================
28
+ let { src, label, shape = "square", class: className }: Props = $props();
29
+
30
+ // =========================
31
+ // STATE
32
+ // =========================
33
+ /** The src that failed, so a later one gets its own chance to load. */
34
+ let broken = $state("");
35
+
36
+ // =========================
37
+ // DERIVED
38
+ // =========================
39
+ let round = $derived(shape === "circle");
40
+ </script>
41
+
42
+ <!-- =========================== -->
43
+ <!-- MARKUP -->
44
+ <!-- =========================== -->
45
+ <!-- WHY the letter is a fallback and not a backdrop: a tile drawn UNDER the mark
46
+ shows its initial through every logo that is not a full-bleed square. -->
47
+ {#if src && broken !== src}
48
+ <img
49
+ {src}
50
+ alt=""
51
+ loading="lazy"
52
+ referrerpolicy="no-referrer"
53
+ class={cn(
54
+ "size-4 shrink-0",
55
+ round ? "rounded-full object-cover" : "object-contain",
56
+ className,
57
+ )}
58
+ onerror={() => (broken = src)}
59
+ />
60
+ {:else}
61
+ <span
62
+ class={cn(
63
+ "bg-muted text-muted-foreground flex size-4 shrink-0 items-center justify-center text-[10px] leading-none font-bold",
64
+ round ? "rounded-full" : "rounded-xs",
65
+ className,
66
+ )}
67
+ >
68
+ {label.charAt(0)}
69
+ </span>
70
+ {/if}
@@ -0,0 +1,6 @@
1
+ export { default as OpenIn } from "./open-in.svelte";
2
+ export type {
3
+ OpenInSize,
4
+ OpenInTool,
5
+ OpenInVariant,
6
+ } from "./open-in.svelte";
@@ -0,0 +1,106 @@
1
+ <script lang="ts" module>
2
+ import type { Component } from "svelte";
3
+
4
+ /**
5
+ * The one way out to a tool: mark, name, go-to arrow, never the verb, which
6
+ * survives as the accessible name.
7
+ */
8
+ export interface OpenInTool {
9
+ /** The visible text, and the word the tool calls itself. */
10
+ name: string;
11
+ /** The default accessible sentence, when the caller passes no translated one. */
12
+ action: string;
13
+ icon: Component<{ size?: number; class?: string }>;
14
+ }
15
+
16
+ /** `chip` is the bordered inline tag, `button` sits in a header beside other
17
+ * buttons, `icon` is the bare mark for a dense row. */
18
+ export type OpenInVariant = "chip" | "button" | "icon";
19
+ export type OpenInSize = "sm" | "default";
20
+ </script>
21
+
22
+ <script lang="ts">
23
+ // =========================
24
+ // IMPORTS
25
+ // =========================
26
+ import { ArrowSquareOut } from "phosphor-svelte";
27
+ import { cn } from "@heyramzi/utils";
28
+ import { buttonVariants } from "../../shadcn/button/index.js";
29
+
30
+ // =========================
31
+ // TYPES
32
+ // =========================
33
+ interface Props {
34
+ /** The tool to link to: its name, accessible sentence and mark. */
35
+ tool: OpenInTool;
36
+ href: string;
37
+ variant?: OpenInVariant;
38
+ size?: OpenInSize;
39
+ /** Overrides the visible text. The accessible name is `title`, never this. */
40
+ label?: string;
41
+ /** The accessible sentence, translated by the caller. */
42
+ title?: string;
43
+ class?: string;
44
+ }
45
+
46
+ // =========================
47
+ // PROPS
48
+ // =========================
49
+ let {
50
+ tool,
51
+ href,
52
+ variant = "chip",
53
+ size = "default",
54
+ label,
55
+ title,
56
+ class: className,
57
+ }: Props = $props();
58
+
59
+ // =========================
60
+ // DERIVED
61
+ // =========================
62
+ let Icon = $derived(tool.icon);
63
+ let iconSize = $derived(size === "sm" ? 12 : 14);
64
+ let action = $derived(title ?? tool.action);
65
+ let text = $derived(label ?? tool.name);
66
+ </script>
67
+
68
+ <!-- =========================== -->
69
+ <!-- MARKUP -->
70
+ <!-- =========================== -->
71
+ <!-- The click stops here because these usually sit in a clickable row, and a
72
+ call site that forgot would open the row as well. -->
73
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
74
+ <div
75
+ class="inline-flex shrink-0"
76
+ onclick={(e) => e.stopPropagation()}
77
+ onkeydown={(e) => e.stopPropagation()}
78
+ >
79
+ <a
80
+ {href}
81
+ target="_blank"
82
+ rel="noopener noreferrer"
83
+ aria-label={action}
84
+ title={action}
85
+ class={cn(
86
+ variant === "button"
87
+ ? buttonVariants({ variant: "outline", size: size === "sm" ? "sm" : "default" })
88
+ : variant === "icon"
89
+ ? "inline-flex shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
90
+ : cn(
91
+ "inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap rounded-md border border-border text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
92
+ size === "sm" ? "px-1.5 py-0.5 text-[11px]" : "px-2 py-1 text-xs",
93
+ ),
94
+ variant === "icon" && (size === "sm" ? "size-5" : "size-6"),
95
+ className,
96
+ )}
97
+ >
98
+ <Icon size={iconSize} />
99
+ {#if variant !== "icon"}
100
+ {text}
101
+ <!-- The one thing the mark can't say: this leaves the app. It stays muted
102
+ so the eye lands on the tool name first. -->
103
+ <ArrowSquareOut size={iconSize} class="text-muted-foreground" />
104
+ {/if}
105
+ </a>
106
+ </div>
@@ -0,0 +1,4 @@
1
+ export { default as Picker } from "./picker.svelte";
2
+ export { default as MarkPicker } from "./mark-picker.svelte";
3
+ export { pickerTriggerClass } from "./trigger.js";
4
+ export type { PickerGroup, PickerOption } from "./types.js";