@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.
- package/README.md +8 -4
- package/dist/customize/sections/llm-provider/constants.d.ts +0 -1
- package/dist/customize/sections/llm-provider/constants.js +0 -2
- package/dist/customize/sections/llm-provider/models-tab-group.d.ts +1 -1
- package/dist/customize/sections/llm-provider/models-tab-group.js +7 -4
- package/dist/customize/sections/llm-provider/models-tab-row.d.ts +2 -18
- package/dist/customize/sections/llm-provider/models-tab-row.js +6 -30
- package/dist/customize/sections/llm-provider/models-tab.d.ts +0 -1
- package/dist/customize/sections/llm-provider/models-tab.js +1 -10
- package/dist/customize/sections/llm-provider/use-connected-providers.js +2 -2
- package/dist/customize/sections/llm-provider/utils.d.ts +1 -1
- package/dist/customize/sections/llm-provider/utils.js +7 -14
- package/dist/customize/sections/view/gateway/_shared.js +1 -2
- package/dist/customize/sections/view/gateway/gateway-routing.js +3 -2
- package/dist/customize/sections/view/sandbox-provider-coverage.d.ts +3 -18
- package/dist/customize/sections/view/sandbox-provider-coverage.js +7 -27
- package/dist/customize/settings/tabs/sandbox-tab.d.ts +2 -27
- package/dist/customize/settings/tabs/sandbox-tab.js +13 -112
- package/dist/customize/shared/entity/entity-modal.d.ts +5 -1
- package/dist/customize/shared/entity/entity-modal.js +7 -4
- package/dist/customize/skills/remove-skill-button.d.ts +16 -0
- package/dist/customize/skills/remove-skill-button.js +42 -0
- package/dist/customize/skills/skills-page.js +1 -1
- package/dist/customize/triggers/schedule/trigger-list.d.ts +0 -2
- package/dist/customize/triggers/schedule/trigger-list.js +1 -18
- package/dist/customize/triggers/schedule/trigger-tile.d.ts +1 -32
- package/dist/customize/triggers/schedule/trigger-tile.js +1 -16
- package/dist/files/project/hooks/use-change-requests.d.ts +14 -1
- package/dist/files/project/hooks/use-change-requests.js +44 -25
- package/dist/files/renderers/docx/docx-viewer.js +4 -1
- package/dist/files/renderers/pdf/pdf-renderer.d.ts +3 -1
- package/dist/files/renderers/pdf/pdf-renderer.js +2 -2
- package/dist/files/renderers/pdf/pdf-viewer.d.ts +3 -0
- package/dist/files/renderers/pdf/pdf-viewer.js +15 -6
- package/dist/files/viewer/file-content-renderer.d.ts +4 -1
- package/dist/files/viewer/file-content-renderer.js +2 -2
- package/dist/i18n/en.d.ts +26 -35
- package/dist/i18n/en.js +35 -44
- package/dist/index.d.ts +1 -1
- package/dist/internal/hooks/use-notification-center.js +21 -15
- package/dist/internal/hooks/use-review-session-summary.d.ts +6 -0
- package/dist/internal/hooks/use-review-session-summary.js +11 -0
- package/dist/internal/lib/browser-error-noise.d.ts +1 -1
- package/dist/internal/lib/browser-error-noise.js +1 -1
- package/dist/internal/lib/browser-noise/rules/rejections.d.ts +10 -0
- package/dist/internal/lib/browser-noise/rules/rejections.js +27 -1
- package/dist/internal/lib/provider-branding.js +14 -3
- package/dist/internal/lib/trigger-clock.d.ts +42 -0
- package/dist/internal/lib/trigger-clock.js +34 -0
- package/dist/internal/lib/web-notifications.d.ts +6 -0
- package/dist/internal/lib/web-notifications.js +57 -7
- package/dist/internal/notifications/notification-bell.d.ts +13 -0
- package/dist/internal/notifications/notification-bell.js +1 -1
- package/dist/internal/reminders/reminder-filter.d.ts +12 -0
- package/dist/internal/reminders/reminder-filter.js +34 -0
- package/dist/internal/reminders/reminder-format.d.ts +6 -0
- package/dist/internal/reminders/reminder-format.js +20 -2
- package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
- package/dist/internal/stores/session-hover-store.d.ts +20 -5
- package/dist/internal/stores/session-hover-store.js +31 -13
- package/dist/internal/stores/session-panel-memory-store.js +2 -0
- package/dist/primitives/count-badge.d.ts +3 -1
- package/dist/primitives/count-badge.js +3 -3
- package/dist/primitives/glyph-paths-source.js +1 -1
- package/dist/primitives/glyph-paths.generated.js +1 -1
- package/dist/providers/ui-provider.d.ts +57 -0
- package/dist/providers/ui-provider.js +4 -0
- package/dist/session/composer/ComposerCard.d.ts +2 -0
- package/dist/session/composer/ComposerCard.js +5 -1
- package/dist/session/composer/agent-selector.js +12 -5
- package/dist/session/composer/composer-toolbar.js +4 -2
- package/dist/session/composer/composer-underbar.js +3 -1
- package/dist/session/composer/composer.js +34 -5
- package/dist/session/composer/menus/slash-actions.d.ts +22 -2
- package/dist/session/composer/menus/slash-actions.js +20 -0
- package/dist/session/composer/menus/slash-items.js +19 -3
- package/dist/session/header/reminder-parts.d.ts +285 -0
- package/dist/session/header/reminder-parts.js +115 -0
- package/dist/session/header/session-reminders-indicator.d.ts +13 -0
- package/dist/session/header/session-reminders-indicator.js +142 -35
- package/dist/session/models/model-availability.d.ts +23 -0
- package/dist/session/models/model-availability.js +20 -0
- package/dist/session/models/model-grouping.js +1 -4
- package/dist/session/models/model-selector.js +23 -14
- package/dist/session/models/use-model-connection-gate.js +4 -1
- package/dist/session/outcomes/session-outcomes-provider.d.ts +3 -1
- package/dist/session/outcomes/session-outcomes-provider.js +31 -3
- package/dist/session/overrides/session-overrides-composer.d.ts +1 -1
- package/dist/session/overrides/session-overrides-composer.js +5 -1
- package/dist/session/panel/mobile-tool-drawer.js +4 -2
- package/dist/session/panel/open-session-quick-view.js +2 -1
- package/dist/session/panel/panel-fallback.js +8 -4
- package/dist/session/panel/session-panel-provider.d.ts +1 -0
- package/dist/session/panel/session-panel-provider.js +13 -1
- package/dist/session/panel/session-reminders-panel.d.ts +4 -0
- package/dist/session/panel/session-reminders-panel.js +109 -0
- package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
- package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
- package/dist/session/print/session-print-header.d.ts +0 -10
- package/dist/session/print/session-print-header.js +16 -1
- package/dist/session/status/session-surface.js +1 -1
- package/dist/session/thread/optimistic-turn.d.ts +5 -1
- package/dist/session/thread/optimistic-turn.js +8 -6
- package/dist/session/thread/session-chat.d.ts +6 -1
- package/dist/session/thread/session-chat.js +22 -3
- package/dist/session/thread/session-thread.d.ts +10 -1
- package/dist/session/thread/session-thread.js +2 -2
- package/dist/session/turn/burst-step-body.js +4 -0
- package/dist/styles.css +1 -1
- package/dist/workspace/sidebar/project-session-list.js +52 -55
- package/dist/workspace/sidebar/session-brief-hover-card.d.ts +35 -18
- package/dist/workspace/sidebar/session-brief-hover-card.js +114 -37
- package/dist/workspace/sidebar/session-status-mark.d.ts +15 -1
- package/dist/workspace/sidebar/session-status-mark.js +63 -17
- package/package.json +7 -3
- package/skills/kortix-ui/SKILL.md +1 -1
- package/dist/customize/sections/view/sandbox-provider-result.d.ts +0 -47
- 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,
|
|
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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 {
|
|
17
|
-
import
|
|
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 {
|
|
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 {
|
|
25
|
-
import {
|
|
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 =
|
|
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 "
|
|
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
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
}
|