@heyramzi/ui 2.1.0 → 2.2.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 (312) hide show
  1. package/package.json +7 -8
  2. package/scripts/sync-shadcn.mjs +3 -4
  3. package/src/agents/agent-avatar.svelte +604 -575
  4. package/src/agents/agent-cast.svelte +179 -183
  5. package/src/agents/demo.svelte +125 -113
  6. package/src/agents/helpers.ts +23 -17
  7. package/src/agents/index.ts +4 -4
  8. package/src/agents/page-state.ts +6 -6
  9. package/src/agents/types.ts +6 -6
  10. package/src/ai/ai-button/AiButton.svelte +4 -12
  11. package/src/ai/generation-progress/GenerationProgress.svelte +10 -3
  12. package/src/ai/prompt-input/PromptInputTextarea.svelte +1 -2
  13. package/src/ai/response/Response.css +2 -3
  14. package/src/animations/avatar-group-hover.css +2 -16
  15. package/src/animations/card-tilt.css +3 -6
  16. package/src/animations/digit-pop-in.css +2 -15
  17. package/src/animations/dropdown.css +4 -17
  18. package/src/animations/input-clear-dissolve.css +5 -18
  19. package/src/animations/input-shake.css +9 -41
  20. package/src/animations/learn-more-hover.css +17 -9
  21. package/src/animations/like-button.css +2 -4
  22. package/src/animations/page-side-by-side.css +2 -10
  23. package/src/animations/plus-menu-morph.css +2 -5
  24. package/src/animations/shimmer-text.css +5 -18
  25. package/src/animations/spinning-counter.css +2 -6
  26. package/src/animations/success-check.css +2 -15
  27. package/src/animations/thinking-states.css +2 -7
  28. package/src/brand/brand.test.ts +21 -5
  29. package/src/brand/index.ts +4 -10
  30. package/src/canvas/card/card-frame.svelte +43 -39
  31. package/src/canvas/card/resize-handles.svelte +89 -80
  32. package/src/canvas/chrome/chrome-zoom.svelte +101 -100
  33. package/src/canvas/chrome/lock-badge.svelte +8 -8
  34. package/src/canvas/chrome/side-handles.svelte +24 -19
  35. package/src/canvas/chrome/size-chip.svelte +30 -30
  36. package/src/canvas/chrome/snap-guides.svelte +20 -20
  37. package/src/canvas/components/icons/ToolIcon.svelte +8 -8
  38. package/src/canvas/components/shared/svg/PreviewSvg.svelte +19 -19
  39. package/src/canvas/components/shared/svg/ShapeOutline.svelte +71 -48
  40. package/src/canvas/components/shared/svg/StrokeNode.svelte +21 -27
  41. package/src/canvas/components/shared/svg/TextRule.svelte +25 -17
  42. package/src/canvas/edges/bend-handle.svelte +46 -46
  43. package/src/canvas/edges/edge-gradient.svelte +10 -10
  44. package/src/canvas/edges/gradient-edge.svelte +45 -29
  45. package/src/canvas/editor/lock.test.ts +6 -1
  46. package/src/canvas/editor/style.ts +7 -1
  47. package/src/canvas/headless/svg-figure.test.ts +5 -1
  48. package/src/canvas/lod/lod-switch.svelte +111 -106
  49. package/src/canvas/pen/PenField.svelte +87 -81
  50. package/src/canvas/pen/PenOverlay.svelte +127 -127
  51. package/src/canvas/pen/ShapeNode.svelte +31 -32
  52. package/src/canvas/pen/TextNode.svelte +54 -53
  53. package/src/canvas/pen/host-field.test.svelte +7 -7
  54. package/src/canvas/pen/shapes.ts +7 -1
  55. package/src/canvas/screen/GlyphIcon.svelte +22 -17
  56. package/src/canvas/screen/PartView.svelte +138 -72
  57. package/src/canvas/screen/ScreenBody.svelte +81 -60
  58. package/src/canvas/screen/ScreenCard.svelte +20 -20
  59. package/src/canvas/ui/CanvasToolbar.svelte +173 -85
  60. package/src/canvas/ui/FrameLayer.svelte +59 -59
  61. package/src/canvas/ui/PenLayer.svelte +39 -30
  62. package/src/canvas/ui/StylePanel.svelte +119 -102
  63. package/src/canvas/ui/canvas-grid.svelte +137 -129
  64. package/src/charts/animated-value.svelte +1 -1
  65. package/src/charts/area-chart.css +2 -5
  66. package/src/charts/area-chart.svelte +1 -1
  67. package/src/charts/bar-chart.css +2 -3
  68. package/src/charts/bar-chart.svelte +1 -1
  69. package/src/charts/charts.css +5 -17
  70. package/src/charts/crosshair.svelte +1 -1
  71. package/src/charts/dashboard/dashboard-empty.svelte +25 -25
  72. package/src/charts/dashboard/dashboard-section.svelte +24 -24
  73. package/src/charts/dashboard/data-panel.svelte +50 -50
  74. package/src/charts/dashboard/data-sort-head.svelte +56 -56
  75. package/src/charts/dashboard/data-table.svelte +138 -138
  76. package/src/charts/dashboard/panel-grid.svelte +30 -30
  77. package/src/charts/dashboard/period-filter.svelte +25 -25
  78. package/src/charts/dashboard/rating-chip.svelte +19 -19
  79. package/src/charts/dashboard/stat-band.svelte +32 -32
  80. package/src/charts/dashboard/stat-tile.svelte +106 -99
  81. package/src/charts/donut-chart.svelte +1 -1
  82. package/src/charts/edge-fade.svelte +1 -1
  83. package/src/charts/figure.svelte +1 -1
  84. package/src/charts/gauge-chart.css +4 -7
  85. package/src/charts/gauge-chart.svelte +1 -1
  86. package/src/charts/hover-focus.svelte +1 -1
  87. package/src/charts/legend.css +1 -3
  88. package/src/charts/legend.svelte +1 -1
  89. package/src/charts/line-chart.svelte +1 -1
  90. package/src/charts/number-chart.css +6 -9
  91. package/src/charts/number-chart.svelte +1 -1
  92. package/src/charts/reference-line.svelte +19 -19
  93. package/src/charts/reveal.svelte +1 -1
  94. package/src/charts/ring-chart.css +2 -4
  95. package/src/charts/ring-chart.svelte +1 -1
  96. package/src/charts/sparkline.svelte +5 -2
  97. package/src/charts/stat-card.svelte +1 -1
  98. package/src/charts/time-series-chart.css +2 -5
  99. package/src/charts/trend-pill.css +2 -4
  100. package/src/charts/trend-pill.svelte +1 -1
  101. package/src/components/copy/copy-button.svelte +93 -98
  102. package/src/components/copy/copy-button.test.ts +10 -2
  103. package/src/components/data-table/infinite-scroll.svelte +42 -42
  104. package/src/components/data-table/pagination.svelte +34 -22
  105. package/src/components/data-table/row-arrow.svelte +19 -16
  106. package/src/components/data-table/row-context-menu.svelte +52 -52
  107. package/src/components/data-table/sortable-head.svelte +25 -25
  108. package/src/components/data-table/table-skeleton.svelte +41 -36
  109. package/src/components/docs/doc-page.svelte +18 -9
  110. package/src/components/docs/on-this-page.svelte +16 -3
  111. package/src/components/docs/scroll-spy.svelte.ts +2 -3
  112. package/src/components/field-row/field-row.svelte +10 -8
  113. package/src/components/hold-to-confirm/hold-to-confirm.svelte +5 -1
  114. package/src/components/icon/icon.svelte +14 -14
  115. package/src/components/language-flag/language-flag.svelte +3 -1
  116. package/src/components/list-page/detail-empty.svelte +34 -34
  117. package/src/components/list-page/detail-header.svelte +55 -47
  118. package/src/components/list-page/detail-panel.svelte +51 -59
  119. package/src/components/list-page/detail-row.svelte +19 -19
  120. package/src/components/list-page/detail-stack.svelte +21 -21
  121. package/src/components/list-page/empty-state.svelte +30 -30
  122. package/src/components/list-page/filter-item.svelte +20 -20
  123. package/src/components/list-page/filter-popover.svelte +60 -55
  124. package/src/components/list-page/grouped-list.svelte +4 -2
  125. package/src/components/list-page/index.ts +5 -1
  126. package/src/components/list-page/list-page-shell.svelte +2 -5
  127. package/src/components/list-page/master-detail.svelte +87 -88
  128. package/src/components/list-page/notice-bar.svelte +38 -38
  129. package/src/components/list-page/prev-next.svelte +63 -63
  130. package/src/components/list-page/rail-body.svelte +18 -18
  131. package/src/components/list-page/rail-empty.svelte +23 -30
  132. package/src/components/list-page/rail-head.svelte +24 -24
  133. package/src/components/list-page/rail-row.svelte +77 -73
  134. package/src/components/list-page/section-host.ts +2 -3
  135. package/src/components/locale-switch/locale-switch.svelte +41 -41
  136. package/src/components/mark/mark.svelte +34 -38
  137. package/src/components/open-in/index.ts +1 -5
  138. package/src/components/open-in/open-in.svelte +40 -40
  139. package/src/components/picker/mark-picker.svelte +49 -49
  140. package/src/components/picker/picker.svelte +5 -3
  141. package/src/components/section-header/section-header.test.ts +7 -1
  142. package/src/components/section-header/section-label.svelte +20 -20
  143. package/src/components/segmented/animated-tabs.svelte +31 -2
  144. package/src/components/segmented/collapse.test.ts +2 -1
  145. package/src/components/segmented/index.ts +2 -0
  146. package/src/components/segmented/segment.svelte +52 -49
  147. package/src/components/segmented/segmented-filter.svelte +79 -80
  148. package/src/components/segmented/segmented-nav.svelte +34 -40
  149. package/src/components/segmented/segmented-overflow.svelte +53 -50
  150. package/src/components/segmented/segmented-track.svelte +62 -46
  151. package/src/components/segmented/segmented.ts +2 -3
  152. package/src/components/star-rating-input/star-rating-input.svelte +103 -103
  153. package/src/components/svg/svg.svelte +4 -4
  154. package/src/components/table-view/table-view.svelte +17 -2
  155. package/src/components/table-view/table-view.test.ts +7 -1
  156. package/src/components/table-view/types.ts +11 -11
  157. package/src/components/tags-input/index.ts +13 -0
  158. package/src/components/tags-input/tags-input.svelte +305 -0
  159. package/src/components/tags-input/tags.test.ts +64 -0
  160. package/src/components/tags-input/tags.ts +142 -0
  161. package/src/components/theme/index.ts +2 -4
  162. package/src/components/theme/theme-provider.svelte +1 -3
  163. package/src/components/toolbar/toolbar-search.svelte +25 -25
  164. package/src/components/toolbar/toolbar.svelte +11 -7
  165. package/src/components/toolbar/toolbar.test.ts +10 -1
  166. package/src/design/index.css +21 -78
  167. package/src/design/polish.css +26 -35
  168. package/src/editor/editor-toolbar.svelte +104 -99
  169. package/src/editor/editor.svelte +296 -266
  170. package/src/editor/image-toolbar.svelte +20 -22
  171. package/src/editor/index.ts +1 -4
  172. package/src/editor/inline-editor.svelte +280 -238
  173. package/src/editor/markdown.ts +15 -15
  174. package/src/editor/renderer.svelte +71 -71
  175. package/src/editor/slash-menu.svelte +24 -27
  176. package/src/effects/animated-beam/diagrams/BeamSvg.svelte +1 -1
  177. package/src/effects/image-comparison/ImageComparison.svelte +11 -1
  178. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +59 -46
  179. package/src/effects/magic-card/MagicCard.css +14 -7
  180. package/src/effects/marquee/Marquee.svelte +3 -5
  181. package/src/effects/reveal/observe.ts +0 -3
  182. package/src/effects/spotlight/Spotlight.svelte +1 -1
  183. package/src/effects/theme-toggle/ThemeToggle.css +25 -22
  184. package/src/effects/theme-toggle/ThemeToggle.svelte +6 -7
  185. package/src/gallery.test.ts +14 -4
  186. package/src/inbox/README.md +1 -3
  187. package/src/inbox/inbox.test.ts +181 -138
  188. package/src/inbox/index.ts +20 -20
  189. package/src/inbox/logic.ts +114 -111
  190. package/src/inbox/thread-inbox.svelte +227 -151
  191. package/src/inbox/types.ts +130 -108
  192. package/src/lib/theme-transition.ts +2 -4
  193. package/src/lib/tone.ts +2 -4
  194. package/src/lib/utils.ts +2 -2
  195. package/src/modules.test.ts +79 -69
  196. package/src/onboarding/define-tours.test.ts +55 -40
  197. package/src/onboarding/define-tours.ts +23 -23
  198. package/src/onboarding/demo.svelte +24 -5
  199. package/src/onboarding/index.ts +1 -8
  200. package/src/onboarding/onboarding-provider.svelte +19 -7
  201. package/src/onboarding/onboarding.svelte.ts +16 -16
  202. package/src/onboarding/pick.test.ts +61 -52
  203. package/src/onboarding/pick.ts +16 -13
  204. package/src/onboarding/stores.test.ts +80 -80
  205. package/src/onboarding/stores.ts +41 -39
  206. package/src/onboarding/tour-card.svelte +30 -5
  207. package/src/onboarding/tour-runner.svelte +12 -3
  208. package/src/onboarding/types.ts +20 -12
  209. package/src/onboarding/wait.ts +22 -22
  210. package/src/panel/side-panel.svelte +94 -93
  211. package/src/record/demo-data.ts +291 -225
  212. package/src/record/field-section.svelte +38 -34
  213. package/src/record/fields.ts +148 -96
  214. package/src/record/format.ts +63 -56
  215. package/src/record/index.ts +49 -35
  216. package/src/record/info-list.svelte +145 -114
  217. package/src/record/kinds.ts +31 -31
  218. package/src/record/logic.ts +216 -214
  219. package/src/record/next-up.svelte +45 -34
  220. package/src/record/project-list.svelte +56 -50
  221. package/src/record/record-demo.svelte +130 -110
  222. package/src/record/record-header.svelte +37 -28
  223. package/src/record/record-shell.svelte +54 -54
  224. package/src/record/record-stats.svelte +25 -25
  225. package/src/record/record-timeline.svelte +134 -114
  226. package/src/record/record.test.ts +430 -309
  227. package/src/record/steps-rail.svelte +225 -216
  228. package/src/record/types.ts +234 -215
  229. package/src/record/where-we-are.svelte +62 -58
  230. package/src/shadcn/button/button.svelte +1 -1
  231. package/src/shadcn/card/card-title.svelte +1 -5
  232. package/src/shadcn/hooks/use-highlighted-code.svelte.ts +2 -13
  233. package/src/shadcn/sheet/sheet-content.svelte +2 -4
  234. package/src/shadcn/table/table-cell.svelte +2 -3
  235. package/src/shadcn/utilities.css +3 -8
  236. package/src/slides/Controls.svelte +150 -124
  237. package/src/slides/Deck.svelte +243 -238
  238. package/src/slides/Fragment.svelte +64 -57
  239. package/src/slides/Overview.svelte +106 -101
  240. package/src/slides/Presenter.svelte +300 -266
  241. package/src/slides/Slide.svelte +185 -170
  242. package/src/slides/SlideList.svelte +16 -12
  243. package/src/slides/ambient.d.ts +1 -1
  244. package/src/slides/bridges.ts +42 -23
  245. package/src/slides/deck-controller.svelte.ts +47 -35
  246. package/src/slides/demo-slides.svelte +330 -306
  247. package/src/slides/demo.svelte +164 -160
  248. package/src/slides/export/capture.ts +20 -16
  249. package/src/slides/export/ffmpeg.ts +43 -38
  250. package/src/slides/export/index.ts +5 -5
  251. package/src/slides/export/launch.ts +3 -1
  252. package/src/slides/export/pdf.ts +30 -27
  253. package/src/slides/export/types.ts +10 -8
  254. package/src/slides/index.ts +11 -11
  255. package/src/slides/labels.ts +22 -19
  256. package/src/slides/layout.ts +9 -2
  257. package/src/slides/model.ts +4 -4
  258. package/src/slides/navigation.ts +43 -31
  259. package/src/slides/theme.css +5 -7
  260. package/src/slides/types.ts +9 -10
  261. package/src/status/define-statuses.ts +17 -24
  262. package/src/status/index.ts +5 -5
  263. package/src/status/status-badge.svelte +3 -1
  264. package/src/status/status-dot.svelte +1 -3
  265. package/src/status/status-icon.svelte +1 -1
  266. package/src/status/status-types.ts +21 -21
  267. package/src/tasks/board-order.ts +2 -4
  268. package/src/tasks/demo.svelte +189 -31
  269. package/src/tasks/detail/detail-demo.svelte +73 -13
  270. package/src/tasks/detail/detail-types.ts +45 -45
  271. package/src/tasks/detail/task-detail.svelte +40 -10
  272. package/src/tasks/detail/task-property.svelte +8 -2
  273. package/src/tasks/detail/task-thread.svelte +53 -12
  274. package/src/tasks/gantt/gantt-bar.svelte +17 -6
  275. package/src/tasks/gantt/gantt-grid.svelte +40 -9
  276. package/src/tasks/gantt/gantt-group-row.svelte +8 -2
  277. package/src/tasks/gantt/gantt-header.svelte +9 -3
  278. package/src/tasks/gantt/gantt-task-row.svelte +26 -6
  279. package/src/tasks/gantt/gantt.css +2 -1
  280. package/src/tasks/gantt/gantt.test.ts +3 -1
  281. package/src/tasks/gantt/logic.test.ts +110 -20
  282. package/src/tasks/gantt/logic.ts +42 -6
  283. package/src/tasks/inbox/inbox-card.svelte +13 -5
  284. package/src/tasks/inbox/inbox-draft.svelte +1 -1
  285. package/src/tasks/inbox/inbox-logic.ts +6 -40
  286. package/src/tasks/inbox/inbox-statuses.ts +23 -23
  287. package/src/tasks/inbox/inbox-trace.svelte +9 -3
  288. package/src/tasks/inbox/inbox-types.ts +54 -58
  289. package/src/tasks/inbox/index.ts +22 -21
  290. package/src/tasks/index.ts +49 -52
  291. package/src/tasks/kanban-board.svelte +16 -12
  292. package/src/tasks/kanban-types.ts +2 -3
  293. package/src/tasks/presets/sales.ts +6 -6
  294. package/src/tasks/task-gantt.svelte +28 -8
  295. package/src/tasks/task-list.svelte +41 -17
  296. package/src/tasks/time/duration.ts +40 -40
  297. package/src/tasks/time/time-chip.svelte +9 -2
  298. package/src/tasks/time/time-dot.svelte +1 -1
  299. package/src/tasks/time/time-ring.svelte +11 -3
  300. package/src/tasks/time/time-tracker.svelte +57 -15
  301. package/src/tasks/time/time-types.ts +57 -57
  302. package/src/tasks/use-kanban-dnd.svelte.ts +2 -3
  303. package/src/thread/index.ts +7 -7
  304. package/src/thread/logic.ts +36 -33
  305. package/src/thread/thread.svelte +82 -16
  306. package/src/thread/thread.test.ts +58 -47
  307. package/src/thread/types.ts +68 -68
  308. package/src/tokens/craft.css +17 -53
  309. package/src/tokens/index.css +3 -13
  310. package/src/tokens/palette.css +13 -11
  311. package/src/tokens/primitives.css +10 -23
  312. package/src/tasks/inbox/inbox.svelte +0 -203
@@ -1,12 +1,21 @@
1
1
  <script lang="ts">
2
2
  import { tick, type Snippet } from "svelte";
3
- import { ArrowLeft, BellSlash, Circle, Envelope, EnvelopeOpen } from "phosphor-svelte";
3
+ import { ArrowSquareOut, BellSlash, Circle, Envelope, EnvelopeOpen } from "phosphor-svelte";
4
4
  import { AgentAvatar } from "../agents/index.js";
5
- import { Badge } from "../shadcn/badge/index.js";
5
+ import DetailHeader from "../components/list-page/detail-header.svelte";
6
+ import DetailPanel from "../components/list-page/detail-panel.svelte";
7
+ import FilterItem from "../components/list-page/filter-item.svelte";
8
+ import FilterPopover from "../components/list-page/filter-popover.svelte";
9
+ import MasterDetail from "../components/list-page/master-detail.svelte";
10
+ import RailBody from "../components/list-page/rail-body.svelte";
11
+ import RailEmpty from "../components/list-page/rail-empty.svelte";
12
+ import RailHead from "../components/list-page/rail-head.svelte";
13
+ import RailRow from "../components/list-page/rail-row.svelte";
14
+ import { filterSectionClass } from "../components/list-page/filter-section.js";
15
+ import { queueRailClass, queueRailWidth } from "../components/list-page/queue-rail.js";
16
+ import SegmentedFilter from "../components/segmented/segmented-filter.svelte";
6
17
  import { Button } from "../shadcn/button/index.js";
7
- import * as Empty from "../shadcn/empty/index.js";
8
18
  import { Kbd, KbdGroup } from "../shadcn/kbd/index.js";
9
- import { ToggleGroup, ToggleGroupItem } from "../shadcn/toggle-group/index.js";
10
19
  import { cn } from "../lib/utils.js";
11
20
  import { TONE_CLASS } from "../lib/tone.js";
12
21
  import InboxCard from "../tasks/inbox/inbox-card.svelte";
@@ -71,7 +80,12 @@
71
80
  const date = new Date(at);
72
81
  return Number.isNaN(date.getTime())
73
82
  ? ""
74
- : date.toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
83
+ : date.toLocaleString(undefined, {
84
+ month: "short",
85
+ day: "numeric",
86
+ hour: "numeric",
87
+ minute: "2-digit",
88
+ });
75
89
  },
76
90
  onSelect,
77
91
  onRead,
@@ -80,10 +94,13 @@
80
94
  class: className,
81
95
  }: Props = $props();
82
96
 
97
+ const NEEDS_YOU = "needs-you";
98
+
83
99
  let labels = $derived(resolveInboxLabels(labelOverrides));
84
100
  let filter = $state<InboxFilter>({ ...NO_FILTER });
85
101
  let snoozing = $state(false);
86
- let detailOpen = $state(false);
102
+ let pane = $state<"rail" | "detail">("rail");
103
+ let root = $state<HTMLDivElement>();
87
104
  let cards = $state<Record<string, ReturnType<typeof InboxCard> | null>>({});
88
105
 
89
106
  let all = $derived(threadsOf(entries, { unreadFirst }));
@@ -93,24 +110,28 @@
93
110
  let current = $derived(visible.find((thread) => thread.key === selected) ?? visible[0]);
94
111
  let needsYouCount = $derived(all.filter((thread) => thread.pending.length > 0).length);
95
112
  let filtered = $derived(filter.type !== ALL || filter.who !== ALL || filter.needsYou);
113
+ let narrowed = $derived(Number(filter.type !== ALL) + Number(filter.who !== ALL));
114
+ let views = $derived([
115
+ { value: ALL, label: labels.all, count: all.length },
116
+ { value: NEEDS_YOU, label: labels.needsYou, count: needsYouCount },
117
+ ]);
96
118
  let pinned = $derived(current?.pending[0]);
97
- let timeline = $derived(current?.entries.filter((entry) => !current.pending.includes(entry)) ?? []);
119
+ let timeline = $derived(
120
+ current?.entries.filter((entry) => !current.pending.includes(entry)) ?? [],
121
+ );
98
122
 
99
123
  function open(thread: InboxThread): void {
100
124
  selected = thread.key;
101
125
  snoozing = false;
102
- detailOpen = true;
103
126
  onSelect?.(thread);
104
127
  }
105
128
 
106
129
  async function move(step: number): Promise<void> {
107
130
  if (!visible.length) return;
108
131
  const from = current ? visible.indexOf(current) : -1;
109
- const next = visible[Math.max(0, Math.min(visible.length - 1, from + step))];
110
- open(next);
111
- detailOpen = false;
132
+ open(visible[Math.max(0, Math.min(visible.length - 1, from + step))]);
112
133
  await tick();
113
- document.querySelector<HTMLElement>(`[data-thread-key="${CSS.escape(next.key)}"]`)?.focus();
134
+ root?.querySelector<HTMLElement>('.md-rail [aria-current="page"]')?.focus();
114
135
  }
115
136
 
116
137
  function toggleRead(): void {
@@ -152,12 +173,12 @@
152
173
  actions[key]();
153
174
  }
154
175
 
155
- function setType(next: string | undefined): void {
156
- filter = { ...filter, type: next || ALL };
157
- }
158
-
159
176
  function snoozeLabel(id: string): string {
160
- return id === "later" ? labels.snoozeLater : id === "tomorrow" ? labels.snoozeTomorrow : labels.snoozeNextWeek;
177
+ return id === "later"
178
+ ? labels.snoozeLater
179
+ : id === "tomorrow"
180
+ ? labels.snoozeTomorrow
181
+ : labels.snoozeNextWeek;
161
182
  }
162
183
 
163
184
  function initial(name: string): string {
@@ -178,121 +199,142 @@
178
199
  {:else if actor.avatar}
179
200
  <img src={actor.avatar} alt="" class="size-6 rounded-full object-cover" />
180
201
  {:else}
181
- <span class="grid size-6 place-items-center rounded-full bg-muted text-xs font-medium text-muted-foreground" aria-hidden="true">
202
+ <span
203
+ class="grid size-6 place-items-center rounded-full bg-muted text-xs font-medium text-muted-foreground"
204
+ aria-hidden="true"
205
+ >
182
206
  {initial(actor.name)}
183
207
  </span>
184
208
  {/if}
185
209
  </span>
186
210
  {/snippet}
187
211
 
212
+ {#snippet subjectStatus()}
213
+ {#if current}{@render status?.(current.subject)}{/if}
214
+ {/snippet}
215
+
188
216
  <!-- WHY the key handler sits on the region: the shortcuts work wherever focus is inside the inbox and nowhere else on the page. -->
189
217
  <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
190
- <div role="region" aria-label={labels.title} class={cn("flex h-full min-h-0 flex-col gap-3", className)} {onkeydown}>
191
- <div class="flex flex-wrap items-center gap-2">
192
- <ToggleGroup type="single" variant="outline" size="sm" value={filter.type} onValueChange={setType} aria-label={labels.filterType}>
193
- <ToggleGroupItem value={ALL}>{labels.all}</ToggleGroupItem>
194
- {#each types as type (type)}
195
- <ToggleGroupItem value={type}>{typeLabels[type] ?? type}</ToggleGroupItem>
196
- {/each}
197
- </ToggleGroup>
198
- <select
199
- aria-label={labels.filterWho}
200
- value={filter.who}
201
- onchange={(event) => (filter = { ...filter, who: event.currentTarget.value })}
202
- class="h-7 rounded-lg border border-border bg-background px-2 text-[0.8rem] outline-none focus-visible:ring-3 focus-visible:ring-ring/50 any-pointer-coarse:h-10"
203
- >
204
- <option value={ALL}>{labels.anyone}</option>
205
- {#each people as name (name)}
206
- <option value={name}>{name}</option>
207
- {/each}
208
- </select>
209
- <Button
210
- size="sm"
211
- variant={filter.needsYou ? "default" : "outline"}
212
- aria-pressed={filter.needsYou}
213
- onclick={() => (filter = { ...filter, needsYou: !filter.needsYou })}
214
- >
215
- {labels.needsYou}
216
- <span class="tabular-nums opacity-70">{needsYouCount}</span>
217
- </Button>
218
- </div>
219
-
220
- <div class="grid min-h-0 flex-1 gap-3 md:grid-cols-[20rem_minmax(0,1fr)]">
221
- <ol class={cn("-mx-1 min-h-0 flex-col gap-1 overflow-y-auto px-1 pb-1", detailOpen ? "max-md:hidden md:flex" : "flex")} aria-label={labels.title}>
222
- {#each visible as thread (thread.key)}
223
- {@const last = lastEntry(thread)}
224
- <li>
225
- <button
226
- type="button"
227
- data-thread-key={thread.key}
228
- aria-current={current?.key === thread.key}
229
- onclick={() => open(thread)}
230
- class={cn(
231
- "flex w-full flex-col gap-1 rounded-lg border border-transparent p-2.5 text-start outline-none focus-visible:ring-3 focus-visible:ring-ring/50",
232
- current?.key === thread.key ? "border-border bg-muted" : "hover:bg-muted/50",
233
- )}
218
+ <div
219
+ role="region"
220
+ aria-label={labels.title}
221
+ bind:this={root}
222
+ class={cn("flex h-full min-h-0 flex-col gap-3", className)}
223
+ {onkeydown}
224
+ >
225
+ <MasterDetail
226
+ bind:pane
227
+ railWidth={queueRailWidth}
228
+ railClass={queueRailClass}
229
+ detailClass="flex flex-col gap-4 overflow-y-auto"
230
+ pad={false}
231
+ backLabel={labels.title}
232
+ class="min-h-0 flex-1"
233
+ >
234
+ {#snippet rail()}
235
+ <RailHead>
236
+ <div class="flex items-center gap-2">
237
+ <SegmentedFilter
238
+ options={views}
239
+ value={filter.needsYou ? NEEDS_YOU : ALL}
240
+ onValueChange={(view) => (filter = { ...filter, needsYou: view === NEEDS_YOU })}
241
+ ariaLabel={labels.title}
242
+ class="min-w-0 flex-1"
243
+ />
244
+ <FilterPopover
245
+ label={labels.filter}
246
+ count={narrowed}
247
+ align="end"
248
+ onClear={() => (filter = { ...NO_FILTER, needsYou: filter.needsYou })}
249
+ clearLabel={labels.clearFilters}
234
250
  >
235
- <span class="flex items-center gap-2">
236
- {#if thread.unread > 0}
237
- <Circle weight="fill" class="size-2 shrink-0 text-primary" aria-hidden="true" />
238
- {/if}
239
- {#if thread.subject.ref}
240
- <span class="text-xs text-muted-foreground tabular-nums">{thread.subject.ref}</span>
241
- {/if}
242
- <span class={cn("truncate text-sm", thread.unread > 0 ? "font-medium" : "")}>{thread.subject.title}</span>
243
- <span class="ms-auto shrink-0 text-xs text-muted-foreground tabular-nums">{when(thread.latestAt)}</span>
244
- </span>
245
- <span class="flex items-center gap-2 ps-4 text-xs text-muted-foreground">
246
- <span class="truncate">{last.actor.name} {entryLine(last, labels)}</span>
247
- {#if thread.pending.length > 0}
248
- <span class={cn("ms-auto inline-flex h-5 shrink-0 items-center rounded-sm border px-1.5 font-medium", TONE_CLASS.warning)}>
249
- {labels.needsYou}
250
- </span>
251
- {:else if thread.unread > 0}
252
- <Badge variant="outline" class="ms-auto tabular-nums" aria-label="{thread.unread} {labels.unread}">{thread.unread}</Badge>
253
- {/if}
254
- </span>
255
- </button>
256
- </li>
257
- {:else}
258
- <li class="h-full">
259
- <Empty.Root class="h-full">
260
- <Empty.Header>
261
- <Empty.Title>
262
- {filtered ? (filter.needsYou ? labels.emptyNeedsYou : labels.emptyFiltered) : labels.empty}
263
- </Empty.Title>
264
- </Empty.Header>
265
- {#if filtered}
266
- <Empty.Content>
267
- <Button size="sm" variant="outline" onclick={() => (filter = { ...NO_FILTER })}>{labels.clearFilters}</Button>
268
- </Empty.Content>
251
+ {#if types.length > 1}
252
+ <p class={filterSectionClass()}>{labels.kind}</p>
253
+ {#each [ALL, ...types] as type (type)}
254
+ <FilterItem
255
+ variant="radio"
256
+ checked={filter.type === type}
257
+ onclick={() => (filter = { ...filter, type })}
258
+ >
259
+ {type === ALL ? labels.all : (typeLabels[type] ?? type)}
260
+ </FilterItem>
261
+ {/each}
269
262
  {/if}
270
- </Empty.Root>
271
- </li>
272
- {/each}
273
- </ol>
263
+ <p class={filterSectionClass()}>{labels.from}</p>
264
+ {#each [ALL, ...people] as who (who)}
265
+ <FilterItem
266
+ variant="radio"
267
+ checked={filter.who === who}
268
+ onclick={() => (filter = { ...filter, who })}
269
+ >
270
+ {who === ALL ? labels.anyone : who}
271
+ </FilterItem>
272
+ {/each}
273
+ </FilterPopover>
274
+ </div>
275
+ </RailHead>
276
+ <RailBody>
277
+ <ol aria-label={labels.title}>
278
+ {#each visible as thread (thread.key)}
279
+ {@const last = lastEntry(thread)}
280
+ <li>
281
+ <RailRow
282
+ title={thread.subject.title}
283
+ titleLines={2}
284
+ selected={current?.key === thread.key}
285
+ onclick={() => open(thread)}
286
+ preview="{last.actor.name} {entryLine(last, labels)}"
287
+ meta={[thread.subject.ref, when(thread.latestAt)]}
288
+ metaTruncate="last"
289
+ >
290
+ {#snippet trailing()}
291
+ {#if thread.pending.length > 0}
292
+ <span
293
+ class={cn(
294
+ "inline-flex h-5 items-center rounded-sm border px-1.5 font-medium",
295
+ TONE_CLASS.warning,
296
+ )}
297
+ >
298
+ {labels.needsYou}
299
+ </span>
300
+ {:else if thread.unread > 0}
301
+ <Circle
302
+ weight="fill"
303
+ class="size-2 text-primary"
304
+ aria-label="{thread.unread} {labels.unread}"
305
+ />
306
+ {/if}
307
+ {/snippet}
308
+ </RailRow>
309
+ </li>
310
+ {:else}
311
+ <li class="flex h-full flex-col items-center">
312
+ <RailEmpty
313
+ title={filtered
314
+ ? filter.needsYou
315
+ ? labels.emptyNeedsYou
316
+ : labels.emptyFiltered
317
+ : labels.empty}
318
+ />
319
+ {#if filtered}
320
+ <Button size="sm" variant="outline" onclick={() => (filter = { ...NO_FILTER })}
321
+ >{labels.clearFilters}</Button
322
+ >
323
+ {/if}
324
+ </li>
325
+ {/each}
326
+ </ol>
327
+ </RailBody>
328
+ {/snippet}
274
329
 
275
- <section
276
- class={cn("min-h-0 flex-col gap-4 overflow-y-auto rounded-xl border border-border p-4", detailOpen ? "flex" : "max-md:hidden md:flex")}
277
- aria-label={current?.subject.title ?? labels.selectThread}
278
- >
330
+ {#snippet detail()}
279
331
  {#if current}
280
- <header class="flex flex-wrap items-center gap-2">
281
- <Button size="sm" variant="ghost" class="md:hidden" onclick={() => (detailOpen = false)} aria-label={labels.title}>
282
- <ArrowLeft aria-hidden="true" />
283
- </Button>
284
- {#if current.subject.ref}
285
- <span class="text-xs text-muted-foreground tabular-nums">{current.subject.ref}</span>
286
- {/if}
287
- <h2 class="min-w-0 truncate text-sm font-medium">
288
- {#if current.subject.href}
289
- <a href={current.subject.href} class="hover:underline">{current.subject.title}</a>
290
- {:else}
291
- {current.subject.title}
292
- {/if}
293
- </h2>
294
- {@render status?.(current.subject)}
295
- <span class="ms-auto flex items-center gap-1">
332
+ <DetailHeader
333
+ title={current.subject.title}
334
+ meta={[current.subject.ref]}
335
+ badge={status ? subjectStatus : undefined}
336
+ >
337
+ {#snippet actions()}
296
338
  <Button size="sm" variant="ghost" onclick={toggleRead} aria-keyshortcuts="u">
297
339
  {#if current.unread > 0}
298
340
  <EnvelopeOpen aria-hidden="true" />{labels.markRead}
@@ -300,11 +342,22 @@
300
342
  <Envelope aria-hidden="true" />{labels.markUnread}
301
343
  {/if}
302
344
  </Button>
303
- <Button size="sm" variant="ghost" onclick={() => (snoozing = !snoozing)} aria-expanded={snoozing} aria-keyshortcuts="h">
345
+ <Button
346
+ size="sm"
347
+ variant="ghost"
348
+ onclick={() => (snoozing = !snoozing)}
349
+ aria-expanded={snoozing}
350
+ aria-keyshortcuts="h"
351
+ >
304
352
  <BellSlash aria-hidden="true" />{labels.snooze}
305
353
  </Button>
306
- </span>
307
- </header>
354
+ {#if current.subject.href}
355
+ <Button size="sm" variant="ghost" href={current.subject.href}>
356
+ <ArrowSquareOut aria-hidden="true" />{labels.open}
357
+ </Button>
358
+ {/if}
359
+ {/snippet}
360
+ </DetailHeader>
308
361
 
309
362
  {#if snoozing}
310
363
  <div class="flex flex-wrap gap-2" role="group" aria-label={labels.snooze}>
@@ -326,38 +379,61 @@
326
379
  />
327
380
  {/each}
328
381
 
329
- <ol class="flex flex-col gap-4">
330
- {#each timeline as entry (entry.id)}
331
- <li class={cn("flex gap-3", !entry.read && "font-medium")}>
332
- {@render face(entry.actor)}
333
- <div class="min-w-0 flex-1">
334
- <div class="flex flex-wrap items-center gap-x-2 text-[13px]">
335
- <span class="truncate font-medium">{entry.actor.name}</span>
336
- {#if entry.actor.kind === "agent"}
337
- <span class={cn("inline-flex h-5 items-center rounded-sm border px-1.5 text-xs font-medium", TONE_CLASS.accent)}>
338
- {labels.agent}
339
- </span>
340
- {/if}
341
- <span class="font-normal text-muted-foreground">{entryLine(entry, labels)}</span>
342
- <span class="ms-auto text-xs font-normal text-muted-foreground tabular-nums">{when(entry.at)}</span>
343
- </div>
344
- {#if entry.body}
345
- <p class="mt-1 text-sm leading-relaxed font-normal whitespace-pre-wrap text-pretty">{entry.body}</p>
346
- {/if}
347
- </div>
348
- </li>
349
- {/each}
350
- </ol>
382
+ {#if timeline.length > 0}
383
+ <DetailPanel>
384
+ <ol class="flex flex-col gap-4">
385
+ {#each timeline as entry (entry.id)}
386
+ <li class={cn("flex gap-3", !entry.read && "font-medium")}>
387
+ {@render face(entry.actor)}
388
+ <div class="min-w-0 flex-1">
389
+ <div class="flex flex-wrap items-center gap-x-2 text-[13px]">
390
+ <span class="truncate font-medium">{entry.actor.name}</span>
391
+ {#if entry.actor.kind === "agent"}
392
+ <span
393
+ class={cn(
394
+ "inline-flex h-5 items-center rounded-sm border px-1.5 text-xs font-medium",
395
+ TONE_CLASS.accent,
396
+ )}
397
+ >
398
+ {labels.agent}
399
+ </span>
400
+ {/if}
401
+ <span class="font-normal text-muted-foreground"
402
+ >{entryLine(entry, labels)}</span
403
+ >
404
+ <span class="ms-auto text-xs font-normal text-muted-foreground tabular-nums"
405
+ >{when(entry.at)}</span
406
+ >
407
+ </div>
408
+ {#if entry.body}
409
+ <p
410
+ class="mt-1 text-sm leading-relaxed font-normal whitespace-pre-wrap text-pretty"
411
+ >
412
+ {entry.body}
413
+ </p>
414
+ {/if}
415
+ </div>
416
+ </li>
417
+ {/each}
418
+ </ol>
419
+ </DetailPanel>
420
+ {/if}
351
421
  {:else}
352
- <p class="text-sm text-muted-foreground">{labels.selectThread}</p>
422
+ <RailEmpty title={labels.selectThread} />
353
423
  {/if}
354
- </section>
355
- </div>
424
+ {/snippet}
425
+ </MasterDetail>
356
426
 
357
- <footer class="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground max-sm:hidden">
358
- <span class="inline-flex items-center gap-1.5"><KbdGroup><Kbd>j</Kbd><Kbd>k</Kbd></KbdGroup> {labels.hintMove}</span>
427
+ <footer
428
+ class="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground max-sm:hidden"
429
+ >
430
+ <span class="inline-flex items-center gap-1.5"
431
+ ><KbdGroup><Kbd>j</Kbd><Kbd>k</Kbd></KbdGroup> {labels.hintMove}</span
432
+ >
359
433
  <span class="inline-flex items-center gap-1.5"><Kbd>u</Kbd> {labels.hintRead}</span>
360
434
  <span class="inline-flex items-center gap-1.5"><Kbd>h</Kbd> {labels.hintSnooze}</span>
361
- <span class="inline-flex items-center gap-1.5"><KbdGroup><Kbd>a</Kbd><Kbd>d</Kbd><Kbd>s</Kbd><Kbd>e</Kbd></KbdGroup> {labels.hintAct}</span>
435
+ <span class="inline-flex items-center gap-1.5"
436
+ ><KbdGroup><Kbd>a</Kbd><Kbd>d</Kbd><Kbd>s</Kbd><Kbd>e</Kbd></KbdGroup> {labels.hintAct}</span
437
+ >
362
438
  </footer>
363
439
  </div>