@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,8 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useId, useState, type ReactNode } from "react";
|
|
4
|
+
import { X } from "lucide-react";
|
|
4
5
|
import { tbModalDanger, tbModalPrimary } from "@/lib/translucentButtonClasses";
|
|
5
6
|
|
|
7
|
+
const MODAL_FADE_MS = 150;
|
|
8
|
+
|
|
6
9
|
function useEscapeClose(open: boolean, onClose: () => void) {
|
|
7
10
|
useEffect(() => {
|
|
8
11
|
if (!open) {
|
|
@@ -18,6 +21,27 @@ function useEscapeClose(open: boolean, onClose: () => void) {
|
|
|
18
21
|
}, [open, onClose]);
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
function useModalPresence(open: boolean) {
|
|
25
|
+
const [shouldRender, setShouldRender] = useState(open);
|
|
26
|
+
const [visible, setVisible] = useState(open);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (open) {
|
|
30
|
+
setShouldRender(true);
|
|
31
|
+
const frame = globalThis.requestAnimationFrame(() => setVisible(true));
|
|
32
|
+
return () => globalThis.cancelAnimationFrame(frame);
|
|
33
|
+
}
|
|
34
|
+
setVisible(false);
|
|
35
|
+
const timeout = globalThis.setTimeout(
|
|
36
|
+
() => setShouldRender(false),
|
|
37
|
+
MODAL_FADE_MS,
|
|
38
|
+
);
|
|
39
|
+
return () => globalThis.clearTimeout(timeout);
|
|
40
|
+
}, [open]);
|
|
41
|
+
|
|
42
|
+
return { shouldRender, visible };
|
|
43
|
+
}
|
|
44
|
+
|
|
21
45
|
/** Message seul, bouton OK (remplace `alert()`). */
|
|
22
46
|
export function DashboardAlertModal({
|
|
23
47
|
open,
|
|
@@ -35,37 +59,47 @@ export function DashboardAlertModal({
|
|
|
35
59
|
const titleId = useId();
|
|
36
60
|
const messageId = useId();
|
|
37
61
|
useEscapeClose(open, onClose);
|
|
62
|
+
const { shouldRender, visible } = useModalPresence(open);
|
|
38
63
|
|
|
39
|
-
if (!
|
|
64
|
+
if (!shouldRender) {
|
|
40
65
|
return null;
|
|
41
66
|
}
|
|
42
67
|
|
|
43
68
|
return (
|
|
44
69
|
<div
|
|
45
|
-
className=
|
|
70
|
+
className={`fixed inset-0 z-[100] flex items-center justify-center bg-black/60 p-4 transition-opacity duration-150 ease-out motion-reduce:transition-none ${
|
|
71
|
+
visible ? "opacity-100" : "opacity-0"
|
|
72
|
+
}`}
|
|
46
73
|
role="alertdialog"
|
|
47
74
|
aria-modal="true"
|
|
48
75
|
aria-labelledby={title ? titleId : messageId}
|
|
49
76
|
aria-describedby={title ? messageId : undefined}
|
|
50
77
|
>
|
|
51
|
-
<div
|
|
78
|
+
<div
|
|
79
|
+
className={`max-h-[90vh] w-full max-w-md overflow-y-auto rounded-xl border border-zinc-200 bg-white p-5 shadow-xl transition-all duration-150 ease-out motion-reduce:transition-none dark:border-zinc-700 dark:bg-zinc-900 ${
|
|
80
|
+
visible
|
|
81
|
+
? "translate-y-0 scale-100 opacity-100"
|
|
82
|
+
: "translate-y-1 scale-[0.985] opacity-0"
|
|
83
|
+
}`}
|
|
84
|
+
>
|
|
52
85
|
{title ? (
|
|
53
|
-
<h2
|
|
86
|
+
<h2
|
|
87
|
+
id={titleId}
|
|
88
|
+
className="text-lg font-semibold text-zinc-900 dark:text-zinc-100"
|
|
89
|
+
>
|
|
54
90
|
{title}
|
|
55
91
|
</h2>
|
|
56
92
|
) : null}
|
|
57
93
|
<p
|
|
58
94
|
id={messageId}
|
|
59
|
-
className={`whitespace-pre-wrap text-sm leading-relaxed text-zinc-600 dark:text-zinc-300 ${
|
|
95
|
+
className={`whitespace-pre-wrap text-sm leading-relaxed text-zinc-600 dark:text-zinc-300 ${
|
|
96
|
+
title ? "mt-3" : ""
|
|
97
|
+
}`}
|
|
60
98
|
>
|
|
61
99
|
{message}
|
|
62
100
|
</p>
|
|
63
101
|
<div className="mt-6 flex justify-end">
|
|
64
|
-
<button
|
|
65
|
-
type="button"
|
|
66
|
-
className={tbModalPrimary}
|
|
67
|
-
onClick={onClose}
|
|
68
|
-
>
|
|
102
|
+
<button type="button" className={tbModalPrimary} onClick={onClose}>
|
|
69
103
|
{okLabel}
|
|
70
104
|
</button>
|
|
71
105
|
</div>
|
|
@@ -125,6 +159,7 @@ export function DashboardConfirmModal({
|
|
|
125
159
|
const messageId = useId();
|
|
126
160
|
const typeFieldId = useId();
|
|
127
161
|
const [typeDraft, setTypeDraft] = useState("");
|
|
162
|
+
const { shouldRender, visible } = useModalPresence(open);
|
|
128
163
|
useEscapeClose(open, () => {
|
|
129
164
|
if (pending) {
|
|
130
165
|
return;
|
|
@@ -138,30 +173,44 @@ export function DashboardConfirmModal({
|
|
|
138
173
|
}
|
|
139
174
|
}, [open]);
|
|
140
175
|
|
|
141
|
-
if (!
|
|
176
|
+
if (!shouldRender) {
|
|
142
177
|
return null;
|
|
143
178
|
}
|
|
144
179
|
|
|
145
180
|
const typedOk = !typeToConfirm || typeDraft === typeToConfirm.expected;
|
|
146
|
-
const confirmClass =
|
|
181
|
+
const confirmClass =
|
|
182
|
+
confirmVariant === "danger" ? tbModalDanger : tbModalPrimary;
|
|
147
183
|
|
|
148
184
|
return (
|
|
149
185
|
<div
|
|
150
|
-
className=
|
|
186
|
+
className={`fixed inset-0 z-[100] flex items-center justify-center bg-black/60 p-4 transition-opacity duration-150 ease-out motion-reduce:transition-none ${
|
|
187
|
+
visible ? "opacity-100" : "opacity-0"
|
|
188
|
+
}`}
|
|
151
189
|
role="dialog"
|
|
152
190
|
aria-modal="true"
|
|
153
191
|
aria-labelledby={title ? titleId : messageId}
|
|
154
192
|
aria-describedby={title ? messageId : undefined}
|
|
155
193
|
>
|
|
156
|
-
<div
|
|
194
|
+
<div
|
|
195
|
+
className={`max-h-[90vh] w-full max-w-md overflow-y-auto rounded-xl border border-zinc-200 bg-white p-5 shadow-xl transition-all duration-150 ease-out motion-reduce:transition-none dark:border-zinc-700 dark:bg-zinc-900 ${
|
|
196
|
+
visible
|
|
197
|
+
? "translate-y-0 scale-100 opacity-100"
|
|
198
|
+
: "translate-y-1 scale-[0.985] opacity-0"
|
|
199
|
+
}`}
|
|
200
|
+
>
|
|
157
201
|
{title ? (
|
|
158
|
-
<h2
|
|
202
|
+
<h2
|
|
203
|
+
id={titleId}
|
|
204
|
+
className="text-lg font-semibold text-zinc-900 dark:text-zinc-100"
|
|
205
|
+
>
|
|
159
206
|
{title}
|
|
160
207
|
</h2>
|
|
161
208
|
) : null}
|
|
162
209
|
<p
|
|
163
210
|
id={messageId}
|
|
164
|
-
className={`whitespace-pre-wrap text-sm leading-relaxed text-zinc-600 dark:text-zinc-300 ${
|
|
211
|
+
className={`whitespace-pre-wrap text-sm leading-relaxed text-zinc-600 dark:text-zinc-300 ${
|
|
212
|
+
title ? "mt-3" : ""
|
|
213
|
+
}`}
|
|
165
214
|
>
|
|
166
215
|
{message}
|
|
167
216
|
</p>
|
|
@@ -180,8 +229,13 @@ export function DashboardConfirmModal({
|
|
|
180
229
|
{extra ? <div className="mt-4">{extra}</div> : null}
|
|
181
230
|
{typeToConfirm ? (
|
|
182
231
|
<div className="mt-4 space-y-2">
|
|
183
|
-
<p className="text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">
|
|
184
|
-
|
|
232
|
+
<p className="text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">
|
|
233
|
+
{typeToConfirm.instruction}
|
|
234
|
+
</p>
|
|
235
|
+
<label
|
|
236
|
+
htmlFor={typeFieldId}
|
|
237
|
+
className="block text-xs font-semibold uppercase tracking-wide text-zinc-500"
|
|
238
|
+
>
|
|
185
239
|
{typeToConfirm.label}
|
|
186
240
|
</label>
|
|
187
241
|
<input
|
|
@@ -265,30 +319,45 @@ export function DashboardTriActionModal({
|
|
|
265
319
|
const titleId = useId();
|
|
266
320
|
const messageId = useId();
|
|
267
321
|
useEscapeClose(open, onDismiss);
|
|
322
|
+
const { shouldRender, visible } = useModalPresence(open);
|
|
268
323
|
|
|
269
|
-
if (!
|
|
324
|
+
if (!shouldRender) {
|
|
270
325
|
return null;
|
|
271
326
|
}
|
|
272
327
|
|
|
273
|
-
const primaryClass =
|
|
328
|
+
const primaryClass =
|
|
329
|
+
primaryVariant === "danger" ? tbModalDanger : tbModalPrimary;
|
|
274
330
|
|
|
275
331
|
return (
|
|
276
332
|
<div
|
|
277
|
-
className=
|
|
333
|
+
className={`fixed inset-0 z-[100] flex items-center justify-center bg-black/60 p-4 transition-opacity duration-150 ease-out motion-reduce:transition-none ${
|
|
334
|
+
visible ? "opacity-100" : "opacity-0"
|
|
335
|
+
}`}
|
|
278
336
|
role="dialog"
|
|
279
337
|
aria-modal="true"
|
|
280
338
|
aria-labelledby={title ? titleId : messageId}
|
|
281
339
|
aria-describedby={title ? messageId : undefined}
|
|
282
340
|
>
|
|
283
|
-
<div
|
|
341
|
+
<div
|
|
342
|
+
className={`max-h-[90vh] w-full max-w-md overflow-y-auto rounded-xl border border-zinc-200 bg-white p-5 shadow-xl transition-all duration-150 ease-out motion-reduce:transition-none dark:border-zinc-700 dark:bg-zinc-900 ${
|
|
343
|
+
visible
|
|
344
|
+
? "translate-y-0 scale-100 opacity-100"
|
|
345
|
+
: "translate-y-1 scale-[0.985] opacity-0"
|
|
346
|
+
}`}
|
|
347
|
+
>
|
|
284
348
|
{title ? (
|
|
285
|
-
<h2
|
|
349
|
+
<h2
|
|
350
|
+
id={titleId}
|
|
351
|
+
className="text-lg font-semibold text-zinc-900 dark:text-zinc-100"
|
|
352
|
+
>
|
|
286
353
|
{title}
|
|
287
354
|
</h2>
|
|
288
355
|
) : null}
|
|
289
356
|
<p
|
|
290
357
|
id={messageId}
|
|
291
|
-
className={`whitespace-pre-wrap text-sm leading-relaxed text-zinc-600 dark:text-zinc-300 ${
|
|
358
|
+
className={`whitespace-pre-wrap text-sm leading-relaxed text-zinc-600 dark:text-zinc-300 ${
|
|
359
|
+
title ? "mt-3" : ""
|
|
360
|
+
}`}
|
|
292
361
|
>
|
|
293
362
|
{message}
|
|
294
363
|
</p>
|
|
@@ -327,7 +396,11 @@ export function DashboardTriActionModal({
|
|
|
327
396
|
>
|
|
328
397
|
{secondaryLabel}
|
|
329
398
|
</button>
|
|
330
|
-
<button
|
|
399
|
+
<button
|
|
400
|
+
type="button"
|
|
401
|
+
className={primaryClass}
|
|
402
|
+
onClick={() => void onPrimary()}
|
|
403
|
+
>
|
|
331
404
|
{primaryLabel}
|
|
332
405
|
</button>
|
|
333
406
|
</div>
|
|
@@ -335,3 +408,73 @@ export function DashboardTriActionModal({
|
|
|
335
408
|
</div>
|
|
336
409
|
);
|
|
337
410
|
}
|
|
411
|
+
|
|
412
|
+
/** Contenu libre scrollable (formulaire, aide longue, carte session) — fermeture ×, Escape, clic fond. */
|
|
413
|
+
export function DashboardPanelModal({
|
|
414
|
+
open,
|
|
415
|
+
title,
|
|
416
|
+
onClose,
|
|
417
|
+
closeAriaLabel,
|
|
418
|
+
children,
|
|
419
|
+
}: {
|
|
420
|
+
open: boolean;
|
|
421
|
+
title: string;
|
|
422
|
+
onClose: () => void;
|
|
423
|
+
closeAriaLabel: string;
|
|
424
|
+
children: ReactNode;
|
|
425
|
+
}) {
|
|
426
|
+
const titleId = useId();
|
|
427
|
+
useEscapeClose(open, onClose);
|
|
428
|
+
const { shouldRender, visible } = useModalPresence(open);
|
|
429
|
+
|
|
430
|
+
if (!shouldRender) {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return (
|
|
435
|
+
<div
|
|
436
|
+
className={`fixed inset-0 z-[98] overflow-y-auto overscroll-y-contain bg-black/55 px-3 pt-[max(5.25rem,env(safe-area-inset-top,0px)+3.25rem)] pb-6 transition-opacity duration-150 ease-out motion-reduce:transition-none sm:px-4 sm:pb-8 sm:pt-28 ${
|
|
437
|
+
visible ? "opacity-100" : "opacity-0"
|
|
438
|
+
}`}
|
|
439
|
+
role="dialog"
|
|
440
|
+
aria-modal="true"
|
|
441
|
+
aria-labelledby={titleId}
|
|
442
|
+
onClick={(e) => {
|
|
443
|
+
if (e.target === e.currentTarget) {
|
|
444
|
+
onClose();
|
|
445
|
+
}
|
|
446
|
+
}}
|
|
447
|
+
>
|
|
448
|
+
<div className="mx-auto w-full max-w-6xl pb-1">
|
|
449
|
+
<div
|
|
450
|
+
className={`flex max-h-[min(42dvh,22rem)] w-full flex-col overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-xl transition-all duration-150 ease-out motion-reduce:transition-none dark:border-zinc-700 dark:bg-zinc-900 sm:max-h-[min(46dvh,26rem)] md:max-h-[min(50dvh,28rem)] ${
|
|
451
|
+
visible
|
|
452
|
+
? "translate-y-0 scale-100 opacity-100"
|
|
453
|
+
: "translate-y-1 scale-[0.99] opacity-0"
|
|
454
|
+
}`}
|
|
455
|
+
onClick={(e) => e.stopPropagation()}
|
|
456
|
+
>
|
|
457
|
+
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-zinc-200 px-3 py-2.5 dark:border-zinc-700 sm:px-4 sm:py-3">
|
|
458
|
+
<h2
|
|
459
|
+
id={titleId}
|
|
460
|
+
className="min-w-0 text-sm font-semibold tracking-tight text-zinc-900 sm:text-base dark:text-zinc-100"
|
|
461
|
+
>
|
|
462
|
+
{title}
|
|
463
|
+
</h2>
|
|
464
|
+
<button
|
|
465
|
+
type="button"
|
|
466
|
+
className="inline-flex size-9 shrink-0 items-center justify-center rounded-lg border border-transparent text-zinc-500 transition hover:border-zinc-300 hover:bg-zinc-100 hover:text-zinc-800 dark:hover:border-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
|
|
467
|
+
aria-label={closeAriaLabel}
|
|
468
|
+
onClick={onClose}
|
|
469
|
+
>
|
|
470
|
+
<X size={20} strokeWidth={2.25} aria-hidden />
|
|
471
|
+
</button>
|
|
472
|
+
</div>
|
|
473
|
+
<div className="min-h-0 flex-1 overflow-y-auto px-3 py-3 sm:px-4 sm:py-4">
|
|
474
|
+
{children}
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
);
|
|
480
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type CSSProperties,
|
|
11
11
|
} from "react";
|
|
12
12
|
import { createPortal } from "react-dom";
|
|
13
|
+
import { postKronosysAction } from "@/lib/kronosysApi";
|
|
13
14
|
import { markDashboardTourCompleted } from "@/lib/dashboardTourStorage";
|
|
14
15
|
import type { DashboardStrings } from "@/lib/dashboardCopy";
|
|
15
16
|
import { tbVioletTextSm } from "@/lib/translucentButtonClasses";
|
|
@@ -26,10 +27,12 @@ const BASE_STEP_SELECTORS = [
|
|
|
26
27
|
] as const;
|
|
27
28
|
|
|
28
29
|
/** Même id que le conteneur du minuteur KronoFocus dans l’en-tête (si affiché). */
|
|
29
|
-
const KRONO_FOCUS_HEADER_TOUR_SELECTOR =
|
|
30
|
+
const KRONO_FOCUS_HEADER_TOUR_SELECTOR =
|
|
31
|
+
"#dashboard-tour-anchor-kronoFocus-header";
|
|
30
32
|
|
|
31
33
|
/** Même id que le conteneur de la bannière identité Git sur la page tableau de bord. */
|
|
32
|
-
const GIT_IDENTITY_BANNER_TOUR_SELECTOR =
|
|
34
|
+
const GIT_IDENTITY_BANNER_TOUR_SELECTOR =
|
|
35
|
+
"#dashboard-tour-anchor-git-identity-banner";
|
|
33
36
|
|
|
34
37
|
const HOLE_PADDING_PX = 10;
|
|
35
38
|
const TOOLTIP_MAX_W = 360;
|
|
@@ -42,7 +45,7 @@ type TipPlacement = "below" | "above" | "right" | "left";
|
|
|
42
45
|
|
|
43
46
|
function tooltipIntersectsHole(
|
|
44
47
|
tip: { top: number; left: number; width: number; height: number },
|
|
45
|
-
hole: HoleRect
|
|
48
|
+
hole: HoleRect,
|
|
46
49
|
): boolean {
|
|
47
50
|
return !(
|
|
48
51
|
tip.left + tip.width <= hole.left ||
|
|
@@ -58,7 +61,7 @@ function clampTipPosition(
|
|
|
58
61
|
ph: number,
|
|
59
62
|
wTip: number,
|
|
60
63
|
vw: number,
|
|
61
|
-
vh: number
|
|
64
|
+
vh: number,
|
|
62
65
|
): { top: number; left: number } {
|
|
63
66
|
return {
|
|
64
67
|
top: Math.max(VIEW_MARGIN, Math.min(top, vh - ph - VIEW_MARGIN)),
|
|
@@ -73,7 +76,7 @@ function computeTooltipPosition(
|
|
|
73
76
|
vh: number,
|
|
74
77
|
wTip: number,
|
|
75
78
|
ph: number,
|
|
76
|
-
placementPriority: readonly TipPlacement[]
|
|
79
|
+
placementPriority: readonly TipPlacement[],
|
|
77
80
|
): { top: number; left: number } {
|
|
78
81
|
const g = TOOLTIP_GAP;
|
|
79
82
|
const cx = hole.left + hole.width / 2 - wTip / 2;
|
|
@@ -94,7 +97,14 @@ function computeTooltipPosition(
|
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
99
|
|
|
97
|
-
const fallback = clampTipPosition(
|
|
100
|
+
const fallback = clampTipPosition(
|
|
101
|
+
variants.below.top,
|
|
102
|
+
variants.below.left,
|
|
103
|
+
ph,
|
|
104
|
+
wTip,
|
|
105
|
+
vw,
|
|
106
|
+
vh,
|
|
107
|
+
);
|
|
98
108
|
return fallback;
|
|
99
109
|
}
|
|
100
110
|
|
|
@@ -185,12 +195,22 @@ export function DashboardTour({
|
|
|
185
195
|
}
|
|
186
196
|
}, [open]);
|
|
187
197
|
|
|
188
|
-
const finish = useCallback(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
198
|
+
const finish = useCallback(
|
|
199
|
+
(reason: "skip" | "done" | "escape") => {
|
|
200
|
+
void postKronosysAction({
|
|
201
|
+
type: "tourInteraction",
|
|
202
|
+
tour: "dashboard_spotlight",
|
|
203
|
+
action: reason,
|
|
204
|
+
stepIndex: step,
|
|
205
|
+
stepTotal: total,
|
|
206
|
+
}).catch(() => undefined);
|
|
207
|
+
markDashboardTourCompleted();
|
|
208
|
+
onOpenChange(false);
|
|
209
|
+
},
|
|
210
|
+
[onOpenChange, step, total],
|
|
211
|
+
);
|
|
192
212
|
|
|
193
|
-
useEscapeDismiss(open, finish);
|
|
213
|
+
useEscapeDismiss(open, () => finish("escape"));
|
|
194
214
|
|
|
195
215
|
const updateHoleFromDom = useCallback(() => {
|
|
196
216
|
if (!open) {
|
|
@@ -212,7 +232,11 @@ export function DashboardTour({
|
|
|
212
232
|
}
|
|
213
233
|
const el = document.querySelector(selector);
|
|
214
234
|
if (el instanceof HTMLElement) {
|
|
215
|
-
el.scrollIntoView({
|
|
235
|
+
el.scrollIntoView({
|
|
236
|
+
block: "nearest",
|
|
237
|
+
inline: "nearest",
|
|
238
|
+
behavior: "auto",
|
|
239
|
+
});
|
|
216
240
|
}
|
|
217
241
|
updateHoleFromDom();
|
|
218
242
|
const raf = requestAnimationFrame(updateHoleFromDom);
|
|
@@ -238,8 +262,14 @@ export function DashboardTour({
|
|
|
238
262
|
if (!open) {
|
|
239
263
|
return;
|
|
240
264
|
}
|
|
241
|
-
const vw =
|
|
242
|
-
|
|
265
|
+
const vw =
|
|
266
|
+
typeof globalThis.window !== "undefined"
|
|
267
|
+
? globalThis.window.innerWidth
|
|
268
|
+
: 1024;
|
|
269
|
+
const vh =
|
|
270
|
+
typeof globalThis.window !== "undefined"
|
|
271
|
+
? globalThis.window.innerHeight
|
|
272
|
+
: 768;
|
|
243
273
|
const w = Math.min(TOOLTIP_MAX_W, vw - 2 * VIEW_MARGIN);
|
|
244
274
|
|
|
245
275
|
if (!hole) {
|
|
@@ -260,8 +290,17 @@ export function DashboardTour({
|
|
|
260
290
|
const ph = panel?.getBoundingClientRect().height ?? 220;
|
|
261
291
|
/** Étape « Sessions » : colonne étroite à gauche — placer le panneau à droite en priorité pour ne pas masquer le trou. */
|
|
262
292
|
const placementPriority: readonly TipPlacement[] =
|
|
263
|
-
step === 2
|
|
264
|
-
|
|
293
|
+
step === 2
|
|
294
|
+
? ["right", "below", "above", "left"]
|
|
295
|
+
: ["below", "above", "right", "left"];
|
|
296
|
+
const { top, left } = computeTooltipPosition(
|
|
297
|
+
hole,
|
|
298
|
+
vw,
|
|
299
|
+
vh,
|
|
300
|
+
w,
|
|
301
|
+
ph,
|
|
302
|
+
placementPriority,
|
|
303
|
+
);
|
|
265
304
|
setTipStyle({
|
|
266
305
|
position: "fixed",
|
|
267
306
|
top,
|
|
@@ -290,7 +329,9 @@ export function DashboardTour({
|
|
|
290
329
|
return null;
|
|
291
330
|
}
|
|
292
331
|
|
|
293
|
-
const progressLabel = dt.tourProgressLabel
|
|
332
|
+
const progressLabel = dt.tourProgressLabel
|
|
333
|
+
.replace("{n}", String(step + 1))
|
|
334
|
+
.replace("{total}", String(total));
|
|
294
335
|
|
|
295
336
|
const secondaryBtn =
|
|
296
337
|
"rounded-lg border border-zinc-300 bg-white px-4 py-2 text-sm font-medium text-zinc-800 transition hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700";
|
|
@@ -316,22 +357,46 @@ export function DashboardTour({
|
|
|
316
357
|
<>
|
|
317
358
|
<div
|
|
318
359
|
className="fixed bg-black/55"
|
|
319
|
-
style={{
|
|
360
|
+
style={{
|
|
361
|
+
top: 0,
|
|
362
|
+
left: 0,
|
|
363
|
+
width: vw,
|
|
364
|
+
height: topH,
|
|
365
|
+
zIndex: 210,
|
|
366
|
+
}}
|
|
320
367
|
aria-hidden
|
|
321
368
|
/>
|
|
322
369
|
<div
|
|
323
370
|
className="fixed bg-black/55"
|
|
324
|
-
style={{
|
|
371
|
+
style={{
|
|
372
|
+
top: bottomTop,
|
|
373
|
+
left: 0,
|
|
374
|
+
width: vw,
|
|
375
|
+
height: bottomH,
|
|
376
|
+
zIndex: 210,
|
|
377
|
+
}}
|
|
325
378
|
aria-hidden
|
|
326
379
|
/>
|
|
327
380
|
<div
|
|
328
381
|
className="fixed bg-black/55"
|
|
329
|
-
style={{
|
|
382
|
+
style={{
|
|
383
|
+
top: t,
|
|
384
|
+
left: 0,
|
|
385
|
+
width: leftW,
|
|
386
|
+
height: h,
|
|
387
|
+
zIndex: 210,
|
|
388
|
+
}}
|
|
330
389
|
aria-hidden
|
|
331
390
|
/>
|
|
332
391
|
<div
|
|
333
392
|
className="fixed bg-black/55"
|
|
334
|
-
style={{
|
|
393
|
+
style={{
|
|
394
|
+
top: t,
|
|
395
|
+
left: rightLeft,
|
|
396
|
+
width: rightW,
|
|
397
|
+
height: h,
|
|
398
|
+
zIndex: 210,
|
|
399
|
+
}}
|
|
335
400
|
aria-hidden
|
|
336
401
|
/>
|
|
337
402
|
<div
|
|
@@ -368,12 +433,20 @@ export function DashboardTour({
|
|
|
368
433
|
<p className="text-xs font-medium uppercase tracking-wide text-violet-600 dark:text-violet-300">
|
|
369
434
|
{progressLabel}
|
|
370
435
|
</p>
|
|
371
|
-
<h2
|
|
436
|
+
<h2
|
|
437
|
+
id="dashboard-tour-title"
|
|
438
|
+
className="mt-1 text-base font-semibold text-zinc-900 dark:text-zinc-100"
|
|
439
|
+
>
|
|
372
440
|
{current.title}
|
|
373
441
|
</h2>
|
|
374
442
|
</div>
|
|
375
|
-
<div
|
|
376
|
-
|
|
443
|
+
<div
|
|
444
|
+
id="dashboard-tour-body"
|
|
445
|
+
className="max-h-[min(42vh,18rem)] overflow-y-auto px-4 py-3"
|
|
446
|
+
>
|
|
447
|
+
<p className="whitespace-pre-wrap text-sm leading-relaxed text-zinc-700 dark:text-zinc-300">
|
|
448
|
+
{current.body}
|
|
449
|
+
</p>
|
|
377
450
|
{kronoFocusTourStep && step === BASE_STEP_SELECTORS.length ? (
|
|
378
451
|
<p className="mt-3 text-sm leading-relaxed">
|
|
379
452
|
<a
|
|
@@ -393,7 +466,11 @@ export function DashboardTour({
|
|
|
393
466
|
{steps.map((_, i) => (
|
|
394
467
|
<span
|
|
395
468
|
key={i}
|
|
396
|
-
className={`h-2 w-2 rounded-full ${
|
|
469
|
+
className={`h-2 w-2 rounded-full ${
|
|
470
|
+
i === step
|
|
471
|
+
? "bg-violet-500 dark:bg-violet-400"
|
|
472
|
+
: "bg-zinc-300 dark:bg-zinc-600"
|
|
473
|
+
}`}
|
|
397
474
|
/>
|
|
398
475
|
))}
|
|
399
476
|
</div>
|
|
@@ -401,17 +478,26 @@ export function DashboardTour({
|
|
|
401
478
|
<button
|
|
402
479
|
type="button"
|
|
403
480
|
className="text-sm text-zinc-500 underline-offset-2 hover:text-zinc-800 hover:underline dark:text-zinc-400 dark:hover:text-zinc-200"
|
|
404
|
-
onClick={finish}
|
|
481
|
+
onClick={() => finish("skip")}
|
|
405
482
|
>
|
|
406
483
|
{dt.tourSkipBtn}
|
|
407
484
|
</button>
|
|
408
485
|
{step > 0 ? (
|
|
409
|
-
<button
|
|
486
|
+
<button
|
|
487
|
+
type="button"
|
|
488
|
+
className={secondaryBtn}
|
|
489
|
+
onClick={() => setStep((s) => Math.max(0, s - 1))}
|
|
490
|
+
>
|
|
410
491
|
{dt.tourBackBtn}
|
|
411
492
|
</button>
|
|
412
493
|
) : null}
|
|
413
494
|
{last ? (
|
|
414
|
-
<button
|
|
495
|
+
<button
|
|
496
|
+
ref={primaryBtnRef}
|
|
497
|
+
type="button"
|
|
498
|
+
className={tbVioletTextSm}
|
|
499
|
+
onClick={() => finish("done")}
|
|
500
|
+
>
|
|
415
501
|
{dt.tourDoneBtn}
|
|
416
502
|
</button>
|
|
417
503
|
) : (
|
|
@@ -428,7 +514,7 @@ export function DashboardTour({
|
|
|
428
514
|
</div>
|
|
429
515
|
</div>
|
|
430
516
|
</>,
|
|
431
|
-
document.body
|
|
517
|
+
document.body,
|
|
432
518
|
);
|
|
433
519
|
|
|
434
520
|
return node;
|