@heyramzi/ui 7.0.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 (253) hide show
  1. package/COMPONENTS.md +28 -17
  2. package/README.md +5 -1
  3. package/package.json +25 -15
  4. package/scripts/sync-shadcn.mjs +17 -13
  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 -6
  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 +14 -14
  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/theme-transition.ts +37 -11
  118. package/src/lib/tone.ts +48 -0
  119. package/src/shadcn/accordion/accordion-trigger.svelte +15 -6
  120. package/src/shadcn/alert/alert.svelte +3 -1
  121. package/src/shadcn/badge/badge.svelte +8 -7
  122. package/src/shadcn/breadcrumb/breadcrumb-ellipsis.svelte +2 -2
  123. package/src/shadcn/breadcrumb/breadcrumb-separator.svelte +2 -2
  124. package/src/shadcn/calendar/calendar-month-select.svelte +2 -2
  125. package/src/shadcn/calendar/calendar-next-button.svelte +2 -2
  126. package/src/shadcn/calendar/calendar-prev-button.svelte +2 -2
  127. package/src/shadcn/calendar/calendar-year-select.svelte +2 -2
  128. package/src/shadcn/calendar/calendar.svelte +6 -0
  129. package/src/shadcn/code-block/code-block-copy-button.svelte +6 -55
  130. package/src/shadcn/command/index.ts +3 -0
  131. package/src/shadcn/field/index.ts +3 -0
  132. package/src/shadcn/field/simple-field.svelte +71 -0
  133. package/src/shadcn/field/simple-field.test.ts +47 -0
  134. package/src/shadcn/message-scroller/message-scroller-button.svelte +2 -2
  135. package/src/shadcn/radio-group/radio-group-item.svelte +3 -2
  136. package/src/shadcn/sheet/sheet-content.svelte +1 -0
  137. package/src/shadcn/sheet/sheet.css +48 -0
  138. package/src/shadcn/sonner/sonner.svelte +7 -10
  139. package/src/shadcn/toggle-group/toggle-group.svelte +10 -3
  140. package/src/shadcn/trace/trace-collapsible-trigger.svelte +2 -2
  141. package/src/ai/composer/Composer.css +0 -108
  142. package/src/ai/composer/Composer.svelte +0 -111
  143. package/src/ai/composer/index.ts +0 -10
  144. package/src/animations/accordion.css +0 -58
  145. package/src/animations/icon-swap.css +0 -43
  146. package/src/animations/notification-badge.css +0 -81
  147. package/src/animations/panel.css +0 -86
  148. package/src/animations/reasoning-stream.css +0 -49
  149. package/src/animations/skeleton-reveal.css +0 -97
  150. package/src/animations/streaming-text.css +0 -31
  151. package/src/animations/tabs-sliding.css +0 -83
  152. package/src/animations/texts-reveal.css +0 -70
  153. package/src/animations/tooltip.css +0 -72
  154. package/src/animations/view-transition-reveal.ts +0 -74
  155. package/src/lib/utils.ts +0 -45
  156. package/src/showcase/AiSection.svelte +0 -150
  157. package/src/showcase/AnimationsSection.svelte +0 -1658
  158. package/src/showcase/EffectsSection.svelte +0 -152
  159. package/src/showcase/ShadcnSection.svelte +0 -27
  160. package/src/showcase/ShowcaseImport.svelte +0 -126
  161. package/src/showcase/ShowcaseList.svelte +0 -132
  162. package/src/showcase/ShowcaseNav.svelte +0 -247
  163. package/src/showcase/ShowcaseSection.svelte +0 -112
  164. package/src/showcase/ShowcaseShell.svelte +0 -55
  165. package/src/showcase/ShowcaseStage.svelte +0 -342
  166. package/src/showcase/ShowcaseVariant.svelte +0 -109
  167. package/src/showcase/assets/image-reveal/art-1.jpg +0 -0
  168. package/src/showcase/assets/image-reveal/art-2.jpg +0 -0
  169. package/src/showcase/assets/image-reveal/art-3.jpg +0 -0
  170. package/src/showcase/check.svg +0 -1
  171. package/src/showcase/chevron-arms.svg +0 -2
  172. package/src/showcase/chevron-down.svg +0 -1
  173. package/src/showcase/copy.svg +0 -2
  174. package/src/showcase/demos/ai/confirmation.svelte +0 -59
  175. package/src/showcase/demos/ai/loading-state.svelte +0 -19
  176. package/src/showcase/demos/ai/prompt-input.svelte +0 -57
  177. package/src/showcase/demos/ai/reasoning.svelte +0 -19
  178. package/src/showcase/demos/ai/thinking-dots.svelte +0 -15
  179. package/src/showcase/demos/ai/voice-waveform.svelte +0 -24
  180. package/src/showcase/demos/effects/animated-beam.svelte +0 -50
  181. package/src/showcase/demos/effects/animated-group.svelte +0 -47
  182. package/src/showcase/demos/effects/border-beam-default.svelte +0 -20
  183. package/src/showcase/demos/effects/border-beam-line.svelte +0 -20
  184. package/src/showcase/demos/effects/border-beam-mono.svelte +0 -20
  185. package/src/showcase/demos/effects/border-beam-sunset.svelte +0 -20
  186. package/src/showcase/demos/effects/image-comparison.svelte +0 -25
  187. package/src/showcase/demos/effects/image-reveal.svelte +0 -49
  188. package/src/showcase/demos/effects/in-view.svelte +0 -41
  189. package/src/showcase/demos/effects/integrations-hub.svelte +0 -82
  190. package/src/showcase/demos/effects/magnetic.svelte +0 -31
  191. package/src/showcase/demos/effects/marquee.svelte +0 -33
  192. package/src/showcase/demos/effects/progressive-blur.svelte +0 -47
  193. package/src/showcase/demos/effects/spotlight.svelte +0 -40
  194. package/src/showcase/demos/effects/text-effect.svelte +0 -55
  195. package/src/showcase/demos/shadcn/accordion.svelte +0 -45
  196. package/src/showcase/demos/shadcn/alert-dialog.svelte +0 -23
  197. package/src/showcase/demos/shadcn/alert.svelte +0 -30
  198. package/src/showcase/demos/shadcn/attachment.svelte +0 -71
  199. package/src/showcase/demos/shadcn/avatar.svelte +0 -30
  200. package/src/showcase/demos/shadcn/badge.svelte +0 -26
  201. package/src/showcase/demos/shadcn/breadcrumb.svelte +0 -34
  202. package/src/showcase/demos/shadcn/bubble.svelte +0 -36
  203. package/src/showcase/demos/shadcn/button.svelte +0 -11
  204. package/src/showcase/demos/shadcn/calendar.svelte +0 -8
  205. package/src/showcase/demos/shadcn/card.svelte +0 -39
  206. package/src/showcase/demos/shadcn/checkbox.svelte +0 -39
  207. package/src/showcase/demos/shadcn/code-block.svelte +0 -80
  208. package/src/showcase/demos/shadcn/collapsible.svelte +0 -20
  209. package/src/showcase/demos/shadcn/command.svelte +0 -48
  210. package/src/showcase/demos/shadcn/context-menu.svelte +0 -54
  211. package/src/showcase/demos/shadcn/demo-harness.svelte +0 -12
  212. package/src/showcase/demos/shadcn/demos.test.ts +0 -20
  213. package/src/showcase/demos/shadcn/dialog.svelte +0 -38
  214. package/src/showcase/demos/shadcn/dropdown-menu.svelte +0 -59
  215. package/src/showcase/demos/shadcn/empty.svelte +0 -29
  216. package/src/showcase/demos/shadcn/field.svelte +0 -115
  217. package/src/showcase/demos/shadcn/hover-card.svelte +0 -32
  218. package/src/showcase/demos/shadcn/index.ts +0 -382
  219. package/src/showcase/demos/shadcn/input-otp.svelte +0 -19
  220. package/src/showcase/demos/shadcn/input.svelte +0 -5
  221. package/src/showcase/demos/shadcn/label.svelte +0 -11
  222. package/src/showcase/demos/shadcn/marker.svelte +0 -30
  223. package/src/showcase/demos/shadcn/message-scroller.svelte +0 -44
  224. package/src/showcase/demos/shadcn/message.svelte +0 -75
  225. package/src/showcase/demos/shadcn/popover.svelte +0 -36
  226. package/src/showcase/demos/shadcn/progress.svelte +0 -13
  227. package/src/showcase/demos/shadcn/radio-group.svelte +0 -19
  228. package/src/showcase/demos/shadcn/scroll-area.svelte +0 -18
  229. package/src/showcase/demos/shadcn/select.svelte +0 -29
  230. package/src/showcase/demos/shadcn/separator.svelte +0 -12
  231. package/src/showcase/demos/shadcn/sheet.svelte +0 -48
  232. package/src/showcase/demos/shadcn/skeleton.svelte +0 -11
  233. package/src/showcase/demos/shadcn/slider.svelte +0 -7
  234. package/src/showcase/demos/shadcn/sonner.svelte +0 -18
  235. package/src/showcase/demos/shadcn/spinner.svelte +0 -11
  236. package/src/showcase/demos/shadcn/switch.svelte +0 -9
  237. package/src/showcase/demos/shadcn/table.svelte +0 -76
  238. package/src/showcase/demos/shadcn/tabs.svelte +0 -62
  239. package/src/showcase/demos/shadcn/textarea.svelte +0 -5
  240. package/src/showcase/demos/shadcn/toggle-group.svelte +0 -18
  241. package/src/showcase/demos/shadcn/toggle.svelte +0 -14
  242. package/src/showcase/demos/shadcn/tooltip.svelte +0 -13
  243. package/src/showcase/demos/shadcn/trace.svelte +0 -51
  244. package/src/showcase/heart.svg +0 -1
  245. package/src/showcase/index.ts +0 -38
  246. package/src/showcase/kit.test.ts +0 -72
  247. package/src/showcase/kit.ts +0 -16
  248. package/src/showcase/plus.svg +0 -1
  249. package/src/showcase/showcase-shell.css +0 -91
  250. package/src/showcase/stacked-section.css +0 -15
  251. package/src/showcase/success-check.svg +0 -1
  252. package/src/showcase/types.ts +0 -29
  253. package/src/svg-raw.d.ts +0 -7
@@ -0,0 +1,232 @@
1
+ <script lang="ts">
2
+ // ========================= IMPORTS
3
+ import { cn } from "@heyramzi/utils";
4
+ import { buttonVariants, type ButtonSize, type ButtonVariant } from "../../shadcn/button/index.js";
5
+ import type { Snippet } from "svelte";
6
+ import type { HTMLButtonAttributes } from "svelte/elements";
7
+
8
+ // ========================= TYPES
9
+ type Props = {
10
+ duration?: number;
11
+ onConfirm: () => void;
12
+ disabled?: boolean;
13
+ /** Uses the button recipe so call sites stop drawing their own.
14
+ * `variant: null` opts out, for a caller that really is a menu row. */
15
+ variant?: ButtonVariant | null;
16
+ size?: ButtonSize;
17
+ /** Square, for a glyph with no label. Mirrors `Button`. */
18
+ icon?: boolean;
19
+ class?: string;
20
+ idle: Snippet;
21
+ holding: Snippet;
22
+ } & HTMLButtonAttributes;
23
+
24
+ // ========================= PROPS
25
+ let {
26
+ duration = 800,
27
+ onConfirm,
28
+ disabled = false,
29
+ variant = "destructive",
30
+ size = "default",
31
+ icon = false,
32
+ class: className = "",
33
+ idle,
34
+ holding,
35
+ ...restProps
36
+ }: Props = $props();
37
+
38
+ // ========================= STATE
39
+ let progress = $state(0);
40
+ let isHolding = $state(false);
41
+ let rafId: number | null = $state(null);
42
+ let startTime: number | null = $state(null);
43
+
44
+ // ========================= FUNCTIONS
45
+ function startHold() {
46
+ if (disabled) return;
47
+ isHolding = true;
48
+ startTime = performance.now();
49
+ tick();
50
+ }
51
+
52
+ function tick() {
53
+ if (!isHolding || !startTime) return;
54
+
55
+ const elapsed = performance.now() - startTime;
56
+ progress = Math.min(elapsed / duration, 1);
57
+
58
+ if (progress >= 1) {
59
+ complete();
60
+ return;
61
+ }
62
+
63
+ rafId = requestAnimationFrame(tick);
64
+ }
65
+
66
+ function cancelHold() {
67
+ if (!isHolding) return;
68
+ isHolding = false;
69
+ startTime = null;
70
+ if (rafId !== null) {
71
+ cancelAnimationFrame(rafId);
72
+ rafId = null;
73
+ }
74
+ // WHY: dropping is-holding lets the fill retract via the CSS transition below
75
+ progress = 0;
76
+ }
77
+
78
+ function complete() {
79
+ isHolding = false;
80
+ startTime = null;
81
+ if (rafId !== null) {
82
+ cancelAnimationFrame(rafId);
83
+ rafId = null;
84
+ }
85
+ onConfirm();
86
+ // Reset after confirm fires
87
+ progress = 0;
88
+ }
89
+
90
+ function handleKeydown(e: KeyboardEvent) {
91
+ if (e.key === " " || e.key === "Enter") {
92
+ e.preventDefault();
93
+ startHold();
94
+ }
95
+ }
96
+
97
+ function handleKeyup(e: KeyboardEvent) {
98
+ if (e.key === " " || e.key === "Enter") {
99
+ cancelHold();
100
+ }
101
+ }
102
+ </script>
103
+
104
+ <!-- ========================= MARKUP -->
105
+ <button
106
+ {...restProps}
107
+ type="button"
108
+ class={cn(
109
+ // Callers size this at about 22px. On touch, a hold needs a box the finger
110
+ // can stay inside for the whole press without cancelling it.
111
+ "hold-to-confirm relative overflow-hidden select-none",
112
+ "any-pointer-coarse:inline-flex any-pointer-coarse:min-h-10 any-pointer-coarse:min-w-10 any-pointer-coarse:items-center any-pointer-coarse:justify-center",
113
+ variant && buttonVariants({ variant, size, icon }),
114
+ disabled && "pointer-events-none opacity-50",
115
+ className,
116
+ )}
117
+ style="--progress: {progress}"
118
+ data-holding={isHolding}
119
+ onpointerdown={startHold}
120
+ onpointerup={cancelHold}
121
+ onpointerleave={cancelHold}
122
+ onpointercancel={cancelHold}
123
+ onkeydown={handleKeydown}
124
+ onkeyup={handleKeyup}
125
+ {disabled}
126
+ >
127
+ <!-- Fill sweep: scales from the left across the whole control -->
128
+ <span
129
+ class="hold-to-confirm-fill pointer-events-none absolute inset-0 z-0 origin-left bg-destructive"
130
+ class:is-holding={isHolding}
131
+ ></span>
132
+
133
+ <!-- Leading edge: a bright sliver that rides the head of the fill -->
134
+ <span
135
+ class="hold-to-confirm-edge pointer-events-none absolute inset-y-0 z-0"
136
+ class:is-holding={isHolding}
137
+ ></span>
138
+
139
+ <!-- Content -->
140
+ <span class="relative z-[1] flex items-center gap-1.5">
141
+ {#if isHolding}
142
+ <span class="hold-to-confirm-text flex items-center gap-1.5 text-destructive-foreground">
143
+ {@render holding()}
144
+ </span>
145
+ {:else}
146
+ <span class="hold-to-confirm-text flex items-center gap-1.5">
147
+ {@render idle()}
148
+ </span>
149
+ {/if}
150
+ </span>
151
+ </button>
152
+
153
+ <!-- ========================= STYLE -->
154
+ <style>
155
+ .hold-to-confirm {
156
+ touch-action: none;
157
+ }
158
+
159
+ .hold-to-confirm:active:not(:disabled) {
160
+ transform: scale(0.97);
161
+ }
162
+
163
+ /* Fill: GPU-friendly scaleX. A vertical sheen survives the horizontal
164
+ scale because it runs top-to-bottom, so the wipe keeps a crisp surface. */
165
+ .hold-to-confirm-fill {
166
+ transform: scaleX(var(--progress, 0));
167
+ background-image: linear-gradient(
168
+ to bottom,
169
+ color-mix(in oklch, var(--color-destructive-foreground) 14%, transparent),
170
+ transparent 55%
171
+ );
172
+ transition: transform var(--duration-quick) var(--ease-smooth-out);
173
+ }
174
+
175
+ .hold-to-confirm-fill.is-holding {
176
+ transition: none;
177
+ }
178
+
179
+ /* Leading edge: positioned (not scaled) so its glow stays crisp at any
180
+ width. Sits exactly at the head of the fill and fades in while held. */
181
+ .hold-to-confirm-edge {
182
+ left: calc(var(--progress, 0) * 100%);
183
+ width: 2px;
184
+ margin-left: -2px;
185
+ opacity: 0;
186
+ background: color-mix(in oklch, var(--color-destructive-foreground) 65%, var(--color-destructive));
187
+ box-shadow: 0 0 4px 0 color-mix(in oklch, var(--color-destructive) 92%, transparent);
188
+ transition:
189
+ left var(--duration-quick) var(--ease-smooth-out),
190
+ opacity var(--duration-quick) var(--ease-out);
191
+ }
192
+
193
+ .hold-to-confirm-edge.is-holding {
194
+ opacity: 1;
195
+ transition: opacity var(--duration-quick) var(--ease-out);
196
+ }
197
+
198
+ .hold-to-confirm-text {
199
+ animation: holdTextFadeIn var(--duration-quick) var(--ease-out);
200
+ }
201
+
202
+ @keyframes holdTextFadeIn {
203
+ from {
204
+ opacity: 0;
205
+ }
206
+ to {
207
+ opacity: 1;
208
+ }
209
+ }
210
+
211
+ @media (prefers-reduced-motion: reduce) {
212
+ .hold-to-confirm:active:not(:disabled) {
213
+ transform: none;
214
+ }
215
+
216
+ .hold-to-confirm-fill {
217
+ transition: none;
218
+ transform: scaleX(var(--progress, 0));
219
+ background-image: none;
220
+ opacity: 0.5;
221
+ }
222
+
223
+ .hold-to-confirm-edge {
224
+ transition: none;
225
+ box-shadow: none;
226
+ }
227
+
228
+ .hold-to-confirm-text {
229
+ animation: none;
230
+ }
231
+ }
232
+ </style>
@@ -0,0 +1 @@
1
+ export { default as HoldToConfirm } from "./hold-to-confirm.svelte";
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ import type { PhosphorIcon, IconWeight } from "../../lib/icon-types.js";
3
+
4
+ // =========================
5
+ // TYPES
6
+ // =========================
7
+ type Props = {
8
+ icon: PhosphorIcon;
9
+ size?: number;
10
+ class?: string;
11
+ weight?: IconWeight;
12
+ };
13
+
14
+ // =========================
15
+ // PROPS
16
+ // =========================
17
+ let { icon: IconComp, size = 24, class: className = "", weight }: Props = $props();
18
+ </script>
19
+
20
+ <!-- =========================== -->
21
+ <!-- MARKUP -->
22
+ <!-- =========================== -->
23
+ <IconComp {size} class={className} {weight} />
@@ -0,0 +1 @@
1
+ export { default as Icon } from "./icon.svelte";
@@ -0,0 +1 @@
1
+ export { default as LanguageFlag } from "./language-flag.svelte";
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import { cn } from "@heyramzi/utils";
6
+
7
+ // =========================
8
+ // CONSTANTS
9
+ // =========================
10
+ // The host serves these files from /flags. Pass `flags` for another set or path.
11
+ const DEFAULT_FLAGS: Record<string, string> = {
12
+ en: "/flags/US.svg",
13
+ fr: "/flags/FR.svg",
14
+ };
15
+
16
+ // =========================
17
+ // PROPS
18
+ // =========================
19
+ interface Props {
20
+ language: string;
21
+ /** Language code to image URL. */
22
+ flags?: Record<string, string>;
23
+ size?: "sm" | "md";
24
+ showLabel?: boolean;
25
+ class?: string;
26
+ }
27
+
28
+ let {
29
+ language,
30
+ flags = DEFAULT_FLAGS,
31
+ size = "sm",
32
+ showLabel = false,
33
+ class: className,
34
+ }: Props = $props();
35
+
36
+ // =========================
37
+ // DERIVED
38
+ // =========================
39
+ let src = $derived(flags[language]);
40
+ let sizeClass = $derived(size === "md" ? "size-5" : "size-3.5");
41
+ </script>
42
+
43
+ <!-- =========================== -->
44
+ <!-- MARKUP -->
45
+ <!-- =========================== -->
46
+ {#if src}
47
+ <span class={cn("inline-flex items-center gap-1.5", className)}>
48
+ <img
49
+ {src}
50
+ alt={language.toUpperCase()}
51
+ class={cn("shrink-0 rounded-sm", sizeClass)}
52
+ loading="lazy"
53
+ />
54
+ {#if showLabel}
55
+ <span class="text-xs font-medium uppercase text-muted-foreground">{language}</span>
56
+ {/if}
57
+ </span>
58
+ {:else if showLabel}
59
+ <span class={cn("text-xs font-medium uppercase text-muted-foreground", className)}>{language}</span>
60
+ {/if}
@@ -0,0 +1,50 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import EmptyState from "./empty-state.svelte";
6
+ import { cn } from "@heyramzi/utils";
7
+ import type { PhosphorIcon, IconWeight } from "../../lib/icon-types.js";
8
+
9
+ // =========================
10
+ // TYPES
11
+ // =========================
12
+ type Props = {
13
+ // Shown when nothing is picked or the board is empty. Both mean "content
14
+ // goes here", so both get a dashed `border-2` edge.
15
+ icon?: PhosphorIcon;
16
+ iconWeight?: IconWeight;
17
+ title: string;
18
+ description?: string;
19
+ actionLabel?: string;
20
+ actionHref?: string;
21
+ onAction?: () => void;
22
+ class?: string;
23
+ };
24
+
25
+ // =========================
26
+ // PROPS
27
+ // =========================
28
+ let {
29
+ icon,
30
+ iconWeight,
31
+ title,
32
+ description,
33
+ actionLabel,
34
+ actionHref,
35
+ onAction,
36
+ class: className,
37
+ }: Props = $props();
38
+ </script>
39
+
40
+ <!-- =========================== -->
41
+ <!-- MARKUP -->
42
+ <!-- =========================== -->
43
+ <div
44
+ class={cn(
45
+ "flex h-full items-center justify-center rounded-xl border-2 border-dashed border-border",
46
+ className,
47
+ )}
48
+ >
49
+ <EmptyState {icon} {iconWeight} {title} {description} {actionLabel} {actionHref} {onAction} />
50
+ </div>
@@ -0,0 +1,107 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import type { Snippet } from "svelte";
6
+ import { cn } from "@heyramzi/utils";
7
+
8
+ // =========================
9
+ // TYPES
10
+ // =========================
11
+ type Props = {
12
+ // Answers three questions in one order: what is this, what state is it in,
13
+ // what can I do with it. Each queue page used to order them differently.
14
+ title: string;
15
+ // The state chip beside the title.
16
+ badge?: Snippet;
17
+ // A picture ahead of the title, like a video frame or a deck cover. Some
18
+ // records aren't recognisable from their title alone.
19
+ leading?: Snippet;
20
+ // Facts about the record, rendered as one dot-separated line under the
21
+ // title. Falsy entries drop out, so a caller can inline its conditionals.
22
+ meta?: Array<string | null | undefined | false>;
23
+ // A sentence under the meta line, when the record explains itself.
24
+ note?: string | null;
25
+ // What you can do to the record. Pinned right of the identity block, EXCEPT
26
+ // when a picture leads: see the note in the markup.
27
+ actions?: Snippet;
28
+ // The top-right control, a record's way out. Pass `undefined` when empty:
29
+ // an empty snippet is still truthy and reserves the corner.
30
+ corner?: Snippet;
31
+ // Hairline-separated rows below the identity block. Use `DetailRow`.
32
+ children?: Snippet;
33
+ class?: string;
34
+ };
35
+
36
+ // =========================
37
+ // PROPS
38
+ // =========================
39
+ let { title, badge, leading, meta, note, actions, corner, children, class: className }: Props =
40
+ $props();
41
+
42
+ // =========================
43
+ // DERIVED
44
+ // =========================
45
+ let facts = $derived((meta ?? []).filter((item): item is string => Boolean(item)));
46
+ // Actions stack under the meta when something else claims the right edge:
47
+ // a picture, or the corner. Pinned beside a corner, they left the title
48
+ // a third of the row.
49
+ let stackActions = $derived(Boolean(leading || corner));
50
+ </script>
51
+
52
+ <!-- =========================== -->
53
+ <!-- MARKUP -->
54
+ <!-- =========================== -->
55
+ <section class={cn("overflow-hidden rounded-xl border border-border bg-card", className)}>
56
+ <div class="flex flex-wrap items-start justify-between gap-x-4 gap-y-3 p-4">
57
+ {#if leading}
58
+ <div class="shrink-0">{@render leading()}</div>
59
+ {/if}
60
+ <div class="min-w-0 flow-root flex-1 basis-64">
61
+ <!-- The corner floats, so only the lines beside it give way. Reserved
62
+ across the whole row it took ~150px off every line and wrapped a
63
+ normal title to three. -->
64
+ {#if corner}
65
+ <div class="float-end ms-3">{@render corner()}</div>
66
+ {/if}
67
+ <!-- The badge sits inline in the h2 so it trails the last word. As a
68
+ flex sibling it wrapped to its own row on every two-line title. -->
69
+ <!-- No text-balance: it evens the lines out, so a two-line title stopped
70
+ short of the corner instead of running the full width. -->
71
+ <h2 class="min-w-0 font-heading text-lg font-semibold leading-snug tracking-tight">
72
+ {title}{#if badge}<span class="ms-2 inline-flex align-middle">{@render badge()}</span
73
+ >{/if}
74
+ </h2>
75
+ {#if facts.length > 0}
76
+ <!-- Each dot hangs off the fact it leads, so a wrapped line's dot stays
77
+ in the gutter. A standalone flex-item dot dangled on the line above. -->
78
+ <div class="mt-1 overflow-hidden">
79
+ <p class="-ms-3 flex flex-wrap items-center text-sm text-muted-foreground">
80
+ {#each facts as fact, i (i)}
81
+ <span
82
+ class="relative ps-3 before:absolute before:start-1 before:opacity-40 before:content-['·']"
83
+ >{fact}</span
84
+ >
85
+ {/each}
86
+ </p>
87
+ </div>
88
+ {/if}
89
+ {#if note}
90
+ <p class="mt-1 text-sm text-muted-foreground">{note}</p>
91
+ {/if}
92
+ <!-- With a picture or a corner, actions sit in this column. Pinned right
93
+ they wrapped under the picture and read like its caption. -->
94
+ {#if actions && stackActions}
95
+ <div class="clear-both flex flex-wrap items-center gap-2 pt-3">
96
+ {@render actions()}
97
+ </div>
98
+ {/if}
99
+ </div>
100
+ {#if actions && !stackActions}
101
+ <div class="flex flex-wrap items-center gap-2">{@render actions()}</div>
102
+ {/if}
103
+ </div>
104
+ {#if children}
105
+ {@render children()}
106
+ {/if}
107
+ </section>
@@ -0,0 +1,90 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import type { Snippet } from "svelte";
6
+ import SectionLabel from "./section-label.svelte";
7
+ import { cn } from "@heyramzi/utils";
8
+ import { panelHeaderClass, panelShellClass } from "../../lib/panel.js";
9
+ import type { PhosphorIcon } from "../../lib/icon-types.js";
10
+
11
+ // =========================
12
+ // TYPES
13
+ // =========================
14
+ type Props = {
15
+ // One panel per idea in a detail pane. Hand-rolled copies drifted between
16
+ // 8px and 12px corners, so rounded-xl lives here once.
17
+ label?: string;
18
+ labelIcon?: PhosphorIcon;
19
+ // A mark the icon set does not carry (a platform logo), drawn ahead of the
20
+ // label at the same optical size.
21
+ labelLead?: Snippet;
22
+ // The right side of the header bar: a count, a hint, a button cluster.
23
+ actions?: Snippet;
24
+ // WHY: a panel whose body is a media grid or a full-bleed list supplies its
25
+ // own padding, so the shell must be able to step out of the way.
26
+ pad?: boolean;
27
+ // Takes the height the panels above it did not use, inside a `fill` stack.
28
+ // One panel per stack, and it is the one the reader works in.
29
+ grow?: boolean;
30
+ children: Snippet;
31
+ class?: string;
32
+ bodyClass?: string;
33
+ };
34
+
35
+ // =========================
36
+ // PROPS
37
+ // =========================
38
+ let {
39
+ label,
40
+ labelIcon,
41
+ labelLead,
42
+ actions,
43
+ pad = true,
44
+ grow = false,
45
+ children,
46
+ class: className,
47
+ bodyClass,
48
+ }: Props = $props();
49
+
50
+ // =========================
51
+ // DERIVED
52
+ // =========================
53
+ let hasHeader = $derived(Boolean(label || labelLead || actions));
54
+ </script>
55
+
56
+ <!-- =========================== -->
57
+ <!-- MARKUP -->
58
+ <!-- =========================== -->
59
+ <section
60
+ class={cn(
61
+ panelShellClass,
62
+ grow && "flex min-h-0 flex-1 flex-col",
63
+ className,
64
+ )}
65
+ >
66
+ {#if hasHeader}
67
+ <div
68
+ class={panelHeaderClass}
69
+ >
70
+ {#if label || labelLead}
71
+ <SectionLabel icon={labelIcon}>
72
+ {#if labelLead}
73
+ {@render labelLead()}
74
+ {/if}
75
+ {label}
76
+ </SectionLabel>
77
+ {:else}
78
+ <span></span>
79
+ {/if}
80
+ {#if actions}
81
+ <div class="flex min-w-0 flex-wrap items-center justify-end gap-2">
82
+ {@render actions()}
83
+ </div>
84
+ {/if}
85
+ </div>
86
+ {/if}
87
+ <div class={cn(pad && "p-4", grow && "flex min-h-0 flex-1 flex-col", bodyClass)}>
88
+ {@render children()}
89
+ </div>
90
+ </section>
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import type { Snippet } from "svelte";
6
+ import { cn } from "@heyramzi/utils";
7
+
8
+ // =========================
9
+ // TYPES
10
+ // =========================
11
+ type Props = {
12
+ // A hairline band inside `DetailHeader` or `DetailPanel` for the record's
13
+ // own facts, so the page never nests cards inside cards.
14
+ muted?: boolean;
15
+ children: Snippet;
16
+ class?: string;
17
+ };
18
+
19
+ // =========================
20
+ // PROPS
21
+ // =========================
22
+ let { muted = false, children, class: className }: Props = $props();
23
+ </script>
24
+
25
+ <!-- =========================== -->
26
+ <!-- MARKUP -->
27
+ <!-- =========================== -->
28
+ <div
29
+ class={cn(
30
+ "border-t border-border px-4 py-3 text-sm leading-relaxed",
31
+ muted && "text-xs text-muted-foreground",
32
+ className,
33
+ )}
34
+ >
35
+ {@render children()}
36
+ </div>
@@ -0,0 +1,33 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import type { Snippet } from "svelte";
6
+ import { cn } from "@heyramzi/utils";
7
+
8
+ // =========================
9
+ // TYPES
10
+ // =========================
11
+ type Props = {
12
+ // A flat stack of panels at one gap. `xl:pr-1` keeps the top panel's
13
+ // corner clear of the shell's scrollbar.
14
+
15
+ // `fill` makes the stack at least as tall as its scroller, so a `grow`
16
+ // panel (like a writing box) takes the empty bottom third back.
17
+ fill?: boolean;
18
+ children: Snippet;
19
+ class?: string;
20
+ };
21
+
22
+ // =========================
23
+ // PROPS
24
+ // =========================
25
+ let { fill = false, children, class: className }: Props = $props();
26
+ </script>
27
+
28
+ <!-- =========================== -->
29
+ <!-- MARKUP -->
30
+ <!-- =========================== -->
31
+ <div class={cn("flex flex-col gap-4 xl:pr-1", fill && "min-h-full", className)}>
32
+ {@render children()}
33
+ </div>