@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,166 +1,169 @@
1
1
  import { isOpen, isTyping } from "../tasks/inbox/inbox-logic.js";
2
2
  import {
3
- ALL,
4
- DEFAULT_INBOX_LABELS,
5
- type InboxEntry,
6
- type InboxFilter,
7
- type InboxKey,
8
- type InboxLabels,
9
- type InboxSubject,
10
- type InboxThread,
3
+ ALL,
4
+ DEFAULT_INBOX_LABELS,
5
+ type InboxEntry,
6
+ type InboxFilter,
7
+ type InboxKey,
8
+ type InboxLabels,
9
+ type InboxSubject,
10
+ type InboxThread,
11
11
  } from "./types.js";
12
12
 
13
13
  /** A timestamp as a number, with NaN for one that doesn't parse. */
14
14
  function ms(at: string): number {
15
- return Date.parse(at);
15
+ return Date.parse(at);
16
16
  }
17
17
 
18
18
  function latestOf(thread: InboxThread): number {
19
- const time = ms(thread.latestAt);
20
- return Number.isNaN(time) ? Number.NEGATIVE_INFINITY : time;
19
+ const time = ms(thread.latestAt);
20
+ return Number.isNaN(time) ? Number.NEGATIVE_INFINITY : time;
21
21
  }
22
22
 
23
23
  export type ThreadsOptions = {
24
- /** Threads with unread entries come before read ones. */
25
- unreadFirst?: boolean;
24
+ /** Threads with unread entries come before read ones. */
25
+ unreadFirst?: boolean;
26
26
  };
27
27
 
28
28
  /** True while a proposal entry still waits for a verdict. */
29
29
  export function isPending(entry: InboxEntry): boolean {
30
- return entry.kind === "proposal" && !!entry.proposal && isOpen(entry.proposal);
30
+ return entry.kind === "proposal" && !!entry.proposal && isOpen(entry.proposal);
31
31
  }
32
32
 
33
33
  /**
34
- * Group entries into one thread per subject and sort by the newest entry. An entry with no subject id
35
- * is dropped. A malformed timestamp sorts last, among threads and inside one.
34
+ * One thread per subject, newest first. An entry with no subject id is dropped, and a malformed
35
+ * timestamp sorts last.
36
36
  */
37
37
  export function threadsOf(entries: InboxEntry[], options: ThreadsOptions = {}): InboxThread[] {
38
- type Row = { entry: InboxEntry; subject: InboxSubject; index: number; time: number };
39
- const groups = new Map<string, Row[]>();
40
- entries.forEach((entry, index) => {
41
- const subject = entry.subject;
42
- if (!subject?.id) return;
43
- const key = `${subject.type}:${subject.id}`;
44
- groups.set(key, [...(groups.get(key) ?? []), { entry, subject, index, time: ms(entry.at) }]);
45
- });
46
-
47
- const threads: InboxThread[] = [];
48
- for (const [key, rows] of groups) {
49
- const ordered = rows.toSorted((a, b) => {
50
- const aBad = Number.isNaN(a.time);
51
- const bBad = Number.isNaN(b.time);
52
- if (aBad !== bBad) return aBad ? 1 : -1;
53
- return (aBad ? 0 : a.time - b.time) || a.index - b.index;
54
- });
55
- const newest = ordered.findLast((row) => !Number.isNaN(row.time)) ?? ordered[0];
56
- const list = ordered.map((row) => row.entry);
57
- threads.push({
58
- key,
59
- // The newest entry carries the freshest title and status.
60
- subject: newest.subject,
61
- entries: list,
62
- unread: list.filter((entry) => !entry.read).length,
63
- latestAt: newest.entry.at,
64
- pending: list.filter(isPending).toReversed(),
65
- });
66
- }
67
-
68
- return threads
69
- .map((thread, index) => ({ thread, index }))
70
- .toSorted((a, b) => {
71
- if (options.unreadFirst) {
72
- const unread = Number(b.thread.unread > 0) - Number(a.thread.unread > 0);
73
- if (unread) return unread;
74
- }
75
- const aTime = latestOf(a.thread);
76
- const bTime = latestOf(b.thread);
77
- if (aTime === bTime) return a.index - b.index;
78
- return bTime - aTime;
79
- })
80
- .map((row) => row.thread);
38
+ type Row = { entry: InboxEntry; subject: InboxSubject; index: number; time: number };
39
+ const groups = new Map<string, Row[]>();
40
+ entries.forEach((entry, index) => {
41
+ const subject = entry.subject;
42
+ if (!subject?.id) return;
43
+ const key = `${subject.type}:${subject.id}`;
44
+ groups.set(key, [...(groups.get(key) ?? []), { entry, subject, index, time: ms(entry.at) }]);
45
+ });
46
+
47
+ const threads: InboxThread[] = [];
48
+ for (const [key, rows] of groups) {
49
+ const ordered = rows.toSorted((a, b) => {
50
+ const aBad = Number.isNaN(a.time);
51
+ const bBad = Number.isNaN(b.time);
52
+ if (aBad !== bBad) return aBad ? 1 : -1;
53
+ return (aBad ? 0 : a.time - b.time) || a.index - b.index;
54
+ });
55
+ const newest = ordered.findLast((row) => !Number.isNaN(row.time)) ?? ordered[0];
56
+ const list = ordered.map((row) => row.entry);
57
+ threads.push({
58
+ key,
59
+ // The newest entry carries the freshest title and status.
60
+ subject: newest.subject,
61
+ entries: list,
62
+ unread: list.filter((entry) => !entry.read).length,
63
+ latestAt: newest.entry.at,
64
+ pending: list.filter(isPending).toReversed(),
65
+ });
66
+ }
67
+
68
+ return threads
69
+ .map((thread, index) => ({ thread, index }))
70
+ .toSorted((a, b) => {
71
+ if (options.unreadFirst) {
72
+ const unread = Number(b.thread.unread > 0) - Number(a.thread.unread > 0);
73
+ if (unread) return unread;
74
+ }
75
+ const aTime = latestOf(a.thread);
76
+ const bTime = latestOf(b.thread);
77
+ if (aTime === bTime) return a.index - b.index;
78
+ return bTime - aTime;
79
+ })
80
+ .map((row) => row.thread);
81
81
  }
82
82
 
83
83
  export const NO_FILTER: InboxFilter = { type: ALL, who: ALL, needsYou: false };
84
84
 
85
85
  /** Keep a thread when it matches the type, holds an entry from the person or agent, and has a pending proposal if asked. */
86
86
  export function filterThreads(threads: InboxThread[], filter: InboxFilter): InboxThread[] {
87
- return threads.filter(
88
- (thread) =>
89
- (filter.type === ALL || thread.subject.type === filter.type) &&
90
- (filter.who === ALL || thread.entries.some((entry) => entry.actor.name === filter.who)) &&
91
- (!filter.needsYou || thread.pending.length > 0),
92
- );
87
+ return threads.filter(
88
+ (thread) =>
89
+ (filter.type === ALL || thread.subject.type === filter.type) &&
90
+ (filter.who === ALL || thread.entries.some((entry) => entry.actor.name === filter.who)) &&
91
+ (!filter.needsYou || thread.pending.length > 0),
92
+ );
93
93
  }
94
94
 
95
95
  export function subjectTypes(threads: InboxThread[]): string[] {
96
- return [...new Set(threads.map((thread) => thread.subject.type))];
96
+ return [...new Set(threads.map((thread) => thread.subject.type))];
97
97
  }
98
98
 
99
99
  export function actorNames(threads: InboxThread[]): string[] {
100
- return [...new Set(threads.flatMap((thread) => thread.entries.map((entry) => entry.actor.name)))];
100
+ return [...new Set(threads.flatMap((thread) => thread.entries.map((entry) => entry.actor.name)))];
101
101
  }
102
102
 
103
- type KeyEventLike = Pick<KeyboardEvent, "key" | "metaKey" | "ctrlKey" | "altKey" | "defaultPrevented" | "target">;
103
+ type KeyEventLike = Pick<
104
+ KeyboardEvent,
105
+ "key" | "metaKey" | "ctrlKey" | "altKey" | "defaultPrevented" | "target"
106
+ >;
104
107
 
105
108
  const KEYS: Record<string, InboxKey> = {
106
- j: "next",
107
- k: "previous",
108
- u: "toggleRead",
109
- h: "snooze",
110
- a: "approve",
111
- d: "decline",
112
- s: "steer",
113
- e: "edit",
109
+ j: "next",
110
+ k: "previous",
111
+ u: "toggleRead",
112
+ h: "snooze",
113
+ a: "approve",
114
+ d: "decline",
115
+ s: "steer",
116
+ e: "edit",
114
117
  };
115
118
 
116
119
  /** What a key press asks for, or null when it isn't ours: a modifier is held, or focus is in a text field. */
117
120
  export function inboxKeyOf(event: KeyEventLike): InboxKey | null {
118
- if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) return null;
119
- if (isTyping(event.target)) return null;
120
- return KEYS[event.key] ?? null;
121
+ if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) return null;
122
+ if (isTyping(event.target)) return null;
123
+ return KEYS[event.key] ?? null;
121
124
  }
122
125
 
123
126
  export type SnoozeChoice = { id: "later" | "tomorrow" | "nextWeek"; until: Date };
124
127
 
125
128
  /** The three times a snooze offers: 3 hours on, tomorrow at 9, next Monday at 9. */
126
129
  export function snoozeChoices(now: Date): SnoozeChoice[] {
127
- const later = new Date(now.getTime() + 3 * 60 * 60 * 1000);
128
- const tomorrow = new Date(now);
129
- tomorrow.setDate(tomorrow.getDate() + 1);
130
- tomorrow.setHours(9, 0, 0, 0);
131
- const nextWeek = new Date(now);
132
- nextWeek.setDate(nextWeek.getDate() + (((8 - nextWeek.getDay()) % 7) || 7));
133
- nextWeek.setHours(9, 0, 0, 0);
134
- return [
135
- { id: "later", until: later },
136
- { id: "tomorrow", until: tomorrow },
137
- { id: "nextWeek", until: nextWeek },
138
- ];
130
+ const later = new Date(now.getTime() + 3 * 60 * 60 * 1000);
131
+ const tomorrow = new Date(now);
132
+ tomorrow.setDate(tomorrow.getDate() + 1);
133
+ tomorrow.setHours(9, 0, 0, 0);
134
+ const nextWeek = new Date(now);
135
+ nextWeek.setDate(nextWeek.getDate() + ((8 - nextWeek.getDay()) % 7 || 7));
136
+ nextWeek.setHours(9, 0, 0, 0);
137
+ return [
138
+ { id: "later", until: later },
139
+ { id: "tomorrow", until: tomorrow },
140
+ { id: "nextWeek", until: nextWeek },
141
+ ];
139
142
  }
140
143
 
141
144
  export function resolveInboxLabels(overrides?: Partial<InboxLabels>): InboxLabels {
142
- return { ...DEFAULT_INBOX_LABELS, ...overrides };
145
+ return { ...DEFAULT_INBOX_LABELS, ...overrides };
143
146
  }
144
147
 
145
148
  /** One line for an entry, in the host's words. `{from}`, `{to}` and `{assignee}` are filled. */
146
149
  export function entryLine(entry: InboxEntry, labels: InboxLabels): string {
147
- const fill = (text: string) =>
148
- text
149
- .replaceAll("{from}", entry.status?.from ?? "")
150
- .replaceAll("{to}", entry.status?.to ?? "")
151
- .replaceAll("{assignee}", entry.assignee ?? "");
152
- switch (entry.kind) {
153
- case "status":
154
- return fill(entry.status?.from ? labels.statusMoved : labels.statusSet);
155
- case "assigned":
156
- return fill(labels.assigned);
157
- case "mention":
158
- return labels.mentioned;
159
- case "agent_response":
160
- return labels.responded;
161
- case "proposal":
162
- return labels.proposed;
163
- default:
164
- return labels.commented;
165
- }
150
+ const fill = (text: string) =>
151
+ text
152
+ .replaceAll("{from}", entry.status?.from ?? "")
153
+ .replaceAll("{to}", entry.status?.to ?? "")
154
+ .replaceAll("{assignee}", entry.assignee ?? "");
155
+ switch (entry.kind) {
156
+ case "status":
157
+ return fill(entry.status?.from ? labels.statusMoved : labels.statusSet);
158
+ case "assigned":
159
+ return fill(labels.assigned);
160
+ case "mention":
161
+ return labels.mentioned;
162
+ case "agent_response":
163
+ return labels.responded;
164
+ case "proposal":
165
+ return labels.proposed;
166
+ default:
167
+ return labels.commented;
168
+ }
166
169
  }