@nightkatana/kronosys-app 1.0.0-beta.2 → 1.0.0-beta.21
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 +28 -1
- package/app/api/action/route.ts +39 -3
- package/app/api/action-logs/route.ts +24 -0
- package/app/api/backup/route.ts +1 -1
- package/app/api/restore/route.ts +145 -0
- package/app/changelog/page.tsx +71 -4
- package/app/globals.css +127 -0
- package/app/guide/page.tsx +61 -15
- package/app/implementation/page.tsx +700 -0
- package/app/layout.tsx +14 -3
- package/app/licenses/page.tsx +99 -37
- package/app/logs/page.tsx +258 -0
- package/app/manifest.ts +5 -5
- package/app/page.tsx +784 -229
- package/app/reporting/page.tsx +1266 -474
- package/app/settings/page.tsx +252 -18
- package/bin/kronosys.mjs +140 -15
- package/components/KronosysPayloadProvider.tsx +2 -0
- package/components/RouteTransition.tsx +18 -0
- package/components/dashboard/AppShellCommandCenterPlaceholder.tsx +17 -0
- package/components/dashboard/AppShellHeaderSessionMeta.tsx +210 -0
- package/components/dashboard/AppShellHeaderWallClock.tsx +54 -0
- package/components/dashboard/AppShellLiveSessionDrawer.tsx +154 -38
- package/components/dashboard/AppShellRouteNav.tsx +323 -48
- package/components/dashboard/DashboardPauseBackdrop.tsx +50 -0
- package/components/dashboard/DashboardSimpleModal.tsx +168 -25
- package/components/dashboard/DashboardTour.tsx +115 -29
- package/components/dashboard/GlobalPauseConfirmModal.tsx +183 -0
- package/components/dashboard/KronosysDatetimePopoverField.tsx +167 -122
- package/components/dashboard/KronosysTimePopoverField.tsx +54 -12
- package/components/dashboard/NewSessionScopeModal.tsx +211 -20
- package/components/dashboard/PlannedTaskBoundaryConflictWatcher.tsx +275 -0
- package/components/dashboard/ReportingTour.tsx +87 -21
- package/components/dashboard/SavedProjectPicker.tsx +16 -3
- package/components/dashboard/SelectedSessionSidebarBlock.tsx +512 -142
- package/components/dashboard/SessionListPanel.tsx +327 -44
- package/components/dashboard/SettingsTagsProjectsSection.tsx +1073 -264
- package/components/dashboard/SettingsTaskTemplatesSection.tsx +316 -0
- package/components/dashboard/SettingsTour.tsx +86 -21
- package/components/dashboard/TagPills.tsx +14 -1
- package/components/dashboard/TaskFocusPanel.tsx +1081 -478
- package/components/dashboard/TaskSessionLiveCard.tsx +650 -135
- package/components/dashboard/TaskTimelineGanttModal.tsx +601 -0
- package/components/dashboard/taskFieldStyles.ts +20 -4
- package/components/dashboard/useReportingInteractionState.ts +80 -0
- package/lib/appShellHeaderClasses.ts +13 -0
- package/lib/businessRulesMatrix.ts +210 -0
- package/lib/copyToClipboard.ts +43 -0
- package/lib/dashboardCopy.ts +494 -84
- package/lib/dashboardQuickSearch.ts +54 -2
- package/lib/dashboardTimeZone.ts +109 -0
- package/lib/formatAppShellWallClock.ts +66 -0
- package/lib/formatSessionNameTemplate.ts +141 -0
- package/lib/generatedUserChangelog.ts +177 -6
- package/lib/globalPausePreview.ts +292 -0
- package/lib/implementationNotes.ts +1188 -0
- package/lib/kronosysApi.ts +6 -0
- package/lib/kronosysDashboardModalGates.ts +24 -0
- package/lib/plannedBoundaryAttention.ts +9 -0
- package/lib/plannedBoundaryConflict.ts +23 -0
- package/lib/reportingAggregate.ts +517 -75
- package/lib/reportingMetricHelp.ts +8 -0
- package/lib/reportingStrings.ts +37 -3
- package/lib/sessionListMerge.ts +4 -0
- package/lib/sessionTaskSidebarStats.ts +182 -21
- package/lib/settingsCopy.ts +178 -4
- package/lib/taskParsing.ts +360 -103
- package/lib/taskTemplateDraft.ts +135 -0
- package/lib/taskTimelineGantt.ts +265 -0
- package/lib/temporalDisplayPlanned.ts +71 -0
- package/lib/userGuideCopy.ts +121 -47
- package/next.config.ts +7 -0
- package/package.json +12 -24
- package/server/actionDispatch.ts +1000 -77
- package/server/actionTaskSession.ts +337 -24
- package/server/db.ts +7 -15
- package/server/dbSchema.ts +24 -0
- package/server/defaultCfg.ts +5 -0
- package/server/gitlabTokenStore.ts +0 -12
- package/server/liveHistorySync.ts +53 -0
- package/server/mainTimerHydrate.ts +38 -2
- package/server/payloadStore.ts +33 -11
- package/server/sessionWallHydrate.ts +66 -3
- package/server/userActionLog.ts +126 -0
- package/sonar-project.properties +11 -0
- package/tsconfig.json +2 -1
- package/components/dashboard/IssuePickerModal.tsx +0 -168
- package/components/dashboard/ThemeToggle.test.tsx +0 -26
- package/lib/backupCsvExport.test.ts +0 -149
- package/lib/dashboardQuickSearchQuery.test.ts +0 -63
- package/lib/dataDir.test.ts +0 -87
- package/lib/formatIsoShort.test.ts +0 -46
- package/lib/kronoFocusRhythm.test.ts +0 -130
- package/lib/kronoFocusTimerUrgency.test.ts +0 -74
- package/lib/legacyKronoFocusStorageKeys.test.ts +0 -29
- package/lib/reportingAggregate.test.ts +0 -325
- package/lib/reportingNonFinalIndicators.test.ts +0 -157
- package/lib/reportingTagWeekBreakdown.test.ts +0 -141
- package/lib/reportingWeekLayout.test.ts +0 -239
- package/lib/sessionAssiduity.test.ts +0 -25
- package/lib/sessionEndWarnings.test.ts +0 -200
- package/lib/sessionListMerge.test.ts +0 -101
- package/lib/sessionTaskSidebarStats.test.ts +0 -24
- package/lib/taskParsing.test.ts +0 -153
- package/lib/usageProfile.test.ts +0 -84
- package/server/actionDispatch.test.ts +0 -723
- package/server/actionTaskSession.test.ts +0 -713
- package/server/kronoFocusHydrate.test.ts +0 -142
- package/server/kronoFocusMigrate.test.ts +0 -53
- package/server/mainTimerHydrate.test.ts +0 -65
- package/server/payloadStore.test.ts +0 -78
- package/server/sessionWallHydrate.test.ts +0 -46
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
3
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import {
|
|
6
|
+
Archive,
|
|
7
|
+
Circle,
|
|
8
|
+
ExternalLink,
|
|
9
|
+
FileText,
|
|
10
|
+
LayoutGrid,
|
|
11
|
+
Loader2,
|
|
12
|
+
Square,
|
|
13
|
+
Trash2,
|
|
14
|
+
UploadCloud,
|
|
15
|
+
} from "lucide-react";
|
|
16
|
+
import {
|
|
17
|
+
sessionTaskCountNoun,
|
|
18
|
+
type DashboardStrings,
|
|
19
|
+
type Lang,
|
|
20
|
+
} from "@/lib/dashboardCopy";
|
|
21
|
+
import {
|
|
22
|
+
DEFAULT_DASHBOARD_TIME_ZONE,
|
|
23
|
+
isValidIanaTimeZone,
|
|
24
|
+
} from "@/lib/dashboardTimeZone";
|
|
6
25
|
import { formatIsoInstantShort } from "@/lib/formatIsoShort";
|
|
7
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
sessionWallClockMinutes,
|
|
28
|
+
type LooseSession,
|
|
29
|
+
} from "@/lib/reportingAggregate";
|
|
8
30
|
import { formatDuration } from "@/lib/taskParsing";
|
|
31
|
+
import { isOpenSessionDisplayPlanned } from "@/lib/temporalDisplayPlanned";
|
|
9
32
|
|
|
10
33
|
export type SessionListEntry = {
|
|
11
34
|
sessionId: string;
|
|
12
35
|
sessionName?: string;
|
|
36
|
+
sessionNote?: string;
|
|
13
37
|
savedAt?: string;
|
|
14
38
|
/** Horodatage immuable de création de la session ; repli : `startAt` pour les anciennes données. */
|
|
15
39
|
createdAt?: string | null;
|
|
@@ -33,12 +57,14 @@ function taskCount(s: SessionListEntry): number {
|
|
|
33
57
|
Array.isArray(s.activeTasks) && s.activeTasks.length > 0
|
|
34
58
|
? s.activeTasks.length
|
|
35
59
|
: s.activeTask
|
|
36
|
-
|
|
37
|
-
|
|
60
|
+
? 1
|
|
61
|
+
: 0;
|
|
38
62
|
return listed + nActive;
|
|
39
63
|
}
|
|
40
64
|
|
|
41
|
-
function sessionMongoPushState(
|
|
65
|
+
function sessionMongoPushState(
|
|
66
|
+
sess: SessionListEntry,
|
|
67
|
+
): "never" | "synced" | "dirty" {
|
|
42
68
|
const saved = sess.savedAt ?? "";
|
|
43
69
|
const lastPush = sess.mongoLastPushedSavedAt;
|
|
44
70
|
if (!lastPush) {
|
|
@@ -80,6 +106,16 @@ export function SessionListPanel({
|
|
|
80
106
|
onPushSessionToMongo,
|
|
81
107
|
pushingSessionId = null,
|
|
82
108
|
sessionDurationAlertThresholdMinutes,
|
|
109
|
+
onOpenSessionGantt,
|
|
110
|
+
sessionRowExitAnimateId = null,
|
|
111
|
+
onSessionRowExitAnimationDone,
|
|
112
|
+
/** Pendant la sortie « fin de session live », conserve le badge live sur l’instantané. */
|
|
113
|
+
liveChromeExitSessionId = null,
|
|
114
|
+
/** Pendant la sortie « fin de session », garde la ligne en tête (le tri global ne la ramène pas plus bas). */
|
|
115
|
+
sortPinSessionId = null,
|
|
116
|
+
/** Détails session (carte latérale) rendus sous la ligne plutôt qu’au-dessus de la liste. */
|
|
117
|
+
sessionDetailInline,
|
|
118
|
+
forcePageScroll = false,
|
|
83
119
|
}: {
|
|
84
120
|
sessions: SessionListEntry[];
|
|
85
121
|
lang: Lang;
|
|
@@ -106,13 +142,113 @@ export function SessionListPanel({
|
|
|
106
142
|
pushingSessionId?: string | null;
|
|
107
143
|
/** Minutes murales : durée affichée en alerte si ≥ seuil (même réglage que le panneau session). */
|
|
108
144
|
sessionDurationAlertThresholdMinutes?: number;
|
|
145
|
+
/** Ouvre la vue Gantt pour la session de cette ligne (historique, archives ou live). */
|
|
146
|
+
onOpenSessionGantt?: (sessionId: string) => void;
|
|
147
|
+
/** Ligne en cours d’animation de disparition (archivage ou fin de session live). */
|
|
148
|
+
sessionRowExitAnimateId?: string | null;
|
|
149
|
+
/** Appelé une fois l’animation terminée (libère l’instantané côté parent). */
|
|
150
|
+
onSessionRowExitAnimationDone?: () => void;
|
|
151
|
+
liveChromeExitSessionId?: string | null;
|
|
152
|
+
sortPinSessionId?: string | null;
|
|
153
|
+
sessionDetailInline?: {
|
|
154
|
+
sessionId: string;
|
|
155
|
+
content: ReactNode;
|
|
156
|
+
} | null;
|
|
157
|
+
/** Quand vrai, laisse la page scroller (pas de scroll interne forcé). */
|
|
158
|
+
forcePageScroll?: boolean;
|
|
109
159
|
}) {
|
|
110
|
-
const
|
|
160
|
+
const sessionRowExitDoneRef = useRef<(() => void) | undefined>(undefined);
|
|
161
|
+
useLayoutEffect(() => {
|
|
162
|
+
sessionRowExitDoneRef.current = onSessionRowExitAnimationDone;
|
|
163
|
+
}, [onSessionRowExitAnimationDone]);
|
|
164
|
+
|
|
165
|
+
/** Double frame pour garantir une transition depuis l’état initial. */
|
|
166
|
+
const [exitStyleSessionId, setExitStyleSessionId] = useState<string | null>(
|
|
167
|
+
null,
|
|
168
|
+
);
|
|
169
|
+
const [detailVisibleSessionId, setDetailVisibleSessionId] = useState<
|
|
170
|
+
string | null
|
|
171
|
+
>(null);
|
|
172
|
+
const [detailEnterSessionId, setDetailEnterSessionId] = useState<
|
|
173
|
+
string | null
|
|
174
|
+
>(null);
|
|
175
|
+
const [listNowMs, setListNowMs] = useState(() => Date.now());
|
|
176
|
+
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (!sessionRowExitAnimateId) {
|
|
179
|
+
setExitStyleSessionId(null);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let raf1 = 0;
|
|
184
|
+
let raf2 = 0;
|
|
185
|
+
raf1 = requestAnimationFrame(() => {
|
|
186
|
+
raf2 = requestAnimationFrame(() => {
|
|
187
|
+
setExitStyleSessionId(sessionRowExitAnimateId);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const exitMs = 320;
|
|
192
|
+
const doneTimer = globalThis.setTimeout(() => {
|
|
193
|
+
sessionRowExitDoneRef.current?.();
|
|
194
|
+
setExitStyleSessionId(null);
|
|
195
|
+
}, exitMs);
|
|
196
|
+
|
|
197
|
+
return () => {
|
|
198
|
+
cancelAnimationFrame(raf1);
|
|
199
|
+
cancelAnimationFrame(raf2);
|
|
200
|
+
clearTimeout(doneTimer);
|
|
201
|
+
};
|
|
202
|
+
}, [sessionRowExitAnimateId]);
|
|
203
|
+
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
const id = globalThis.setInterval(() => setListNowMs(Date.now()), 1000);
|
|
206
|
+
return () => globalThis.clearInterval(id);
|
|
207
|
+
}, []);
|
|
208
|
+
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
const targetId =
|
|
211
|
+
typeof sessionDetailInline?.sessionId === "string" &&
|
|
212
|
+
sessionDetailInline.sessionId.trim() !== ""
|
|
213
|
+
? sessionDetailInline.sessionId
|
|
214
|
+
: null;
|
|
215
|
+
if (!targetId) {
|
|
216
|
+
setDetailVisibleSessionId(null);
|
|
217
|
+
setDetailEnterSessionId(null);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
setDetailVisibleSessionId(targetId);
|
|
221
|
+
setDetailEnterSessionId(null);
|
|
222
|
+
let raf1 = 0;
|
|
223
|
+
let raf2 = 0;
|
|
224
|
+
raf1 = requestAnimationFrame(() => {
|
|
225
|
+
raf2 = requestAnimationFrame(() => setDetailEnterSessionId(targetId));
|
|
226
|
+
});
|
|
227
|
+
return () => {
|
|
228
|
+
cancelAnimationFrame(raf1);
|
|
229
|
+
cancelAnimationFrame(raf2);
|
|
230
|
+
};
|
|
231
|
+
}, [sessionDetailInline?.sessionId]);
|
|
232
|
+
|
|
233
|
+
const sorted = useMemo(() => {
|
|
234
|
+
const pin = sortPinSessionId?.trim();
|
|
235
|
+
if (!pin) {
|
|
236
|
+
return sortSessions(sessions);
|
|
237
|
+
}
|
|
238
|
+
const pinned = sessions.find((s) => s.sessionId === pin);
|
|
239
|
+
const rest = sessions.filter((s) => s.sessionId !== pin);
|
|
240
|
+
if (!pinned) {
|
|
241
|
+
return sortSessions(sessions);
|
|
242
|
+
}
|
|
243
|
+
return [pinned, ...sortSessions(rest)];
|
|
244
|
+
}, [sessions, sortPinSessionId]);
|
|
111
245
|
const archivesLabel =
|
|
112
|
-
archivedCount > 0
|
|
246
|
+
archivedCount > 0
|
|
247
|
+
? `${t.archivesModalTitle} (${archivedCount})`
|
|
248
|
+
: t.archivesModalTitle;
|
|
113
249
|
|
|
114
250
|
return (
|
|
115
|
-
<aside className="flex h-min min-w-0 max-w-full
|
|
251
|
+
<aside className="flex h-full min-h-0 min-w-0 max-w-full flex-1 flex-col rounded-xl border border-zinc-200 bg-white/90 shadow-sm dark:border-zinc-800 dark:bg-zinc-800/50 dark:shadow-none xl:z-0">
|
|
116
252
|
<div className="flex flex-wrap items-center justify-end gap-3 border-b border-zinc-200 px-5 py-3 dark:border-zinc-800">
|
|
117
253
|
{onOpenArchives ? (
|
|
118
254
|
<button
|
|
@@ -122,7 +258,12 @@ export function SessionListPanel({
|
|
|
122
258
|
aria-label={archivesLabel}
|
|
123
259
|
title={archivesLabel}
|
|
124
260
|
>
|
|
125
|
-
<Archive
|
|
261
|
+
<Archive
|
|
262
|
+
size={18}
|
|
263
|
+
strokeWidth={1.75}
|
|
264
|
+
className="text-zinc-600 dark:text-zinc-300"
|
|
265
|
+
aria-hidden
|
|
266
|
+
/>
|
|
126
267
|
{archivedCount > 0 ? (
|
|
127
268
|
<span className="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-violet-600 px-0.5 text-[0.6rem] font-semibold leading-none text-white">
|
|
128
269
|
{archivedCount > 99 ? "99+" : archivedCount}
|
|
@@ -134,29 +275,73 @@ export function SessionListPanel({
|
|
|
134
275
|
)}
|
|
135
276
|
</div>
|
|
136
277
|
|
|
137
|
-
<nav
|
|
278
|
+
<nav
|
|
279
|
+
className={`min-h-0 flex-1 px-3 py-3 pr-4 ${
|
|
280
|
+
forcePageScroll
|
|
281
|
+
? "overflow-visible"
|
|
282
|
+
: "overflow-y-auto overscroll-contain"
|
|
283
|
+
}`}
|
|
284
|
+
aria-label={t.sessionsListAriaLabel}
|
|
285
|
+
>
|
|
138
286
|
<ul className="space-y-1.5">
|
|
139
287
|
{sorted.map((sess) => {
|
|
140
288
|
const id = sess.sessionId;
|
|
141
|
-
const
|
|
289
|
+
const isLiveRow =
|
|
290
|
+
typeof liveSessionId === "string" && id === liveSessionId;
|
|
291
|
+
const showLiveChrome =
|
|
292
|
+
isLiveRow ||
|
|
293
|
+
(typeof liveChromeExitSessionId === "string" &&
|
|
294
|
+
liveChromeExitSessionId === id);
|
|
142
295
|
const isSelected = id === selectedSessionId;
|
|
143
296
|
const n = taskCount(sess);
|
|
297
|
+
const hasAssociatedTasks = n > 0;
|
|
144
298
|
const tz =
|
|
145
|
-
displayTimeZone.trim() &&
|
|
299
|
+
displayTimeZone.trim() &&
|
|
300
|
+
isValidIanaTimeZone(displayTimeZone.trim())
|
|
146
301
|
? displayTimeZone.trim()
|
|
147
302
|
: DEFAULT_DASHBOARD_TIME_ZONE;
|
|
148
|
-
const createdIso = (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
303
|
+
const createdIso = (
|
|
304
|
+
sess.createdAt?.trim() ||
|
|
305
|
+
sess.startAt?.trim() ||
|
|
306
|
+
sess.savedAt?.trim() ||
|
|
307
|
+
""
|
|
308
|
+
).trim();
|
|
309
|
+
const createdLabel = formatIsoInstantShort(
|
|
310
|
+
createdIso,
|
|
311
|
+
lang,
|
|
312
|
+
tz,
|
|
313
|
+
use24HourClock,
|
|
314
|
+
);
|
|
315
|
+
const label =
|
|
316
|
+
sess.sessionName?.trim() ||
|
|
317
|
+
(createdLabel
|
|
318
|
+
? `${id.slice(0, 8)} · ${createdLabel}`
|
|
319
|
+
: id.slice(0, 8));
|
|
320
|
+
const startIso = (
|
|
321
|
+
sess.startAt?.trim() ||
|
|
322
|
+
sess.savedAt?.trim() ||
|
|
323
|
+
""
|
|
324
|
+
).trim();
|
|
325
|
+
const startLabel =
|
|
326
|
+
formatIsoInstantShort(startIso, lang, tz, use24HourClock) ?? "—";
|
|
153
327
|
const hasEnd =
|
|
154
328
|
typeof sess.endAt === "string" && sess.endAt.trim() !== "";
|
|
155
329
|
const endLabel = hasEnd
|
|
156
|
-
? formatIsoInstantShort(
|
|
330
|
+
? formatIsoInstantShort(
|
|
331
|
+
sess.endAt!.trim(),
|
|
332
|
+
lang,
|
|
333
|
+
tz,
|
|
334
|
+
use24HourClock,
|
|
335
|
+
) ?? "—"
|
|
157
336
|
: null;
|
|
158
337
|
|
|
159
338
|
const taskNoun = sessionTaskCountNoun(n, t, lang);
|
|
339
|
+
const noteRaw =
|
|
340
|
+
typeof sess.sessionNote === "string"
|
|
341
|
+
? sess.sessionNote.trim()
|
|
342
|
+
: "";
|
|
343
|
+
const notePreview = noteRaw.replaceAll(/\s+/g, " ");
|
|
344
|
+
const hasSessionNote = notePreview.length > 0;
|
|
160
345
|
const wallMins = sessionWallClockMinutes(sess as LooseSession);
|
|
161
346
|
const durationLabel = wallMins > 0 ? formatDuration(wallMins) : "—";
|
|
162
347
|
const thresholdMin =
|
|
@@ -166,31 +351,62 @@ export function SessionListPanel({
|
|
|
166
351
|
: null;
|
|
167
352
|
const durationAlert =
|
|
168
353
|
thresholdMin !== null && wallMins > 0 && wallMins >= thresholdMin;
|
|
169
|
-
const thresholdHours =
|
|
354
|
+
const thresholdHours =
|
|
355
|
+
thresholdMin !== null ? Math.round(thresholdMin / 60) : 0;
|
|
170
356
|
const durationTitle = durationAlert
|
|
171
|
-
? t.sessionListWallDurationAlertTooltip.replace(
|
|
357
|
+
? t.sessionListWallDurationAlertTooltip.replace(
|
|
358
|
+
"{hours}",
|
|
359
|
+
String(thresholdHours),
|
|
360
|
+
)
|
|
172
361
|
: t.sessionListWallDurationTitle;
|
|
173
362
|
const mongoState = sessionMongoPushState(sess);
|
|
174
363
|
const mongoTitle =
|
|
175
364
|
mongoState === "synced"
|
|
176
365
|
? t.sessionMongoPushSyncedTitle
|
|
177
366
|
: mongoState === "dirty"
|
|
178
|
-
|
|
179
|
-
|
|
367
|
+
? t.sessionMongoPushDirtyTitle
|
|
368
|
+
: t.sessionMongoPushNeverTitle;
|
|
180
369
|
const mongoIconClass =
|
|
181
370
|
mongoState === "synced"
|
|
182
371
|
? "text-emerald-400/95"
|
|
183
372
|
: mongoState === "dirty"
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
373
|
+
? "text-amber-400/95"
|
|
374
|
+
: "text-zinc-500";
|
|
375
|
+
const livePlanned =
|
|
376
|
+
showLiveChrome &&
|
|
377
|
+
isOpenSessionDisplayPlanned(
|
|
378
|
+
{ startAt: sess.startAt, endAt: sess.endAt },
|
|
379
|
+
listNowMs,
|
|
380
|
+
);
|
|
381
|
+
const rowHasExpandedDetail =
|
|
382
|
+
detailVisibleSessionId === id &&
|
|
383
|
+
sessionDetailInline?.sessionId === id;
|
|
384
|
+
const isExitAnimatingRow = exitStyleSessionId === id;
|
|
187
385
|
return (
|
|
188
|
-
<li
|
|
386
|
+
<li
|
|
387
|
+
key={id}
|
|
388
|
+
id={`kronosys-session-${id}`}
|
|
389
|
+
className={`scroll-mt-20 overflow-hidden transition-[max-height,margin-top] duration-300 ease-out motion-reduce:transition-none ${
|
|
390
|
+
isExitAnimatingRow
|
|
391
|
+
? "pointer-events-none max-h-0 -mt-1.5"
|
|
392
|
+
: rowHasExpandedDetail
|
|
393
|
+
? "max-h-[160rem]"
|
|
394
|
+
: "max-h-[28rem]"
|
|
395
|
+
}`}
|
|
396
|
+
>
|
|
189
397
|
<div
|
|
190
|
-
className={`grid grid-cols-[minmax(0,1fr)_auto] grid-rows-[auto_auto] gap-x-1.5 gap-y-1 rounded-lg px-3.5 py-3 transition-
|
|
398
|
+
className={`grid grid-cols-[minmax(0,1fr)_auto] grid-rows-[auto_auto] gap-x-1.5 gap-y-1 rounded-lg px-3.5 py-3 transition-[opacity,transform,box-shadow,background-color] duration-300 ease-out motion-reduce:transition-none ${
|
|
399
|
+
isExitAnimatingRow
|
|
400
|
+
? "pointer-events-none opacity-0 translate-x-2 scale-[0.98]"
|
|
401
|
+
: ""
|
|
402
|
+
} ${
|
|
191
403
|
isSelected
|
|
192
404
|
? "bg-violet-500/15 ring-1 ring-violet-500/45 dark:bg-violet-600/20 dark:ring-violet-500/50"
|
|
193
|
-
:
|
|
405
|
+
: livePlanned
|
|
406
|
+
? "bg-sky-50/80 hover:bg-sky-100/85 dark:bg-sky-950/25 dark:hover:bg-sky-950/40"
|
|
407
|
+
: hasAssociatedTasks
|
|
408
|
+
? "bg-emerald-50/75 hover:bg-emerald-100/80 dark:bg-emerald-950/20 dark:hover:bg-emerald-950/35"
|
|
409
|
+
: "bg-zinc-100/65 hover:bg-zinc-100/90 dark:bg-zinc-900/30 dark:hover:bg-zinc-800/80"
|
|
194
410
|
}`}
|
|
195
411
|
>
|
|
196
412
|
<button
|
|
@@ -199,28 +415,36 @@ export function SessionListPanel({
|
|
|
199
415
|
className="group col-start-1 row-start-1 row-span-2 flex min-w-0 flex-col gap-0.5 text-left outline-none focus-visible:ring-2 focus-visible:ring-violet-500/60 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-zinc-900 rounded-md -mx-1 px-1 -my-0.5 py-0.5"
|
|
200
416
|
>
|
|
201
417
|
<span className="flex min-w-0 items-center gap-2 text-sm font-medium text-zinc-800 group-hover:text-zinc-950 dark:text-zinc-100 dark:group-hover:text-white">
|
|
202
|
-
{
|
|
418
|
+
{showLiveChrome ? (
|
|
203
419
|
<Circle
|
|
204
|
-
className=
|
|
420
|
+
className={
|
|
421
|
+
livePlanned
|
|
422
|
+
? "shrink-0 fill-sky-500 text-sky-500"
|
|
423
|
+
: "shrink-0 fill-emerald-400 text-emerald-400"
|
|
424
|
+
}
|
|
205
425
|
size={10}
|
|
206
426
|
aria-hidden
|
|
207
427
|
/>
|
|
208
|
-
)}
|
|
428
|
+
) : null}
|
|
209
429
|
<span className="truncate">{label}</span>
|
|
210
|
-
{
|
|
430
|
+
{showLiveChrome && !livePlanned ? (
|
|
211
431
|
<span className="shrink-0 rounded bg-emerald-100 px-1.5 py-0 text-[0.6rem] font-bold uppercase tracking-wide text-emerald-900 ring-1 ring-emerald-600/20 dark:bg-emerald-900/50 dark:text-emerald-300 dark:ring-0">
|
|
212
432
|
{t.sessionLiveBadge}
|
|
213
433
|
</span>
|
|
214
|
-
)}
|
|
434
|
+
) : null}
|
|
215
435
|
</span>
|
|
216
436
|
<span className="flex min-w-0 flex-col gap-0.5 text-[0.7rem] leading-snug text-zinc-500 group-hover:text-zinc-600 dark:group-hover:text-zinc-400">
|
|
217
437
|
<span className="min-w-0 break-words">
|
|
218
|
-
<span className="text-zinc-600 dark:text-zinc-500">
|
|
438
|
+
<span className="text-zinc-600 dark:text-zinc-500">
|
|
439
|
+
{t.sessionListStartedPrefix}
|
|
440
|
+
</span>{" "}
|
|
219
441
|
{startLabel}
|
|
220
442
|
</span>
|
|
221
443
|
{endLabel !== null ? (
|
|
222
444
|
<span className="min-w-0 break-words">
|
|
223
|
-
<span className="text-zinc-600 dark:text-zinc-500">
|
|
445
|
+
<span className="text-zinc-600 dark:text-zinc-500">
|
|
446
|
+
{t.sessionListEndedPrefix}
|
|
447
|
+
</span>{" "}
|
|
224
448
|
{endLabel}
|
|
225
449
|
</span>
|
|
226
450
|
) : null}
|
|
@@ -228,16 +452,34 @@ export function SessionListPanel({
|
|
|
228
452
|
<span>
|
|
229
453
|
{n} {taskNoun}
|
|
230
454
|
</span>
|
|
231
|
-
<span className="text-zinc-400 dark:text-zinc-600">
|
|
455
|
+
<span className="text-zinc-400 dark:text-zinc-600">
|
|
456
|
+
{" "}
|
|
457
|
+
·{" "}
|
|
458
|
+
</span>
|
|
232
459
|
<span
|
|
233
460
|
className={`inline tabular-nums ${
|
|
234
|
-
durationAlert
|
|
461
|
+
durationAlert
|
|
462
|
+
? "kronosys-session-duration-alert font-semibold"
|
|
463
|
+
: ""
|
|
235
464
|
}`}
|
|
236
465
|
title={durationTitle}
|
|
237
466
|
>
|
|
238
467
|
{durationLabel}
|
|
239
468
|
</span>
|
|
240
469
|
</span>
|
|
470
|
+
{hasSessionNote ? (
|
|
471
|
+
<span
|
|
472
|
+
className="mt-0.5 inline-flex min-w-0 items-center gap-1 text-zinc-500 dark:text-zinc-400"
|
|
473
|
+
title={notePreview}
|
|
474
|
+
>
|
|
475
|
+
<FileText
|
|
476
|
+
size={12}
|
|
477
|
+
className="shrink-0"
|
|
478
|
+
aria-hidden
|
|
479
|
+
/>
|
|
480
|
+
<span className="truncate">{notePreview}</span>
|
|
481
|
+
</span>
|
|
482
|
+
) : null}
|
|
241
483
|
</span>
|
|
242
484
|
</button>
|
|
243
485
|
<div className="col-start-2 row-start-2 flex shrink-0 items-center gap-0.5 self-center">
|
|
@@ -260,11 +502,14 @@ export function SessionListPanel({
|
|
|
260
502
|
aria-label={t.sessionMongoPushBusy}
|
|
261
503
|
/>
|
|
262
504
|
) : (
|
|
263
|
-
<UploadCloud
|
|
505
|
+
<UploadCloud
|
|
506
|
+
size={sessionRowActionIconSize}
|
|
507
|
+
aria-hidden
|
|
508
|
+
/>
|
|
264
509
|
)}
|
|
265
510
|
</button>
|
|
266
511
|
) : null}
|
|
267
|
-
{
|
|
512
|
+
{isLiveRow && onEndLiveSession ? (
|
|
268
513
|
<button
|
|
269
514
|
type="button"
|
|
270
515
|
className={`${sessionRowActionBase} text-zinc-500 hover:text-rose-600 dark:hover:text-rose-300`}
|
|
@@ -275,6 +520,23 @@ export function SessionListPanel({
|
|
|
275
520
|
<Square size={sessionRowActionIconSize} aria-hidden />
|
|
276
521
|
</button>
|
|
277
522
|
) : null}
|
|
523
|
+
{onOpenSessionGantt ? (
|
|
524
|
+
<button
|
|
525
|
+
type="button"
|
|
526
|
+
className={`${sessionRowActionBase} text-zinc-500 hover:text-violet-700 dark:hover:text-violet-300`}
|
|
527
|
+
title={t.tasksTimelineGanttOpenBtn}
|
|
528
|
+
aria-label={t.sessionListOpenGanttAria}
|
|
529
|
+
onClick={(e) => {
|
|
530
|
+
e.stopPropagation();
|
|
531
|
+
onOpenSessionGantt(id);
|
|
532
|
+
}}
|
|
533
|
+
>
|
|
534
|
+
<LayoutGrid
|
|
535
|
+
size={sessionRowActionIconSize}
|
|
536
|
+
aria-hidden
|
|
537
|
+
/>
|
|
538
|
+
</button>
|
|
539
|
+
) : null}
|
|
278
540
|
{onOpenSessionInNewTab ? (
|
|
279
541
|
<button
|
|
280
542
|
type="button"
|
|
@@ -283,10 +545,15 @@ export function SessionListPanel({
|
|
|
283
545
|
aria-label={t.openSessionInNewTab}
|
|
284
546
|
onClick={() => onOpenSessionInNewTab(id)}
|
|
285
547
|
>
|
|
286
|
-
<ExternalLink
|
|
548
|
+
<ExternalLink
|
|
549
|
+
size={sessionRowActionIconSize}
|
|
550
|
+
aria-hidden
|
|
551
|
+
/>
|
|
287
552
|
</button>
|
|
288
553
|
) : null}
|
|
289
|
-
{onArchiveSession &&
|
|
554
|
+
{onArchiveSession &&
|
|
555
|
+
!isLiveRow &&
|
|
556
|
+
liveChromeExitSessionId !== id ? (
|
|
290
557
|
<button
|
|
291
558
|
type="button"
|
|
292
559
|
className={`${sessionRowActionBase} text-zinc-500 hover:text-amber-700 dark:hover:text-amber-200/90`}
|
|
@@ -297,7 +564,9 @@ export function SessionListPanel({
|
|
|
297
564
|
<Archive size={sessionRowActionIconSize} aria-hidden />
|
|
298
565
|
</button>
|
|
299
566
|
) : null}
|
|
300
|
-
{onDeleteSession &&
|
|
567
|
+
{onDeleteSession &&
|
|
568
|
+
!isLiveRow &&
|
|
569
|
+
liveChromeExitSessionId !== id ? (
|
|
301
570
|
<button
|
|
302
571
|
type="button"
|
|
303
572
|
className={`${sessionRowActionBase} text-zinc-500 hover:text-red-600 dark:hover:text-red-300`}
|
|
@@ -310,6 +579,20 @@ export function SessionListPanel({
|
|
|
310
579
|
) : null}
|
|
311
580
|
</div>
|
|
312
581
|
</div>
|
|
582
|
+
{detailVisibleSessionId === id &&
|
|
583
|
+
sessionDetailInline?.sessionId === id ? (
|
|
584
|
+
<section
|
|
585
|
+
id={`kronosys-session-${id}-metrics`}
|
|
586
|
+
className={`overflow-hidden border-t transition-[max-height,opacity,margin-top,padding-top,border-color] duration-300 ease-out motion-reduce:transition-none ${
|
|
587
|
+
detailEnterSessionId === id
|
|
588
|
+
? "mt-2 max-h-[80rem] border-zinc-200/90 pt-3 pr-1 opacity-100 dark:border-zinc-700/70"
|
|
589
|
+
: "mt-0 max-h-0 border-transparent pt-0 opacity-0"
|
|
590
|
+
}`}
|
|
591
|
+
aria-label={t.selectedSessionSidebarTitle}
|
|
592
|
+
>
|
|
593
|
+
{sessionDetailInline.content}
|
|
594
|
+
</section>
|
|
595
|
+
) : null}
|
|
313
596
|
</li>
|
|
314
597
|
);
|
|
315
598
|
})}
|