@nightkatana/kronosys-app 1.0.0-beta.0
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 +81 -0
- package/app/api/action/route.ts +16 -0
- package/app/api/backup/route.ts +84 -0
- package/app/api/health/route.ts +22 -0
- package/app/api/state/route.ts +27 -0
- package/app/apple-icon.png +0 -0
- package/app/changelog/page.tsx +122 -0
- package/app/globals.css +210 -0
- package/app/guide/layout.tsx +11 -0
- package/app/guide/page.tsx +278 -0
- package/app/icon.png +0 -0
- package/app/layout.tsx +77 -0
- package/app/licenses/layout.tsx +11 -0
- package/app/licenses/page.tsx +246 -0
- package/app/manifest.ts +32 -0
- package/app/page.tsx +1610 -0
- package/app/reporting/page.tsx +2943 -0
- package/app/settings/layout.tsx +10 -0
- package/app/settings/page.tsx +3518 -0
- package/bin/kronosys.mjs +46 -0
- package/components/KronosysPackageVersionProvider.tsx +19 -0
- package/components/KronosysPayloadProvider.tsx +109 -0
- package/components/PwaRegister.tsx +25 -0
- package/components/SiteLegalFooter.tsx +21 -0
- package/components/ThemeProvider.tsx +78 -0
- package/components/dashboard/AppShellLiveSessionDrawer.tsx +394 -0
- package/components/dashboard/AppShellRouteNav.tsx +131 -0
- package/components/dashboard/AppVersionStamp.tsx +16 -0
- package/components/dashboard/DashboardCollapsibleSection.tsx +57 -0
- package/components/dashboard/DashboardColumnHintsBanner.tsx +159 -0
- package/components/dashboard/DashboardCommandCenter.tsx +470 -0
- package/components/dashboard/DashboardLangGateModal.tsx +118 -0
- package/components/dashboard/DashboardLoadingOverlay.tsx +42 -0
- package/components/dashboard/DashboardSimpleModal.tsx +337 -0
- package/components/dashboard/DashboardSuspenseFallback.tsx +52 -0
- package/components/dashboard/DashboardToastProvider.tsx +64 -0
- package/components/dashboard/DashboardTour.tsx +435 -0
- package/components/dashboard/DeferredDescriptionPopoverWrap.tsx +39 -0
- package/components/dashboard/DeleteSessionModal.tsx +130 -0
- package/components/dashboard/DescriptionTooltipPortaled.tsx +31 -0
- package/components/dashboard/GitIdentityQuickSetupModal.tsx +211 -0
- package/components/dashboard/HeaderIntegrationBadges.tsx +69 -0
- package/components/dashboard/InlineMetricHelpTrigger.tsx +102 -0
- package/components/dashboard/IssuePickerModal.tsx +168 -0
- package/components/dashboard/KronoFocusPanel.tsx +834 -0
- package/components/dashboard/KronosysDatetimePopoverField.tsx +357 -0
- package/components/dashboard/KronosysTimePopoverField.tsx +233 -0
- package/components/dashboard/LanguageMenu.tsx +123 -0
- package/components/dashboard/MongoMirrorSyncLine.tsx +57 -0
- package/components/dashboard/NewSessionScopeModal.tsx +410 -0
- package/components/dashboard/PageRefreshButton.tsx +130 -0
- package/components/dashboard/PlainHelpPopover.tsx +97 -0
- package/components/dashboard/ReportingPageToc.tsx +68 -0
- package/components/dashboard/ReportingTour.tsx +342 -0
- package/components/dashboard/SavedProjectPicker.tsx +92 -0
- package/components/dashboard/SavedTagPicker.tsx +115 -0
- package/components/dashboard/ScrollToTopFab.tsx +41 -0
- package/components/dashboard/SelectedSessionSidebarBlock.tsx +630 -0
- package/components/dashboard/SessionEndReasonEditor.tsx +114 -0
- package/components/dashboard/SessionListPanel.tsx +320 -0
- package/components/dashboard/SessionLocMetricsSection.tsx +128 -0
- package/components/dashboard/SettingsTagsProjectsSection.tsx +993 -0
- package/components/dashboard/SettingsTour.tsx +332 -0
- package/components/dashboard/TagPills.tsx +149 -0
- package/components/dashboard/TagsHelpTrigger.tsx +84 -0
- package/components/dashboard/TaskFocusPanel.tsx +1261 -0
- package/components/dashboard/TaskSessionLiveCard.tsx +832 -0
- package/components/dashboard/TaskSubtasksBlock.tsx +748 -0
- package/components/dashboard/ThemeToggle.test.tsx +26 -0
- package/components/dashboard/ThemeToggle.tsx +36 -0
- package/components/dashboard/UserGuideBodyText.tsx +62 -0
- package/components/dashboard/WorkspaceGitRepoCard.tsx +191 -0
- package/components/dashboard/taskFieldStyles.ts +139 -0
- package/components/dashboard/useAnchoredFloatingPortalStyle.ts +71 -0
- package/components/dashboard/useDescriptionPopoverAfterMs.ts +220 -0
- package/components/dashboard/useKronoFocusLiveSeconds.ts +36 -0
- package/components/dashboard/useSmoothStopwatchMs.ts +25 -0
- package/lib/appShellHeaderClasses.ts +12 -0
- package/lib/backupCsvExport.test.ts +149 -0
- package/lib/backupCsvExport.ts +392 -0
- package/lib/changelogCopy.ts +34 -0
- package/lib/concurrentTaskStartPreference.ts +29 -0
- package/lib/dashboardClockFormat.ts +13 -0
- package/lib/dashboardColumnChrome.ts +3 -0
- package/lib/dashboardColumnHintsStorage.ts +57 -0
- package/lib/dashboardCopy.ts +1831 -0
- package/lib/dashboardDetachedUrlHintStorage.ts +24 -0
- package/lib/dashboardGitIdentityBannerStorage.ts +36 -0
- package/lib/dashboardLangStorage.ts +72 -0
- package/lib/dashboardQuickSearch.ts +476 -0
- package/lib/dashboardQuickSearchQuery.test.ts +63 -0
- package/lib/dashboardQuickSearchQuery.ts +179 -0
- package/lib/dashboardSessionNav.ts +33 -0
- package/lib/dashboardShortcuts.ts +268 -0
- package/lib/dashboardTimeZone.ts +91 -0
- package/lib/dashboardTourStorage.ts +68 -0
- package/lib/dataDir.test.ts +87 -0
- package/lib/dataDir.ts +83 -0
- package/lib/devDataPreferenceFile.ts +55 -0
- package/lib/devDataRuntimeInfo.ts +34 -0
- package/lib/formatIsoShort.test.ts +46 -0
- package/lib/formatIsoShort.ts +29 -0
- package/lib/generatedUserChangelog.ts +34 -0
- package/lib/gitlabIssueSearch.ts +8 -0
- package/lib/kronoFocusDurationHistory.ts +71 -0
- package/lib/kronoFocusRhythm.test.ts +130 -0
- package/lib/kronoFocusRhythm.ts +46 -0
- package/lib/kronoFocusTimerUrgency.test.ts +74 -0
- package/lib/kronoFocusTimerUrgency.ts +24 -0
- package/lib/kronosysApi.ts +143 -0
- package/lib/legacyEditorPayloadKeys.ts +52 -0
- package/lib/legacyKronoFocusStorageKeys.test.ts +29 -0
- package/lib/legacyKronoFocusStorageKeys.ts +32 -0
- package/lib/licensesCopy.ts +128 -0
- package/lib/openPlainTextInNewTab.ts +49 -0
- package/lib/readKronosysPackageVersion.ts +10 -0
- package/lib/reportingAggregate.test.ts +325 -0
- package/lib/reportingAggregate.ts +819 -0
- package/lib/reportingDatePresets.ts +41 -0
- package/lib/reportingMetricHelp.ts +430 -0
- package/lib/reportingNonFinalIndicators.test.ts +157 -0
- package/lib/reportingNonFinalIndicators.ts +102 -0
- package/lib/reportingStrings.ts +491 -0
- package/lib/reportingTagWeekBreakdown.test.ts +141 -0
- package/lib/reportingTagWeekBreakdown.ts +181 -0
- package/lib/reportingWeekLayout.test.ts +239 -0
- package/lib/reportingWeekLayout.ts +313 -0
- package/lib/sessionAssiduity.test.ts +25 -0
- package/lib/sessionAssiduity.ts +33 -0
- package/lib/sessionEndReason.ts +55 -0
- package/lib/sessionEndWarnings.test.ts +200 -0
- package/lib/sessionEndWarnings.ts +125 -0
- package/lib/sessionListMerge.test.ts +101 -0
- package/lib/sessionListMerge.ts +70 -0
- package/lib/sessionTaskSidebarStats.test.ts +24 -0
- package/lib/sessionTaskSidebarStats.ts +54 -0
- package/lib/settingsCopy.ts +1276 -0
- package/lib/taskParsing.test.ts +153 -0
- package/lib/taskParsing.ts +737 -0
- package/lib/theme.ts +15 -0
- package/lib/translucentButtonClasses.ts +34 -0
- package/lib/usageProfile.test.ts +84 -0
- package/lib/usageProfile.ts +52 -0
- package/lib/userGuideCopy.ts +464 -0
- package/lib/workspaceLocDefaults.ts +21 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +15 -0
- package/package.json +87 -0
- package/postcss.config.mjs +12 -0
- package/public/apple-icon.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon.png +0 -0
- package/public/next.svg +1 -0
- package/public/sw.js +13 -0
- package/public/traceback.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/server/actionDispatch.test.ts +723 -0
- package/server/actionDispatch.ts +1476 -0
- package/server/actionTaskSession.test.ts +713 -0
- package/server/actionTaskSession.ts +717 -0
- package/server/db.ts +42 -0
- package/server/defaultCfg.ts +87 -0
- package/server/gitlabTokenStore.ts +34 -0
- package/server/kronoFocusHydrate.test.ts +142 -0
- package/server/kronoFocusHydrate.ts +69 -0
- package/server/kronoFocusMigrate.test.ts +53 -0
- package/server/kronoFocusMigrate.ts +78 -0
- package/server/mainTimerHydrate.test.ts +65 -0
- package/server/mainTimerHydrate.ts +53 -0
- package/server/payloadStore.test.ts +78 -0
- package/server/payloadStore.ts +83 -0
- package/server/sessionWallHydrate.test.ts +46 -0
- package/server/sessionWallHydrate.ts +88 -0
- package/tsconfig.json +41 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ArrowDown, ArrowRight, ChevronRight, X } from "lucide-react";
|
|
4
|
+
import { useCallback, useEffect, useId, useState } from "react";
|
|
5
|
+
import type { DashboardStrings } from "@/lib/dashboardCopy";
|
|
6
|
+
import {
|
|
7
|
+
readDashboardColumnHintsClosed,
|
|
8
|
+
readDashboardColumnHintsDismissed,
|
|
9
|
+
writeDashboardColumnHintsClosed,
|
|
10
|
+
writeDashboardColumnHintsDismissed,
|
|
11
|
+
} from "@/lib/dashboardColumnHintsStorage";
|
|
12
|
+
|
|
13
|
+
type Props = Readonly<{
|
|
14
|
+
dt: DashboardStrings;
|
|
15
|
+
}>;
|
|
16
|
+
|
|
17
|
+
const arrowPillClass =
|
|
18
|
+
"inline-flex size-11 items-center justify-center rounded-full border border-violet-300/90 bg-linear-to-br from-violet-100 via-white to-violet-50/90 shadow-md shadow-violet-500/10 dark:border-violet-700/70 dark:from-violet-950 dark:via-zinc-900 dark:to-violet-950/80 dark:shadow-violet-950/40 sm:size-12";
|
|
19
|
+
|
|
20
|
+
const arrowIconClass =
|
|
21
|
+
"size-5 text-violet-600 sm:size-[1.35rem] dark:text-violet-300";
|
|
22
|
+
|
|
23
|
+
/** Flèche entre deux blocs : vers le bas sur mobile, vers la droite à partir de `sm`. */
|
|
24
|
+
function FlowBetweenArrows() {
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
className="flex shrink-0 justify-center py-0.5 sm:w-12 sm:items-center sm:justify-center sm:py-0 md:w-14"
|
|
28
|
+
aria-hidden
|
|
29
|
+
>
|
|
30
|
+
<span className={`${arrowPillClass} sm:hidden`}>
|
|
31
|
+
<ArrowDown className={arrowIconClass} strokeWidth={2.35} aria-hidden />
|
|
32
|
+
</span>
|
|
33
|
+
<span className={`${arrowPillClass} hidden sm:inline-flex`}>
|
|
34
|
+
<ArrowRight className={arrowIconClass} strokeWidth={2.35} aria-hidden />
|
|
35
|
+
</span>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function DashboardColumnHintsBanner({ dt }: Props) {
|
|
41
|
+
const [dismissed, setDismissed] = useState(false);
|
|
42
|
+
const [open, setOpen] = useState(true);
|
|
43
|
+
const headingId = useId();
|
|
44
|
+
const panelId = useId();
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (readDashboardColumnHintsDismissed()) {
|
|
48
|
+
setDismissed(true);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (readDashboardColumnHintsClosed()) {
|
|
52
|
+
setOpen(false);
|
|
53
|
+
}
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
const toggle = useCallback(() => {
|
|
57
|
+
setOpen((prev) => {
|
|
58
|
+
const next = !prev;
|
|
59
|
+
writeDashboardColumnHintsClosed(!next);
|
|
60
|
+
return next;
|
|
61
|
+
});
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
const dismiss = useCallback(() => {
|
|
65
|
+
writeDashboardColumnHintsDismissed(true);
|
|
66
|
+
setDismissed(true);
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
if (dismissed) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const cellClass =
|
|
74
|
+
"min-w-0 flex-1 rounded-lg border border-zinc-200/90 bg-white/80 px-3 py-3 shadow-sm dark:border-zinc-700/80 dark:bg-zinc-950/40 sm:px-4";
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<section
|
|
78
|
+
className="mb-4 rounded-lg border border-zinc-200 bg-zinc-50/90 shadow-sm dark:border-zinc-700/90 dark:bg-zinc-900/60"
|
|
79
|
+
aria-labelledby={headingId}
|
|
80
|
+
>
|
|
81
|
+
<div className="flex w-full items-stretch">
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
id={headingId}
|
|
85
|
+
className="flex min-w-0 flex-1 items-center gap-2.5 px-3 py-2.5 text-left transition-colors hover:bg-zinc-100/90 dark:hover:bg-zinc-800/50 sm:px-4"
|
|
86
|
+
aria-expanded={open ? "true" : "false"}
|
|
87
|
+
aria-controls={panelId}
|
|
88
|
+
aria-label={open ? dt.dashboardColumnHintsCollapseAria : dt.dashboardColumnHintsExpandAria}
|
|
89
|
+
onClick={toggle}
|
|
90
|
+
>
|
|
91
|
+
<ChevronRight
|
|
92
|
+
className={`size-4 shrink-0 text-zinc-500 transition-transform duration-200 dark:text-zinc-400 ${open ? "rotate-90" : ""}`}
|
|
93
|
+
strokeWidth={2}
|
|
94
|
+
aria-hidden
|
|
95
|
+
/>
|
|
96
|
+
<span className="min-w-0 text-sm font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">
|
|
97
|
+
{dt.dashboardColumnHintsPanelTitle}
|
|
98
|
+
</span>
|
|
99
|
+
</button>
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
className="flex shrink-0 items-center justify-center px-2.5 text-zinc-500 transition-colors hover:bg-zinc-100/90 hover:text-zinc-800 dark:hover:bg-zinc-800/50 dark:hover:text-zinc-200 sm:px-3"
|
|
103
|
+
title={dt.dashboardColumnHintsDismissTitle}
|
|
104
|
+
aria-label={dt.dashboardColumnHintsDismissTitle}
|
|
105
|
+
onClick={(e) => {
|
|
106
|
+
e.stopPropagation();
|
|
107
|
+
dismiss();
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
<X className="size-4" strokeWidth={2} aria-hidden />
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{open ? (
|
|
115
|
+
<div
|
|
116
|
+
id={panelId}
|
|
117
|
+
className="border-t border-zinc-200/90 px-3 pb-3 pt-3 dark:border-zinc-700/80 sm:px-4 sm:pb-4"
|
|
118
|
+
aria-labelledby={headingId}
|
|
119
|
+
>
|
|
120
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:items-stretch sm:gap-1 md:gap-2">
|
|
121
|
+
<div className={cellClass}>
|
|
122
|
+
<p className="text-xs font-bold uppercase tracking-wide text-violet-600 dark:text-violet-400">
|
|
123
|
+
{dt.sessionsColumnTitle}
|
|
124
|
+
</p>
|
|
125
|
+
<p className="mt-1.5 text-sm font-semibold leading-snug text-zinc-900 dark:text-zinc-100">
|
|
126
|
+
{dt.dashboardColSessionTeaser}
|
|
127
|
+
</p>
|
|
128
|
+
<p className="mt-1 text-xs font-medium text-zinc-600 dark:text-zinc-400">{dt.dashboardColSessionFlowHint}</p>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<FlowBetweenArrows />
|
|
132
|
+
|
|
133
|
+
<div className={cellClass}>
|
|
134
|
+
<p className="text-xs font-bold uppercase tracking-wide text-violet-600 dark:text-violet-400">
|
|
135
|
+
{dt.rightColumnTitle}
|
|
136
|
+
</p>
|
|
137
|
+
<p className="mt-1.5 text-sm font-semibold leading-snug text-zinc-900 dark:text-zinc-100">
|
|
138
|
+
{dt.dashboardColTasksTeaser}
|
|
139
|
+
</p>
|
|
140
|
+
<p className="mt-1 text-xs font-medium text-zinc-600 dark:text-zinc-400">{dt.dashboardColTasksFlowHint}</p>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<FlowBetweenArrows />
|
|
144
|
+
|
|
145
|
+
<div className={cellClass}>
|
|
146
|
+
<p className="text-xs font-bold uppercase tracking-wide text-violet-600 dark:text-violet-400">
|
|
147
|
+
{dt.tagsProjectsColumnTitle}
|
|
148
|
+
</p>
|
|
149
|
+
<p className="mt-1.5 text-sm font-semibold leading-snug text-zinc-900 dark:text-zinc-100">
|
|
150
|
+
{dt.dashboardColTagsTeaser}
|
|
151
|
+
</p>
|
|
152
|
+
<p className="mt-1 text-xs font-medium text-zinc-600 dark:text-zinc-400">{dt.dashboardColTagsFlowHint}</p>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
) : null}
|
|
157
|
+
</section>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { Keyboard, Search, X } from "lucide-react";
|
|
6
|
+
import { useKronosysTheme } from "@/components/ThemeProvider";
|
|
7
|
+
import type { DashboardStrings } from "@/lib/dashboardCopy";
|
|
8
|
+
import {
|
|
9
|
+
eventMatchesCombo,
|
|
10
|
+
eventOpensCommandPalette,
|
|
11
|
+
formatComboForDisplay,
|
|
12
|
+
isReservedPaletteCombo,
|
|
13
|
+
loadComboOverrides,
|
|
14
|
+
mergeCombos,
|
|
15
|
+
saveComboOverrides,
|
|
16
|
+
serializeEventAsCombo,
|
|
17
|
+
} from "@/lib/dashboardShortcuts";
|
|
18
|
+
import type { DashboardSearchItem } from "@/lib/dashboardQuickSearch";
|
|
19
|
+
import { dataSearchItemMatches } from "@/lib/dashboardQuickSearchQuery";
|
|
20
|
+
|
|
21
|
+
export type DashboardCommandHandlers = {
|
|
22
|
+
newSession: () => void;
|
|
23
|
+
refresh: () => void;
|
|
24
|
+
openReporting: () => void;
|
|
25
|
+
openSettings: () => void;
|
|
26
|
+
openUserGuide: () => void;
|
|
27
|
+
focusSessions: () => void;
|
|
28
|
+
focusTasks: () => void;
|
|
29
|
+
focusTags: () => void;
|
|
30
|
+
toggleLang: () => void;
|
|
31
|
+
endLiveSession?: () => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function isEditableTarget(n: EventTarget | null): boolean {
|
|
35
|
+
if (!n || !(n instanceof HTMLElement)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const tag = n.tagName;
|
|
39
|
+
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (n.isContentEditable) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return Boolean(n.closest("[contenteditable='true']"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const DATA_SEARCH_TIMER_BASE =
|
|
49
|
+
"block w-full min-w-0 truncate text-[0.7rem] leading-snug tabular-nums";
|
|
50
|
+
|
|
51
|
+
function classForDataSearchTimer(
|
|
52
|
+
state: DashboardSearchItem["searchTimerState"]
|
|
53
|
+
): string {
|
|
54
|
+
if (state === "active") {
|
|
55
|
+
return `${DATA_SEARCH_TIMER_BASE} text-emerald-600 dark:text-emerald-400/95`;
|
|
56
|
+
}
|
|
57
|
+
if (state === "paused") {
|
|
58
|
+
return `${DATA_SEARCH_TIMER_BASE} text-amber-600 dark:text-amber-400/90`;
|
|
59
|
+
}
|
|
60
|
+
return `${DATA_SEARCH_TIMER_BASE} text-zinc-500 dark:text-zinc-400`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function DashboardCommandCenter({
|
|
64
|
+
dt,
|
|
65
|
+
handlers,
|
|
66
|
+
searchItems,
|
|
67
|
+
toolbarDomId,
|
|
68
|
+
}: {
|
|
69
|
+
dt: DashboardStrings;
|
|
70
|
+
handlers: DashboardCommandHandlers;
|
|
71
|
+
searchItems: DashboardSearchItem[];
|
|
72
|
+
/** Ancre pour la visite guidée (spotlight). */
|
|
73
|
+
toolbarDomId?: string;
|
|
74
|
+
}) {
|
|
75
|
+
const { toggleTheme } = useKronosysTheme();
|
|
76
|
+
const [paletteOpen, setPaletteOpen] = useState(false);
|
|
77
|
+
const [query, setQuery] = useState("");
|
|
78
|
+
const [highlight, setHighlight] = useState(0);
|
|
79
|
+
const [shortcutsOpen, setShortcutsOpen] = useState(false);
|
|
80
|
+
const [overrides, setOverrides] = useState<Record<string, string>>({});
|
|
81
|
+
const [recordingId, setRecordingId] = useState<string | null>(null);
|
|
82
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
setOverrides(loadComboOverrides());
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
const combos = useMemo(() => mergeCombos(overrides), [overrides]);
|
|
89
|
+
|
|
90
|
+
const isMac =
|
|
91
|
+
typeof navigator !== "undefined" && /mac|iphone|ipad|ipod/i.test(navigator.platform ?? "");
|
|
92
|
+
|
|
93
|
+
const actions = useMemo(() => {
|
|
94
|
+
const list: Array<{ id: string; label: string; kw: string; run: () => void }> = [
|
|
95
|
+
{ id: "new-session", label: dt.commandNewSession, kw: "new session nouvelle", run: handlers.newSession },
|
|
96
|
+
{ id: "refresh", label: dt.commandRefresh, kw: "reload actualiser rafraîchir", run: handlers.refresh },
|
|
97
|
+
{ id: "reporting", label: dt.commandOpenReporting, kw: "reporting graph bilan stats", run: handlers.openReporting },
|
|
98
|
+
{ id: "settings", label: dt.commandOpenSettings, kw: "settings paramètres", run: handlers.openSettings },
|
|
99
|
+
{
|
|
100
|
+
id: "user-guide",
|
|
101
|
+
label: dt.commandOpenUserGuide,
|
|
102
|
+
kw: "help aide manuel guide book livre",
|
|
103
|
+
run: handlers.openUserGuide,
|
|
104
|
+
},
|
|
105
|
+
{ id: "focus-sessions", label: dt.commandFocusSessions, kw: "sessions colonne liste", run: handlers.focusSessions },
|
|
106
|
+
{ id: "focus-tasks", label: dt.commandFocusTasks, kw: "tasks tâches", run: handlers.focusTasks },
|
|
107
|
+
{ id: "focus-tags", label: dt.commandFocusTags, kw: "tags étiquettes projects projets", run: handlers.focusTags },
|
|
108
|
+
{
|
|
109
|
+
id: "toggle-theme",
|
|
110
|
+
label: dt.commandToggleTheme,
|
|
111
|
+
kw: "theme dark light sombre clair",
|
|
112
|
+
run: () => toggleTheme(),
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "toggle-lang",
|
|
116
|
+
label: dt.commandToggleLang,
|
|
117
|
+
kw: "language langue fr en",
|
|
118
|
+
run: handlers.toggleLang,
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
if (handlers.endLiveSession) {
|
|
122
|
+
list.push({
|
|
123
|
+
id: "end-live-session",
|
|
124
|
+
label: dt.commandEndLiveSession,
|
|
125
|
+
kw: "end terminer session live",
|
|
126
|
+
run: handlers.endLiveSession,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return list;
|
|
130
|
+
}, [dt, handlers, toggleTheme]);
|
|
131
|
+
|
|
132
|
+
const filtered = useMemo(() => {
|
|
133
|
+
const q = query.trim();
|
|
134
|
+
if (!q) {
|
|
135
|
+
return [] as DashboardSearchItem[];
|
|
136
|
+
}
|
|
137
|
+
return searchItems.filter((item) => dataSearchItemMatches(item, q));
|
|
138
|
+
}, [query, searchItems]);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
setHighlight(0);
|
|
142
|
+
}, [query, paletteOpen]);
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
setHighlight((i) => Math.min(i, Math.max(0, filtered.length - 1)));
|
|
146
|
+
}, [filtered.length, paletteOpen]);
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (paletteOpen) {
|
|
150
|
+
const t = window.setTimeout(() => inputRef.current?.focus(), 30);
|
|
151
|
+
return () => window.clearTimeout(t);
|
|
152
|
+
}
|
|
153
|
+
setQuery("");
|
|
154
|
+
return undefined;
|
|
155
|
+
}, [paletteOpen]);
|
|
156
|
+
|
|
157
|
+
const openPalette = useCallback(() => {
|
|
158
|
+
setPaletteOpen(true);
|
|
159
|
+
}, []);
|
|
160
|
+
|
|
161
|
+
const closePalette = useCallback(() => {
|
|
162
|
+
setPaletteOpen(false);
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
165
|
+
const runSearchRow = useCallback(
|
|
166
|
+
(index: number) => {
|
|
167
|
+
const row = filtered[index];
|
|
168
|
+
if (!row) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
row.onSelect();
|
|
172
|
+
closePalette();
|
|
173
|
+
},
|
|
174
|
+
[closePalette, filtered]
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
const onKey = (e: KeyboardEvent) => {
|
|
179
|
+
if (recordingId && shortcutsOpen) {
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
e.stopPropagation();
|
|
182
|
+
if (e.key === "Escape") {
|
|
183
|
+
setRecordingId(null);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const ser = serializeEventAsCombo(e);
|
|
187
|
+
if (!ser || isReservedPaletteCombo(ser)) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
setOverrides((prev) => {
|
|
191
|
+
const next = { ...prev, [recordingId]: ser };
|
|
192
|
+
saveComboOverrides(next);
|
|
193
|
+
return next;
|
|
194
|
+
});
|
|
195
|
+
setRecordingId(null);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (eventOpensCommandPalette(e)) {
|
|
200
|
+
if (shortcutsOpen) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (isEditableTarget(e.target)) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
e.preventDefault();
|
|
207
|
+
setPaletteOpen((o) => !o);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (paletteOpen) {
|
|
212
|
+
if (e.key === "Escape") {
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
closePalette();
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (shortcutsOpen || isEditableTarget(e.target)) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const [id, combo] of Object.entries(combos)) {
|
|
224
|
+
if (!combo?.trim()) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (id === "end-live-session" && !handlers.endLiveSession) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (eventMatchesCombo(e, combo)) {
|
|
231
|
+
const a = actions.find((x) => x.id === id);
|
|
232
|
+
if (a) {
|
|
233
|
+
e.preventDefault();
|
|
234
|
+
a.run();
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
window.addEventListener("keydown", onKey, true);
|
|
241
|
+
return () => window.removeEventListener("keydown", onKey, true);
|
|
242
|
+
}, [actions, closePalette, combos, handlers, paletteOpen, recordingId, shortcutsOpen]);
|
|
243
|
+
|
|
244
|
+
const onPaletteKeyDown = (e: React.KeyboardEvent) => {
|
|
245
|
+
if (e.key === "ArrowDown") {
|
|
246
|
+
e.preventDefault();
|
|
247
|
+
setHighlight((i) => Math.min(i + 1, Math.max(0, filtered.length - 1)));
|
|
248
|
+
} else if (e.key === "ArrowUp") {
|
|
249
|
+
e.preventDefault();
|
|
250
|
+
setHighlight((i) => Math.max(0, i - 1));
|
|
251
|
+
} else if (e.key === "Enter") {
|
|
252
|
+
e.preventDefault();
|
|
253
|
+
runSearchRow(highlight);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const resetBindings = () => {
|
|
258
|
+
setOverrides({});
|
|
259
|
+
saveComboOverrides({});
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const palette = paletteOpen
|
|
263
|
+
? createPortal(
|
|
264
|
+
<div
|
|
265
|
+
className="fixed inset-0 z-[200] flex items-start justify-center bg-black/50 p-4 pt-[min(20vh,8rem)]"
|
|
266
|
+
role="dialog"
|
|
267
|
+
aria-modal="true"
|
|
268
|
+
aria-label={dt.dataSearchPaletteAriaLabel}
|
|
269
|
+
data-dash-palette=""
|
|
270
|
+
onMouseDown={(ev) => {
|
|
271
|
+
if (ev.target === ev.currentTarget) {
|
|
272
|
+
closePalette();
|
|
273
|
+
}
|
|
274
|
+
}}
|
|
275
|
+
>
|
|
276
|
+
<div
|
|
277
|
+
className="w-full max-w-lg rounded-xl border border-zinc-300 bg-white shadow-2xl dark:border-zinc-600 dark:bg-zinc-900"
|
|
278
|
+
onKeyDown={onPaletteKeyDown}
|
|
279
|
+
>
|
|
280
|
+
<div className="flex items-center gap-2 border-b border-zinc-200 px-3 py-2 dark:border-zinc-700">
|
|
281
|
+
<Search className="shrink-0 text-zinc-400" size={18} aria-hidden />
|
|
282
|
+
<input
|
|
283
|
+
ref={inputRef}
|
|
284
|
+
type="search"
|
|
285
|
+
className="min-w-0 flex-1 border-0 bg-transparent py-1 text-sm text-zinc-900 outline-none dark:text-zinc-100"
|
|
286
|
+
placeholder={dt.dataSearchPlaceholder}
|
|
287
|
+
value={query}
|
|
288
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
289
|
+
aria-label={dt.dataSearchPlaceholder}
|
|
290
|
+
aria-controls="dash-palette-results"
|
|
291
|
+
/>
|
|
292
|
+
<kbd className="hidden shrink-0 rounded border border-zinc-300 px-1.5 py-0.5 font-mono text-[0.65rem] text-zinc-500 sm:inline dark:border-zinc-600">
|
|
293
|
+
{isMac ? "⌘K" : "Ctrl+K"}
|
|
294
|
+
</kbd>
|
|
295
|
+
</div>
|
|
296
|
+
<ul id="dash-palette-results" className="max-h-[min(50vh,22rem)] list-none overflow-y-auto py-1">
|
|
297
|
+
{query.trim() === "" ? (
|
|
298
|
+
<li className="px-3 py-4 text-center text-sm text-zinc-500">{dt.dataSearchTypePrompt}</li>
|
|
299
|
+
) : filtered.length === 0 ? (
|
|
300
|
+
<li className="px-3 py-4 text-center text-sm text-zinc-500">{dt.dataSearchEmpty}</li>
|
|
301
|
+
) : (
|
|
302
|
+
filtered.map((row, idx) => (
|
|
303
|
+
<li key={row.id} className="list-none">
|
|
304
|
+
<button
|
|
305
|
+
type="button"
|
|
306
|
+
className={`flex w-full flex-col gap-0.5 px-3 py-2 text-left text-sm ${
|
|
307
|
+
idx === highlight
|
|
308
|
+
? "bg-violet-500/15 text-violet-950 dark:bg-violet-500/20 dark:text-violet-100"
|
|
309
|
+
: "text-zinc-800 hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-800"
|
|
310
|
+
}`}
|
|
311
|
+
onMouseEnter={() => setHighlight(idx)}
|
|
312
|
+
onClick={() => runSearchRow(idx)}
|
|
313
|
+
>
|
|
314
|
+
<span className="flex w-full min-w-0 items-center justify-between gap-2">
|
|
315
|
+
<span className="min-w-0 truncate font-medium">{row.title}</span>
|
|
316
|
+
{row.subtitle ? (
|
|
317
|
+
<span className="shrink-0 text-[0.65rem] uppercase tracking-wide text-zinc-500 dark:text-zinc-400">
|
|
318
|
+
{row.subtitle}
|
|
319
|
+
</span>
|
|
320
|
+
) : null}
|
|
321
|
+
</span>
|
|
322
|
+
{row.searchTimerLine ? (
|
|
323
|
+
<span className={classForDataSearchTimer(row.searchTimerState)}>
|
|
324
|
+
{row.searchTimerLine}
|
|
325
|
+
</span>
|
|
326
|
+
) : null}
|
|
327
|
+
{row.metaLine ? (
|
|
328
|
+
<span className="block w-full min-w-0 truncate text-[0.7rem] leading-snug text-zinc-500 dark:text-zinc-400">
|
|
329
|
+
{row.metaLine}
|
|
330
|
+
</span>
|
|
331
|
+
) : null}
|
|
332
|
+
</button>
|
|
333
|
+
</li>
|
|
334
|
+
))
|
|
335
|
+
)}
|
|
336
|
+
</ul>
|
|
337
|
+
<p className="border-t border-zinc-200 px-3 py-2 text-[0.65rem] text-zinc-500 dark:border-zinc-700 dark:text-zinc-400">
|
|
338
|
+
{dt.dataSearchFooter}
|
|
339
|
+
</p>
|
|
340
|
+
</div>
|
|
341
|
+
</div>,
|
|
342
|
+
document.body
|
|
343
|
+
)
|
|
344
|
+
: null;
|
|
345
|
+
|
|
346
|
+
const shortcutsModal = shortcutsOpen
|
|
347
|
+
? createPortal(
|
|
348
|
+
<div
|
|
349
|
+
className="fixed inset-0 z-[190] flex items-center justify-center bg-black/50 p-4"
|
|
350
|
+
role="dialog"
|
|
351
|
+
aria-modal="true"
|
|
352
|
+
aria-labelledby="dash-shortcuts-title"
|
|
353
|
+
onMouseDown={(ev) => {
|
|
354
|
+
if (ev.target === ev.currentTarget) {
|
|
355
|
+
setShortcutsOpen(false);
|
|
356
|
+
setRecordingId(null);
|
|
357
|
+
}
|
|
358
|
+
}}
|
|
359
|
+
>
|
|
360
|
+
<div className="max-h-[min(90vh,36rem)] w-full max-w-lg overflow-hidden rounded-xl border border-zinc-300 bg-white shadow-2xl dark:border-zinc-600 dark:bg-zinc-900">
|
|
361
|
+
<div className="flex items-center justify-between border-b border-zinc-200 px-4 py-3 dark:border-zinc-700">
|
|
362
|
+
<h2 id="dash-shortcuts-title" className="text-base font-semibold text-zinc-900 dark:text-zinc-100">
|
|
363
|
+
{dt.shortcutsModalTitle}
|
|
364
|
+
</h2>
|
|
365
|
+
<button
|
|
366
|
+
type="button"
|
|
367
|
+
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:text-zinc-400 dark:hover:border-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-100"
|
|
368
|
+
onClick={() => {
|
|
369
|
+
setShortcutsOpen(false);
|
|
370
|
+
setRecordingId(null);
|
|
371
|
+
}}
|
|
372
|
+
aria-label={dt.shortcutsModalCloseAriaLabel}
|
|
373
|
+
>
|
|
374
|
+
<X size={20} strokeWidth={2} aria-hidden />
|
|
375
|
+
</button>
|
|
376
|
+
</div>
|
|
377
|
+
<p className="border-b border-zinc-200 px-4 py-2 text-xs text-zinc-600 dark:border-zinc-700 dark:text-zinc-400">
|
|
378
|
+
{dt.shortcutsModalIntro}
|
|
379
|
+
</p>
|
|
380
|
+
<div className="max-h-[min(60vh,28rem)] overflow-y-auto px-2 py-2">
|
|
381
|
+
<table className="w-full text-left text-sm">
|
|
382
|
+
<thead>
|
|
383
|
+
<tr className="text-[0.65rem] uppercase tracking-wide text-zinc-500">
|
|
384
|
+
<th className="px-2 py-1.5">{dt.shortcutsTableAction}</th>
|
|
385
|
+
<th className="px-2 py-1.5">{dt.shortcutsTableShortcut}</th>
|
|
386
|
+
<th className="px-2 py-1.5">{dt.shortcutsTableAssign}</th>
|
|
387
|
+
</tr>
|
|
388
|
+
</thead>
|
|
389
|
+
<tbody>
|
|
390
|
+
{actions
|
|
391
|
+
.filter((a) => a.id !== "end-live-session" || handlers.endLiveSession)
|
|
392
|
+
.map((a) => {
|
|
393
|
+
const combo = combos[a.id] ?? "";
|
|
394
|
+
const recording = recordingId === a.id;
|
|
395
|
+
return (
|
|
396
|
+
<tr key={a.id} className="border-t border-zinc-100 dark:border-zinc-800">
|
|
397
|
+
<td className="px-2 py-2 text-zinc-800 dark:text-zinc-200">{a.label}</td>
|
|
398
|
+
<td className="px-2 py-2 font-mono text-xs text-zinc-600 dark:text-zinc-400">
|
|
399
|
+
{recording ? (
|
|
400
|
+
<span className="text-violet-600 dark:text-violet-300">{dt.shortcutsRecordPrompt}</span>
|
|
401
|
+
) : (
|
|
402
|
+
formatComboForDisplay(combo, isMac)
|
|
403
|
+
)}
|
|
404
|
+
</td>
|
|
405
|
+
<td className="px-2 py-2">
|
|
406
|
+
<button
|
|
407
|
+
type="button"
|
|
408
|
+
className="rounded border border-zinc-400 px-2 py-0.5 text-xs font-medium text-zinc-700 hover:bg-zinc-100 dark:border-zinc-600 dark:text-zinc-300 dark:hover:bg-zinc-800"
|
|
409
|
+
onClick={() => setRecordingId(a.id)}
|
|
410
|
+
>
|
|
411
|
+
{dt.shortcutsChangeBtn}
|
|
412
|
+
</button>
|
|
413
|
+
</td>
|
|
414
|
+
</tr>
|
|
415
|
+
);
|
|
416
|
+
})}
|
|
417
|
+
<tr className="border-t border-zinc-200 dark:border-zinc-700">
|
|
418
|
+
<td colSpan={3} className="px-2 py-2 text-xs text-zinc-500">
|
|
419
|
+
{dt.shortcutsPaletteRow}
|
|
420
|
+
</td>
|
|
421
|
+
</tr>
|
|
422
|
+
</tbody>
|
|
423
|
+
</table>
|
|
424
|
+
</div>
|
|
425
|
+
<div className="flex flex-wrap justify-end gap-2 border-t border-zinc-200 px-4 py-3 dark:border-zinc-700">
|
|
426
|
+
<button
|
|
427
|
+
type="button"
|
|
428
|
+
className="rounded-lg border border-zinc-400 px-3 py-1.5 text-xs font-medium text-zinc-700 hover:bg-zinc-100 dark:border-zinc-600 dark:text-zinc-300 dark:hover:bg-zinc-800"
|
|
429
|
+
onClick={resetBindings}
|
|
430
|
+
>
|
|
431
|
+
{dt.shortcutsResetAll}
|
|
432
|
+
</button>
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
</div>,
|
|
436
|
+
document.body
|
|
437
|
+
)
|
|
438
|
+
: null;
|
|
439
|
+
|
|
440
|
+
return (
|
|
441
|
+
<>
|
|
442
|
+
<div id={toolbarDomId} className="flex shrink-0 items-center gap-1.5">
|
|
443
|
+
<button
|
|
444
|
+
type="button"
|
|
445
|
+
className="inline-flex h-10 w-[9.5rem] max-w-[min(42vw,11rem)] shrink-0 items-center gap-1.5 rounded-lg border border-zinc-300 bg-white px-2 text-left text-xs text-zinc-500 shadow-sm transition hover:border-zinc-400 hover:bg-zinc-50 sm:w-44 sm:max-w-none sm:px-2.5 sm:text-sm dark:border-zinc-600 dark:bg-zinc-800/90 dark:text-zinc-400 dark:hover:border-zinc-500 dark:hover:bg-zinc-800"
|
|
446
|
+
onClick={openPalette}
|
|
447
|
+
aria-haspopup="dialog"
|
|
448
|
+
aria-expanded={paletteOpen ? "true" : "false"}
|
|
449
|
+
>
|
|
450
|
+
<Search className="shrink-0" size={16} aria-hidden />
|
|
451
|
+
<span className="min-w-0 flex-1 truncate">{dt.dataSearchTrigger}</span>
|
|
452
|
+
<kbd className="hidden shrink-0 rounded border border-zinc-300 px-1 py-0.5 font-mono text-[0.6rem] text-zinc-500 sm:inline dark:border-zinc-600">
|
|
453
|
+
{isMac ? "⌘K" : "Ctrl+K"}
|
|
454
|
+
</kbd>
|
|
455
|
+
</button>
|
|
456
|
+
<button
|
|
457
|
+
type="button"
|
|
458
|
+
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 dark:border-zinc-600 dark:bg-zinc-800/90 dark:text-zinc-200 dark:hover:border-zinc-500 dark:hover:bg-zinc-800"
|
|
459
|
+
onClick={() => setShortcutsOpen(true)}
|
|
460
|
+
title={dt.shortcutsModalTitle}
|
|
461
|
+
aria-label={dt.shortcutsModalTitle}
|
|
462
|
+
>
|
|
463
|
+
<Keyboard size={18} aria-hidden />
|
|
464
|
+
</button>
|
|
465
|
+
</div>
|
|
466
|
+
{palette}
|
|
467
|
+
{shortcutsModal}
|
|
468
|
+
</>
|
|
469
|
+
);
|
|
470
|
+
}
|