@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,21 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import Link from "next/link";
|
|
4
|
-
import {
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import {
|
|
6
|
+
BarChart3,
|
|
7
|
+
BookOpen,
|
|
8
|
+
FileCode2,
|
|
9
|
+
FileText,
|
|
10
|
+
LayoutDashboard,
|
|
11
|
+
LayoutGrid,
|
|
12
|
+
Logs,
|
|
13
|
+
Pause,
|
|
14
|
+
Play,
|
|
15
|
+
Settings,
|
|
16
|
+
} from "lucide-react";
|
|
5
17
|
import { withDashboardSessionParam } from "@/lib/dashboardSessionNav";
|
|
18
|
+
import { PLANNED_BOUNDARY_ATTENTION_EVENT } from "@/lib/plannedBoundaryAttention";
|
|
6
19
|
|
|
7
20
|
const iconLinkClass =
|
|
8
21
|
"inline-flex size-10 items-center justify-center rounded-lg border border-zinc-300 bg-white text-zinc-700 transition hover:border-zinc-400 hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800/80 dark:text-zinc-200 dark:hover:border-zinc-500 dark:hover:bg-zinc-800";
|
|
@@ -10,17 +23,91 @@ const iconLinkClass =
|
|
|
10
23
|
const iconActiveClass =
|
|
11
24
|
"inline-flex size-10 items-center justify-center rounded-lg border border-violet-400/70 bg-violet-100/90 text-violet-950 dark:border-violet-600/60 dark:bg-violet-950/40 dark:text-violet-100";
|
|
12
25
|
|
|
26
|
+
const dashboardPulseChromeClass =
|
|
27
|
+
"ring-2 ring-amber-400/85 motion-safe:animate-pulse shadow-[0_0_14px_rgba(251,191,36,0.35)] dark:ring-amber-300/80";
|
|
28
|
+
|
|
29
|
+
/** Anneau autour du bouton pause globale lorsque la reprise est disponible (pause active). */
|
|
30
|
+
const globalPauseResumeHighlightClass =
|
|
31
|
+
"ring-2 ring-amber-500/90 ring-offset-2 ring-offset-zinc-100 shadow-[0_0_18px_rgba(245,158,11,0.42)] dark:ring-amber-400/85 dark:ring-offset-zinc-900 dark:shadow-[0_0_20px_rgba(251,191,36,0.28)]";
|
|
32
|
+
|
|
33
|
+
type GlobalPauseControlProps = Readonly<{
|
|
34
|
+
active: boolean;
|
|
35
|
+
label: string;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
disabledTooltip?: string;
|
|
38
|
+
onPress: () => void;
|
|
39
|
+
}>;
|
|
40
|
+
|
|
41
|
+
function AppShellGlobalPauseButton({
|
|
42
|
+
control,
|
|
43
|
+
}: Readonly<{ control: GlobalPauseControlProps }>) {
|
|
44
|
+
const gp = control;
|
|
45
|
+
const idlePause =
|
|
46
|
+
!gp.active &&
|
|
47
|
+
gp.disabled === true &&
|
|
48
|
+
typeof gp.disabledTooltip === "string" &&
|
|
49
|
+
gp.disabledTooltip.trim() !== "";
|
|
50
|
+
const aria =
|
|
51
|
+
idlePause && gp.disabledTooltip
|
|
52
|
+
? `${gp.label}. ${gp.disabledTooltip}`
|
|
53
|
+
: gp.label;
|
|
54
|
+
return (
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
aria-disabled={idlePause}
|
|
58
|
+
tabIndex={idlePause ? -1 : 0}
|
|
59
|
+
onClick={() => {
|
|
60
|
+
if (idlePause) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
gp.onPress();
|
|
64
|
+
}}
|
|
65
|
+
onKeyDown={(e) => {
|
|
66
|
+
if (
|
|
67
|
+
idlePause &&
|
|
68
|
+
(e.key === "Enter" || e.key === " " || e.code === "Space")
|
|
69
|
+
) {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
}
|
|
72
|
+
}}
|
|
73
|
+
className={`${gp.active ? iconActiveClass : iconLinkClass}${
|
|
74
|
+
gp.active ? ` ${globalPauseResumeHighlightClass}` : ""
|
|
75
|
+
}${idlePause ? " cursor-not-allowed opacity-45" : ""}`}
|
|
76
|
+
title={idlePause ? gp.disabledTooltip : gp.label}
|
|
77
|
+
aria-label={aria}
|
|
78
|
+
>
|
|
79
|
+
{gp.active ? (
|
|
80
|
+
<Play size={20} strokeWidth={2} className="shrink-0" aria-hidden />
|
|
81
|
+
) : (
|
|
82
|
+
<Pause size={20} strokeWidth={2} className="shrink-0" aria-hidden />
|
|
83
|
+
)}
|
|
84
|
+
</button>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
13
88
|
export type AppShellRouteNavLabels = {
|
|
14
89
|
dashboard: string;
|
|
15
90
|
reporting: string;
|
|
16
91
|
settings: string;
|
|
92
|
+
logs?: string;
|
|
17
93
|
/** Guide d’utilisation in-app. */
|
|
18
94
|
guide: string;
|
|
95
|
+
/** Détails d’implémentation (documentation technique maintenue). */
|
|
96
|
+
implementation?: string;
|
|
19
97
|
/** Libellé infobulle / `aria-label` pour l’icône « licences » (page licences uniquement). */
|
|
20
98
|
licenses?: string;
|
|
99
|
+
/** Infobulle lorsque l’icône tableau de bord pulse (rappel conflit minuteurs). */
|
|
100
|
+
dashboardAttentionHint?: string;
|
|
21
101
|
};
|
|
22
102
|
|
|
23
|
-
export type AppShellRouteNavCurrent =
|
|
103
|
+
export type AppShellRouteNavCurrent =
|
|
104
|
+
| "dashboard"
|
|
105
|
+
| "reporting"
|
|
106
|
+
| "settings"
|
|
107
|
+
| "logs"
|
|
108
|
+
| "licenses"
|
|
109
|
+
| "guide"
|
|
110
|
+
| "implementation";
|
|
24
111
|
|
|
25
112
|
type Props = Readonly<{
|
|
26
113
|
current: AppShellRouteNavCurrent;
|
|
@@ -30,6 +117,10 @@ type Props = Readonly<{
|
|
|
30
117
|
className?: string;
|
|
31
118
|
/** Identifiant de session du tableau de bord (`?session=`) à conserver sur les liens internes. */
|
|
32
119
|
dashboardSessionId?: string | null;
|
|
120
|
+
globalPauseControl?: GlobalPauseControlProps;
|
|
121
|
+
ganttControl?: { label: string; onPress: () => void };
|
|
122
|
+
/** Réserve l’emplacement du bouton pause globale (tableau de bord) pour éviter les sauts. */
|
|
123
|
+
reserveGlobalPauseSlot?: boolean;
|
|
33
124
|
}>;
|
|
34
125
|
|
|
35
126
|
export function AppShellRouteNav({
|
|
@@ -38,52 +129,114 @@ export function AppShellRouteNav({
|
|
|
38
129
|
navAriaLabel,
|
|
39
130
|
className,
|
|
40
131
|
dashboardSessionId,
|
|
132
|
+
globalPauseControl,
|
|
133
|
+
ganttControl,
|
|
134
|
+
reserveGlobalPauseSlot,
|
|
41
135
|
}: Props) {
|
|
42
136
|
const wrapClass = className ?? "flex flex-wrap items-center gap-1.5";
|
|
43
|
-
const dash = (path: string) =>
|
|
137
|
+
const dash = (path: string) =>
|
|
138
|
+
withDashboardSessionParam(path, dashboardSessionId);
|
|
44
139
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
title={labels.settings}
|
|
68
|
-
aria-label={labels.settings}
|
|
69
|
-
>
|
|
70
|
-
<Settings size={20} strokeWidth={2} className="shrink-0" aria-hidden />
|
|
71
|
-
</Link>
|
|
72
|
-
</nav>
|
|
140
|
+
const licensesTitle = labels.licenses ?? "Licenses";
|
|
141
|
+
const logsTitle = labels.logs ?? "Action logs";
|
|
142
|
+
|
|
143
|
+
const [docHidden, setDocHidden] = useState(false);
|
|
144
|
+
const [dashboardPulse, setDashboardPulse] = useState(false);
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
const syncHidden = () =>
|
|
148
|
+
setDocHidden(
|
|
149
|
+
typeof document !== "undefined" &&
|
|
150
|
+
document.visibilityState === "hidden",
|
|
151
|
+
);
|
|
152
|
+
syncHidden();
|
|
153
|
+
document.addEventListener("visibilitychange", syncHidden);
|
|
154
|
+
return () => document.removeEventListener("visibilitychange", syncHidden);
|
|
155
|
+
}, []);
|
|
156
|
+
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
const onAttention = () => setDashboardPulse(true);
|
|
159
|
+
globalThis.window.addEventListener(
|
|
160
|
+
PLANNED_BOUNDARY_ATTENTION_EVENT,
|
|
161
|
+
onAttention,
|
|
73
162
|
);
|
|
74
|
-
|
|
163
|
+
return () =>
|
|
164
|
+
globalThis.window.removeEventListener(
|
|
165
|
+
PLANNED_BOUNDARY_ATTENTION_EVENT,
|
|
166
|
+
onAttention,
|
|
167
|
+
);
|
|
168
|
+
}, []);
|
|
75
169
|
|
|
76
|
-
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (!dashboardPulse) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const t = globalThis.setTimeout(() => setDashboardPulse(false), 14000);
|
|
175
|
+
return () => globalThis.clearTimeout(t);
|
|
176
|
+
}, [dashboardPulse]);
|
|
177
|
+
|
|
178
|
+
const showDashboardPulse =
|
|
179
|
+
dashboardPulse &&
|
|
180
|
+
(current !== "dashboard" ||
|
|
181
|
+
(current === "dashboard" && docHidden === true));
|
|
182
|
+
|
|
183
|
+
const dashboardAttentionHint = labels.dashboardAttentionHint?.trim();
|
|
184
|
+
const dashboardPulseTitle =
|
|
185
|
+
showDashboardPulse && dashboardAttentionHint
|
|
186
|
+
? `${labels.dashboard} — ${dashboardAttentionHint}`
|
|
187
|
+
: labels.dashboard;
|
|
188
|
+
|
|
189
|
+
const dashboardBaseNavClass =
|
|
190
|
+
current === "dashboard" ? iconActiveClass : iconLinkClass;
|
|
191
|
+
const dashboardNavClass = showDashboardPulse
|
|
192
|
+
? `${dashboardBaseNavClass} ${dashboardPulseChromeClass}`
|
|
193
|
+
: dashboardBaseNavClass;
|
|
77
194
|
|
|
78
195
|
return (
|
|
79
196
|
<nav className={wrapClass} aria-label={navAriaLabel}>
|
|
80
|
-
|
|
81
|
-
<
|
|
82
|
-
|
|
197
|
+
{current === "dashboard" ? (
|
|
198
|
+
<span
|
|
199
|
+
className={dashboardNavClass}
|
|
200
|
+
title={dashboardPulseTitle}
|
|
201
|
+
aria-label={dashboardPulseTitle}
|
|
202
|
+
aria-current="page"
|
|
203
|
+
>
|
|
204
|
+
<LayoutDashboard
|
|
205
|
+
size={20}
|
|
206
|
+
strokeWidth={2}
|
|
207
|
+
className="shrink-0"
|
|
208
|
+
aria-hidden
|
|
209
|
+
/>
|
|
210
|
+
</span>
|
|
211
|
+
) : (
|
|
212
|
+
<Link
|
|
213
|
+
href={dash("/")}
|
|
214
|
+
className={dashboardNavClass}
|
|
215
|
+
title={dashboardPulseTitle}
|
|
216
|
+
aria-label={dashboardPulseTitle}
|
|
217
|
+
>
|
|
218
|
+
<LayoutDashboard
|
|
219
|
+
size={20}
|
|
220
|
+
strokeWidth={2}
|
|
221
|
+
className="shrink-0"
|
|
222
|
+
aria-hidden
|
|
223
|
+
/>
|
|
224
|
+
</Link>
|
|
225
|
+
)}
|
|
83
226
|
|
|
84
227
|
{current === "reporting" ? (
|
|
85
|
-
<span
|
|
86
|
-
|
|
228
|
+
<span
|
|
229
|
+
className={iconActiveClass}
|
|
230
|
+
title={labels.reporting}
|
|
231
|
+
aria-label={labels.reporting}
|
|
232
|
+
aria-current="page"
|
|
233
|
+
>
|
|
234
|
+
<BarChart3
|
|
235
|
+
size={20}
|
|
236
|
+
strokeWidth={2}
|
|
237
|
+
className="shrink-0"
|
|
238
|
+
aria-hidden
|
|
239
|
+
/>
|
|
87
240
|
</span>
|
|
88
241
|
) : (
|
|
89
242
|
<Link
|
|
@@ -92,13 +245,28 @@ export function AppShellRouteNav({
|
|
|
92
245
|
title={labels.reporting}
|
|
93
246
|
aria-label={labels.reporting}
|
|
94
247
|
>
|
|
95
|
-
<BarChart3
|
|
248
|
+
<BarChart3
|
|
249
|
+
size={20}
|
|
250
|
+
strokeWidth={2}
|
|
251
|
+
className="shrink-0"
|
|
252
|
+
aria-hidden
|
|
253
|
+
/>
|
|
96
254
|
</Link>
|
|
97
255
|
)}
|
|
98
256
|
|
|
99
257
|
{current === "settings" ? (
|
|
100
|
-
<span
|
|
101
|
-
|
|
258
|
+
<span
|
|
259
|
+
className={iconActiveClass}
|
|
260
|
+
title={labels.settings}
|
|
261
|
+
aria-label={labels.settings}
|
|
262
|
+
aria-current="page"
|
|
263
|
+
>
|
|
264
|
+
<Settings
|
|
265
|
+
size={20}
|
|
266
|
+
strokeWidth={2}
|
|
267
|
+
className="shrink-0"
|
|
268
|
+
aria-hidden
|
|
269
|
+
/>
|
|
102
270
|
</span>
|
|
103
271
|
) : (
|
|
104
272
|
<Link
|
|
@@ -107,25 +275,132 @@ export function AppShellRouteNav({
|
|
|
107
275
|
title={labels.settings}
|
|
108
276
|
aria-label={labels.settings}
|
|
109
277
|
>
|
|
110
|
-
<Settings
|
|
278
|
+
<Settings
|
|
279
|
+
size={20}
|
|
280
|
+
strokeWidth={2}
|
|
281
|
+
className="shrink-0"
|
|
282
|
+
aria-hidden
|
|
283
|
+
/>
|
|
284
|
+
</Link>
|
|
285
|
+
)}
|
|
286
|
+
{current === "logs" ? (
|
|
287
|
+
<span
|
|
288
|
+
className={iconActiveClass}
|
|
289
|
+
title={logsTitle}
|
|
290
|
+
aria-label={logsTitle}
|
|
291
|
+
aria-current="page"
|
|
292
|
+
>
|
|
293
|
+
<Logs size={20} strokeWidth={2} className="shrink-0" aria-hidden />
|
|
294
|
+
</span>
|
|
295
|
+
) : (
|
|
296
|
+
<Link
|
|
297
|
+
href={dash("/logs")}
|
|
298
|
+
className={iconLinkClass}
|
|
299
|
+
title={logsTitle}
|
|
300
|
+
aria-label={logsTitle}
|
|
301
|
+
>
|
|
302
|
+
<Logs size={20} strokeWidth={2} className="shrink-0" aria-hidden />
|
|
111
303
|
</Link>
|
|
112
304
|
)}
|
|
113
305
|
|
|
114
306
|
{current === "guide" ? (
|
|
115
|
-
<span
|
|
116
|
-
|
|
307
|
+
<span
|
|
308
|
+
className={iconActiveClass}
|
|
309
|
+
title={labels.guide}
|
|
310
|
+
aria-label={labels.guide}
|
|
311
|
+
aria-current="page"
|
|
312
|
+
>
|
|
313
|
+
<BookOpen
|
|
314
|
+
size={20}
|
|
315
|
+
strokeWidth={2}
|
|
316
|
+
className="shrink-0"
|
|
317
|
+
aria-hidden
|
|
318
|
+
/>
|
|
117
319
|
</span>
|
|
118
320
|
) : (
|
|
119
|
-
<Link
|
|
120
|
-
|
|
321
|
+
<Link
|
|
322
|
+
href={dash("/guide")}
|
|
323
|
+
className={iconLinkClass}
|
|
324
|
+
title={labels.guide}
|
|
325
|
+
aria-label={labels.guide}
|
|
326
|
+
>
|
|
327
|
+
<BookOpen
|
|
328
|
+
size={20}
|
|
329
|
+
strokeWidth={2}
|
|
330
|
+
className="shrink-0"
|
|
331
|
+
aria-hidden
|
|
332
|
+
/>
|
|
121
333
|
</Link>
|
|
122
334
|
)}
|
|
123
335
|
|
|
336
|
+
{labels.implementation ? (
|
|
337
|
+
current === "implementation" ? (
|
|
338
|
+
<span
|
|
339
|
+
className={iconActiveClass}
|
|
340
|
+
title={labels.implementation}
|
|
341
|
+
aria-label={labels.implementation}
|
|
342
|
+
aria-current="page"
|
|
343
|
+
>
|
|
344
|
+
<FileCode2
|
|
345
|
+
size={20}
|
|
346
|
+
strokeWidth={2}
|
|
347
|
+
className="shrink-0"
|
|
348
|
+
aria-hidden
|
|
349
|
+
/>
|
|
350
|
+
</span>
|
|
351
|
+
) : (
|
|
352
|
+
<Link
|
|
353
|
+
href={dash("/implementation")}
|
|
354
|
+
className={iconLinkClass}
|
|
355
|
+
title={labels.implementation}
|
|
356
|
+
aria-label={labels.implementation}
|
|
357
|
+
>
|
|
358
|
+
<FileCode2
|
|
359
|
+
size={20}
|
|
360
|
+
strokeWidth={2}
|
|
361
|
+
className="shrink-0"
|
|
362
|
+
aria-hidden
|
|
363
|
+
/>
|
|
364
|
+
</Link>
|
|
365
|
+
)
|
|
366
|
+
) : null}
|
|
367
|
+
|
|
124
368
|
{current === "licenses" ? (
|
|
125
|
-
<span
|
|
126
|
-
|
|
369
|
+
<span
|
|
370
|
+
className={iconActiveClass}
|
|
371
|
+
title={licensesTitle}
|
|
372
|
+
aria-label={licensesTitle}
|
|
373
|
+
aria-current="page"
|
|
374
|
+
>
|
|
375
|
+
<FileText
|
|
376
|
+
size={20}
|
|
377
|
+
strokeWidth={2}
|
|
378
|
+
className="shrink-0"
|
|
379
|
+
aria-hidden
|
|
380
|
+
/>
|
|
127
381
|
</span>
|
|
128
382
|
) : null}
|
|
383
|
+
{ganttControl ? (
|
|
384
|
+
<button
|
|
385
|
+
type="button"
|
|
386
|
+
onClick={ganttControl.onPress}
|
|
387
|
+
className={iconLinkClass}
|
|
388
|
+
title={ganttControl.label}
|
|
389
|
+
aria-label={ganttControl.label}
|
|
390
|
+
>
|
|
391
|
+
<LayoutGrid
|
|
392
|
+
size={20}
|
|
393
|
+
strokeWidth={2}
|
|
394
|
+
className="shrink-0"
|
|
395
|
+
aria-hidden
|
|
396
|
+
/>
|
|
397
|
+
</button>
|
|
398
|
+
) : null}
|
|
399
|
+
{globalPauseControl ? (
|
|
400
|
+
<AppShellGlobalPauseButton control={globalPauseControl} />
|
|
401
|
+
) : reserveGlobalPauseSlot ? (
|
|
402
|
+
<span className="inline-flex size-10 shrink-0" aria-hidden />
|
|
403
|
+
) : null}
|
|
129
404
|
</nav>
|
|
130
405
|
);
|
|
131
406
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { DashboardStrings } from "@/lib/dashboardCopy";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Voile plein écran (sous l’en-tête sticky du tableau de bord) lorsque la session live est en pause.
|
|
7
|
+
* L’en-tête reste au-dessus (z-50) pour permettre la reprise via la pause globale ou la navigation.
|
|
8
|
+
*/
|
|
9
|
+
export function DashboardPauseBackdrop({
|
|
10
|
+
variant,
|
|
11
|
+
dt,
|
|
12
|
+
}: Readonly<{
|
|
13
|
+
variant: "global" | "session";
|
|
14
|
+
dt: DashboardStrings;
|
|
15
|
+
}>) {
|
|
16
|
+
const title =
|
|
17
|
+
variant === "global"
|
|
18
|
+
? dt.dashboardPauseBackdropTitleGlobal
|
|
19
|
+
: dt.dashboardPauseBackdropTitleSession;
|
|
20
|
+
const detail =
|
|
21
|
+
variant === "global"
|
|
22
|
+
? dt.dashboardPauseBackdropDetailGlobal
|
|
23
|
+
: dt.dashboardPauseBackdropDetailSession;
|
|
24
|
+
const aria =
|
|
25
|
+
variant === "global"
|
|
26
|
+
? dt.dashboardPauseBackdropAriaGlobal
|
|
27
|
+
: dt.dashboardPauseBackdropAriaSession;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
className="fixed inset-0 z-40 flex items-center justify-center p-5 pt-20 sm:p-8 sm:pt-24"
|
|
32
|
+
role="status"
|
|
33
|
+
aria-live="polite"
|
|
34
|
+
aria-label={aria}
|
|
35
|
+
>
|
|
36
|
+
<div
|
|
37
|
+
className="absolute inset-0 bg-zinc-950/40 dark:bg-black/50"
|
|
38
|
+
aria-hidden
|
|
39
|
+
/>
|
|
40
|
+
<div className="relative z-10 max-w-md rounded-2xl border border-amber-500/45 bg-[#120d0a]/95 px-5 py-5 text-center shadow-2xl backdrop-blur-[2px] dark:border-amber-600/40 sm:px-7 sm:py-6">
|
|
41
|
+
<p className="text-xs font-semibold uppercase tracking-wide text-amber-200/95">
|
|
42
|
+
{title}
|
|
43
|
+
</p>
|
|
44
|
+
<p className="mt-2.5 text-sm leading-snug text-amber-100/92">
|
|
45
|
+
{detail}
|
|
46
|
+
</p>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|