@kortix/ui 0.14.4 → 0.14.5

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 (118) hide show
  1. package/README.md +8 -4
  2. package/dist/customize/sections/llm-provider/constants.d.ts +0 -1
  3. package/dist/customize/sections/llm-provider/constants.js +0 -2
  4. package/dist/customize/sections/llm-provider/models-tab-group.d.ts +1 -1
  5. package/dist/customize/sections/llm-provider/models-tab-group.js +7 -4
  6. package/dist/customize/sections/llm-provider/models-tab-row.d.ts +2 -18
  7. package/dist/customize/sections/llm-provider/models-tab-row.js +6 -30
  8. package/dist/customize/sections/llm-provider/models-tab.d.ts +0 -1
  9. package/dist/customize/sections/llm-provider/models-tab.js +1 -10
  10. package/dist/customize/sections/llm-provider/use-connected-providers.js +2 -2
  11. package/dist/customize/sections/llm-provider/utils.d.ts +1 -1
  12. package/dist/customize/sections/llm-provider/utils.js +7 -14
  13. package/dist/customize/sections/view/gateway/_shared.js +1 -2
  14. package/dist/customize/sections/view/gateway/gateway-routing.js +3 -2
  15. package/dist/customize/sections/view/sandbox-provider-coverage.d.ts +3 -18
  16. package/dist/customize/sections/view/sandbox-provider-coverage.js +7 -27
  17. package/dist/customize/settings/tabs/sandbox-tab.d.ts +2 -27
  18. package/dist/customize/settings/tabs/sandbox-tab.js +13 -112
  19. package/dist/customize/shared/entity/entity-modal.d.ts +5 -1
  20. package/dist/customize/shared/entity/entity-modal.js +7 -4
  21. package/dist/customize/skills/remove-skill-button.d.ts +16 -0
  22. package/dist/customize/skills/remove-skill-button.js +42 -0
  23. package/dist/customize/skills/skills-page.js +1 -1
  24. package/dist/customize/triggers/schedule/trigger-list.d.ts +0 -2
  25. package/dist/customize/triggers/schedule/trigger-list.js +1 -18
  26. package/dist/customize/triggers/schedule/trigger-tile.d.ts +1 -32
  27. package/dist/customize/triggers/schedule/trigger-tile.js +1 -16
  28. package/dist/files/project/hooks/use-change-requests.d.ts +14 -1
  29. package/dist/files/project/hooks/use-change-requests.js +44 -25
  30. package/dist/files/renderers/docx/docx-viewer.js +4 -1
  31. package/dist/files/renderers/pdf/pdf-renderer.d.ts +3 -1
  32. package/dist/files/renderers/pdf/pdf-renderer.js +2 -2
  33. package/dist/files/renderers/pdf/pdf-viewer.d.ts +3 -0
  34. package/dist/files/renderers/pdf/pdf-viewer.js +15 -6
  35. package/dist/files/viewer/file-content-renderer.d.ts +4 -1
  36. package/dist/files/viewer/file-content-renderer.js +2 -2
  37. package/dist/i18n/en.d.ts +26 -35
  38. package/dist/i18n/en.js +35 -44
  39. package/dist/index.d.ts +1 -1
  40. package/dist/internal/hooks/use-notification-center.js +21 -15
  41. package/dist/internal/hooks/use-review-session-summary.d.ts +6 -0
  42. package/dist/internal/hooks/use-review-session-summary.js +11 -0
  43. package/dist/internal/lib/browser-error-noise.d.ts +1 -1
  44. package/dist/internal/lib/browser-error-noise.js +1 -1
  45. package/dist/internal/lib/browser-noise/rules/rejections.d.ts +10 -0
  46. package/dist/internal/lib/browser-noise/rules/rejections.js +27 -1
  47. package/dist/internal/lib/provider-branding.js +14 -3
  48. package/dist/internal/lib/trigger-clock.d.ts +42 -0
  49. package/dist/internal/lib/trigger-clock.js +34 -0
  50. package/dist/internal/lib/web-notifications.d.ts +6 -0
  51. package/dist/internal/lib/web-notifications.js +57 -7
  52. package/dist/internal/notifications/notification-bell.d.ts +13 -0
  53. package/dist/internal/notifications/notification-bell.js +1 -1
  54. package/dist/internal/reminders/reminder-filter.d.ts +12 -0
  55. package/dist/internal/reminders/reminder-filter.js +34 -0
  56. package/dist/internal/reminders/reminder-format.d.ts +6 -0
  57. package/dist/internal/reminders/reminder-format.js +20 -2
  58. package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
  59. package/dist/internal/stores/session-hover-store.d.ts +20 -5
  60. package/dist/internal/stores/session-hover-store.js +31 -13
  61. package/dist/internal/stores/session-panel-memory-store.js +2 -0
  62. package/dist/primitives/count-badge.d.ts +3 -1
  63. package/dist/primitives/count-badge.js +3 -3
  64. package/dist/primitives/glyph-paths-source.js +1 -1
  65. package/dist/primitives/glyph-paths.generated.js +1 -1
  66. package/dist/providers/ui-provider.d.ts +57 -0
  67. package/dist/providers/ui-provider.js +4 -0
  68. package/dist/session/composer/ComposerCard.d.ts +2 -0
  69. package/dist/session/composer/ComposerCard.js +5 -1
  70. package/dist/session/composer/agent-selector.js +12 -5
  71. package/dist/session/composer/composer-toolbar.js +4 -2
  72. package/dist/session/composer/composer-underbar.js +3 -1
  73. package/dist/session/composer/composer.js +34 -5
  74. package/dist/session/composer/menus/slash-actions.d.ts +22 -2
  75. package/dist/session/composer/menus/slash-actions.js +20 -0
  76. package/dist/session/composer/menus/slash-items.js +19 -3
  77. package/dist/session/header/reminder-parts.d.ts +285 -0
  78. package/dist/session/header/reminder-parts.js +115 -0
  79. package/dist/session/header/session-reminders-indicator.d.ts +13 -0
  80. package/dist/session/header/session-reminders-indicator.js +142 -35
  81. package/dist/session/models/model-availability.d.ts +23 -0
  82. package/dist/session/models/model-availability.js +20 -0
  83. package/dist/session/models/model-grouping.js +1 -4
  84. package/dist/session/models/model-selector.js +23 -14
  85. package/dist/session/models/use-model-connection-gate.js +4 -1
  86. package/dist/session/outcomes/session-outcomes-provider.d.ts +3 -1
  87. package/dist/session/outcomes/session-outcomes-provider.js +31 -3
  88. package/dist/session/overrides/session-overrides-composer.d.ts +1 -1
  89. package/dist/session/overrides/session-overrides-composer.js +5 -1
  90. package/dist/session/panel/mobile-tool-drawer.js +4 -2
  91. package/dist/session/panel/open-session-quick-view.js +2 -1
  92. package/dist/session/panel/panel-fallback.js +8 -4
  93. package/dist/session/panel/session-panel-provider.d.ts +1 -0
  94. package/dist/session/panel/session-panel-provider.js +13 -1
  95. package/dist/session/panel/session-reminders-panel.d.ts +4 -0
  96. package/dist/session/panel/session-reminders-panel.js +109 -0
  97. package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
  98. package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
  99. package/dist/session/print/session-print-header.d.ts +0 -10
  100. package/dist/session/print/session-print-header.js +16 -1
  101. package/dist/session/status/session-surface.js +1 -1
  102. package/dist/session/thread/optimistic-turn.d.ts +5 -1
  103. package/dist/session/thread/optimistic-turn.js +8 -6
  104. package/dist/session/thread/session-chat.d.ts +6 -1
  105. package/dist/session/thread/session-chat.js +22 -3
  106. package/dist/session/thread/session-thread.d.ts +10 -1
  107. package/dist/session/thread/session-thread.js +2 -2
  108. package/dist/session/turn/burst-step-body.js +4 -0
  109. package/dist/styles.css +1 -1
  110. package/dist/workspace/sidebar/project-session-list.js +52 -55
  111. package/dist/workspace/sidebar/session-brief-hover-card.d.ts +35 -18
  112. package/dist/workspace/sidebar/session-brief-hover-card.js +114 -37
  113. package/dist/workspace/sidebar/session-status-mark.d.ts +15 -1
  114. package/dist/workspace/sidebar/session-status-mark.js +63 -17
  115. package/package.json +7 -3
  116. package/skills/kortix-ui/SKILL.md +1 -1
  117. package/dist/customize/sections/view/sandbox-provider-result.d.ts +0 -47
  118. package/dist/customize/sections/view/sandbox-provider-result.js +0 -79
@@ -0,0 +1,285 @@
1
+ import type { SessionReminder } from '@kortix/sdk';
2
+ import { type ReactNode } from 'react';
3
+ export declare const isFailedReminder: (reminder: SessionReminder) => boolean;
4
+ /** Soonest next fire first; paused ones, which have no next fire, last. */
5
+ export declare function sortReminders(reminders: SessionReminder[]): SessionReminder[];
6
+ /** A ring filled clockwise from the top by `share`; an empty track around a pause glyph while paused. */
7
+ export declare function CountdownRing({ share, size, stroke, tone, paused, children, }: {
8
+ share: number | null;
9
+ size: number;
10
+ stroke: number;
11
+ tone: 'purple' | 'red';
12
+ paused: boolean;
13
+ children?: ReactNode;
14
+ }): import("react").JSX.Element;
15
+ /** "6h" over "12m": the time left in its two largest units, narrow and localized. */
16
+ export declare function countdownParts(ms: number, locale: string): [string, string | null];
17
+ /** What a row and its details show for one reminder at `now`. */
18
+ export declare function useReminderView(now: number): (reminder: SessionReminder) => {
19
+ paused: boolean;
20
+ isFailed: boolean;
21
+ next: string | null;
22
+ nextAt: number;
23
+ countdown: [string, string | null];
24
+ share: number | null;
25
+ tone: "purple" | "red";
26
+ pauseLabel: string;
27
+ schedule: string;
28
+ meta: string;
29
+ };
30
+ /** Repeats, time zone, last run, and the last error when the run failed. */
31
+ export declare function ReminderFacts({ reminder, now, className, }: {
32
+ reminder: SessionReminder;
33
+ now: number;
34
+ className?: string;
35
+ }): import("react").JSX.Element;
36
+ /** Pause or resume, remove, and the link into the Reminders tab. */
37
+ export declare function ReminderActions({ reminder, controls, manageHref, onNavigate, className, }: {
38
+ reminder: SessionReminder;
39
+ controls: ReminderControls;
40
+ manageHref: string | null;
41
+ onNavigate?: () => void;
42
+ className?: string;
43
+ }): import("react").JSX.Element;
44
+ export interface ReminderControls {
45
+ pending: boolean;
46
+ setEnabled: (reminder: SessionReminder, enabled: boolean) => void;
47
+ requestRemove: (reminder: SessionReminder) => void;
48
+ }
49
+ /**
50
+ * The session's reminders with pause, resume and remove. Remove asks first:
51
+ * render `confirm` once next to whatever calls `requestRemove`.
52
+ */
53
+ export declare function useReminderControls(projectId: string | null, sessionId: string | null | undefined): {
54
+ reminders: {
55
+ update: import("@tanstack/react-query").UseMutationResult<SessionReminder, Error, {
56
+ reminderId: string;
57
+ enabled: boolean;
58
+ }, unknown>;
59
+ remove: import("@tanstack/react-query").UseMutationResult<{
60
+ ok: boolean;
61
+ }, Error, string, unknown>;
62
+ data: {
63
+ reminders: SessionReminder[];
64
+ };
65
+ error: Error;
66
+ isError: true;
67
+ isPending: false;
68
+ isLoading: false;
69
+ isLoadingError: false;
70
+ isRefetchError: true;
71
+ isSuccess: false;
72
+ isPlaceholderData: false;
73
+ status: "error";
74
+ dataUpdatedAt: number;
75
+ errorUpdatedAt: number;
76
+ failureCount: number;
77
+ failureReason: Error | null;
78
+ errorUpdateCount: number;
79
+ isFetched: boolean;
80
+ isFetchedAfterMount: boolean;
81
+ isFetching: boolean;
82
+ isInitialLoading: boolean;
83
+ isPaused: boolean;
84
+ isRefetching: boolean;
85
+ isStale: boolean;
86
+ isEnabled: boolean;
87
+ refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<{
88
+ reminders: SessionReminder[];
89
+ }, Error>>;
90
+ fetchStatus: import("@tanstack/react-query").FetchStatus;
91
+ } | {
92
+ update: import("@tanstack/react-query").UseMutationResult<SessionReminder, Error, {
93
+ reminderId: string;
94
+ enabled: boolean;
95
+ }, unknown>;
96
+ remove: import("@tanstack/react-query").UseMutationResult<{
97
+ ok: boolean;
98
+ }, Error, string, unknown>;
99
+ data: {
100
+ reminders: SessionReminder[];
101
+ };
102
+ error: null;
103
+ isError: false;
104
+ isPending: false;
105
+ isLoading: false;
106
+ isLoadingError: false;
107
+ isRefetchError: false;
108
+ isSuccess: true;
109
+ isPlaceholderData: false;
110
+ status: "success";
111
+ dataUpdatedAt: number;
112
+ errorUpdatedAt: number;
113
+ failureCount: number;
114
+ failureReason: Error | null;
115
+ errorUpdateCount: number;
116
+ isFetched: boolean;
117
+ isFetchedAfterMount: boolean;
118
+ isFetching: boolean;
119
+ isInitialLoading: boolean;
120
+ isPaused: boolean;
121
+ isRefetching: boolean;
122
+ isStale: boolean;
123
+ isEnabled: boolean;
124
+ refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<{
125
+ reminders: SessionReminder[];
126
+ }, Error>>;
127
+ fetchStatus: import("@tanstack/react-query").FetchStatus;
128
+ } | {
129
+ update: import("@tanstack/react-query").UseMutationResult<SessionReminder, Error, {
130
+ reminderId: string;
131
+ enabled: boolean;
132
+ }, unknown>;
133
+ remove: import("@tanstack/react-query").UseMutationResult<{
134
+ ok: boolean;
135
+ }, Error, string, unknown>;
136
+ data: undefined;
137
+ error: Error;
138
+ isError: true;
139
+ isPending: false;
140
+ isLoading: false;
141
+ isLoadingError: true;
142
+ isRefetchError: false;
143
+ isSuccess: false;
144
+ isPlaceholderData: false;
145
+ status: "error";
146
+ dataUpdatedAt: number;
147
+ errorUpdatedAt: number;
148
+ failureCount: number;
149
+ failureReason: Error | null;
150
+ errorUpdateCount: number;
151
+ isFetched: boolean;
152
+ isFetchedAfterMount: boolean;
153
+ isFetching: boolean;
154
+ isInitialLoading: boolean;
155
+ isPaused: boolean;
156
+ isRefetching: boolean;
157
+ isStale: boolean;
158
+ isEnabled: boolean;
159
+ refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<{
160
+ reminders: SessionReminder[];
161
+ }, Error>>;
162
+ fetchStatus: import("@tanstack/react-query").FetchStatus;
163
+ } | {
164
+ update: import("@tanstack/react-query").UseMutationResult<SessionReminder, Error, {
165
+ reminderId: string;
166
+ enabled: boolean;
167
+ }, unknown>;
168
+ remove: import("@tanstack/react-query").UseMutationResult<{
169
+ ok: boolean;
170
+ }, Error, string, unknown>;
171
+ data: undefined;
172
+ error: null;
173
+ isError: false;
174
+ isPending: true;
175
+ isLoading: true;
176
+ isLoadingError: false;
177
+ isRefetchError: false;
178
+ isSuccess: false;
179
+ isPlaceholderData: false;
180
+ status: "pending";
181
+ dataUpdatedAt: number;
182
+ errorUpdatedAt: number;
183
+ failureCount: number;
184
+ failureReason: Error | null;
185
+ errorUpdateCount: number;
186
+ isFetched: boolean;
187
+ isFetchedAfterMount: boolean;
188
+ isFetching: boolean;
189
+ isInitialLoading: boolean;
190
+ isPaused: boolean;
191
+ isRefetching: boolean;
192
+ isStale: boolean;
193
+ isEnabled: boolean;
194
+ refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<{
195
+ reminders: SessionReminder[];
196
+ }, Error>>;
197
+ fetchStatus: import("@tanstack/react-query").FetchStatus;
198
+ } | {
199
+ update: import("@tanstack/react-query").UseMutationResult<SessionReminder, Error, {
200
+ reminderId: string;
201
+ enabled: boolean;
202
+ }, unknown>;
203
+ remove: import("@tanstack/react-query").UseMutationResult<{
204
+ ok: boolean;
205
+ }, Error, string, unknown>;
206
+ data: undefined;
207
+ error: null;
208
+ isError: false;
209
+ isPending: true;
210
+ isLoadingError: false;
211
+ isRefetchError: false;
212
+ isSuccess: false;
213
+ isPlaceholderData: false;
214
+ status: "pending";
215
+ dataUpdatedAt: number;
216
+ errorUpdatedAt: number;
217
+ failureCount: number;
218
+ failureReason: Error | null;
219
+ errorUpdateCount: number;
220
+ isFetched: boolean;
221
+ isFetchedAfterMount: boolean;
222
+ isFetching: boolean;
223
+ isLoading: boolean;
224
+ isInitialLoading: boolean;
225
+ isPaused: boolean;
226
+ isRefetching: boolean;
227
+ isStale: boolean;
228
+ isEnabled: boolean;
229
+ refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<{
230
+ reminders: SessionReminder[];
231
+ }, Error>>;
232
+ fetchStatus: import("@tanstack/react-query").FetchStatus;
233
+ } | {
234
+ update: import("@tanstack/react-query").UseMutationResult<SessionReminder, Error, {
235
+ reminderId: string;
236
+ enabled: boolean;
237
+ }, unknown>;
238
+ remove: import("@tanstack/react-query").UseMutationResult<{
239
+ ok: boolean;
240
+ }, Error, string, unknown>;
241
+ data: {
242
+ reminders: SessionReminder[];
243
+ };
244
+ isError: false;
245
+ error: null;
246
+ isPending: false;
247
+ isLoading: false;
248
+ isLoadingError: false;
249
+ isRefetchError: false;
250
+ isSuccess: true;
251
+ isPlaceholderData: true;
252
+ status: "success";
253
+ dataUpdatedAt: number;
254
+ errorUpdatedAt: number;
255
+ failureCount: number;
256
+ failureReason: Error | null;
257
+ errorUpdateCount: number;
258
+ isFetched: boolean;
259
+ isFetchedAfterMount: boolean;
260
+ isFetching: boolean;
261
+ isInitialLoading: boolean;
262
+ isPaused: boolean;
263
+ isRefetching: boolean;
264
+ isStale: boolean;
265
+ isEnabled: boolean;
266
+ refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<{
267
+ reminders: SessionReminder[];
268
+ }, Error>>;
269
+ fetchStatus: import("@tanstack/react-query").FetchStatus;
270
+ };
271
+ controls: ReminderControls;
272
+ confirm: import("react").JSX.Element;
273
+ };
274
+ /**
275
+ * A reminder row's cells, for a `REMINDER_ROW_GRID` container: the ring and
276
+ * the title share the first grid row, so the ring centres on the title line;
277
+ * the next run sits at the end of that line, the schedule under the title.
278
+ */
279
+ export declare const REMINDER_ROW_GRID = "grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-2.5";
280
+ export declare function ReminderRowCells({ reminder, view, trailing, }: {
281
+ reminder: SessionReminder;
282
+ view: ReturnType<ReturnType<typeof useReminderView>>;
283
+ /** Replaces the next-run time in the end column, e.g. a disclosure caret. */
284
+ trailing?: ReactNode;
285
+ }): import("react").JSX.Element;
@@ -0,0 +1,115 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /**
4
+ * The pieces a session reminder is drawn with, shared by the header popover's
5
+ * hover card and the panel's Reminders view (`session-reminders-panel.tsx`):
6
+ * the countdown ring, the facts block, the action row, and the pause/remove
7
+ * mutations with their confirm dialog.
8
+ */
9
+ import { HoverPrefetchLink } from '../../internal/components/hover-prefetch-link.js';
10
+ import { Button } from '../../primitives/button.js';
11
+ import { ConfirmDialog } from '../../primitives/confirm-dialog.js';
12
+ import Hint from '../../primitives/hint.js';
13
+ import { errorToast, successToast } from '../../primitives/toast.js';
14
+ import { elapsedShare, formatFireTime, reminderTitle, scheduleLabel } from '../../internal/reminders/reminder-format.js';
15
+ import { cn } from '../../internal/lib/utils.js';
16
+ import { useLocale, useTranslations } from '../../i18n/index.js';
17
+ import { useSessionReminders } from '@kortix/sdk/react';
18
+ import { ArrowUpRightIcon, PauseIcon, PlayIcon } from '@phosphor-icons/react';
19
+ import { useState } from 'react';
20
+ export const isFailedReminder = (reminder) => reminder.last_status === 'failed';
21
+ /** Soonest next fire first; paused ones, which have no next fire, last. */
22
+ export function sortReminders(reminders) {
23
+ const key = (reminder) => reminder.state === 'active' && reminder.next_fire_at ? Date.parse(reminder.next_fire_at) : Infinity;
24
+ return [...reminders].sort((a, b) => key(a) - key(b));
25
+ }
26
+ /** A ring filled clockwise from the top by `share`; an empty track around a pause glyph while paused. */
27
+ export function CountdownRing({ share, size, stroke, tone, paused, children, }) {
28
+ const r = (size - stroke) / 2;
29
+ const length = 2 * Math.PI * r;
30
+ return (_jsxs("span", { className: "relative flex shrink-0 items-center justify-center", style: { width: size, height: size }, children: [_jsxs("svg", { "aria-hidden": true, width: size, height: size, className: "absolute inset-0 -rotate-90", children: [_jsx("circle", { cx: size / 2, cy: size / 2, r: r, fill: "none", stroke: "currentColor", strokeWidth: stroke, className: "text-border" }), !paused && share !== null ? (_jsx("circle", { cx: size / 2, cy: size / 2, r: r, fill: "none", stroke: "currentColor", strokeWidth: stroke, strokeLinecap: "round", strokeDasharray: `${length * share} ${length}`, className: tone === 'red' ? 'text-kortix-red' : 'text-kortix-purple' })) : null] }), paused ? (_jsx(PauseIcon, { "aria-hidden": true, weight: "fill", className: "text-muted-foreground", style: { width: Math.round(size * 0.4), height: Math.round(size * 0.4) } })) : (children)] }));
31
+ }
32
+ /** "6h" over "12m": the time left in its two largest units, narrow and localized. */
33
+ export function countdownParts(ms, locale) {
34
+ const min = Math.max(0, Math.round(ms / 60_000));
35
+ const unit = (value, name) => new Intl.NumberFormat(locale, { style: 'unit', unit: name, unitDisplay: 'narrow' }).format(value);
36
+ if (min >= 24 * 60)
37
+ return [unit(Math.floor(min / (24 * 60)), 'day'), unit(Math.floor((min % (24 * 60)) / 60), 'hour')];
38
+ if (min >= 60)
39
+ return [unit(Math.floor(min / 60), 'hour'), unit(min % 60, 'minute')];
40
+ return [unit(min, 'minute'), null];
41
+ }
42
+ /** What a row and its details show for one reminder at `now`. */
43
+ export function useReminderView(now) {
44
+ const t = useTranslations('reminders');
45
+ const locale = useLocale();
46
+ return (reminder) => {
47
+ const paused = reminder.state !== 'active';
48
+ const nextAt = reminder.next_fire_at ? Date.parse(reminder.next_fire_at) : NaN;
49
+ const failed = isFailedReminder(reminder);
50
+ return {
51
+ paused,
52
+ isFailed: failed,
53
+ next: !paused && reminder.next_fire_at ? formatFireTime(reminder.next_fire_at, locale, now) : null,
54
+ nextAt,
55
+ countdown: countdownParts(nextAt - now, locale),
56
+ share: elapsedShare(reminder, now),
57
+ tone: failed ? 'red' : 'purple',
58
+ pauseLabel: paused ? t('resume') : t('pause'),
59
+ schedule: scheduleLabel(reminder, (key, values) => t(key, values)),
60
+ meta: [failed ? t('lastRunFailed') : paused ? t('pausedLabel') : null, scheduleLabel(reminder, (key, values) => t(key, values))]
61
+ .filter(Boolean)
62
+ .join(' · '),
63
+ };
64
+ };
65
+ }
66
+ /** Repeats, time zone, last run, and the last error when the run failed. */
67
+ export function ReminderFacts({ reminder, now, className, }) {
68
+ const t = useTranslations('reminders');
69
+ const locale = useLocale();
70
+ const failed = isFailedReminder(reminder);
71
+ return (_jsxs("dl", { className: cn('space-y-1.5 text-xs', className), children: [_jsxs("div", { className: "flex justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: t('repeats') }), _jsx("dd", { className: "text-foreground truncate", children: scheduleLabel(reminder, (key, values) => t(key, values)) })] }), _jsxs("div", { className: "flex justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: t('timeZone') }), _jsx("dd", { className: "text-foreground truncate", children: reminder.timezone })] }), _jsxs("div", { className: "flex justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: t('lastRun') }), _jsx("dd", { className: "text-foreground flex items-center gap-1.5", children: reminder.last_fired_at ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, className: cn('size-1.5 rounded-full', failed ? 'bg-kortix-red' : 'bg-kortix-green') }), formatFireTime(reminder.last_fired_at, locale, now)] })) : (t('neverRun')) })] }), failed && reminder.last_error ? (_jsx("p", { className: "text-foreground bg-kortix-red/15 mt-2 rounded-md px-2 py-1.5 font-mono break-words", children: reminder.last_error })) : null] }));
72
+ }
73
+ /** Pause or resume, remove, and the link into the Reminders tab. */
74
+ export function ReminderActions({ reminder, controls, manageHref, onNavigate, className, }) {
75
+ const t = useTranslations('reminders');
76
+ const paused = reminder.state !== 'active';
77
+ return (_jsxs("div", { className: cn('flex items-center gap-1', className), children: [_jsxs(Button, { variant: "secondary", size: "xs", disabled: controls.pending, onClick: () => controls.setEnabled(reminder, paused), children: [paused ? _jsx(PlayIcon, { className: "size-3.5" }) : _jsx(PauseIcon, { className: "size-3.5" }), paused ? t('resume') : t('pause')] }), _jsx(Button, { variant: "ghost", size: "xs", className: "text-destructive", onClick: () => controls.requestRemove(reminder), children: t('remove') }), manageHref ? (
78
+ // An icon, not a label: three text buttons overflow the 288px card.
79
+ _jsx(Hint, { label: t('openInReminders'), side: "top", sideOffset: 6, alignOffset: 0, children: _jsx(Button, { asChild: true, variant: "ghost", size: "icon-xs", className: "text-muted-foreground ml-auto", "aria-label": t('openInReminders'), children: _jsx(HoverPrefetchLink, { href: manageHref, onClick: onNavigate, children: _jsx(ArrowUpRightIcon, { className: "size-3.5" }) }) }) })) : null] }));
80
+ }
81
+ /**
82
+ * The session's reminders with pause, resume and remove. Remove asks first:
83
+ * render `confirm` once next to whatever calls `requestRemove`.
84
+ */
85
+ export function useReminderControls(projectId, sessionId) {
86
+ const t = useTranslations('reminders');
87
+ const reminders = useSessionReminders(projectId, sessionId);
88
+ const [removing, setRemoving] = useState(null);
89
+ const controls = {
90
+ pending: reminders.update.isPending,
91
+ setEnabled: (reminder, enabled) => reminders.update.mutate({ reminderId: reminder.id, enabled }, {
92
+ onSuccess: () => successToast(enabled ? t('resumed') : t('paused')),
93
+ onError: (error) => errorToast(error instanceof Error ? error.message : t('updateFailed')),
94
+ }),
95
+ requestRemove: setRemoving,
96
+ };
97
+ const confirm = (_jsx(ConfirmDialog, { open: !!removing, onOpenChange: (next) => !next && setRemoving(null), title: t('removeTitle'), description: t('removeDescription'), confirmLabel: t('remove'), confirmVariant: "destructive", isPending: reminders.remove.isPending, onConfirm: () => removing &&
98
+ reminders.remove.mutate(removing.id, {
99
+ onSuccess: () => {
100
+ successToast(t('removed'));
101
+ setRemoving(null);
102
+ },
103
+ onError: (error) => errorToast(error instanceof Error ? error.message : t('updateFailed')),
104
+ }) }));
105
+ return { reminders, controls, confirm };
106
+ }
107
+ /**
108
+ * A reminder row's cells, for a `REMINDER_ROW_GRID` container: the ring and
109
+ * the title share the first grid row, so the ring centres on the title line;
110
+ * the next run sits at the end of that line, the schedule under the title.
111
+ */
112
+ export const REMINDER_ROW_GRID = 'grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-2.5';
113
+ export function ReminderRowCells({ reminder, view, trailing, }) {
114
+ return (_jsxs(_Fragment, { children: [_jsx(CountdownRing, { share: view.share, size: 14, stroke: 2, tone: view.tone, paused: view.paused }), _jsx("span", { className: cn('truncate text-sm leading-5', view.paused ? 'text-muted-foreground' : 'text-foreground font-medium'), children: reminderTitle(reminder) }), trailing ?? (view.next ? (_jsx("span", { className: "text-muted-foreground col-start-3 row-start-1 text-xs tabular-nums pointer-coarse:hidden", children: view.next })) : null), _jsx("span", { className: "text-muted-foreground col-start-2 truncate text-xs leading-4", children: view.meta })] }));
115
+ }
@@ -1,3 +1,16 @@
1
+ /** More live reminders than this open in the side panel, not the popover. */
2
+ export declare const REMINDERS_POPOVER_MAX = 30;
3
+ /**
4
+ * The clock in `BellMark`'s badge-glow style: the glyph, then again in the
5
+ * count's colour under the bell's corner mask, so the colour reaches in from
6
+ * the corner the count sits on. Purple for active reminders, red while one's
7
+ * last run failed. With nothing active the glyph is muted and has no count.
8
+ */
9
+ export declare function ReminderMark({ count, failed: anyFailed, surface, }: {
10
+ count: number;
11
+ failed: boolean;
12
+ surface?: 'bg-background' | 'bg-sidebar';
13
+ }): import("react").JSX.Element;
1
14
  export declare function SessionRemindersIndicator({ runtimeSessionId }: {
2
15
  runtimeSessionId: string;
3
16
  }): import("react").JSX.Element | null;
@@ -1,44 +1,128 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  /**
4
4
  * Header chip for this session's reminders (scheduled check-ins that re-prompt
5
- * the session). Renders nothing until the session has a reminder that can
6
- * still fire; then an alarm with the active count, and a popover to pause,
7
- * resume, or remove one, or to open the Customize Reminders tab on this
8
- * session. The session reminders read needs `project.session.start`, so a
9
- * read-only viewer's 403 simply leaves the chip hidden. The tab link shows only
10
- * to a caller who has the tab (`capabilityAccess()`): a plain member manages
11
- * their reminders here.
5
+ * the session), Paper R08 "Countdown rings". Renders nothing until the session
6
+ * has a reminder that can still fire; then `ReminderMark`, and a popover that
7
+ * lists each reminder behind a ring of how much of its wait has passed. On a
8
+ * pointer that hovers, a row's details and actions open in a hover card on the
9
+ * popover's left; on a touch screen the pause and remove buttons sit in the row.
10
+ * Past `REMINDERS_POPOVER_MAX` the popover would be a wall to scroll, so the
11
+ * chip opens the panel's Reminders view instead (`session-reminders-panel.tsx`).
12
+ * The session reminders read needs `project.session.start`, so a read-only
13
+ * viewer's 403 simply leaves the chip hidden. The "Open in Reminders" link
14
+ * shows only to a caller who has the tab (`capabilityAccess()`).
12
15
  */
13
16
  import { HoverPrefetchLink } from '../../internal/components/hover-prefetch-link.js';
14
- import { Badge } from '../../primitives/badge.js';
15
17
  import { Button } from '../../primitives/button.js';
16
- import { ConfirmDialog } from '../../primitives/confirm-dialog.js';
17
- import Hint from '../../primitives/hint.js';
18
+ import { CountBadge } from '../../primitives/count-badge.js';
19
+ import { FLOATING_PANEL_SURFACE } from '../../primitives/menu-recipe.js';
18
20
  import { Popover, PopoverContent, PopoverTrigger } from '../../primitives/popover.js';
19
- import { errorToast, successToast } from '../../primitives/toast.js';
20
21
  import { remindersHref } from '../../internal/lib/capability-tab-routes.js';
21
22
  import { useCapabilityAccess } from '../../internal/layout/project-settings-nav.js';
22
- import { formatFireTime, reminderTitle, } from '../../internal/reminders/reminder-format.js';
23
+ import { BELL_REFLECTION_MASK } from '../../internal/notifications/notification-bell.js';
24
+ import { badgeCount } from '../../internal/notifications/notification-rows.js';
25
+ import { reminderTitle } from '../../internal/reminders/reminder-format.js';
23
26
  import { useNow, useRefetchAfterFire, } from '../../internal/reminders/use-refetch-after-fire.js';
24
- import { useLocale, useTranslations } from '../../i18n/index.js';
25
- import { qk, useFeatureFlag, useSessionReminders, useSessionWorking } from '@kortix/sdk/react';
27
+ import { cn } from '../../internal/lib/utils.js';
28
+ import { useTranslations } from '../../i18n/index.js';
29
+ import { qk, useFeatureFlag, useSessionWorking } from '@kortix/sdk/react';
26
30
  import { AlarmIcon, PauseIcon, PlayIcon, TrashIcon } from '@phosphor-icons/react';
27
31
  import { useQueryClient } from '@tanstack/react-query';
28
32
  import { useSessionIds } from '../../providers/session-ids.js';
29
- import { useEffect, useRef, useState } from 'react';
33
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
34
+ import { openSessionQuickView } from '../panel/open-session-quick-view.js';
35
+ import { CountdownRing, isFailedReminder, REMINDER_ROW_GRID, ReminderActions, ReminderFacts, ReminderRowCells, sortReminders, useReminderControls, useReminderView, } from './reminder-parts.js';
36
+ /** More live reminders than this open in the side panel, not the popover. */
37
+ export const REMINDERS_POPOVER_MAX = 30;
38
+ /**
39
+ * The clock in `BellMark`'s badge-glow style: the glyph, then again in the
40
+ * count's colour under the bell's corner mask, so the colour reaches in from
41
+ * the corner the count sits on. Purple for active reminders, red while one's
42
+ * last run failed. With nothing active the glyph is muted and has no count.
43
+ */
44
+ export function ReminderMark({ count, failed: anyFailed, surface = 'bg-background', }) {
45
+ const tone = anyFailed
46
+ ? { reflection: 'text-kortix-red', disc: 'bg-kortix-red' }
47
+ : { reflection: 'text-kortix-purple', disc: 'bg-kortix-purple' };
48
+ return (_jsxs("span", { className: "relative size-4 shrink-0", children: [_jsx(AlarmIcon, { className: cn('size-4', count === 0 && 'text-muted-foreground') }), count > 0 ? (_jsxs(_Fragment, { children: [_jsx(AlarmIcon, { "aria-hidden": true, className: cn('absolute inset-0 size-4', tone.reflection), style: { maskImage: BELL_REFLECTION_MASK, WebkitMaskImage: BELL_REFLECTION_MASK } }), _jsx("span", { "aria-hidden": true, className: cn('absolute -right-1.5 -bottom-1.5 flex rounded-full p-px', surface), children: _jsx("span", { "data-slot": "reminder-count", className: cn('text-background flex h-3 min-w-3 items-center justify-center rounded-full px-0.5 text-[9px] leading-none font-semibold tabular-nums', // audit:allow a numeral in an 11px disc on a 16px glyph; no type rung fits
49
+ tone.disc), children: badgeCount(count) }) })] })) : null] }));
50
+ }
51
+ const CARD_OPEN_DELAY_MS = 200;
52
+ /** While open, a move to another row waits this long; a newer row restarts it. */
53
+ const CARD_MOVE_DEBOUNCE_MS = 50;
54
+ const CARD_CLOSE_DELAY_MS = 100;
55
+ const CARD_EDGE_PX = 8;
56
+ /**
57
+ * The reminder card's position beside the popover. The first hover opens it
58
+ * after a delay; while it is open, hovering another row moves it there after a
59
+ * short debounce, so a sweep across rows moves it once, and it glides (no glide
60
+ * and no debounce for keyboard focus; no glide under reduced motion).
61
+ */
62
+ function useGlidingCard() {
63
+ const panelRef = useRef(null);
64
+ const cardRef = useRef(null);
65
+ const [active, setActive] = useState(null);
66
+ const openTimer = useRef(undefined);
67
+ const closeTimer = useRef(undefined);
68
+ const clearTimers = () => {
69
+ window.clearTimeout(openTimer.current);
70
+ window.clearTimeout(closeTimer.current);
71
+ };
72
+ useEffect(() => clearTimers, []);
73
+ // Before paint: keep the card inside the viewport, then place it.
74
+ useLayoutEffect(() => {
75
+ const panel = panelRef.current;
76
+ const el = cardRef.current;
77
+ if (!active || !panel || !el)
78
+ return;
79
+ const panelTop = panel.getBoundingClientRect().top;
80
+ const max = window.innerHeight - CARD_EDGE_PX - panelTop - el.offsetHeight;
81
+ const top = Math.max(CARD_EDGE_PX - panelTop, Math.min(active.top, max));
82
+ el.style.transform = `translateY(${top}px)`;
83
+ }, [active]);
84
+ const show = (id, row, keyboard) => {
85
+ const panel = panelRef.current;
86
+ if (!panel)
87
+ return;
88
+ clearTimers();
89
+ const panelRect = panel.getBoundingClientRect();
90
+ const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
91
+ const place = (glide) => setActive({
92
+ id,
93
+ top: row.getBoundingClientRect().top - panelRect.top,
94
+ // 288px card + 12px bridge; the right side only when the left has no room.
95
+ side: panelRect.left >= 300 + CARD_EDGE_PX ? 'left' : 'right',
96
+ glide,
97
+ });
98
+ if (keyboard)
99
+ place(false);
100
+ else if (active)
101
+ openTimer.current = window.setTimeout(() => place(!reduce), CARD_MOVE_DEBOUNCE_MS);
102
+ else
103
+ openTimer.current = window.setTimeout(() => place(false), CARD_OPEN_DELAY_MS);
104
+ };
105
+ const leave = () => {
106
+ clearTimers();
107
+ closeTimer.current = window.setTimeout(() => setActive(null), CARD_CLOSE_DELAY_MS);
108
+ };
109
+ const close = () => {
110
+ clearTimers();
111
+ setActive(null);
112
+ };
113
+ return { panelRef, cardRef, active, show, leave, close, cancelClose: clearTimers };
114
+ }
30
115
  export function SessionRemindersIndicator({ runtimeSessionId }) {
31
116
  const t = useTranslations('reminders');
32
- const locale = useLocale();
33
117
  // useSessionIds: the project and the Kortix session reminders key on.
34
118
  const { projectId, sessionId } = useSessionIds();
35
119
  const gate = useFeatureFlag(projectId, 'reminders');
36
- const reminders = useSessionReminders(gate.enabled ? projectId : null, sessionId);
120
+ const { reminders, controls, confirm } = useReminderControls(gate.enabled ? projectId : null, sessionId);
37
121
  useRefetchAfterFire(reminders.data?.reminders, reminders.refetch);
38
122
  const now = useNow();
39
123
  const hasTab = useCapabilityAccess(projectId).tabs.includes('reminders');
40
- // Null when the host serves no customize pages: no "Manage" link.
41
- const manageHref = hasTab ? remindersHref(projectId) : null;
124
+ // Null when the host serves no customize pages: no "Open in Reminders" link.
125
+ const manageHref = hasTab ? `${remindersHref(projectId)}?session=${sessionId}` : null;
42
126
  // An agent sets reminders mid-turn with `kortix remind`; nothing announces
43
127
  // that to this query. The turn ending is the moment to look again.
44
128
  const working = useSessionWorking(projectId ?? '', sessionId ?? '', {
@@ -56,23 +140,46 @@ export function SessionRemindersIndicator({ runtimeSessionId }) {
56
140
  wasWorking.current = isWorking;
57
141
  }, [isWorking, projectId, queryClient]);
58
142
  const [open, setOpen] = useState(false);
59
- const [removing, setRemoving] = useState(null);
143
+ const { panelRef, cardRef, active: activeCard, show: showCard, leave: leaveCard, close: closeCard, cancelClose } = useGlidingCard();
144
+ const view = useReminderView(now);
60
145
  const live = (reminders.data?.reminders ?? []).filter((reminder) => reminder.state !== 'done');
61
146
  if (live.length === 0)
62
147
  return null;
63
148
  const activeCount = live.filter((reminder) => reminder.state === 'active').length;
64
- const setEnabled = (reminder, enabled) => reminders.update.mutate({ reminderId: reminder.id, enabled }, {
65
- onSuccess: () => successToast(enabled ? t('resumed') : t('paused')),
66
- onError: (error) => errorToast(error instanceof Error ? error.message : t('updateFailed')),
67
- });
68
- return (_jsxs(_Fragment, { children: [_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "icon", "aria-label": t('chipLabel', { count: activeCount }), className: "relative", "data-testid": "session-reminders-chip", children: [_jsx(AlarmIcon, { className: "size-4" }), activeCount > 0 ? (_jsx(Badge, { variant: "secondary", size: "tabular", className: "absolute -top-1 -right-1", children: activeCount })) : null] }) }), _jsxs(PopoverContent, { align: "end", sideOffset: 8, className: "w-80 overflow-hidden p-0", children: [_jsx("div", { className: "border-b px-4 pt-4 pb-3", children: _jsx("h3", { className: "text-foreground text-sm font-medium", children: t('chipTitle') }) }), _jsx("ul", { className: "max-h-64 divide-y overflow-auto", children: live.map((reminder) => (_jsxs("li", { className: "flex items-center gap-2 px-4 py-2.5", children: [_jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [_jsx("p", { className: "text-foreground truncate text-sm", title: reminder.prompt, children: reminderTitle(reminder) }), _jsx("p", { className: "text-muted-foreground truncate text-xs", children: reminder.state === 'active' && reminder.next_fire_at
69
- ? t('nextFire', { time: formatFireTime(reminder.next_fire_at, locale, now) })
70
- : t('pausedLabel') })] }), _jsx(Hint, { label: reminder.state === 'active' ? t('pause') : t('resume'), children: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": reminder.state === 'active' ? t('pause') : t('resume'), disabled: reminders.update.isPending, onClick: () => setEnabled(reminder, reminder.state !== 'active'), children: reminder.state === 'active' ? (_jsx(PauseIcon, { className: "size-4 shrink-0" })) : (_jsx(PlayIcon, { className: "size-4 shrink-0" })) }) }), _jsx(Hint, { label: t('remove'), children: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": t('remove'), onClick: () => setRemoving(reminder), children: _jsx(TrashIcon, { className: "size-4 shrink-0" }) }) })] }, reminder.id))) }), manageHref ? (_jsx("div", { className: "border-t px-4 py-2", children: _jsx(Button, { asChild: true, variant: "ghost", size: "sm", className: "w-full justify-center", children: _jsx(HoverPrefetchLink, { href: `${manageHref}?session=${sessionId}`, onClick: () => setOpen(false), children: t('manage') }) }) })) : null] })] }), _jsx(ConfirmDialog, { open: !!removing, onOpenChange: (next) => !next && setRemoving(null), title: t('removeTitle'), description: t('removeDescription'), confirmLabel: t('remove'), confirmVariant: "destructive", isPending: reminders.remove.isPending, onConfirm: () => removing &&
71
- reminders.remove.mutate(removing.id, {
72
- onSuccess: () => {
73
- successToast(t('removed'));
74
- setRemoving(null);
75
- },
76
- onError: (error) => errorToast(error instanceof Error ? error.message : t('updateFailed')),
77
- }) })] }));
149
+ const rows = sortReminders(live);
150
+ const mark = _jsx(ReminderMark, { count: activeCount, failed: live.some(isFailedReminder) });
151
+ if (live.length > REMINDERS_POPOVER_MAX) {
152
+ return (_jsx(Button, { variant: "ghost", size: "icon", "aria-label": t('chipLabel', { count: activeCount }), className: "relative", "data-testid": "session-reminders-chip", onClick: () => openSessionQuickView('reminders', 'chip'), children: mark }));
153
+ }
154
+ const shown = activeCard ? rows.find((reminder) => reminder.id === activeCard?.id) : undefined;
155
+ return (_jsxs(_Fragment, { children: [_jsxs(Popover, { open: open, onOpenChange: (next) => {
156
+ setOpen(next);
157
+ if (!next)
158
+ closeCard();
159
+ }, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": t('chipLabel', { count: activeCount }), className: "relative", "data-testid": "session-reminders-chip", children: mark }) }), _jsxs(PopoverContent, { ref: panelRef, align: "end", sideOffset: 8, className: "relative w-96 p-0", children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-4 pt-3 pb-1", children: [_jsxs("h3", { className: "text-foreground flex items-center gap-2 text-sm font-medium", children: [t('title'), activeCount > 0 ? _jsx(CountBadge, { count: activeCount, tone: "reminder" }) : null] }), manageHref ? (_jsx(Button, { asChild: true, variant: "ghost", size: "xs", className: "text-muted-foreground -mr-2.5", children: _jsx(HoverPrefetchLink, { href: manageHref, onClick: () => setOpen(false), children: t('cardManage') }) })) : null] }), _jsx("ul", { className: "max-h-96 overflow-y-auto p-1", onPointerLeave: leaveCard, onScroll: closeCard, children: rows.map((reminder) => {
160
+ const v = view(reminder);
161
+ return (_jsx("li", { children: _jsxs("div", { tabIndex: 0, "data-testid": "session-reminder-row", "data-active": activeCard?.id === reminder.id || undefined, onPointerEnter: (event) => {
162
+ if (event.pointerType !== 'touch')
163
+ showCard(reminder.id, event.currentTarget, false);
164
+ }, onFocus: (event) => {
165
+ if (event.currentTarget.matches(':focus-visible'))
166
+ showCard(reminder.id, event.currentTarget, true);
167
+ }, className: cn(REMINDER_ROW_GRID, 'hover:bg-hover focus-visible:ring-ring data-active:bg-hover rounded-lg px-2.5 py-1.5 outline-none focus-visible:ring-2'), children: [_jsx(ReminderRowCells, { reminder: reminder, view: v }), _jsxs("span", { className: "col-start-3 row-span-2 row-start-1 hidden items-center pointer-coarse:flex", children: [_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": v.pauseLabel, disabled: controls.pending, onClick: () => controls.setEnabled(reminder, v.paused), children: v.paused ? _jsx(PlayIcon, { className: "size-4" }) : _jsx(PauseIcon, { className: "size-4" }) }), _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": t('remove'), onClick: () => controls.requestRemove(reminder), children: _jsx(TrashIcon, { className: "size-4" }) })] })] }) }, reminder.id));
168
+ }) }), shown && activeCard
169
+ ? (() => {
170
+ const { paused, next, nextAt, countdown, share, tone } = view(shown);
171
+ const [major, minor] = countdown;
172
+ return (
173
+ // One card for the whole list: hovering the next row moves it
174
+ // instead of closing one card and opening another. The padding
175
+ // on the popover side bridges the gap, so the pointer never
176
+ // leaves on its way into the card.
177
+ _jsx("div", { ref: cardRef, onPointerEnter: cancelClose, onPointerLeave: leaveCard, className: cn('absolute top-0 starting:opacity-0 pointer-coarse:hidden', activeCard.side === 'left' ? 'right-full pr-3' : 'left-full pl-3'), style: {
178
+ transition: activeCard.glide
179
+ ? 'transform var(--duration-normal) var(--ease-in-out)'
180
+ : 'opacity var(--duration-fast) var(--ease-out)',
181
+ }, children: _jsxs("div", { "data-testid": "session-reminder-card", className: cn(FLOATING_PANEL_SURFACE, 'w-72 overflow-hidden'), children: [_jsxs("div", { className: "flex items-center gap-3 px-4 pt-4 pb-3", children: [_jsx(CountdownRing, { share: share, size: 44, stroke: 3, tone: tone, paused: paused, children: !paused && Number.isFinite(nextAt) ? (_jsxs("span", { className: "flex flex-col items-center leading-none tabular-nums", children: [_jsx("span", { className: "text-foreground text-xs font-medium", children: major }), minor ? (_jsx("span", { className: "text-muted-foreground mt-0.5 text-[10px]" // audit:allow the minor unit inside a 44px ring
182
+ , children: minor })) : null] })) : null }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "text-foreground text-sm font-medium text-pretty", children: reminderTitle(shown) }), _jsx("p", { className: "text-muted-foreground mt-0.5 text-xs", children: next ? t('nextRun', { time: next }) : t('pausedLabel') })] })] }), shown.name ? (_jsx("p", { className: "text-muted-foreground line-clamp-4 px-4 pb-3 text-xs text-pretty whitespace-pre-line", children: shown.prompt })) : null, _jsx(ReminderFacts, { reminder: shown, now: now, className: "border-t px-4 py-3" }), _jsx(ReminderActions, { reminder: shown, controls: controls, manageHref: manageHref, onNavigate: () => setOpen(false), className: "border-t p-2" })] }) }));
183
+ })()
184
+ : null] })] }), confirm] }));
78
185
  }