@nightkatana/kronosys-app 1.0.0-beta.20 → 1.0.0-beta.22

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.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/app/changelog/page.tsx +87 -19
  3. package/app/globals.css +10 -8
  4. package/app/guide/page.tsx +71 -34
  5. package/app/implementation/page.tsx +70 -60
  6. package/app/licenses/page.tsx +79 -47
  7. package/app/logs/page.tsx +103 -47
  8. package/app/page.tsx +104 -169
  9. package/app/reporting/page.tsx +1918 -1436
  10. package/app/settings/page.tsx +66 -44
  11. package/components/KronosysPayloadProvider.tsx +19 -5
  12. package/components/dashboard/AppShellHeaderKronoFocus.tsx +78 -0
  13. package/components/dashboard/AppShellHeaderToolbarLayout.tsx +36 -0
  14. package/components/dashboard/AppShellHeaderUtilityRibbon.tsx +19 -0
  15. package/components/dashboard/AppShellHeaderWallClock.tsx +23 -17
  16. package/components/dashboard/AppShellRouteNav.tsx +336 -209
  17. package/components/dashboard/AppShellToolbarCommandCenter.tsx +225 -0
  18. package/components/dashboard/AppShellToolbarRouteNav.tsx +204 -0
  19. package/components/dashboard/DashboardCommandCenter.tsx +119 -30
  20. package/components/dashboard/KronoFocusPanel.tsx +287 -260
  21. package/components/dashboard/LanguageMenu.tsx +23 -7
  22. package/components/dashboard/PageRefreshButton.tsx +42 -16
  23. package/components/dashboard/ReportingTour.tsx +20 -2
  24. package/components/dashboard/SessionListPanel.tsx +4 -4
  25. package/components/dashboard/ThemeToggle.tsx +4 -3
  26. package/components/dashboard/useAnchoredFloatingPortalStyle.ts +9 -2
  27. package/components/dashboard/useKronoFocusLiveSeconds.ts +4 -2
  28. package/lib/appShellHeaderClasses.ts +22 -3
  29. package/lib/appShellToolbarChrome.ts +112 -0
  30. package/lib/appShellToolbarDeferredIntents.ts +112 -0
  31. package/lib/appShellToolbarSessionSlices.ts +67 -0
  32. package/lib/dashboardCopy.ts +78 -29
  33. package/lib/dashboardQuickSearch.ts +37 -6
  34. package/lib/dashboardUrlSession.ts +36 -0
  35. package/lib/generatedUserChangelog.ts +26 -0
  36. package/lib/implementationNotes.ts +22 -18
  37. package/lib/reportingAggregate.ts +68 -9
  38. package/lib/reportingMetricHelp.ts +8 -8
  39. package/lib/reportingStrings.ts +118 -9
  40. package/lib/reportingTagWeekBreakdown.ts +55 -13
  41. package/lib/settingsCopy.ts +6 -7
  42. package/lib/taskParsing.ts +44 -16
  43. package/lib/userGuideCopy.ts +33 -30
  44. package/package.json +7 -5
  45. package/server/db.ts +6 -4
  46. package/server/dbSchema.ts +2 -2
  47. package/components/dashboard/AppShellCommandCenterPlaceholder.tsx +0 -17
package/README.md CHANGED
@@ -99,7 +99,7 @@ Sinon : `NODE_EXTRA_CA_CERTS` pointant vers le PEM racine de l’organisation, o
99
99
 
100
100
  ## Publication npm (`@nightkatana/kronosys-app`)
101
101
 
102
- La version est définie dans `package.json` (référence actuelle : **`1.0.0-beta.20`**). Avant la première publication : créer le scope **@nightkatana** sur [npmjs.com](https://www.npmjs.com/) si besoin, puis `npm login`. Depuis ce dossier :
102
+ La version est définie dans `package.json` (référence actuelle : **`1.0.0-beta.22`**). Avant la première publication : créer le scope **@nightkatana** sur [npmjs.com](https://www.npmjs.com/) si besoin, puis `npm login`. Depuis ce dossier :
103
103
 
104
104
  ```bash
105
105
  npm publish --access public
@@ -1,22 +1,31 @@
1
1
  "use client";
2
2
 
3
- import { Suspense, useMemo } from "react";
3
+ import { Suspense, useCallback, useMemo } from "react";
4
4
  import Link from "next/link";
5
5
  import { useSearchParams } from "next/navigation";
6
6
  import ReactMarkdown, { type Components } from "react-markdown";
7
7
  import { AppVersionStamp } from "@/components/dashboard/AppVersionStamp";
8
+ import { AppShellHeaderKronoFocus } from "@/components/dashboard/AppShellHeaderKronoFocus";
9
+ import { AppShellHeaderSessionMeta } from "@/components/dashboard/AppShellHeaderSessionMeta";
8
10
  import { AppShellHeaderWallClock } from "@/components/dashboard/AppShellHeaderWallClock";
11
+ import { AppShellHeaderUtilityRibbon } from "@/components/dashboard/AppShellHeaderUtilityRibbon";
12
+ import { AppShellToolbarCommandCenter } from "@/components/dashboard/AppShellToolbarCommandCenter";
13
+ import { AppShellToolbarRouteNav } from "@/components/dashboard/AppShellToolbarRouteNav";
14
+ import { LanguageMenu } from "@/components/dashboard/LanguageMenu";
9
15
  import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
10
16
  import { PageRefreshButton } from "@/components/dashboard/PageRefreshButton";
11
17
  import { ScrollToTopFab } from "@/components/dashboard/ScrollToTopFab";
12
18
  import { useKronosysPayload } from "@/components/KronosysPayloadProvider";
13
19
  import {
14
20
  appShellHeaderClassName,
15
- appShellHeaderToolRowClassName,
21
+ appShellHeaderTitleMetaRowClassName,
16
22
  } from "@/lib/appShellHeaderClasses";
23
+ import { AppShellHeaderToolbarLayout } from "@/components/dashboard/AppShellHeaderToolbarLayout";
17
24
  import { changelogBundle } from "@/lib/changelogCopy";
18
25
  import { dashboardStrings, type Lang } from "@/lib/dashboardCopy";
19
26
  import { withDashboardSessionParam } from "@/lib/dashboardSessionNav";
27
+ import { postKronosysAction } from "@/lib/kronosysApi";
28
+ import { reportingNav } from "@/lib/reportingStrings";
20
29
 
21
30
  type LiveShape = { language?: string };
22
31
 
@@ -77,11 +86,28 @@ function ChangelogBody() {
77
86
  const lang: Lang = live?.language === "fr" ? "fr" : "en";
78
87
  const dt = dashboardStrings(lang);
79
88
  const c = useMemo(() => changelogBundle(lang), [lang]);
89
+ const nav = useMemo(() => reportingNav(lang), [lang]);
90
+
91
+ const postLang = useCallback(
92
+ async (next: Lang) => {
93
+ await postKronosysAction({ type: "setLanguage", lang: next });
94
+ await refresh({ routerInvalidate: true });
95
+ },
96
+ [refresh],
97
+ );
98
+
99
+ const postHeaderAction = useCallback(
100
+ async (body: Record<string, unknown>) => {
101
+ await postKronosysAction(body);
102
+ await refresh({ routerInvalidate: true });
103
+ },
104
+ [refresh],
105
+ );
80
106
 
81
107
  return (
82
108
  <div className="min-h-screen bg-zinc-100 text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100">
83
109
  <header className={appShellHeaderClassName}>
84
- <div className={appShellHeaderToolRowClassName}>
110
+ <div className={appShellHeaderTitleMetaRowClassName}>
85
111
  <div className="flex min-w-0 flex-col gap-1">
86
112
  <div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5">
87
113
  <Link
@@ -105,23 +131,65 @@ function ChangelogBody() {
105
131
  <AppVersionStamp ariaLabelTemplate={dt.appVersionAriaLabel} />
106
132
  </p>
107
133
  </div>
108
- <div className="flex flex-wrap items-center gap-1.5">
109
- <AppShellHeaderWallClock lang={lang} dt={dt} />
110
- <ThemeToggle lang={lang} />
111
- <PageRefreshButton
112
- title={dt.pageRefreshTitle}
113
- ariaLabel={dt.pageRefreshAriaLabel}
114
- inlineMessages={{
115
- loading: dt.pageRefreshProgressLabel,
116
- success: dt.pageRefreshDoneToast,
117
- error: dt.pageRefreshFailedToast,
118
- }}
119
- onRefresh={async () => {
120
- return await refresh({ routerInvalidate: true });
121
- }}
122
- />
123
- </div>
134
+ <AppShellHeaderSessionMeta payload={payload} dt={dt} />
124
135
  </div>
136
+ <AppShellHeaderToolbarLayout
137
+ leading={
138
+ <>
139
+ <AppShellHeaderWallClock lang={lang} dt={dt} />
140
+ <AppShellHeaderKronoFocus
141
+ payload={payload}
142
+ dt={dt}
143
+ post={postHeaderAction}
144
+ />
145
+ <AppShellToolbarCommandCenter
146
+ dt={dt}
147
+ lang={lang}
148
+ dashboardSessionNavId={dashboardSessionNavId}
149
+ onManualRefresh={() => refresh({ routerInvalidate: true })}
150
+ />
151
+ </>
152
+ }
153
+ nav={
154
+ <AppShellToolbarRouteNav
155
+ current="changelog"
156
+ labels={nav}
157
+ navAriaLabel={dt.appShellRouteNavAria}
158
+ dashboardSessionId={dashboardSessionNavId}
159
+ lang={lang}
160
+ dt={dt}
161
+ />
162
+ }
163
+ trailing={
164
+ <AppShellHeaderUtilityRibbon
165
+ ariaLabel={dt.appShellUtilityToolbarGroupAria}
166
+ >
167
+ <ThemeToggle lang={lang} />
168
+ <PageRefreshButton
169
+ title={dt.pageRefreshTitle}
170
+ ariaLabel={dt.pageRefreshAriaLabel}
171
+ inlineMessages={{
172
+ loading: dt.pageRefreshProgressLabel,
173
+ success: dt.pageRefreshDoneToast,
174
+ error: dt.pageRefreshFailedToast,
175
+ }}
176
+ onRefresh={async () => {
177
+ return await refresh({ routerInvalidate: true });
178
+ }}
179
+ />
180
+ <LanguageMenu
181
+ lang={lang}
182
+ labelEn="English"
183
+ labelFr="Français"
184
+ menuHeading={lang === "fr" ? "Langue" : "Language"}
185
+ triggerAriaLabel={
186
+ lang === "fr" ? "Langue de l’interface" : "Interface language"
187
+ }
188
+ onSelect={(next) => void postLang(next)}
189
+ />
190
+ </AppShellHeaderUtilityRibbon>
191
+ }
192
+ />
125
193
  </header>
126
194
 
127
195
  <main className="mx-auto w-full max-w-3xl px-5 pb-16 pt-6 sm:px-8 lg:px-10">
package/app/globals.css CHANGED
@@ -7,7 +7,8 @@
7
7
  :root {
8
8
  /* Aligné sur le remap zinc clair (chaud, moins d’éblouissement). */
9
9
  --background: oklch(93.8% 0.011 82);
10
- --foreground: oklch(24.5% 0.014 285);
10
+ /* Même teinte que zinc-900 du thème clair (évite texte légèrement bleuté sur fond chaud). */
11
+ --foreground: oklch(24.5% 0.013 84);
11
12
  }
12
13
 
13
14
  html.dark {
@@ -34,13 +35,14 @@ html:not(.dark) {
34
35
  --color-zinc-100: oklch(93.8% 0.011 82);
35
36
  --color-zinc-200: oklch(88.5% 0.012 84);
36
37
  --color-zinc-300: oklch(82% 0.014 84);
37
- --color-zinc-400: oklch(68% 0.016 280);
38
- --color-zinc-500: oklch(54% 0.018 285);
39
- --color-zinc-600: oklch(44% 0.017 285);
40
- --color-zinc-700: oklch(37% 0.014 285);
41
- --color-zinc-800: oklch(30% 0.012 285);
42
- --color-zinc-900: oklch(24.5% 0.014 285);
43
- --color-zinc-950: oklch(18% 0.012 285);
38
+ /* Même famille chaude que zinc-50…300 (évite le saut vers ~280° qui « violettait » bordures / secondaire). */
39
+ --color-zinc-400: oklch(68% 0.014 84);
40
+ --color-zinc-500: oklch(54% 0.016 84);
41
+ --color-zinc-600: oklch(44% 0.015 84);
42
+ --color-zinc-700: oklch(37% 0.014 84);
43
+ --color-zinc-800: oklch(30% 0.013 84);
44
+ --color-zinc-900: oklch(24.5% 0.013 84);
45
+ --color-zinc-950: oklch(18% 0.012 84);
44
46
  }
45
47
 
46
48
  body {
@@ -1,6 +1,12 @@
1
1
  "use client";
2
2
 
3
- import { Suspense, useMemo, useState, type ReactNode } from "react";
3
+ import {
4
+ Suspense,
5
+ useCallback,
6
+ useMemo,
7
+ useState,
8
+ type ReactNode,
9
+ } from "react";
4
10
  import Link from "next/link";
5
11
  import { useSearchParams } from "next/navigation";
6
12
  import { BookOpen, Search, X } from "lucide-react";
@@ -10,12 +16,14 @@ import { AppVersionStamp } from "@/components/dashboard/AppVersionStamp";
10
16
  import {
11
17
  appShellHeaderClassName,
12
18
  appShellHeaderTitleMetaRowClassName,
13
- appShellHeaderToolbarClassName,
14
19
  } from "@/lib/appShellHeaderClasses";
15
- import { AppShellCommandCenterPlaceholder } from "@/components/dashboard/AppShellCommandCenterPlaceholder";
20
+ import { AppShellHeaderToolbarLayout } from "@/components/dashboard/AppShellHeaderToolbarLayout";
21
+ import { AppShellToolbarCommandCenter } from "@/components/dashboard/AppShellToolbarCommandCenter";
16
22
  import { AppShellHeaderSessionMeta } from "@/components/dashboard/AppShellHeaderSessionMeta";
17
23
  import { AppShellHeaderWallClock } from "@/components/dashboard/AppShellHeaderWallClock";
18
- import { AppShellRouteNav } from "@/components/dashboard/AppShellRouteNav";
24
+ import { AppShellHeaderKronoFocus } from "@/components/dashboard/AppShellHeaderKronoFocus";
25
+ import { AppShellHeaderUtilityRibbon } from "@/components/dashboard/AppShellHeaderUtilityRibbon";
26
+ import { AppShellToolbarRouteNav } from "@/components/dashboard/AppShellToolbarRouteNav";
19
27
  import { UserGuideBodyText } from "@/components/dashboard/UserGuideBodyText";
20
28
  import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
21
29
  import { PageRefreshButton } from "@/components/dashboard/PageRefreshButton";
@@ -64,6 +72,14 @@ function GuideContent() {
64
72
  await refresh();
65
73
  };
66
74
 
75
+ const postHeaderAction = useCallback(
76
+ async (body: Record<string, unknown>) => {
77
+ await postKronosysAction(body);
78
+ await refresh({ routerInvalidate: true });
79
+ },
80
+ [refresh],
81
+ );
82
+
67
83
  const bulletList = (
68
84
  lines: string[],
69
85
  idPrefix: string,
@@ -120,42 +136,63 @@ function GuideContent() {
120
136
  </div>
121
137
  <AppShellHeaderSessionMeta payload={payload} dt={dt} />
122
138
  </div>
123
- <div className="flex w-full justify-end">
124
- <div className={appShellHeaderToolbarClassName}>
125
- <AppShellHeaderWallClock lang={lang} dt={dt} />
126
- <AppShellCommandCenterPlaceholder />
127
- <AppShellRouteNav
139
+ <AppShellHeaderToolbarLayout
140
+ leading={
141
+ <>
142
+ <AppShellHeaderWallClock lang={lang} dt={dt} />
143
+ <AppShellHeaderKronoFocus
144
+ payload={payload}
145
+ dt={dt}
146
+ post={postHeaderAction}
147
+ />
148
+ <AppShellToolbarCommandCenter
149
+ dt={dt}
150
+ lang={lang}
151
+ dashboardSessionNavId={dashboardSessionNavId}
152
+ onManualRefresh={() => refresh({ routerInvalidate: true })}
153
+ />
154
+ </>
155
+ }
156
+ nav={
157
+ <AppShellToolbarRouteNav
128
158
  current="guide"
129
159
  labels={nav}
130
160
  navAriaLabel={dt.appShellRouteNavAria}
131
161
  dashboardSessionId={dashboardSessionNavId}
132
- reserveGlobalPauseSlot
133
- />
134
- <ThemeToggle lang={lang} />
135
- <PageRefreshButton
136
- title={dt.pageRefreshTitle}
137
- ariaLabel={dt.pageRefreshAriaLabel}
138
- inlineMessages={{
139
- loading: dt.pageRefreshProgressLabel,
140
- success: dt.pageRefreshDoneToast,
141
- error: dt.pageRefreshFailedToast,
142
- }}
143
- onRefresh={async () => {
144
- return await refresh({ routerInvalidate: true });
145
- }}
146
- />
147
- <LanguageMenu
148
162
  lang={lang}
149
- labelEn="English"
150
- labelFr="Français"
151
- menuHeading={lang === "fr" ? "Langue" : "Language"}
152
- triggerAriaLabel={
153
- lang === "fr" ? "Langue de l’interface" : "Interface language"
154
- }
155
- onSelect={(next) => void postLang(next)}
163
+ dt={dt}
156
164
  />
157
- </div>
158
- </div>
165
+ }
166
+ trailing={
167
+ <AppShellHeaderUtilityRibbon
168
+ ariaLabel={dt.appShellUtilityToolbarGroupAria}
169
+ >
170
+ <ThemeToggle lang={lang} />
171
+ <PageRefreshButton
172
+ title={dt.pageRefreshTitle}
173
+ ariaLabel={dt.pageRefreshAriaLabel}
174
+ inlineMessages={{
175
+ loading: dt.pageRefreshProgressLabel,
176
+ success: dt.pageRefreshDoneToast,
177
+ error: dt.pageRefreshFailedToast,
178
+ }}
179
+ onRefresh={async () => {
180
+ return await refresh({ routerInvalidate: true });
181
+ }}
182
+ />
183
+ <LanguageMenu
184
+ lang={lang}
185
+ labelEn="English"
186
+ labelFr="Français"
187
+ menuHeading={lang === "fr" ? "Langue" : "Language"}
188
+ triggerAriaLabel={
189
+ lang === "fr" ? "Langue de l’interface" : "Interface language"
190
+ }
191
+ onSelect={(next) => void postLang(next)}
192
+ />
193
+ </AppShellHeaderUtilityRibbon>
194
+ }
195
+ />
159
196
  </header>
160
197
 
161
198
  <main
@@ -10,29 +10,23 @@ import {
10
10
  } from "react";
11
11
  import Link from "next/link";
12
12
  import { useSearchParams } from "next/navigation";
13
- import {
14
- Ban,
15
- Check,
16
- ChevronDown,
17
- Copy,
18
- FileCode2,
19
- ScrollText,
20
- X,
21
- } from "lucide-react";
13
+ import { Ban, Check, ChevronDown, Copy, FileCode2, X } from "lucide-react";
22
14
  import { AppVersionStamp } from "@/components/dashboard/AppVersionStamp";
23
15
  import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
24
16
  import { PageRefreshButton } from "@/components/dashboard/PageRefreshButton";
25
17
  import { ScrollToTopFab } from "@/components/dashboard/ScrollToTopFab";
26
- import { AppShellRouteNav } from "@/components/dashboard/AppShellRouteNav";
18
+ import { AppShellToolbarRouteNav } from "@/components/dashboard/AppShellToolbarRouteNav";
27
19
  import { useKronosysPayload } from "@/components/KronosysPayloadProvider";
28
20
  import {
29
21
  appShellHeaderClassName,
30
22
  appShellHeaderTitleMetaRowClassName,
31
- appShellHeaderToolbarClassName,
32
23
  } from "@/lib/appShellHeaderClasses";
33
- import { AppShellCommandCenterPlaceholder } from "@/components/dashboard/AppShellCommandCenterPlaceholder";
24
+ import { AppShellHeaderToolbarLayout } from "@/components/dashboard/AppShellHeaderToolbarLayout";
25
+ import { AppShellToolbarCommandCenter } from "@/components/dashboard/AppShellToolbarCommandCenter";
34
26
  import { AppShellHeaderSessionMeta } from "@/components/dashboard/AppShellHeaderSessionMeta";
35
27
  import { AppShellHeaderWallClock } from "@/components/dashboard/AppShellHeaderWallClock";
28
+ import { AppShellHeaderKronoFocus } from "@/components/dashboard/AppShellHeaderKronoFocus";
29
+ import { AppShellHeaderUtilityRibbon } from "@/components/dashboard/AppShellHeaderUtilityRibbon";
36
30
  import { dashboardStrings, type Lang } from "@/lib/dashboardCopy";
37
31
  import { withDashboardSessionParam } from "@/lib/dashboardSessionNav";
38
32
  import { implementationNotesBundle } from "@/lib/implementationNotes";
@@ -75,12 +69,7 @@ function coverageScreenReaderLabel(
75
69
  ? `${columnLabel} : sans objet pour une ligne écartée.`
76
70
  : `${columnLabel}: not applicable for a discarded intent.`;
77
71
  }
78
- const yn =
79
- lang === "fr"
80
- ? checked ? "oui" : "non"
81
- : checked
82
- ? "yes"
83
- : "no";
72
+ const yn = lang === "fr" ? (checked ? "oui" : "non") : checked ? "yes" : "no";
84
73
  return `${columnLabel}: ${yn}`;
85
74
  }
86
75
 
@@ -145,7 +134,10 @@ function ImplementationGlyph(
145
134
  const { implemented, discarded } = props;
146
135
  if (discarded) {
147
136
  return (
148
- <span className="inline-flex text-zinc-400 dark:text-zinc-500" aria-hidden>
137
+ <span
138
+ className="inline-flex text-zinc-400 dark:text-zinc-500"
139
+ aria-hidden
140
+ >
149
141
  <Ban className="size-4 shrink-0" strokeWidth={2.5} />
150
142
  </span>
151
143
  );
@@ -310,6 +302,14 @@ function ImplementationBody() {
310
302
  await refresh();
311
303
  };
312
304
 
305
+ const postHeaderAction = useCallback(
306
+ async (body: Record<string, unknown>) => {
307
+ await postKronosysAction(body);
308
+ await refresh({ routerInvalidate: true });
309
+ },
310
+ [refresh],
311
+ );
312
+
313
313
  const copyStoryId = useCallback((storyId: string) => {
314
314
  copyTextToClipboard(storyId).then((ok) =>
315
315
  applyStoryCopyResult(storyId, ok, setCopyFeedback),
@@ -351,53 +351,63 @@ function ImplementationBody() {
351
351
  </div>
352
352
  <AppShellHeaderSessionMeta payload={payload} dt={dt} />
353
353
  </div>
354
- <div className="flex w-full justify-end">
355
- <div className={appShellHeaderToolbarClassName}>
356
- <AppShellHeaderWallClock lang={lang} dt={dt} />
357
- <AppShellCommandCenterPlaceholder />
358
- <Link
359
- href={withDashboardSessionParam(
360
- "/changelog",
361
- dashboardSessionNavId,
362
- )}
363
- className="inline-flex size-10 shrink-0 items-center justify-center rounded-lg border border-zinc-300 bg-white text-zinc-700 shadow-sm transition hover:border-zinc-400 hover:bg-zinc-50 hover:text-violet-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-violet-500 dark:border-zinc-600 dark:bg-zinc-800/90 dark:text-zinc-200 dark:hover:border-zinc-500 dark:hover:bg-zinc-800 dark:hover:text-violet-300"
364
- aria-label={notes.openChangelogAria}
365
- title={notes.openChangelogTooltip}
366
- >
367
- <ScrollText size={18} />
368
- </Link>
369
- <AppShellRouteNav
354
+ <AppShellHeaderToolbarLayout
355
+ leading={
356
+ <>
357
+ <AppShellHeaderWallClock lang={lang} dt={dt} />
358
+ <AppShellHeaderKronoFocus
359
+ payload={payload}
360
+ dt={dt}
361
+ post={postHeaderAction}
362
+ />
363
+ <AppShellToolbarCommandCenter
364
+ dt={dt}
365
+ lang={lang}
366
+ dashboardSessionNavId={dashboardSessionNavId}
367
+ onManualRefresh={() => refresh({ routerInvalidate: true })}
368
+ />
369
+ </>
370
+ }
371
+ nav={
372
+ <AppShellToolbarRouteNav
370
373
  current="implementation"
371
374
  labels={nav}
372
375
  navAriaLabel={dt.appShellRouteNavAria}
373
376
  dashboardSessionId={dashboardSessionNavId}
374
- reserveGlobalPauseSlot
375
- />
376
- <ThemeToggle lang={lang} />
377
- <PageRefreshButton
378
- title={dt.pageRefreshTitle}
379
- ariaLabel={dt.pageRefreshAriaLabel}
380
- inlineMessages={{
381
- loading: dt.pageRefreshProgressLabel,
382
- success: dt.pageRefreshDoneToast,
383
- error: dt.pageRefreshFailedToast,
384
- }}
385
- onRefresh={async () => {
386
- return await refresh({ routerInvalidate: true });
387
- }}
388
- />
389
- <LanguageMenu
390
377
  lang={lang}
391
- labelEn="English"
392
- labelFr="Français"
393
- menuHeading={lang === "fr" ? "Langue" : "Language"}
394
- triggerAriaLabel={
395
- lang === "fr" ? "Langue de l’interface" : "Interface language"
396
- }
397
- onSelect={(next) => void postLang(next)}
378
+ dt={dt}
398
379
  />
399
- </div>
400
- </div>
380
+ }
381
+ trailing={
382
+ <AppShellHeaderUtilityRibbon
383
+ ariaLabel={dt.appShellUtilityToolbarGroupAria}
384
+ >
385
+ <ThemeToggle lang={lang} />
386
+ <PageRefreshButton
387
+ title={dt.pageRefreshTitle}
388
+ ariaLabel={dt.pageRefreshAriaLabel}
389
+ inlineMessages={{
390
+ loading: dt.pageRefreshProgressLabel,
391
+ success: dt.pageRefreshDoneToast,
392
+ error: dt.pageRefreshFailedToast,
393
+ }}
394
+ onRefresh={async () => {
395
+ return await refresh({ routerInvalidate: true });
396
+ }}
397
+ />
398
+ <LanguageMenu
399
+ lang={lang}
400
+ labelEn="English"
401
+ labelFr="Français"
402
+ menuHeading={lang === "fr" ? "Langue" : "Language"}
403
+ triggerAriaLabel={
404
+ lang === "fr" ? "Langue de l’interface" : "Interface language"
405
+ }
406
+ onSelect={(next) => void postLang(next)}
407
+ />
408
+ </AppShellHeaderUtilityRibbon>
409
+ }
410
+ />
401
411
  </header>
402
412
 
403
413
  <main className="mx-auto w-full max-w-4xl px-5 pb-16 pt-6 sm:px-8 lg:px-10">