@nightkatana/kronosys-app 1.0.0-beta.21 → 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.
- package/README.md +1 -1
- package/app/changelog/page.tsx +87 -19
- package/app/globals.css +10 -8
- package/app/guide/page.tsx +71 -34
- package/app/implementation/page.tsx +70 -60
- package/app/licenses/page.tsx +79 -47
- package/app/logs/page.tsx +103 -47
- package/app/page.tsx +104 -169
- package/app/reporting/page.tsx +1918 -1436
- package/app/settings/page.tsx +66 -44
- package/components/KronosysPayloadProvider.tsx +19 -5
- package/components/dashboard/AppShellHeaderKronoFocus.tsx +78 -0
- package/components/dashboard/AppShellHeaderToolbarLayout.tsx +36 -0
- package/components/dashboard/AppShellHeaderUtilityRibbon.tsx +19 -0
- package/components/dashboard/AppShellHeaderWallClock.tsx +23 -17
- package/components/dashboard/AppShellRouteNav.tsx +336 -209
- package/components/dashboard/AppShellToolbarCommandCenter.tsx +225 -0
- package/components/dashboard/AppShellToolbarRouteNav.tsx +204 -0
- package/components/dashboard/DashboardCommandCenter.tsx +119 -30
- package/components/dashboard/KronoFocusPanel.tsx +287 -260
- package/components/dashboard/LanguageMenu.tsx +23 -7
- package/components/dashboard/PageRefreshButton.tsx +42 -16
- package/components/dashboard/ReportingTour.tsx +20 -2
- package/components/dashboard/SessionListPanel.tsx +4 -4
- package/components/dashboard/ThemeToggle.tsx +4 -3
- package/components/dashboard/useAnchoredFloatingPortalStyle.ts +9 -2
- package/components/dashboard/useKronoFocusLiveSeconds.ts +4 -2
- package/lib/appShellHeaderClasses.ts +22 -3
- package/lib/appShellToolbarChrome.ts +112 -0
- package/lib/appShellToolbarDeferredIntents.ts +112 -0
- package/lib/appShellToolbarSessionSlices.ts +67 -0
- package/lib/dashboardCopy.ts +78 -29
- package/lib/dashboardQuickSearch.ts +37 -6
- package/lib/dashboardUrlSession.ts +36 -0
- package/lib/generatedUserChangelog.ts +14 -0
- package/lib/implementationNotes.ts +18 -14
- package/lib/reportingAggregate.ts +68 -9
- package/lib/reportingMetricHelp.ts +8 -8
- package/lib/reportingStrings.ts +118 -9
- package/lib/reportingTagWeekBreakdown.ts +55 -13
- package/lib/settingsCopy.ts +6 -7
- package/lib/userGuideCopy.ts +29 -26
- package/package.json +7 -5
- package/server/db.ts +6 -4
- package/server/dbSchema.ts +2 -2
- package/components/dashboard/AppShellCommandCenterPlaceholder.tsx +0 -17
package/app/settings/page.tsx
CHANGED
|
@@ -36,11 +36,13 @@ import { settingsCopy, type SettingsCopy } from "@/lib/settingsCopy";
|
|
|
36
36
|
import {
|
|
37
37
|
appShellHeaderClassName,
|
|
38
38
|
appShellHeaderTitleMetaRowClassName,
|
|
39
|
-
appShellHeaderToolbarClassName,
|
|
40
39
|
} from "@/lib/appShellHeaderClasses";
|
|
41
|
-
import {
|
|
40
|
+
import { AppShellHeaderToolbarLayout } from "@/components/dashboard/AppShellHeaderToolbarLayout";
|
|
41
|
+
import { AppShellToolbarCommandCenter } from "@/components/dashboard/AppShellToolbarCommandCenter";
|
|
42
42
|
import { AppShellHeaderSessionMeta } from "@/components/dashboard/AppShellHeaderSessionMeta";
|
|
43
43
|
import { AppShellHeaderWallClock } from "@/components/dashboard/AppShellHeaderWallClock";
|
|
44
|
+
import { AppShellHeaderKronoFocus } from "@/components/dashboard/AppShellHeaderKronoFocus";
|
|
45
|
+
import { AppShellHeaderUtilityRibbon } from "@/components/dashboard/AppShellHeaderUtilityRibbon";
|
|
44
46
|
import { workspaceFolderPathStrings } from "@/lib/legacyEditorPayloadKeys";
|
|
45
47
|
import { showWorkspaceFoldersEmptyMessage } from "@/lib/usageProfile";
|
|
46
48
|
import { readDashboardUse24HourClockFromCfg } from "@/lib/dashboardClockFormat";
|
|
@@ -54,7 +56,7 @@ import { LanguageMenu } from "@/components/dashboard/LanguageMenu";
|
|
|
54
56
|
import { ScrollToTopFab } from "@/components/dashboard/ScrollToTopFab";
|
|
55
57
|
import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
|
|
56
58
|
import { PageRefreshButton } from "@/components/dashboard/PageRefreshButton";
|
|
57
|
-
import {
|
|
59
|
+
import { AppShellToolbarRouteNav } from "@/components/dashboard/AppShellToolbarRouteNav";
|
|
58
60
|
import { SettingsTagsProjectsSection } from "@/components/dashboard/SettingsTagsProjectsSection";
|
|
59
61
|
import { SettingsTour } from "@/components/dashboard/SettingsTour";
|
|
60
62
|
import { resetGitIdentityBannerDismissed } from "@/lib/dashboardGitIdentityBannerStorage";
|
|
@@ -1040,6 +1042,14 @@ function SettingsPageContent() {
|
|
|
1040
1042
|
return await refresh({ routerInvalidate: true, preserveForm: true });
|
|
1041
1043
|
}, [refresh]);
|
|
1042
1044
|
|
|
1045
|
+
const postHeaderAction = useCallback(
|
|
1046
|
+
async (body: Record<string, unknown>) => {
|
|
1047
|
+
await postKronosysAction(body);
|
|
1048
|
+
await refresh({ routerInvalidate: true, preserveForm: true });
|
|
1049
|
+
},
|
|
1050
|
+
[refresh],
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1043
1053
|
const restoreDashboardColumnHintsPreference = useCallback(() => {
|
|
1044
1054
|
writeDashboardColumnHintsDismissed(false);
|
|
1045
1055
|
writeDashboardColumnHintsClosed(false);
|
|
@@ -1263,6 +1273,7 @@ function SettingsPageContent() {
|
|
|
1263
1273
|
type: "updateKronosysSettings",
|
|
1264
1274
|
settings: {
|
|
1265
1275
|
...formRest,
|
|
1276
|
+
dashboardShowKronoFocusInHeader: true,
|
|
1266
1277
|
workspaceLocExcludedDirectoryNames: splitLinesToWorkspaceLocDirs(
|
|
1267
1278
|
workspaceLocExcludedDirsText,
|
|
1268
1279
|
),
|
|
@@ -1651,40 +1662,61 @@ function SettingsPageContent() {
|
|
|
1651
1662
|
</div>
|
|
1652
1663
|
<AppShellHeaderSessionMeta payload={payload} dt={dt} />
|
|
1653
1664
|
</div>
|
|
1654
|
-
<
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1665
|
+
<AppShellHeaderToolbarLayout
|
|
1666
|
+
leading={
|
|
1667
|
+
<>
|
|
1668
|
+
<AppShellHeaderWallClock lang={lang} dt={dt} />
|
|
1669
|
+
<AppShellHeaderKronoFocus
|
|
1670
|
+
payload={payload}
|
|
1671
|
+
dt={dt}
|
|
1672
|
+
post={postHeaderAction}
|
|
1673
|
+
/>
|
|
1674
|
+
<AppShellToolbarCommandCenter
|
|
1675
|
+
dt={dt}
|
|
1676
|
+
lang={lang}
|
|
1677
|
+
dashboardSessionNavId={dashboardSessionNavId}
|
|
1678
|
+
onManualRefresh={handleManualRefresh}
|
|
1679
|
+
/>
|
|
1680
|
+
</>
|
|
1681
|
+
}
|
|
1682
|
+
nav={
|
|
1683
|
+
<AppShellToolbarRouteNav
|
|
1659
1684
|
current="settings"
|
|
1660
1685
|
labels={nav}
|
|
1661
1686
|
navAriaLabel={dt.appShellRouteNavAria}
|
|
1662
1687
|
dashboardSessionId={dashboardSessionNavId}
|
|
1663
|
-
reserveGlobalPauseSlot
|
|
1664
|
-
/>
|
|
1665
|
-
<ThemeToggle lang={lang} />
|
|
1666
|
-
<PageRefreshButton
|
|
1667
|
-
title={dt.pageRefreshTitle}
|
|
1668
|
-
ariaLabel={dt.pageRefreshAriaLabel}
|
|
1669
|
-
inlineMessages={{
|
|
1670
|
-
loading: dt.pageRefreshProgressLabel,
|
|
1671
|
-
success: dt.pageRefreshDoneToast,
|
|
1672
|
-
error: dt.pageRefreshFailedToast,
|
|
1673
|
-
}}
|
|
1674
|
-
onRefresh={handleManualRefresh}
|
|
1675
|
-
/>
|
|
1676
|
-
<LanguageMenu
|
|
1677
1688
|
lang={lang}
|
|
1678
|
-
|
|
1679
|
-
labelFr="Français"
|
|
1680
|
-
menuHeading={lang === "fr" ? "Langue" : "Language"}
|
|
1681
|
-
triggerAriaLabel={
|
|
1682
|
-
lang === "fr" ? "Langue de l’interface" : "Interface language"
|
|
1683
|
-
}
|
|
1684
|
-
onSelect={(next) => void postLang(next)}
|
|
1689
|
+
dt={dt}
|
|
1685
1690
|
/>
|
|
1686
|
-
|
|
1687
|
-
|
|
1691
|
+
}
|
|
1692
|
+
trailing={
|
|
1693
|
+
<AppShellHeaderUtilityRibbon
|
|
1694
|
+
ariaLabel={dt.appShellUtilityToolbarGroupAria}
|
|
1695
|
+
>
|
|
1696
|
+
<ThemeToggle lang={lang} />
|
|
1697
|
+
<PageRefreshButton
|
|
1698
|
+
title={dt.pageRefreshTitle}
|
|
1699
|
+
ariaLabel={dt.pageRefreshAriaLabel}
|
|
1700
|
+
inlineMessages={{
|
|
1701
|
+
loading: dt.pageRefreshProgressLabel,
|
|
1702
|
+
success: dt.pageRefreshDoneToast,
|
|
1703
|
+
error: dt.pageRefreshFailedToast,
|
|
1704
|
+
}}
|
|
1705
|
+
onRefresh={handleManualRefresh}
|
|
1706
|
+
/>
|
|
1707
|
+
<LanguageMenu
|
|
1708
|
+
lang={lang}
|
|
1709
|
+
labelEn="English"
|
|
1710
|
+
labelFr="Français"
|
|
1711
|
+
menuHeading={lang === "fr" ? "Langue" : "Language"}
|
|
1712
|
+
triggerAriaLabel={
|
|
1713
|
+
lang === "fr" ? "Langue de l’interface" : "Interface language"
|
|
1714
|
+
}
|
|
1715
|
+
onSelect={(next) => void postLang(next)}
|
|
1716
|
+
/>
|
|
1717
|
+
</AppShellHeaderUtilityRibbon>
|
|
1718
|
+
}
|
|
1719
|
+
/>
|
|
1688
1720
|
</header>
|
|
1689
1721
|
|
|
1690
1722
|
<main className="mx-auto w-full max-w-[1920px] px-5 py-8 sm:px-8 lg:px-10 xl:px-12 2xl:px-14">
|
|
@@ -2305,19 +2337,9 @@ function SettingsPageContent() {
|
|
|
2305
2337
|
<h2 className="text-xs font-semibold uppercase tracking-wide text-zinc-500">
|
|
2306
2338
|
{s.sectionKronoFocus}
|
|
2307
2339
|
</h2>
|
|
2308
|
-
<
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
>
|
|
2312
|
-
<SettingsCheckbox
|
|
2313
|
-
checked={form.dashboardShowKronoFocusInHeader}
|
|
2314
|
-
onChange={(v) =>
|
|
2315
|
-
update("dashboardShowKronoFocusInHeader", v)
|
|
2316
|
-
}
|
|
2317
|
-
disabled={formLocked}
|
|
2318
|
-
ariaLabel={s.kronoFocusShowInHeader}
|
|
2319
|
-
/>
|
|
2320
|
-
</Field>
|
|
2340
|
+
<p className="mb-4 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">
|
|
2341
|
+
{s.kronoFocusHeaderPinnedNote}
|
|
2342
|
+
</p>
|
|
2321
2343
|
<Field
|
|
2322
2344
|
label={s.kronoFocusShowInTaskOps}
|
|
2323
2345
|
description={s.kronoFocusShowInTaskOpsDesc}
|
|
@@ -12,7 +12,10 @@ import {
|
|
|
12
12
|
} from "react";
|
|
13
13
|
import { useRouter } from "next/navigation";
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
fetchKronosysState,
|
|
17
|
+
type KronosysUpdatePayload,
|
|
18
|
+
} from "@/lib/kronosysApi";
|
|
16
19
|
|
|
17
20
|
export type KronosysPayloadContextValue = {
|
|
18
21
|
payload: KronosysUpdatePayload | null;
|
|
@@ -27,7 +30,8 @@ export type KronosysPayloadContextValue = {
|
|
|
27
30
|
getLatestPayload: () => KronosysUpdatePayload | null;
|
|
28
31
|
};
|
|
29
32
|
|
|
30
|
-
const KronosysPayloadContext =
|
|
33
|
+
const KronosysPayloadContext =
|
|
34
|
+
createContext<KronosysPayloadContextValue | null>(null);
|
|
31
35
|
|
|
32
36
|
export function KronosysPayloadProvider({ children }: { children: ReactNode }) {
|
|
33
37
|
const router = useRouter();
|
|
@@ -80,9 +84,13 @@ export function KronosysPayloadProvider({ children }: { children: ReactNode }) {
|
|
|
80
84
|
timer = setTimeout(() => void pollState(), delay);
|
|
81
85
|
};
|
|
82
86
|
|
|
83
|
-
|
|
87
|
+
/** Différer le premier fetch après le commit pour éviter les courses avec Suspense / enfants (React 19). */
|
|
88
|
+
const kickoff = globalThis.setTimeout(() => {
|
|
89
|
+
void pollState();
|
|
90
|
+
}, 0);
|
|
84
91
|
return () => {
|
|
85
92
|
cancelled = true;
|
|
93
|
+
globalThis.clearTimeout(kickoff);
|
|
86
94
|
if (timer) {
|
|
87
95
|
clearTimeout(timer);
|
|
88
96
|
}
|
|
@@ -99,13 +107,19 @@ export function KronosysPayloadProvider({ children }: { children: ReactNode }) {
|
|
|
99
107
|
[payload, error, refresh, getLatestPayload],
|
|
100
108
|
);
|
|
101
109
|
|
|
102
|
-
return
|
|
110
|
+
return (
|
|
111
|
+
<KronosysPayloadContext.Provider value={value}>
|
|
112
|
+
{children}
|
|
113
|
+
</KronosysPayloadContext.Provider>
|
|
114
|
+
);
|
|
103
115
|
}
|
|
104
116
|
|
|
105
117
|
export function useKronosysPayload(): KronosysPayloadContextValue {
|
|
106
118
|
const v = useContext(KronosysPayloadContext);
|
|
107
119
|
if (!v) {
|
|
108
|
-
throw new Error(
|
|
120
|
+
throw new Error(
|
|
121
|
+
"useKronosysPayload doit être utilisé sous KronosysPayloadProvider.",
|
|
122
|
+
);
|
|
109
123
|
}
|
|
110
124
|
return v;
|
|
111
125
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, type ComponentProps } from "react";
|
|
4
|
+
import { KronoFocusPanel } from "@/components/dashboard/KronoFocusPanel";
|
|
5
|
+
import type { DashboardStrings } from "@/lib/dashboardCopy";
|
|
6
|
+
|
|
7
|
+
type LiveForHeader = {
|
|
8
|
+
archived?: boolean;
|
|
9
|
+
kronoFocus?: ComponentProps<typeof KronoFocusPanel>["kronoFocus"];
|
|
10
|
+
activeTasks?: Array<{ id?: string }>;
|
|
11
|
+
activeTask?: { id?: string };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function AppShellHeaderKronoFocus({
|
|
15
|
+
payload,
|
|
16
|
+
dt,
|
|
17
|
+
post,
|
|
18
|
+
viewingArchive = false,
|
|
19
|
+
tourDomId,
|
|
20
|
+
className = "flex min-w-0 max-w-[min(100vw-12rem,40rem)] shrink-0 items-center",
|
|
21
|
+
}: Readonly<{
|
|
22
|
+
payload: unknown;
|
|
23
|
+
dt: DashboardStrings;
|
|
24
|
+
post: (body: Record<string, unknown>) => Promise<void>;
|
|
25
|
+
/** Tableau de bord : consultation d’une session archivée tout en pilotant la session live. */
|
|
26
|
+
viewingArchive?: boolean;
|
|
27
|
+
tourDomId?: string;
|
|
28
|
+
className?: string;
|
|
29
|
+
}>) {
|
|
30
|
+
const noopPost = useCallback(async (_body: Record<string, unknown>) => {},
|
|
31
|
+
[]);
|
|
32
|
+
|
|
33
|
+
const live = (payload as { current?: LiveForHeader } | null | undefined)
|
|
34
|
+
?.current;
|
|
35
|
+
|
|
36
|
+
if (!payload || !live) {
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
id={tourDomId}
|
|
40
|
+
className={`${className} opacity-85`}
|
|
41
|
+
aria-busy="true"
|
|
42
|
+
aria-label={dt.appShellHeaderKronoFocusLoadingAria}
|
|
43
|
+
>
|
|
44
|
+
<KronoFocusPanel
|
|
45
|
+
variant="headerBar"
|
|
46
|
+
kronoFocus={undefined}
|
|
47
|
+
liveActiveTaskIds={undefined}
|
|
48
|
+
t={dt}
|
|
49
|
+
post={noopPost}
|
|
50
|
+
viewingArchive={false}
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let liveActiveTaskIds: string[] | undefined;
|
|
57
|
+
if (Array.isArray(live.activeTasks) && live.activeTasks.length > 0) {
|
|
58
|
+
const ids = live.activeTasks
|
|
59
|
+
.map((t) => t.id)
|
|
60
|
+
.filter((id): id is string => typeof id === "string" && id.length > 0);
|
|
61
|
+
liveActiveTaskIds = ids.length > 0 ? ids : undefined;
|
|
62
|
+
} else if (live.activeTask?.id) {
|
|
63
|
+
liveActiveTaskIds = [live.activeTask.id];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div id={tourDomId} className={className}>
|
|
68
|
+
<KronoFocusPanel
|
|
69
|
+
variant="headerBar"
|
|
70
|
+
kronoFocus={live.kronoFocus}
|
|
71
|
+
liveActiveTaskIds={liveActiveTaskIds}
|
|
72
|
+
t={dt}
|
|
73
|
+
post={post}
|
|
74
|
+
viewingArchive={viewingArchive}
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
appShellHeaderToolbarClassName,
|
|
4
|
+
appShellHeaderToolbarClusterClassName,
|
|
5
|
+
appShellHeaderToolbarLeadingClassName,
|
|
6
|
+
appShellHeaderToolbarNavClassName,
|
|
7
|
+
appShellHeaderToolbarTrailingClassName,
|
|
8
|
+
} from "@/lib/appShellHeaderClasses";
|
|
9
|
+
|
|
10
|
+
type AppShellHeaderToolbarLayoutProps = Readonly<{
|
|
11
|
+
id?: string;
|
|
12
|
+
leading: ReactNode;
|
|
13
|
+
nav: ReactNode;
|
|
14
|
+
trailing: ReactNode;
|
|
15
|
+
}>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Toute la barre d’outils (leading + nav + trailing) est rendue comme **un seul bloc** centré
|
|
19
|
+
* horizontalement dans la rangée ; défilement horizontal si la fenêtre est plus étroite que le bloc.
|
|
20
|
+
*/
|
|
21
|
+
export function AppShellHeaderToolbarLayout({
|
|
22
|
+
id,
|
|
23
|
+
leading,
|
|
24
|
+
nav,
|
|
25
|
+
trailing,
|
|
26
|
+
}: AppShellHeaderToolbarLayoutProps) {
|
|
27
|
+
return (
|
|
28
|
+
<div id={id} className={appShellHeaderToolbarClassName}>
|
|
29
|
+
<div className={appShellHeaderToolbarClusterClassName}>
|
|
30
|
+
<div className={appShellHeaderToolbarLeadingClassName}>{leading}</div>
|
|
31
|
+
<div className={appShellHeaderToolbarNavClassName}>{nav}</div>
|
|
32
|
+
<div className={appShellHeaderToolbarTrailingClassName}>{trailing}</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { appShellToolbarRibbonGroupClass } from "@/lib/appShellToolbarChrome";
|
|
5
|
+
|
|
6
|
+
export function AppShellHeaderUtilityRibbon({
|
|
7
|
+
ariaLabel,
|
|
8
|
+
children,
|
|
9
|
+
}: Readonly<{ ariaLabel: string; children: ReactNode }>) {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
role="group"
|
|
13
|
+
aria-label={ariaLabel}
|
|
14
|
+
className={appShellToolbarRibbonGroupClass}
|
|
15
|
+
>
|
|
16
|
+
{children}
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -5,6 +5,10 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
5
5
|
import { useKronosysPayload } from "@/components/KronosysPayloadProvider";
|
|
6
6
|
import type { DashboardStrings, Lang } from "@/lib/dashboardCopy";
|
|
7
7
|
import { formatAppShellWallClock } from "@/lib/formatAppShellWallClock";
|
|
8
|
+
import {
|
|
9
|
+
appShellToolbarRaisedWideTriggerClass,
|
|
10
|
+
appShellToolbarRibbonGroupClass,
|
|
11
|
+
} from "@/lib/appShellToolbarChrome";
|
|
8
12
|
|
|
9
13
|
export function AppShellHeaderWallClock({
|
|
10
14
|
lang,
|
|
@@ -33,22 +37,24 @@ export function AppShellHeaderWallClock({
|
|
|
33
37
|
.replace("{datetime}", ariaDatetime);
|
|
34
38
|
|
|
35
39
|
return (
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
40
|
+
<div className={appShellToolbarRibbonGroupClass}>
|
|
41
|
+
<time
|
|
42
|
+
suppressHydrationWarning
|
|
43
|
+
dateTime={now ? now.toISOString() : ""}
|
|
44
|
+
className={`${appShellToolbarRaisedWideTriggerClass} gap-1.5 px-2.5 font-mono text-sm tabular-nums text-zinc-800 dark:text-zinc-100`}
|
|
45
|
+
title={aria}
|
|
46
|
+
aria-label={aria}
|
|
47
|
+
>
|
|
48
|
+
<Clock
|
|
49
|
+
size={16}
|
|
50
|
+
strokeWidth={2}
|
|
51
|
+
className="shrink-0 text-zinc-500 dark:text-zinc-400"
|
|
52
|
+
aria-hidden
|
|
53
|
+
/>
|
|
54
|
+
<span className="whitespace-nowrap" suppressHydrationWarning>
|
|
55
|
+
{now ? display : "--:--:--"}
|
|
56
|
+
</span>
|
|
57
|
+
</time>
|
|
58
|
+
</div>
|
|
53
59
|
);
|
|
54
60
|
}
|