@heyramzi/ui 2.1.0 → 2.3.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 (317) hide show
  1. package/COMPONENTS.md +1 -1
  2. package/package.json +7 -8
  3. package/scripts/sync-shadcn.mjs +3 -4
  4. package/src/agents/agent-avatar.svelte +604 -575
  5. package/src/agents/agent-cast.svelte +179 -183
  6. package/src/agents/demo.svelte +125 -113
  7. package/src/agents/helpers.ts +23 -17
  8. package/src/agents/index.ts +4 -4
  9. package/src/agents/page-state.ts +6 -6
  10. package/src/agents/types.ts +6 -6
  11. package/src/ai/ai-button/AiButton.svelte +4 -12
  12. package/src/ai/generation-progress/GenerationProgress.svelte +10 -3
  13. package/src/ai/prompt-input/PromptInputTextarea.svelte +1 -2
  14. package/src/ai/response/Response.css +2 -3
  15. package/src/animations/avatar-group-hover.css +2 -16
  16. package/src/animations/card-tilt.css +3 -6
  17. package/src/animations/digit-pop-in.css +2 -15
  18. package/src/animations/dropdown.css +4 -17
  19. package/src/animations/input-clear-dissolve.css +5 -18
  20. package/src/animations/input-shake.css +9 -41
  21. package/src/animations/learn-more-hover.css +17 -9
  22. package/src/animations/like-button.css +2 -4
  23. package/src/animations/page-side-by-side.css +2 -10
  24. package/src/animations/plus-menu-morph.css +2 -5
  25. package/src/animations/shimmer-text.css +5 -18
  26. package/src/animations/spinning-counter.css +2 -6
  27. package/src/animations/success-check.css +2 -15
  28. package/src/animations/thinking-states.css +2 -7
  29. package/src/brand/brand.test.ts +21 -5
  30. package/src/brand/index.ts +4 -10
  31. package/src/canvas/card/card-frame.svelte +43 -39
  32. package/src/canvas/card/resize-handles.svelte +89 -80
  33. package/src/canvas/chrome/chrome-zoom.svelte +101 -100
  34. package/src/canvas/chrome/lock-badge.svelte +8 -8
  35. package/src/canvas/chrome/side-handles.svelte +24 -19
  36. package/src/canvas/chrome/size-chip.svelte +30 -30
  37. package/src/canvas/chrome/snap-guides.svelte +20 -20
  38. package/src/canvas/components/icons/ToolIcon.svelte +8 -8
  39. package/src/canvas/components/shared/svg/PreviewSvg.svelte +19 -19
  40. package/src/canvas/components/shared/svg/ShapeOutline.svelte +71 -48
  41. package/src/canvas/components/shared/svg/StrokeNode.svelte +21 -27
  42. package/src/canvas/components/shared/svg/TextRule.svelte +25 -17
  43. package/src/canvas/edges/bend-handle.svelte +46 -46
  44. package/src/canvas/edges/edge-gradient.svelte +10 -10
  45. package/src/canvas/edges/gradient-edge.svelte +45 -29
  46. package/src/canvas/editor/lock.test.ts +6 -1
  47. package/src/canvas/editor/style.ts +7 -1
  48. package/src/canvas/headless/canvas-cleanup.test.ts +1 -1
  49. package/src/canvas/headless/svg-figure.test.ts +5 -1
  50. package/src/canvas/lod/lod-switch.svelte +111 -106
  51. package/src/canvas/ops/ops.ts +1 -1
  52. package/src/canvas/pen/PenField.svelte +87 -81
  53. package/src/canvas/pen/PenOverlay.svelte +127 -127
  54. package/src/canvas/pen/ShapeNode.svelte +31 -32
  55. package/src/canvas/pen/TextNode.svelte +54 -53
  56. package/src/canvas/pen/host-field.test.svelte +7 -7
  57. package/src/canvas/pen/shapes.ts +7 -1
  58. package/src/canvas/screen/GlyphIcon.svelte +22 -17
  59. package/src/canvas/screen/PartView.svelte +138 -72
  60. package/src/canvas/screen/ScreenBody.svelte +81 -60
  61. package/src/canvas/screen/ScreenCard.svelte +20 -20
  62. package/src/canvas/ui/CanvasToolbar.svelte +173 -85
  63. package/src/canvas/ui/FrameLayer.svelte +59 -59
  64. package/src/canvas/ui/PenLayer.svelte +39 -30
  65. package/src/canvas/ui/StylePanel.svelte +119 -102
  66. package/src/canvas/ui/canvas-grid.svelte +137 -129
  67. package/src/charts/animated-value.svelte +1 -1
  68. package/src/charts/area-chart.css +2 -5
  69. package/src/charts/area-chart.svelte +1 -1
  70. package/src/charts/bar-chart.css +2 -3
  71. package/src/charts/bar-chart.svelte +1 -1
  72. package/src/charts/charts.css +5 -17
  73. package/src/charts/crosshair.svelte +1 -1
  74. package/src/charts/dashboard/dashboard-empty.svelte +25 -25
  75. package/src/charts/dashboard/dashboard-section.svelte +24 -24
  76. package/src/charts/dashboard/data-panel.svelte +50 -50
  77. package/src/charts/dashboard/data-sort-head.svelte +56 -56
  78. package/src/charts/dashboard/data-table.svelte +138 -138
  79. package/src/charts/dashboard/panel-grid.svelte +30 -30
  80. package/src/charts/dashboard/period-filter.svelte +25 -25
  81. package/src/charts/dashboard/rating-chip.svelte +19 -19
  82. package/src/charts/dashboard/stat-band.svelte +32 -32
  83. package/src/charts/dashboard/stat-tile.svelte +106 -99
  84. package/src/charts/donut-chart.svelte +1 -1
  85. package/src/charts/edge-fade.svelte +1 -1
  86. package/src/charts/figure.svelte +1 -1
  87. package/src/charts/gauge-chart.css +4 -7
  88. package/src/charts/gauge-chart.svelte +1 -1
  89. package/src/charts/hover-focus.svelte +1 -1
  90. package/src/charts/legend.css +1 -3
  91. package/src/charts/legend.svelte +1 -1
  92. package/src/charts/line-chart.svelte +1 -1
  93. package/src/charts/number-chart.css +6 -9
  94. package/src/charts/number-chart.svelte +1 -1
  95. package/src/charts/reference-line.svelte +19 -19
  96. package/src/charts/reveal.svelte +1 -1
  97. package/src/charts/ring-chart.css +2 -4
  98. package/src/charts/ring-chart.svelte +1 -1
  99. package/src/charts/sparkline.svelte +5 -2
  100. package/src/charts/stat-card.svelte +1 -1
  101. package/src/charts/time-series-chart.css +2 -5
  102. package/src/charts/trend-pill.css +2 -4
  103. package/src/charts/trend-pill.svelte +1 -1
  104. package/src/components/copy/copy-button.svelte +93 -98
  105. package/src/components/copy/copy-button.test.ts +10 -2
  106. package/src/components/data-table/infinite-scroll.svelte +42 -42
  107. package/src/components/data-table/pagination.svelte +34 -22
  108. package/src/components/data-table/row-arrow.svelte +19 -16
  109. package/src/components/data-table/row-context-menu.svelte +52 -52
  110. package/src/components/data-table/sortable-head.svelte +25 -25
  111. package/src/components/data-table/table-skeleton.svelte +41 -36
  112. package/src/components/docs/doc-page.svelte +18 -9
  113. package/src/components/docs/on-this-page.svelte +16 -3
  114. package/src/components/docs/scroll-spy.svelte.ts +2 -3
  115. package/src/components/field-row/field-row.svelte +10 -8
  116. package/src/components/hold-to-confirm/hold-to-confirm.svelte +5 -1
  117. package/src/components/icon/icon.svelte +14 -14
  118. package/src/components/language-flag/language-flag.svelte +3 -1
  119. package/src/components/list-page/detail-empty.svelte +34 -34
  120. package/src/components/list-page/detail-header.svelte +55 -47
  121. package/src/components/list-page/detail-panel.svelte +51 -59
  122. package/src/components/list-page/detail-row.svelte +19 -19
  123. package/src/components/list-page/detail-stack.svelte +21 -21
  124. package/src/components/list-page/empty-state.svelte +30 -30
  125. package/src/components/list-page/filter-item.svelte +20 -20
  126. package/src/components/list-page/filter-popover.svelte +60 -55
  127. package/src/components/list-page/grouped-list.svelte +4 -2
  128. package/src/components/list-page/index.ts +5 -1
  129. package/src/components/list-page/list-page-shell.svelte +2 -5
  130. package/src/components/list-page/master-detail.svelte +87 -88
  131. package/src/components/list-page/notice-bar.svelte +38 -38
  132. package/src/components/list-page/prev-next.svelte +63 -63
  133. package/src/components/list-page/rail-body.svelte +18 -18
  134. package/src/components/list-page/rail-empty.svelte +23 -30
  135. package/src/components/list-page/rail-head.svelte +24 -24
  136. package/src/components/list-page/rail-row.svelte +77 -73
  137. package/src/components/list-page/section-host.ts +2 -3
  138. package/src/components/locale-switch/locale-switch.svelte +41 -41
  139. package/src/components/mark/mark.svelte +34 -38
  140. package/src/components/open-in/index.ts +1 -5
  141. package/src/components/open-in/open-in.svelte +40 -40
  142. package/src/components/picker/mark-picker.svelte +49 -49
  143. package/src/components/picker/picker.svelte +5 -3
  144. package/src/components/section-header/section-header.test.ts +7 -1
  145. package/src/components/section-header/section-label.svelte +20 -20
  146. package/src/components/segmented/animated-tabs.svelte +31 -2
  147. package/src/components/segmented/collapse.test.ts +2 -1
  148. package/src/components/segmented/index.ts +2 -0
  149. package/src/components/segmented/segment.svelte +52 -49
  150. package/src/components/segmented/segmented-filter.svelte +79 -80
  151. package/src/components/segmented/segmented-nav.svelte +34 -40
  152. package/src/components/segmented/segmented-overflow.svelte +53 -50
  153. package/src/components/segmented/segmented-track.svelte +62 -46
  154. package/src/components/segmented/segmented.ts +2 -3
  155. package/src/components/star-rating-input/star-rating-input.svelte +103 -103
  156. package/src/components/svg/svg.svelte +4 -4
  157. package/src/components/table-view/index.ts +1 -1
  158. package/src/components/table-view/table-view.svelte +352 -13
  159. package/src/components/table-view/table-view.test.ts +108 -1
  160. package/src/components/table-view/types.ts +22 -11
  161. package/src/components/tags-input/index.ts +13 -0
  162. package/src/components/tags-input/tags-input.svelte +305 -0
  163. package/src/components/tags-input/tags.test.ts +64 -0
  164. package/src/components/tags-input/tags.ts +142 -0
  165. package/src/components/theme/index.ts +2 -4
  166. package/src/components/theme/theme-provider.svelte +1 -3
  167. package/src/components/toolbar/toolbar-search.svelte +25 -25
  168. package/src/components/toolbar/toolbar.svelte +11 -7
  169. package/src/components/toolbar/toolbar.test.ts +10 -1
  170. package/src/design/index.css +21 -78
  171. package/src/design/polish.css +26 -35
  172. package/src/editor/editor-toolbar.svelte +104 -99
  173. package/src/editor/editor.svelte +296 -266
  174. package/src/editor/image-toolbar.svelte +20 -22
  175. package/src/editor/index.ts +1 -4
  176. package/src/editor/inline-editor.svelte +280 -238
  177. package/src/editor/markdown.ts +15 -15
  178. package/src/editor/renderer.svelte +71 -71
  179. package/src/editor/slash-menu.svelte +24 -27
  180. package/src/effects/animated-beam/diagrams/BeamSvg.svelte +1 -1
  181. package/src/effects/image-comparison/ImageComparison.svelte +11 -1
  182. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +59 -46
  183. package/src/effects/magic-card/MagicCard.css +14 -7
  184. package/src/effects/marquee/Marquee.svelte +3 -5
  185. package/src/effects/reveal/observe.ts +0 -3
  186. package/src/effects/spotlight/Spotlight.svelte +1 -1
  187. package/src/effects/theme-toggle/ThemeToggle.css +25 -22
  188. package/src/effects/theme-toggle/ThemeToggle.svelte +6 -7
  189. package/src/gallery.test.ts +14 -4
  190. package/src/icons/trades.ts +1 -1
  191. package/src/inbox/README.md +1 -3
  192. package/src/inbox/inbox.test.ts +181 -138
  193. package/src/inbox/index.ts +20 -20
  194. package/src/inbox/logic.ts +114 -111
  195. package/src/inbox/thread-inbox.svelte +227 -151
  196. package/src/inbox/types.ts +130 -108
  197. package/src/lib/theme-transition.ts +2 -4
  198. package/src/lib/tone.ts +2 -4
  199. package/src/lib/utils.ts +2 -2
  200. package/src/modules.test.ts +79 -69
  201. package/src/onboarding/define-tours.test.ts +55 -40
  202. package/src/onboarding/define-tours.ts +23 -23
  203. package/src/onboarding/demo.svelte +24 -5
  204. package/src/onboarding/index.ts +1 -8
  205. package/src/onboarding/onboarding-provider.svelte +19 -7
  206. package/src/onboarding/onboarding.svelte.ts +16 -16
  207. package/src/onboarding/pick.test.ts +61 -52
  208. package/src/onboarding/pick.ts +16 -13
  209. package/src/onboarding/stores.test.ts +80 -80
  210. package/src/onboarding/stores.ts +41 -39
  211. package/src/onboarding/tour-card.svelte +30 -5
  212. package/src/onboarding/tour-runner.svelte +12 -3
  213. package/src/onboarding/types.ts +20 -12
  214. package/src/onboarding/wait.ts +22 -22
  215. package/src/panel/side-panel.svelte +94 -93
  216. package/src/record/demo-data.ts +291 -225
  217. package/src/record/field-section.svelte +38 -34
  218. package/src/record/fields.ts +148 -96
  219. package/src/record/format.ts +63 -56
  220. package/src/record/index.ts +49 -35
  221. package/src/record/info-list.svelte +145 -114
  222. package/src/record/kinds.ts +31 -31
  223. package/src/record/logic.ts +216 -214
  224. package/src/record/next-up.svelte +45 -34
  225. package/src/record/project-list.svelte +56 -50
  226. package/src/record/record-demo.svelte +130 -110
  227. package/src/record/record-header.svelte +37 -28
  228. package/src/record/record-shell.svelte +54 -54
  229. package/src/record/record-stats.svelte +25 -25
  230. package/src/record/record-timeline.svelte +134 -114
  231. package/src/record/record.test.ts +430 -309
  232. package/src/record/steps-rail.svelte +225 -216
  233. package/src/record/types.ts +234 -215
  234. package/src/record/where-we-are.svelte +62 -58
  235. package/src/shadcn/button/button.svelte +1 -1
  236. package/src/shadcn/card/card-title.svelte +1 -5
  237. package/src/shadcn/hooks/use-highlighted-code.svelte.ts +2 -13
  238. package/src/shadcn/sheet/sheet-content.svelte +2 -4
  239. package/src/shadcn/table/table-cell.svelte +2 -3
  240. package/src/shadcn/utilities.css +3 -8
  241. package/src/slides/Controls.svelte +150 -124
  242. package/src/slides/Deck.svelte +243 -238
  243. package/src/slides/Fragment.svelte +64 -57
  244. package/src/slides/Overview.svelte +106 -101
  245. package/src/slides/Presenter.svelte +300 -266
  246. package/src/slides/Slide.svelte +185 -170
  247. package/src/slides/SlideList.svelte +16 -12
  248. package/src/slides/ambient.d.ts +1 -1
  249. package/src/slides/bridges.ts +42 -23
  250. package/src/slides/deck-controller.svelte.ts +47 -35
  251. package/src/slides/demo-slides.svelte +330 -306
  252. package/src/slides/demo.svelte +164 -160
  253. package/src/slides/export/capture.ts +20 -16
  254. package/src/slides/export/ffmpeg.ts +43 -38
  255. package/src/slides/export/index.ts +5 -5
  256. package/src/slides/export/launch.ts +3 -1
  257. package/src/slides/export/pdf.ts +30 -27
  258. package/src/slides/export/types.ts +10 -8
  259. package/src/slides/index.ts +11 -11
  260. package/src/slides/labels.ts +22 -19
  261. package/src/slides/layout.ts +9 -2
  262. package/src/slides/model.ts +4 -4
  263. package/src/slides/navigation.ts +43 -31
  264. package/src/slides/theme.css +5 -7
  265. package/src/slides/types.ts +9 -10
  266. package/src/status/define-statuses.ts +17 -24
  267. package/src/status/index.ts +5 -5
  268. package/src/status/status-badge.svelte +3 -1
  269. package/src/status/status-dot.svelte +1 -3
  270. package/src/status/status-icon.svelte +1 -1
  271. package/src/status/status-types.ts +21 -21
  272. package/src/tasks/board-order.ts +2 -4
  273. package/src/tasks/demo.svelte +189 -31
  274. package/src/tasks/detail/detail-demo.svelte +73 -13
  275. package/src/tasks/detail/detail-types.ts +45 -45
  276. package/src/tasks/detail/task-detail.svelte +40 -10
  277. package/src/tasks/detail/task-property.svelte +8 -2
  278. package/src/tasks/detail/task-thread.svelte +53 -12
  279. package/src/tasks/gantt/gantt-bar.svelte +17 -6
  280. package/src/tasks/gantt/gantt-grid.svelte +40 -9
  281. package/src/tasks/gantt/gantt-group-row.svelte +8 -2
  282. package/src/tasks/gantt/gantt-header.svelte +9 -3
  283. package/src/tasks/gantt/gantt-task-row.svelte +26 -6
  284. package/src/tasks/gantt/gantt.css +2 -1
  285. package/src/tasks/gantt/gantt.test.ts +3 -1
  286. package/src/tasks/gantt/logic.test.ts +110 -20
  287. package/src/tasks/gantt/logic.ts +42 -6
  288. package/src/tasks/inbox/inbox-card.svelte +13 -5
  289. package/src/tasks/inbox/inbox-draft.svelte +1 -1
  290. package/src/tasks/inbox/inbox-logic.ts +6 -40
  291. package/src/tasks/inbox/inbox-statuses.ts +23 -23
  292. package/src/tasks/inbox/inbox-trace.svelte +9 -3
  293. package/src/tasks/inbox/inbox-types.ts +54 -58
  294. package/src/tasks/inbox/index.ts +22 -21
  295. package/src/tasks/index.ts +49 -52
  296. package/src/tasks/kanban-board.svelte +16 -12
  297. package/src/tasks/kanban-types.ts +2 -3
  298. package/src/tasks/presets/sales.ts +6 -6
  299. package/src/tasks/task-gantt.svelte +28 -8
  300. package/src/tasks/task-list.svelte +41 -17
  301. package/src/tasks/time/duration.ts +40 -40
  302. package/src/tasks/time/time-chip.svelte +9 -2
  303. package/src/tasks/time/time-dot.svelte +1 -1
  304. package/src/tasks/time/time-ring.svelte +11 -3
  305. package/src/tasks/time/time-tracker.svelte +57 -15
  306. package/src/tasks/time/time-types.ts +57 -57
  307. package/src/tasks/use-kanban-dnd.svelte.ts +2 -3
  308. package/src/thread/index.ts +7 -7
  309. package/src/thread/logic.ts +36 -33
  310. package/src/thread/thread.svelte +82 -16
  311. package/src/thread/thread.test.ts +58 -47
  312. package/src/thread/types.ts +68 -68
  313. package/src/tokens/craft.css +17 -53
  314. package/src/tokens/index.css +3 -13
  315. package/src/tokens/palette.css +13 -11
  316. package/src/tokens/primitives.css +10 -23
  317. 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
  }