@nightkatana/kronosys-app 1.0.0-beta.13 → 1.0.0-beta.15
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/app/globals.css +98 -0
- package/app/guide/page.tsx +59 -15
- package/app/implementation/page.tsx +317 -0
- package/app/layout.tsx +11 -3
- package/app/licenses/page.tsx +97 -37
- package/app/page.tsx +578 -192
- package/app/reporting/page.tsx +1073 -203
- package/app/settings/page.tsx +101 -16
- package/bin/kronosys.mjs +6 -0
- 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/AppShellRouteNav.tsx +237 -48
- package/components/dashboard/DashboardSimpleModal.tsx +168 -25
- package/components/dashboard/GlobalPauseConfirmModal.tsx +183 -0
- package/components/dashboard/KronosysDatetimePopoverField.tsx +128 -119
- package/components/dashboard/NewSessionScopeModal.tsx +13 -0
- package/components/dashboard/SelectedSessionSidebarBlock.tsx +333 -116
- package/components/dashboard/SessionListPanel.tsx +279 -43
- package/components/dashboard/SettingsTagsProjectsSection.tsx +655 -239
- package/components/dashboard/SettingsTaskTemplatesSection.tsx +314 -0
- package/components/dashboard/TaskFocusPanel.tsx +602 -373
- package/components/dashboard/TaskSessionLiveCard.tsx +341 -115
- package/components/dashboard/TaskTimelineGanttModal.tsx +557 -0
- package/components/dashboard/taskFieldStyles.ts +3 -2
- package/lib/appShellHeaderClasses.ts +13 -0
- package/lib/dashboardCopy.ts +326 -81
- package/lib/dashboardQuickSearch.ts +54 -2
- package/lib/formatSessionNameTemplate.test.ts +53 -0
- package/lib/formatSessionNameTemplate.ts +141 -0
- package/lib/generatedUserChangelog.ts +9 -0
- package/lib/globalPausePreview.test.ts +170 -0
- package/lib/globalPausePreview.ts +292 -0
- package/lib/implementationNotes.ts +647 -0
- package/lib/reportingAggregate.test.ts +31 -4
- package/lib/reportingAggregate.ts +73 -0
- package/lib/reportingMetricHelp.ts +8 -0
- package/lib/reportingStrings.ts +28 -3
- package/lib/reportingTagWeekBreakdown.test.ts +4 -3
- package/lib/sessionTaskSidebarStats.test.ts +27 -1
- package/lib/sessionTaskSidebarStats.ts +124 -9
- package/lib/settingsCopy.ts +105 -0
- package/lib/taskTemplateDraft.test.ts +52 -0
- package/lib/taskTemplateDraft.ts +121 -0
- package/lib/taskTimelineGantt.test.ts +50 -0
- package/lib/taskTimelineGantt.ts +165 -0
- package/lib/userGuideCopy.ts +44 -28
- package/package.json +1 -1
- package/server/actionDispatch.test.ts +184 -0
- package/server/actionDispatch.ts +453 -6
- package/server/actionTaskSession.ts +58 -11
- package/server/defaultCfg.ts +5 -0
- package/server/sessionWallHydrate.ts +13 -0
- package/components/dashboard/IssuePickerModal.tsx +0 -168
package/app/globals.css
CHANGED
|
@@ -48,6 +48,22 @@ body {
|
|
|
48
48
|
color: var(--foreground);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
@keyframes kronosys-route-transition-in-kf {
|
|
52
|
+
from {
|
|
53
|
+
opacity: 0;
|
|
54
|
+
transform: translateY(6px);
|
|
55
|
+
}
|
|
56
|
+
to {
|
|
57
|
+
opacity: 1;
|
|
58
|
+
transform: translateY(0);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.kronosys-route-transition-in {
|
|
63
|
+
animation: kronosys-route-transition-in-kf 170ms ease-out both;
|
|
64
|
+
will-change: opacity, transform;
|
|
65
|
+
}
|
|
66
|
+
|
|
51
67
|
/* Minuteur focus : clignotement sous 5 min (travail / longue pause), accent violet sous 30 s */
|
|
52
68
|
@keyframes kronosys-krono-focus-blink {
|
|
53
69
|
0%,
|
|
@@ -187,11 +203,77 @@ html.dark .kronosys-datetime-popover select option {
|
|
|
187
203
|
transform-origin: center center;
|
|
188
204
|
}
|
|
189
205
|
|
|
206
|
+
/* Tâche en pause : halo ambre léger (minuteur arrêté) */
|
|
207
|
+
@keyframes kronosys-task-paused-ring-kf {
|
|
208
|
+
0%,
|
|
209
|
+
100% {
|
|
210
|
+
box-shadow:
|
|
211
|
+
0 0 0 1px rgb(234 179 8 / 0.28),
|
|
212
|
+
0 0 12px -4px rgb(234 179 8 / 0.12);
|
|
213
|
+
}
|
|
214
|
+
50% {
|
|
215
|
+
box-shadow:
|
|
216
|
+
0 0 0 3px rgb(234 179 8 / 0.42),
|
|
217
|
+
0 0 18px -2px rgb(234 179 8 / 0.22);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.kronosys-task-paused-blink {
|
|
222
|
+
animation: kronosys-task-paused-ring-kf 1.75s ease-in-out infinite;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* Tâche lancée : pulse léger tant que le minuteur roule */
|
|
226
|
+
@keyframes kronosys-task-running-pulse-kf {
|
|
227
|
+
0%,
|
|
228
|
+
100% {
|
|
229
|
+
box-shadow:
|
|
230
|
+
0 0 0 1px rgb(16 185 129 / 0.22),
|
|
231
|
+
0 0 10px -5px rgb(16 185 129 / 0.1);
|
|
232
|
+
}
|
|
233
|
+
50% {
|
|
234
|
+
box-shadow:
|
|
235
|
+
0 0 0 2px rgb(16 185 129 / 0.36),
|
|
236
|
+
0 0 18px -4px rgb(16 185 129 / 0.18);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.kronosys-task-running-pulse {
|
|
241
|
+
animation: kronosys-task-running-pulse-kf 2.2s ease-in-out infinite;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* Tâche terminée : pulse unique vert (succès) */
|
|
245
|
+
@keyframes kronosys-task-finish-celebrate-kf {
|
|
246
|
+
0% {
|
|
247
|
+
box-shadow: 0 0 0 0 rgb(16 185 129 / 0);
|
|
248
|
+
transform: scale(1);
|
|
249
|
+
}
|
|
250
|
+
45% {
|
|
251
|
+
box-shadow:
|
|
252
|
+
0 0 0 4px rgb(16 185 129 / 0.35),
|
|
253
|
+
0 0 20px -4px rgb(16 185 129 / 0.28);
|
|
254
|
+
transform: scale(1.012);
|
|
255
|
+
}
|
|
256
|
+
100% {
|
|
257
|
+
box-shadow: 0 0 0 0 rgb(16 185 129 / 0);
|
|
258
|
+
transform: scale(1);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.kronosys-task-finish-celebrate {
|
|
263
|
+
animation: kronosys-task-finish-celebrate-kf 0.7s ease-out 1 both;
|
|
264
|
+
transform-origin: center center;
|
|
265
|
+
}
|
|
266
|
+
|
|
190
267
|
@media (prefers-reduced-motion: reduce) {
|
|
191
268
|
html {
|
|
192
269
|
scroll-behavior: auto !important;
|
|
193
270
|
}
|
|
194
271
|
|
|
272
|
+
.kronosys-route-transition-in {
|
|
273
|
+
animation: none;
|
|
274
|
+
transform: none;
|
|
275
|
+
}
|
|
276
|
+
|
|
195
277
|
.kronosys-krono-focus-time-blink {
|
|
196
278
|
animation: none;
|
|
197
279
|
opacity: 1;
|
|
@@ -207,4 +289,20 @@ html.dark .kronosys-datetime-popover select option {
|
|
|
207
289
|
animation: none;
|
|
208
290
|
opacity: 1;
|
|
209
291
|
}
|
|
292
|
+
|
|
293
|
+
.kronosys-task-paused-blink {
|
|
294
|
+
animation: none;
|
|
295
|
+
box-shadow: none;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.kronosys-task-running-pulse {
|
|
299
|
+
animation: none;
|
|
300
|
+
box-shadow: none;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.kronosys-task-finish-celebrate {
|
|
304
|
+
animation: none;
|
|
305
|
+
transform: none;
|
|
306
|
+
box-shadow: none;
|
|
307
|
+
}
|
|
210
308
|
}
|
package/app/guide/page.tsx
CHANGED
|
@@ -7,7 +7,13 @@ import { BookOpen, Search, X } from "lucide-react";
|
|
|
7
7
|
import { useKronosysPayload } from "@/components/KronosysPayloadProvider";
|
|
8
8
|
import { postKronosysAction } from "@/lib/kronosysApi";
|
|
9
9
|
import { AppVersionStamp } from "@/components/dashboard/AppVersionStamp";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
appShellHeaderClassName,
|
|
12
|
+
appShellHeaderTitleMetaRowClassName,
|
|
13
|
+
appShellHeaderToolbarClassName,
|
|
14
|
+
} from "@/lib/appShellHeaderClasses";
|
|
15
|
+
import { AppShellCommandCenterPlaceholder } from "@/components/dashboard/AppShellCommandCenterPlaceholder";
|
|
16
|
+
import { AppShellHeaderSessionMeta } from "@/components/dashboard/AppShellHeaderSessionMeta";
|
|
11
17
|
import { AppShellRouteNav } from "@/components/dashboard/AppShellRouteNav";
|
|
12
18
|
import { UserGuideBodyText } from "@/components/dashboard/UserGuideBodyText";
|
|
13
19
|
import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
|
|
@@ -21,7 +27,10 @@ import { sectionSearchHaystack, userGuideBundle } from "@/lib/userGuideCopy";
|
|
|
21
27
|
|
|
22
28
|
type LiveShape = { language?: string };
|
|
23
29
|
|
|
24
|
-
function filterSections(
|
|
30
|
+
function filterSections(
|
|
31
|
+
query: string,
|
|
32
|
+
sections: ReturnType<typeof userGuideBundle>["sections"],
|
|
33
|
+
): number[] {
|
|
25
34
|
const q = query.trim().toLowerCase();
|
|
26
35
|
if (q.length === 0) {
|
|
27
36
|
return sections.map((_, i) => i);
|
|
@@ -44,14 +53,21 @@ function GuideContent() {
|
|
|
44
53
|
const nav = useMemo(() => reportingNav(lang), [lang]);
|
|
45
54
|
const dt = dashboardStrings(lang);
|
|
46
55
|
const bundle = useMemo(() => userGuideBundle(lang), [lang]);
|
|
47
|
-
const visible = useMemo(
|
|
56
|
+
const visible = useMemo(
|
|
57
|
+
() => filterSections(q, bundle.sections),
|
|
58
|
+
[q, bundle.sections],
|
|
59
|
+
);
|
|
48
60
|
|
|
49
61
|
const postLang = async (next: Lang) => {
|
|
50
62
|
await postKronosysAction({ type: "setLanguage", lang: next });
|
|
51
63
|
await refresh();
|
|
52
64
|
};
|
|
53
65
|
|
|
54
|
-
const bulletList = (
|
|
66
|
+
const bulletList = (
|
|
67
|
+
lines: string[],
|
|
68
|
+
idPrefix: string,
|
|
69
|
+
listType: "ul" | "ol" = "ul",
|
|
70
|
+
): ReactNode => {
|
|
55
71
|
const listClass =
|
|
56
72
|
listType === "ol"
|
|
57
73
|
? "mt-2 list-inside list-decimal space-y-1.5 pl-0.5 text-sm leading-relaxed text-zinc-600 dark:text-zinc-300"
|
|
@@ -71,7 +87,7 @@ function GuideContent() {
|
|
|
71
87
|
return (
|
|
72
88
|
<div className="min-h-screen bg-zinc-100 text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100">
|
|
73
89
|
<header className={appShellHeaderClassName}>
|
|
74
|
-
<div className={
|
|
90
|
+
<div className={appShellHeaderTitleMetaRowClassName}>
|
|
75
91
|
<div className="flex min-w-0 flex-col gap-1">
|
|
76
92
|
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
|
77
93
|
<Link
|
|
@@ -83,8 +99,13 @@ function GuideContent() {
|
|
|
83
99
|
<span className="text-zinc-400 dark:text-zinc-600" aria-hidden>
|
|
84
100
|
/
|
|
85
101
|
</span>
|
|
86
|
-
<span className="text-lg font-medium text-zinc-700 dark:text-zinc-300">
|
|
87
|
-
|
|
102
|
+
<span className="text-lg font-medium text-zinc-700 dark:text-zinc-300">
|
|
103
|
+
{bundle.pageTitle}
|
|
104
|
+
</span>
|
|
105
|
+
<span
|
|
106
|
+
className="inline-flex items-center text-violet-500 dark:text-violet-400"
|
|
107
|
+
aria-hidden
|
|
108
|
+
>
|
|
88
109
|
<BookOpen className="size-5" strokeWidth={2} />
|
|
89
110
|
</span>
|
|
90
111
|
</div>
|
|
@@ -96,12 +117,17 @@ function GuideContent() {
|
|
|
96
117
|
<AppVersionStamp ariaLabelTemplate={dt.appVersionAriaLabel} />
|
|
97
118
|
</p>
|
|
98
119
|
</div>
|
|
99
|
-
<
|
|
120
|
+
<AppShellHeaderSessionMeta payload={payload} dt={dt} />
|
|
121
|
+
</div>
|
|
122
|
+
<div className="flex w-full justify-end">
|
|
123
|
+
<div className={appShellHeaderToolbarClassName}>
|
|
124
|
+
<AppShellCommandCenterPlaceholder />
|
|
100
125
|
<AppShellRouteNav
|
|
101
126
|
current="guide"
|
|
102
127
|
labels={nav}
|
|
103
128
|
navAriaLabel={dt.appShellRouteNavAria}
|
|
104
129
|
dashboardSessionId={dashboardSessionNavId}
|
|
130
|
+
reserveGlobalPauseSlot
|
|
105
131
|
/>
|
|
106
132
|
<ThemeToggle lang={lang} />
|
|
107
133
|
<PageRefreshButton
|
|
@@ -121,7 +147,9 @@ function GuideContent() {
|
|
|
121
147
|
labelEn="English"
|
|
122
148
|
labelFr="Français"
|
|
123
149
|
menuHeading={lang === "fr" ? "Langue" : "Language"}
|
|
124
|
-
triggerAriaLabel={
|
|
150
|
+
triggerAriaLabel={
|
|
151
|
+
lang === "fr" ? "Langue de l’interface" : "Interface language"
|
|
152
|
+
}
|
|
125
153
|
onSelect={(next) => void postLang(next)}
|
|
126
154
|
/>
|
|
127
155
|
</div>
|
|
@@ -133,9 +161,14 @@ function GuideContent() {
|
|
|
133
161
|
data-user-guide-scrolled=""
|
|
134
162
|
>
|
|
135
163
|
<p className="max-w-3xl text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">
|
|
136
|
-
<UserGuideBodyText
|
|
164
|
+
<UserGuideBodyText
|
|
165
|
+
line={bundle.pageSubtitle}
|
|
166
|
+
sessionId={dashboardSessionNavId}
|
|
167
|
+
/>
|
|
168
|
+
</p>
|
|
169
|
+
<p className="mt-2 text-xs text-zinc-500 dark:text-zinc-500">
|
|
170
|
+
{bundle.lastUpdated}
|
|
137
171
|
</p>
|
|
138
|
-
<p className="mt-2 text-xs text-zinc-500 dark:text-zinc-500">{bundle.lastUpdated}</p>
|
|
139
172
|
|
|
140
173
|
<form
|
|
141
174
|
onSubmit={(e) => e.preventDefault()}
|
|
@@ -179,7 +212,10 @@ function GuideContent() {
|
|
|
179
212
|
</form>
|
|
180
213
|
|
|
181
214
|
{visible.length === 0 && q.trim() ? (
|
|
182
|
-
<output
|
|
215
|
+
<output
|
|
216
|
+
className="mt-8 block text-sm text-zinc-500 dark:text-zinc-400"
|
|
217
|
+
aria-live="polite"
|
|
218
|
+
>
|
|
183
219
|
{bundle.searchNoResults}
|
|
184
220
|
</output>
|
|
185
221
|
) : null}
|
|
@@ -230,8 +266,14 @@ function GuideContent() {
|
|
|
230
266
|
</h2>
|
|
231
267
|
<div className="mt-3 space-y-3 text-sm leading-relaxed text-zinc-600 dark:text-zinc-300">
|
|
232
268
|
{s.paragraphs.map((p, pIdx) => (
|
|
233
|
-
<p
|
|
234
|
-
|
|
269
|
+
<p
|
|
270
|
+
key={`${s.id}-p${pIdx}`}
|
|
271
|
+
className="[&:not(:first-of-type)]:pt-0.5"
|
|
272
|
+
>
|
|
273
|
+
<UserGuideBodyText
|
|
274
|
+
line={p}
|
|
275
|
+
sessionId={dashboardSessionNavId}
|
|
276
|
+
/>
|
|
235
277
|
</p>
|
|
236
278
|
))}
|
|
237
279
|
{s.steps && s.steps.length > 0 ? (
|
|
@@ -251,7 +293,9 @@ function GuideContent() {
|
|
|
251
293
|
</div>
|
|
252
294
|
) : null}
|
|
253
295
|
</div>
|
|
254
|
-
{s.bullets && s.bullets.length > 0
|
|
296
|
+
{s.bullets && s.bullets.length > 0
|
|
297
|
+
? bulletList(s.bullets, s.id)
|
|
298
|
+
: null}
|
|
255
299
|
</article>
|
|
256
300
|
);
|
|
257
301
|
})}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { useSearchParams } from "next/navigation";
|
|
6
|
+
import { Check, FileCode2, X } from "lucide-react";
|
|
7
|
+
import { AppVersionStamp } from "@/components/dashboard/AppVersionStamp";
|
|
8
|
+
import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
|
|
9
|
+
import { PageRefreshButton } from "@/components/dashboard/PageRefreshButton";
|
|
10
|
+
import { ScrollToTopFab } from "@/components/dashboard/ScrollToTopFab";
|
|
11
|
+
import { AppShellRouteNav } from "@/components/dashboard/AppShellRouteNav";
|
|
12
|
+
import { useKronosysPayload } from "@/components/KronosysPayloadProvider";
|
|
13
|
+
import {
|
|
14
|
+
appShellHeaderClassName,
|
|
15
|
+
appShellHeaderTitleMetaRowClassName,
|
|
16
|
+
appShellHeaderToolbarClassName,
|
|
17
|
+
} from "@/lib/appShellHeaderClasses";
|
|
18
|
+
import { AppShellCommandCenterPlaceholder } from "@/components/dashboard/AppShellCommandCenterPlaceholder";
|
|
19
|
+
import { AppShellHeaderSessionMeta } from "@/components/dashboard/AppShellHeaderSessionMeta";
|
|
20
|
+
import { dashboardStrings, type Lang } from "@/lib/dashboardCopy";
|
|
21
|
+
import { withDashboardSessionParam } from "@/lib/dashboardSessionNav";
|
|
22
|
+
import { implementationNotesBundle } from "@/lib/implementationNotes";
|
|
23
|
+
import { reportingNav } from "@/lib/reportingStrings";
|
|
24
|
+
import { postKronosysAction } from "@/lib/kronosysApi";
|
|
25
|
+
import { LanguageMenu } from "@/components/dashboard/LanguageMenu";
|
|
26
|
+
|
|
27
|
+
type LiveShape = { language?: string };
|
|
28
|
+
|
|
29
|
+
const IMPLEMENTATION_REVIEW_STORAGE_KEY = "kronosys.implementation.review.v2";
|
|
30
|
+
|
|
31
|
+
function userStoryScreenReaderLabel(lang: Lang, implemented: boolean): string {
|
|
32
|
+
if (lang === "fr") {
|
|
33
|
+
return implemented ? "Capacité livrée." : "Absent ou hors périmètre.";
|
|
34
|
+
}
|
|
35
|
+
return implemented ? "Capability shipped." : "Missing or out of scope.";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function useImplementationReviewState() {
|
|
39
|
+
const [reviewed, setReviewed] = useState<Record<string, boolean>>({});
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
try {
|
|
43
|
+
const raw = localStorage.getItem(IMPLEMENTATION_REVIEW_STORAGE_KEY);
|
|
44
|
+
if (raw) {
|
|
45
|
+
const parsed = JSON.parse(raw) as Record<string, boolean>;
|
|
46
|
+
if (parsed && typeof parsed === "object") {
|
|
47
|
+
setReviewed(parsed);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
/* ignore */
|
|
52
|
+
}
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
const setReviewedKey = useCallback((key: string, value: boolean) => {
|
|
56
|
+
setReviewed((prev) => {
|
|
57
|
+
const next = { ...prev, [key]: value };
|
|
58
|
+
try {
|
|
59
|
+
localStorage.setItem(
|
|
60
|
+
IMPLEMENTATION_REVIEW_STORAGE_KEY,
|
|
61
|
+
JSON.stringify(next),
|
|
62
|
+
);
|
|
63
|
+
} catch {
|
|
64
|
+
/* ignore */
|
|
65
|
+
}
|
|
66
|
+
return next;
|
|
67
|
+
});
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const resetReviewed = useCallback(() => {
|
|
71
|
+
setReviewed({});
|
|
72
|
+
try {
|
|
73
|
+
localStorage.removeItem(IMPLEMENTATION_REVIEW_STORAGE_KEY);
|
|
74
|
+
} catch {
|
|
75
|
+
/* ignore */
|
|
76
|
+
}
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
return { reviewed, setReviewedKey, resetReviewed };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ImplementationGlyph(props: Readonly<{ implemented: boolean }>) {
|
|
83
|
+
const { implemented } = props;
|
|
84
|
+
if (implemented) {
|
|
85
|
+
return (
|
|
86
|
+
<span
|
|
87
|
+
className="inline-flex text-emerald-600 dark:text-emerald-500"
|
|
88
|
+
aria-hidden
|
|
89
|
+
>
|
|
90
|
+
<Check className="size-4 shrink-0" strokeWidth={2.5} />
|
|
91
|
+
</span>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return (
|
|
95
|
+
<span className="inline-flex text-red-600 dark:text-red-500" aria-hidden>
|
|
96
|
+
<X className="size-4 shrink-0" strokeWidth={2.5} />
|
|
97
|
+
</span>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function ImplementationBody() {
|
|
102
|
+
const searchParams = useSearchParams();
|
|
103
|
+
const dashboardSessionNavId = searchParams.get("session");
|
|
104
|
+
const { payload, refresh } = useKronosysPayload();
|
|
105
|
+
const live = payload?.current as LiveShape | undefined;
|
|
106
|
+
const lang: Lang = live?.language === "fr" ? "fr" : "en";
|
|
107
|
+
const dt = dashboardStrings(lang);
|
|
108
|
+
const nav = useMemo(() => reportingNav(lang), [lang]);
|
|
109
|
+
const notes = useMemo(() => implementationNotesBundle(lang), [lang]);
|
|
110
|
+
const { reviewed, setReviewedKey, resetReviewed } =
|
|
111
|
+
useImplementationReviewState();
|
|
112
|
+
|
|
113
|
+
const postLang = async (next: Lang) => {
|
|
114
|
+
await postKronosysAction({ type: "setLanguage", lang: next });
|
|
115
|
+
await refresh();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className="min-h-screen bg-zinc-100 text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100">
|
|
120
|
+
<header className={appShellHeaderClassName}>
|
|
121
|
+
<div className={appShellHeaderTitleMetaRowClassName}>
|
|
122
|
+
<div className="flex min-w-0 flex-col gap-1">
|
|
123
|
+
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
|
124
|
+
<Link
|
|
125
|
+
href={withDashboardSessionParam("/", dashboardSessionNavId)}
|
|
126
|
+
className="text-xl font-semibold tracking-tight text-zinc-900 hover:text-violet-700 dark:text-zinc-100 dark:hover:text-violet-300"
|
|
127
|
+
>
|
|
128
|
+
Kronosys
|
|
129
|
+
</Link>
|
|
130
|
+
<span className="text-zinc-400 dark:text-zinc-600" aria-hidden>
|
|
131
|
+
/
|
|
132
|
+
</span>
|
|
133
|
+
<span className="text-lg font-medium text-zinc-700 dark:text-zinc-300">
|
|
134
|
+
{notes.pageTitle}
|
|
135
|
+
</span>
|
|
136
|
+
<span
|
|
137
|
+
className="inline-flex items-center text-violet-500 dark:text-violet-400"
|
|
138
|
+
aria-hidden
|
|
139
|
+
>
|
|
140
|
+
<FileCode2 className="size-5" strokeWidth={2} />
|
|
141
|
+
</span>
|
|
142
|
+
</div>
|
|
143
|
+
<p className="flex flex-wrap items-center gap-x-2 text-xs font-medium leading-snug text-zinc-500 dark:text-zinc-400">
|
|
144
|
+
<span>{dt.brandTagline}</span>
|
|
145
|
+
<span className="text-zinc-400/70 dark:text-zinc-600" aria-hidden>
|
|
146
|
+
·
|
|
147
|
+
</span>
|
|
148
|
+
<AppVersionStamp ariaLabelTemplate={dt.appVersionAriaLabel} />
|
|
149
|
+
</p>
|
|
150
|
+
</div>
|
|
151
|
+
<AppShellHeaderSessionMeta payload={payload} dt={dt} />
|
|
152
|
+
</div>
|
|
153
|
+
<div className="flex w-full justify-end">
|
|
154
|
+
<div className={appShellHeaderToolbarClassName}>
|
|
155
|
+
<AppShellCommandCenterPlaceholder />
|
|
156
|
+
<AppShellRouteNav
|
|
157
|
+
current="implementation"
|
|
158
|
+
labels={nav}
|
|
159
|
+
navAriaLabel={dt.appShellRouteNavAria}
|
|
160
|
+
dashboardSessionId={dashboardSessionNavId}
|
|
161
|
+
reserveGlobalPauseSlot
|
|
162
|
+
/>
|
|
163
|
+
<ThemeToggle lang={lang} />
|
|
164
|
+
<PageRefreshButton
|
|
165
|
+
title={dt.pageRefreshTitle}
|
|
166
|
+
ariaLabel={dt.pageRefreshAriaLabel}
|
|
167
|
+
inlineMessages={{
|
|
168
|
+
loading: dt.pageRefreshProgressLabel,
|
|
169
|
+
success: dt.pageRefreshDoneToast,
|
|
170
|
+
error: dt.pageRefreshFailedToast,
|
|
171
|
+
}}
|
|
172
|
+
onRefresh={async () => {
|
|
173
|
+
return await refresh({ routerInvalidate: true });
|
|
174
|
+
}}
|
|
175
|
+
/>
|
|
176
|
+
<LanguageMenu
|
|
177
|
+
lang={lang}
|
|
178
|
+
labelEn="English"
|
|
179
|
+
labelFr="Français"
|
|
180
|
+
menuHeading={lang === "fr" ? "Langue" : "Language"}
|
|
181
|
+
triggerAriaLabel={
|
|
182
|
+
lang === "fr" ? "Langue de l’interface" : "Interface language"
|
|
183
|
+
}
|
|
184
|
+
onSelect={(next) => void postLang(next)}
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</header>
|
|
189
|
+
|
|
190
|
+
<main className="mx-auto w-full max-w-4xl px-5 pb-16 pt-6 sm:px-8 lg:px-10">
|
|
191
|
+
<p className="text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">
|
|
192
|
+
{notes.pageSubtitle}
|
|
193
|
+
</p>
|
|
194
|
+
<p className="mt-2 text-xs font-medium text-zinc-700 dark:text-zinc-300">
|
|
195
|
+
{notes.statusKeyLine}
|
|
196
|
+
</p>
|
|
197
|
+
<p className="mt-2 text-xs text-zinc-500 dark:text-zinc-500">
|
|
198
|
+
{notes.lastUpdated}
|
|
199
|
+
</p>
|
|
200
|
+
<div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1">
|
|
201
|
+
<p className="text-xs text-zinc-500 dark:text-zinc-500">
|
|
202
|
+
{notes.repositoryDocLabel}
|
|
203
|
+
</p>
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
className="rounded-md border border-zinc-300 bg-white px-2 py-1 text-xs font-medium text-zinc-700 shadow-sm hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700/80"
|
|
207
|
+
onClick={resetReviewed}
|
|
208
|
+
aria-label={notes.checklistResetAria}
|
|
209
|
+
>
|
|
210
|
+
{notes.checklistResetLabel}
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<section
|
|
215
|
+
className="mt-8 rounded-xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-800/60"
|
|
216
|
+
aria-labelledby="implementation-user-stories-heading"
|
|
217
|
+
>
|
|
218
|
+
<h2
|
|
219
|
+
id="implementation-user-stories-heading"
|
|
220
|
+
className="text-base font-semibold text-zinc-900 dark:text-zinc-100"
|
|
221
|
+
>
|
|
222
|
+
{notes.storyGroupsHeading}
|
|
223
|
+
</h2>
|
|
224
|
+
<div className="mt-6 space-y-8">
|
|
225
|
+
{notes.storyGroups.map((group) => (
|
|
226
|
+
<section
|
|
227
|
+
key={group.id}
|
|
228
|
+
id={group.id}
|
|
229
|
+
className="rounded-lg border border-zinc-100 bg-zinc-50/80 p-3 dark:border-zinc-700/80 dark:bg-zinc-900/40"
|
|
230
|
+
aria-labelledby={`${group.id}-heading`}
|
|
231
|
+
>
|
|
232
|
+
<h3
|
|
233
|
+
id={`${group.id}-heading`}
|
|
234
|
+
className="text-sm font-semibold text-zinc-900 dark:text-zinc-100"
|
|
235
|
+
>
|
|
236
|
+
{group.title}
|
|
237
|
+
</h3>
|
|
238
|
+
{group.lead ? (
|
|
239
|
+
<p className="mt-1.5 text-xs leading-snug text-zinc-600 dark:text-zinc-400">
|
|
240
|
+
{group.lead}
|
|
241
|
+
</p>
|
|
242
|
+
) : null}
|
|
243
|
+
<div className="mt-3 overflow-x-auto">
|
|
244
|
+
<table className="w-full border-collapse text-left text-sm text-zinc-700 dark:text-zinc-300">
|
|
245
|
+
<caption className="sr-only">{group.title}</caption>
|
|
246
|
+
<tbody>
|
|
247
|
+
{group.stories.map((story, idx) => {
|
|
248
|
+
const rowKey = `sg:${group.id}:s${idx}`;
|
|
249
|
+
const isReviewed = !!reviewed[rowKey];
|
|
250
|
+
const srMark = userStoryScreenReaderLabel(
|
|
251
|
+
lang,
|
|
252
|
+
story.implemented,
|
|
253
|
+
);
|
|
254
|
+
return (
|
|
255
|
+
<tr
|
|
256
|
+
key={`${group.id}-${idx}`}
|
|
257
|
+
className={
|
|
258
|
+
idx === 0
|
|
259
|
+
? ""
|
|
260
|
+
: "border-t border-zinc-200 dark:border-zinc-700/80"
|
|
261
|
+
}
|
|
262
|
+
>
|
|
263
|
+
<td className="w-10 align-top px-2 py-2.5">
|
|
264
|
+
<span className="sr-only">{srMark} </span>
|
|
265
|
+
<ImplementationGlyph
|
|
266
|
+
implemented={story.implemented}
|
|
267
|
+
/>
|
|
268
|
+
</td>
|
|
269
|
+
<td className="py-2.5 pr-2 leading-snug">
|
|
270
|
+
{story.text}
|
|
271
|
+
</td>
|
|
272
|
+
<td className="w-12 shrink-0 align-top px-1 py-2.5 text-center">
|
|
273
|
+
<input
|
|
274
|
+
type="checkbox"
|
|
275
|
+
className="size-4 rounded border-zinc-300 text-violet-600 focus:ring-violet-500 dark:border-zinc-600 dark:bg-zinc-900 dark:text-violet-500 dark:focus:ring-violet-400"
|
|
276
|
+
checked={isReviewed}
|
|
277
|
+
onChange={(e) =>
|
|
278
|
+
setReviewedKey(rowKey, e.target.checked)
|
|
279
|
+
}
|
|
280
|
+
aria-label={
|
|
281
|
+
isReviewed
|
|
282
|
+
? notes.checklistUnmarkAria
|
|
283
|
+
: notes.checklistMarkAria
|
|
284
|
+
}
|
|
285
|
+
/>
|
|
286
|
+
</td>
|
|
287
|
+
</tr>
|
|
288
|
+
);
|
|
289
|
+
})}
|
|
290
|
+
</tbody>
|
|
291
|
+
</table>
|
|
292
|
+
</div>
|
|
293
|
+
</section>
|
|
294
|
+
))}
|
|
295
|
+
</div>
|
|
296
|
+
</section>
|
|
297
|
+
</main>
|
|
298
|
+
<ScrollToTopFab
|
|
299
|
+
ariaLabel={lang === "fr" ? "Retour en haut de la page" : "Back to top"}
|
|
300
|
+
/>
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export default function ImplementationPage() {
|
|
306
|
+
return (
|
|
307
|
+
<Suspense
|
|
308
|
+
fallback={
|
|
309
|
+
<div className="min-h-screen bg-zinc-100 px-6 py-10 text-sm text-zinc-500 dark:bg-zinc-900">
|
|
310
|
+
Kronosys…
|
|
311
|
+
</div>
|
|
312
|
+
}
|
|
313
|
+
>
|
|
314
|
+
<ImplementationBody />
|
|
315
|
+
</Suspense>
|
|
316
|
+
);
|
|
317
|
+
}
|
package/app/layout.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { KronosysPackageVersionProvider } from "@/components/KronosysPackageVers
|
|
|
7
7
|
import { DashboardToastProvider } from "@/components/dashboard/DashboardToastProvider";
|
|
8
8
|
import { KronosysPayloadProvider } from "@/components/KronosysPayloadProvider";
|
|
9
9
|
import { AppShellLiveSessionDrawer } from "@/components/dashboard/AppShellLiveSessionDrawer";
|
|
10
|
+
import { RouteTransition } from "@/components/RouteTransition";
|
|
10
11
|
import { readKronosysPackageVersion } from "@/lib/readKronosysPackageVersion";
|
|
11
12
|
import { getThemeBootstrapScript } from "@/lib/theme";
|
|
12
13
|
import "./globals.css";
|
|
@@ -27,7 +28,10 @@ export const metadata: Metadata = {
|
|
|
27
28
|
"Kronosys : sessions, tâches, KronoFocus — application web locale (Next.js + SQLite).",
|
|
28
29
|
applicationName: "Kronosys Dashboard",
|
|
29
30
|
icons: {
|
|
30
|
-
icon: [
|
|
31
|
+
icon: [
|
|
32
|
+
{ url: "/icon-192.png", sizes: "192x192", type: "image/png" },
|
|
33
|
+
{ url: "/icon-512.png", sizes: "512x512", type: "image/png" },
|
|
34
|
+
],
|
|
31
35
|
apple: [{ url: "/apple-icon.png", sizes: "180x180", type: "image/png" }],
|
|
32
36
|
},
|
|
33
37
|
appleWebApp: {
|
|
@@ -58,13 +62,17 @@ export default function RootLayout({
|
|
|
58
62
|
suppressHydrationWarning
|
|
59
63
|
>
|
|
60
64
|
<body className="flex min-h-full flex-col font-sans">
|
|
61
|
-
<script
|
|
65
|
+
<script
|
|
66
|
+
dangerouslySetInnerHTML={{ __html: getThemeBootstrapScript() }}
|
|
67
|
+
/>
|
|
62
68
|
<ThemeProvider>
|
|
63
69
|
<KronosysPackageVersionProvider version={kronosysPackageVersion}>
|
|
64
70
|
<DashboardToastProvider>
|
|
65
71
|
<KronosysPayloadProvider>
|
|
66
72
|
<PwaRegister />
|
|
67
|
-
<div className="flex min-h-full flex-1 flex-col">
|
|
73
|
+
<div className="flex min-h-full flex-1 flex-col">
|
|
74
|
+
<RouteTransition>{children}</RouteTransition>
|
|
75
|
+
</div>
|
|
68
76
|
<AppShellLiveSessionDrawer />
|
|
69
77
|
<SiteLegalFooter />
|
|
70
78
|
</KronosysPayloadProvider>
|