@nightkatana/kronosys-app 1.0.0-beta.2 → 1.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -1
- package/app/api/action/route.ts +39 -3
- package/app/api/action-logs/route.ts +24 -0
- package/app/api/backup/route.ts +1 -1
- package/app/api/restore/route.ts +145 -0
- package/app/changelog/page.tsx +71 -4
- package/app/globals.css +127 -0
- package/app/guide/page.tsx +61 -15
- package/app/implementation/page.tsx +700 -0
- package/app/layout.tsx +14 -3
- package/app/licenses/page.tsx +99 -37
- package/app/logs/page.tsx +258 -0
- package/app/manifest.ts +5 -5
- package/app/page.tsx +784 -229
- package/app/reporting/page.tsx +1266 -474
- package/app/settings/page.tsx +252 -18
- package/bin/kronosys.mjs +140 -15
- package/components/KronosysPayloadProvider.tsx +2 -0
- package/components/RouteTransition.tsx +18 -0
- package/components/dashboard/AppShellCommandCenterPlaceholder.tsx +17 -0
- package/components/dashboard/AppShellHeaderSessionMeta.tsx +210 -0
- package/components/dashboard/AppShellHeaderWallClock.tsx +54 -0
- package/components/dashboard/AppShellLiveSessionDrawer.tsx +154 -38
- package/components/dashboard/AppShellRouteNav.tsx +323 -48
- package/components/dashboard/DashboardPauseBackdrop.tsx +50 -0
- package/components/dashboard/DashboardSimpleModal.tsx +168 -25
- package/components/dashboard/DashboardTour.tsx +115 -29
- package/components/dashboard/GlobalPauseConfirmModal.tsx +183 -0
- package/components/dashboard/KronosysDatetimePopoverField.tsx +167 -122
- package/components/dashboard/KronosysTimePopoverField.tsx +54 -12
- package/components/dashboard/NewSessionScopeModal.tsx +211 -20
- package/components/dashboard/PlannedTaskBoundaryConflictWatcher.tsx +275 -0
- package/components/dashboard/ReportingTour.tsx +87 -21
- package/components/dashboard/SavedProjectPicker.tsx +16 -3
- package/components/dashboard/SelectedSessionSidebarBlock.tsx +512 -142
- package/components/dashboard/SessionListPanel.tsx +327 -44
- package/components/dashboard/SettingsTagsProjectsSection.tsx +1073 -264
- package/components/dashboard/SettingsTaskTemplatesSection.tsx +316 -0
- package/components/dashboard/SettingsTour.tsx +86 -21
- package/components/dashboard/TagPills.tsx +14 -1
- package/components/dashboard/TaskFocusPanel.tsx +1081 -478
- package/components/dashboard/TaskSessionLiveCard.tsx +650 -135
- package/components/dashboard/TaskTimelineGanttModal.tsx +601 -0
- package/components/dashboard/taskFieldStyles.ts +20 -4
- package/components/dashboard/useReportingInteractionState.ts +80 -0
- package/lib/appShellHeaderClasses.ts +13 -0
- package/lib/businessRulesMatrix.ts +210 -0
- package/lib/copyToClipboard.ts +43 -0
- package/lib/dashboardCopy.ts +494 -84
- package/lib/dashboardQuickSearch.ts +54 -2
- package/lib/dashboardTimeZone.ts +109 -0
- package/lib/formatAppShellWallClock.ts +66 -0
- package/lib/formatSessionNameTemplate.ts +141 -0
- package/lib/generatedUserChangelog.ts +177 -6
- package/lib/globalPausePreview.ts +292 -0
- package/lib/implementationNotes.ts +1188 -0
- package/lib/kronosysApi.ts +6 -0
- package/lib/kronosysDashboardModalGates.ts +24 -0
- package/lib/plannedBoundaryAttention.ts +9 -0
- package/lib/plannedBoundaryConflict.ts +23 -0
- package/lib/reportingAggregate.ts +517 -75
- package/lib/reportingMetricHelp.ts +8 -0
- package/lib/reportingStrings.ts +37 -3
- package/lib/sessionListMerge.ts +4 -0
- package/lib/sessionTaskSidebarStats.ts +182 -21
- package/lib/settingsCopy.ts +178 -4
- package/lib/taskParsing.ts +360 -103
- package/lib/taskTemplateDraft.ts +135 -0
- package/lib/taskTimelineGantt.ts +265 -0
- package/lib/temporalDisplayPlanned.ts +71 -0
- package/lib/userGuideCopy.ts +121 -47
- package/next.config.ts +7 -0
- package/package.json +12 -24
- package/server/actionDispatch.ts +1000 -77
- package/server/actionTaskSession.ts +337 -24
- package/server/db.ts +7 -15
- package/server/dbSchema.ts +24 -0
- package/server/defaultCfg.ts +5 -0
- package/server/gitlabTokenStore.ts +0 -12
- package/server/liveHistorySync.ts +53 -0
- package/server/mainTimerHydrate.ts +38 -2
- package/server/payloadStore.ts +33 -11
- package/server/sessionWallHydrate.ts +66 -3
- package/server/userActionLog.ts +126 -0
- package/sonar-project.properties +11 -0
- package/tsconfig.json +2 -1
- package/components/dashboard/IssuePickerModal.tsx +0 -168
- package/components/dashboard/ThemeToggle.test.tsx +0 -26
- package/lib/backupCsvExport.test.ts +0 -149
- package/lib/dashboardQuickSearchQuery.test.ts +0 -63
- package/lib/dataDir.test.ts +0 -87
- package/lib/formatIsoShort.test.ts +0 -46
- package/lib/kronoFocusRhythm.test.ts +0 -130
- package/lib/kronoFocusTimerUrgency.test.ts +0 -74
- package/lib/legacyKronoFocusStorageKeys.test.ts +0 -29
- package/lib/reportingAggregate.test.ts +0 -325
- package/lib/reportingNonFinalIndicators.test.ts +0 -157
- package/lib/reportingTagWeekBreakdown.test.ts +0 -141
- package/lib/reportingWeekLayout.test.ts +0 -239
- package/lib/sessionAssiduity.test.ts +0 -25
- package/lib/sessionEndWarnings.test.ts +0 -200
- package/lib/sessionListMerge.test.ts +0 -101
- package/lib/sessionTaskSidebarStats.test.ts +0 -24
- package/lib/taskParsing.test.ts +0 -153
- package/lib/usageProfile.test.ts +0 -84
- package/server/actionDispatch.test.ts +0 -723
- package/server/actionTaskSession.test.ts +0 -713
- package/server/kronoFocusHydrate.test.ts +0 -142
- package/server/kronoFocusMigrate.test.ts +0 -53
- package/server/mainTimerHydrate.test.ts +0 -65
- package/server/payloadStore.test.ts +0 -78
- package/server/sessionWallHydrate.test.ts +0 -46
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import { useKronosysPayload } from "@/components/KronosysPayloadProvider";
|
|
6
|
+
import { useDashboardToast } from "@/components/dashboard/DashboardToastProvider";
|
|
7
|
+
import { DashboardTriActionModal } from "@/components/dashboard/DashboardSimpleModal";
|
|
8
|
+
import { postKronosysAction } from "@/lib/kronosysApi";
|
|
9
|
+
import {
|
|
10
|
+
readConcurrentTaskStartPreference,
|
|
11
|
+
writeConcurrentTaskStartPreference,
|
|
12
|
+
type ConcurrentTaskStartPreference,
|
|
13
|
+
} from "@/lib/concurrentTaskStartPreference";
|
|
14
|
+
import { dashboardStrings, type Lang } from "@/lib/dashboardCopy";
|
|
15
|
+
import { readStoredDashboardLang } from "@/lib/dashboardLangStorage";
|
|
16
|
+
import {
|
|
17
|
+
isConcurrentTaskStartConflictModalOpen,
|
|
18
|
+
setPlannedBoundaryConflictModalOpen,
|
|
19
|
+
} from "@/lib/kronosysDashboardModalGates";
|
|
20
|
+
import { dispatchPlannedBoundaryAttention } from "@/lib/plannedBoundaryAttention";
|
|
21
|
+
import { detectPlannedBoundaryDisplayConflict } from "@/lib/plannedBoundaryConflict";
|
|
22
|
+
import { isTaskDisplayPlanned } from "@/lib/temporalDisplayPlanned";
|
|
23
|
+
|
|
24
|
+
type TaskRow = {
|
|
25
|
+
id: string;
|
|
26
|
+
isDone?: boolean;
|
|
27
|
+
manualTaskTimerPaused?: boolean;
|
|
28
|
+
startTime?: string;
|
|
29
|
+
endTime?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type LiveSessionShape = {
|
|
33
|
+
sessionId?: string;
|
|
34
|
+
endAt?: string | null;
|
|
35
|
+
isPaused?: boolean;
|
|
36
|
+
language?: string;
|
|
37
|
+
globalPauseContext?: unknown;
|
|
38
|
+
activeTasks?: TaskRow[];
|
|
39
|
+
activeTask?: TaskRow | null;
|
|
40
|
+
tasks?: TaskRow[];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function runningTasksFromSession(
|
|
44
|
+
session: LiveSessionShape | null | undefined,
|
|
45
|
+
): TaskRow[] {
|
|
46
|
+
if (!session) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const raw =
|
|
50
|
+
Array.isArray(session.activeTasks) && session.activeTasks.length > 0
|
|
51
|
+
? session.activeTasks
|
|
52
|
+
: session.activeTask
|
|
53
|
+
? [session.activeTask]
|
|
54
|
+
: [];
|
|
55
|
+
return raw.filter((t) => t && !t.isDone && !t.manualTaskTimerPaused);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function displayRunningIds(
|
|
59
|
+
session: LiveSessionShape | null | undefined,
|
|
60
|
+
nowMs: number,
|
|
61
|
+
): Set<string> {
|
|
62
|
+
return new Set(
|
|
63
|
+
runningTasksFromSession(session)
|
|
64
|
+
.filter((t) => !isTaskDisplayPlanned(t, nowMs))
|
|
65
|
+
.map((t) => String(t.id)),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function shouldDispatchAttention(pathname: string): boolean {
|
|
70
|
+
if (typeof document === "undefined") {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const offDashboard = pathname !== "/" && pathname !== "";
|
|
74
|
+
const hidden = document.visibilityState === "hidden";
|
|
75
|
+
return offDashboard || hidden;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function PlannedTaskBoundaryConflictWatcher() {
|
|
79
|
+
const { payload, refresh, getLatestPayload } = useKronosysPayload();
|
|
80
|
+
const { pushToast } = useDashboardToast();
|
|
81
|
+
const pathname = usePathname() ?? "";
|
|
82
|
+
const [bucketNowMs, setBucketNowMs] = useState(() => Date.now());
|
|
83
|
+
const [boundaryModalOpen, setBoundaryModalOpen] = useState(false);
|
|
84
|
+
const [rememberBoundaryChoice, setRememberBoundaryChoice] = useState(false);
|
|
85
|
+
const rememberBoundaryRef = useRef(false);
|
|
86
|
+
const prevDisplayRunningIdsRef = useRef<Set<string> | null>(null);
|
|
87
|
+
const boundaryTargetsRef = useRef<string[]>([]);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
rememberBoundaryRef.current = rememberBoundaryChoice;
|
|
91
|
+
}, [rememberBoundaryChoice]);
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
const id = globalThis.setInterval(() => {
|
|
95
|
+
setBucketNowMs(Date.now());
|
|
96
|
+
}, 1000);
|
|
97
|
+
return () => globalThis.clearInterval(id);
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
const live = payload?.current as LiveSessionShape | undefined;
|
|
101
|
+
const storedLang = readStoredDashboardLang();
|
|
102
|
+
const serverLang: Lang = live?.language === "fr" ? "fr" : "en";
|
|
103
|
+
const lang: Lang = storedLang ?? serverLang;
|
|
104
|
+
const t = dashboardStrings(lang);
|
|
105
|
+
|
|
106
|
+
const syncPrevFromPayload = useCallback(() => {
|
|
107
|
+
const p = getLatestPayload();
|
|
108
|
+
const sess = p?.current as LiveSessionShape | undefined;
|
|
109
|
+
prevDisplayRunningIdsRef.current = displayRunningIds(sess, Date.now());
|
|
110
|
+
}, [getLatestPayload]);
|
|
111
|
+
|
|
112
|
+
const resolveBoundaryConflict = useCallback(
|
|
113
|
+
async (mode: ConcurrentTaskStartPreference, persistPreference: boolean) => {
|
|
114
|
+
if (persistPreference) {
|
|
115
|
+
writeConcurrentTaskStartPreference(mode);
|
|
116
|
+
}
|
|
117
|
+
const targets = boundaryTargetsRef.current;
|
|
118
|
+
setBoundaryModalOpen(false);
|
|
119
|
+
setRememberBoundaryChoice(false);
|
|
120
|
+
try {
|
|
121
|
+
if (mode !== "parallel") {
|
|
122
|
+
for (const taskId of targets) {
|
|
123
|
+
if (mode === "pause") {
|
|
124
|
+
await postKronosysAction({
|
|
125
|
+
type: "setTaskTimerPaused",
|
|
126
|
+
taskId,
|
|
127
|
+
paused: true,
|
|
128
|
+
});
|
|
129
|
+
} else {
|
|
130
|
+
await postKronosysAction({
|
|
131
|
+
type: "finishTask",
|
|
132
|
+
taskId,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
await refresh();
|
|
138
|
+
} catch (error: unknown) {
|
|
139
|
+
pushToast(
|
|
140
|
+
typeof error === "string"
|
|
141
|
+
? error
|
|
142
|
+
: error instanceof Error
|
|
143
|
+
? error.message
|
|
144
|
+
: t.taskPlannedBoundaryConflictResolveError,
|
|
145
|
+
);
|
|
146
|
+
} finally {
|
|
147
|
+
syncPrevFromPayload();
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
[
|
|
151
|
+
pushToast,
|
|
152
|
+
refresh,
|
|
153
|
+
syncPrevFromPayload,
|
|
154
|
+
t.taskPlannedBoundaryConflictResolveError,
|
|
155
|
+
],
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const cancelBoundaryModal = useCallback(() => {
|
|
159
|
+
setBoundaryModalOpen(false);
|
|
160
|
+
setRememberBoundaryChoice(false);
|
|
161
|
+
syncPrevFromPayload();
|
|
162
|
+
}, [syncPrevFromPayload]);
|
|
163
|
+
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
setPlannedBoundaryConflictModalOpen(boundaryModalOpen);
|
|
166
|
+
return () => setPlannedBoundaryConflictModalOpen(false);
|
|
167
|
+
}, [boundaryModalOpen]);
|
|
168
|
+
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (boundaryModalOpen) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (isConcurrentTaskStartConflictModalOpen()) {
|
|
174
|
+
prevDisplayRunningIdsRef.current = displayRunningIds(live, bucketNowMs);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (!live || typeof live.sessionId !== "string" || !live.sessionId.trim()) {
|
|
178
|
+
prevDisplayRunningIdsRef.current = displayRunningIds(live, bucketNowMs);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (live.endAt && String(live.endAt).trim() !== "") {
|
|
182
|
+
prevDisplayRunningIdsRef.current = displayRunningIds(live, bucketNowMs);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (live.isPaused === true) {
|
|
186
|
+
prevDisplayRunningIdsRef.current = displayRunningIds(live, bucketNowMs);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (live.globalPauseContext) {
|
|
190
|
+
prevDisplayRunningIdsRef.current = displayRunningIds(live, bucketNowMs);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (payload?.inspectingSessionId) {
|
|
194
|
+
prevDisplayRunningIdsRef.current = displayRunningIds(live, bucketNowMs);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const curr = displayRunningIds(live, bucketNowMs);
|
|
199
|
+
const prev = prevDisplayRunningIdsRef.current;
|
|
200
|
+
const { conflict, previousRunnerIds } =
|
|
201
|
+
detectPlannedBoundaryDisplayConflict(prev, curr);
|
|
202
|
+
|
|
203
|
+
if (!conflict) {
|
|
204
|
+
prevDisplayRunningIdsRef.current = curr;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
boundaryTargetsRef.current = previousRunnerIds;
|
|
209
|
+
const pref = readConcurrentTaskStartPreference();
|
|
210
|
+
if (pref === "pause") {
|
|
211
|
+
prevDisplayRunningIdsRef.current = curr;
|
|
212
|
+
void resolveBoundaryConflict("pause", false);
|
|
213
|
+
if (shouldDispatchAttention(pathname)) {
|
|
214
|
+
dispatchPlannedBoundaryAttention();
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (pref === "finish") {
|
|
219
|
+
prevDisplayRunningIdsRef.current = curr;
|
|
220
|
+
void resolveBoundaryConflict("finish", false);
|
|
221
|
+
if (shouldDispatchAttention(pathname)) {
|
|
222
|
+
dispatchPlannedBoundaryAttention();
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (pref === "parallel") {
|
|
227
|
+
prevDisplayRunningIdsRef.current = curr;
|
|
228
|
+
if (shouldDispatchAttention(pathname)) {
|
|
229
|
+
dispatchPlannedBoundaryAttention();
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
setRememberBoundaryChoice(false);
|
|
235
|
+
setBoundaryModalOpen(true);
|
|
236
|
+
if (shouldDispatchAttention(pathname)) {
|
|
237
|
+
dispatchPlannedBoundaryAttention();
|
|
238
|
+
}
|
|
239
|
+
}, [
|
|
240
|
+
boundaryModalOpen,
|
|
241
|
+
bucketNowMs,
|
|
242
|
+
live,
|
|
243
|
+
payload?.inspectingSessionId,
|
|
244
|
+
pathname,
|
|
245
|
+
resolveBoundaryConflict,
|
|
246
|
+
]);
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<DashboardTriActionModal
|
|
250
|
+
open={boundaryModalOpen}
|
|
251
|
+
title={t.taskPlannedBoundaryConflictTitle}
|
|
252
|
+
message={t.taskPlannedBoundaryConflictMessage}
|
|
253
|
+
dismissLabel={t.dialogCancelBtn}
|
|
254
|
+
tertiaryLabel={t.taskConcurrentTrackingConflictParallelBtn}
|
|
255
|
+
secondaryLabel={t.taskConcurrentTrackingConflictPauseBtn}
|
|
256
|
+
primaryLabel={t.taskConcurrentTrackingConflictFinishBtn}
|
|
257
|
+
primaryVariant="danger"
|
|
258
|
+
dismissCheckbox={{
|
|
259
|
+
label: t.taskConcurrentTrackingDontShowAgain,
|
|
260
|
+
checked: rememberBoundaryChoice,
|
|
261
|
+
onChange: setRememberBoundaryChoice,
|
|
262
|
+
}}
|
|
263
|
+
onDismiss={cancelBoundaryModal}
|
|
264
|
+
onTertiary={() =>
|
|
265
|
+
void resolveBoundaryConflict("parallel", rememberBoundaryRef.current)
|
|
266
|
+
}
|
|
267
|
+
onSecondary={() =>
|
|
268
|
+
void resolveBoundaryConflict("pause", rememberBoundaryRef.current)
|
|
269
|
+
}
|
|
270
|
+
onPrimary={() =>
|
|
271
|
+
void resolveBoundaryConflict("finish", rememberBoundaryRef.current)
|
|
272
|
+
}
|
|
273
|
+
/>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type CSSProperties,
|
|
11
11
|
} from "react";
|
|
12
12
|
import { createPortal } from "react-dom";
|
|
13
|
+
import { postKronosysAction } from "@/lib/kronosysApi";
|
|
13
14
|
import { markReportingTourCompleted } from "@/lib/dashboardTourStorage";
|
|
14
15
|
import type { DashboardStrings } from "@/lib/dashboardCopy";
|
|
15
16
|
|
|
@@ -81,11 +82,14 @@ export function ReportingTour({
|
|
|
81
82
|
if (hasReportingChartData) {
|
|
82
83
|
s.push(
|
|
83
84
|
{ title: dt.reportingTourStep4Title, body: dt.reportingTourStep4Body },
|
|
84
|
-
{ title: dt.reportingTourStep5Title, body: dt.reportingTourStep5Body }
|
|
85
|
+
{ title: dt.reportingTourStep5Title, body: dt.reportingTourStep5Body },
|
|
85
86
|
);
|
|
86
87
|
sel.push(CHART_SELECTOR, TAG_TIME_SELECTOR);
|
|
87
88
|
}
|
|
88
|
-
s.push({
|
|
89
|
+
s.push({
|
|
90
|
+
title: dt.reportingTourStep6Title,
|
|
91
|
+
body: dt.reportingTourStep6Body,
|
|
92
|
+
});
|
|
89
93
|
sel.push(TOC_LAYOUT_SELECTOR);
|
|
90
94
|
return { selectors: sel, steps: s };
|
|
91
95
|
}, [dt, hasReportingChartData]);
|
|
@@ -101,12 +105,22 @@ export function ReportingTour({
|
|
|
101
105
|
}
|
|
102
106
|
}, [open]);
|
|
103
107
|
|
|
104
|
-
const finish = useCallback(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
const finish = useCallback(
|
|
109
|
+
(reason: "skip" | "done" | "escape") => {
|
|
110
|
+
void postKronosysAction({
|
|
111
|
+
type: "tourInteraction",
|
|
112
|
+
tour: "reporting_spotlight",
|
|
113
|
+
action: reason,
|
|
114
|
+
stepIndex: step,
|
|
115
|
+
stepTotal: total,
|
|
116
|
+
}).catch(() => undefined);
|
|
117
|
+
markReportingTourCompleted();
|
|
118
|
+
onOpenChange(false);
|
|
119
|
+
},
|
|
120
|
+
[onOpenChange, step, total],
|
|
121
|
+
);
|
|
108
122
|
|
|
109
|
-
useEscapeDismiss(open, finish);
|
|
123
|
+
useEscapeDismiss(open, () => finish("escape"));
|
|
110
124
|
|
|
111
125
|
const updateHoleFromDom = useCallback(() => {
|
|
112
126
|
if (!open) {
|
|
@@ -128,7 +142,11 @@ export function ReportingTour({
|
|
|
128
142
|
}
|
|
129
143
|
const el = document.querySelector(selector);
|
|
130
144
|
if (el instanceof HTMLElement) {
|
|
131
|
-
el.scrollIntoView({
|
|
145
|
+
el.scrollIntoView({
|
|
146
|
+
block: "center",
|
|
147
|
+
inline: "nearest",
|
|
148
|
+
behavior: "auto",
|
|
149
|
+
});
|
|
132
150
|
}
|
|
133
151
|
updateHoleFromDom();
|
|
134
152
|
const raf = requestAnimationFrame(updateHoleFromDom);
|
|
@@ -175,7 +193,10 @@ export function ReportingTour({
|
|
|
175
193
|
const ph = panel?.getBoundingClientRect().height ?? 220;
|
|
176
194
|
|
|
177
195
|
let top = hole.top + hole.height + TOOLTIP_GAP;
|
|
178
|
-
if (
|
|
196
|
+
if (
|
|
197
|
+
top + ph > vh - VIEW_MARGIN &&
|
|
198
|
+
hole.top - TOOLTIP_GAP - ph >= VIEW_MARGIN
|
|
199
|
+
) {
|
|
179
200
|
top = hole.top - TOOLTIP_GAP - ph;
|
|
180
201
|
}
|
|
181
202
|
top = Math.max(VIEW_MARGIN, Math.min(top, vh - ph - VIEW_MARGIN));
|
|
@@ -236,22 +257,46 @@ export function ReportingTour({
|
|
|
236
257
|
<>
|
|
237
258
|
<div
|
|
238
259
|
className="fixed bg-black/55"
|
|
239
|
-
style={{
|
|
260
|
+
style={{
|
|
261
|
+
top: 0,
|
|
262
|
+
left: 0,
|
|
263
|
+
width: vw,
|
|
264
|
+
height: topH,
|
|
265
|
+
zIndex: 210,
|
|
266
|
+
}}
|
|
240
267
|
aria-hidden
|
|
241
268
|
/>
|
|
242
269
|
<div
|
|
243
270
|
className="fixed bg-black/55"
|
|
244
|
-
style={{
|
|
271
|
+
style={{
|
|
272
|
+
top: bottomTop,
|
|
273
|
+
left: 0,
|
|
274
|
+
width: vw,
|
|
275
|
+
height: bottomH,
|
|
276
|
+
zIndex: 210,
|
|
277
|
+
}}
|
|
245
278
|
aria-hidden
|
|
246
279
|
/>
|
|
247
280
|
<div
|
|
248
281
|
className="fixed bg-black/55"
|
|
249
|
-
style={{
|
|
282
|
+
style={{
|
|
283
|
+
top: t,
|
|
284
|
+
left: 0,
|
|
285
|
+
width: leftW,
|
|
286
|
+
height: h,
|
|
287
|
+
zIndex: 210,
|
|
288
|
+
}}
|
|
250
289
|
aria-hidden
|
|
251
290
|
/>
|
|
252
291
|
<div
|
|
253
292
|
className="fixed bg-black/55"
|
|
254
|
-
style={{
|
|
293
|
+
style={{
|
|
294
|
+
top: t,
|
|
295
|
+
left: rightLeft,
|
|
296
|
+
width: rightW,
|
|
297
|
+
height: h,
|
|
298
|
+
zIndex: 210,
|
|
299
|
+
}}
|
|
255
300
|
aria-hidden
|
|
256
301
|
/>
|
|
257
302
|
<div
|
|
@@ -288,19 +333,31 @@ export function ReportingTour({
|
|
|
288
333
|
<p className="text-xs font-medium uppercase tracking-wide text-violet-600 dark:text-violet-300">
|
|
289
334
|
{progressLabel}
|
|
290
335
|
</p>
|
|
291
|
-
<h2
|
|
336
|
+
<h2
|
|
337
|
+
id="reporting-tour-title"
|
|
338
|
+
className="mt-1 text-base font-semibold text-zinc-900 dark:text-zinc-100"
|
|
339
|
+
>
|
|
292
340
|
{current.title}
|
|
293
341
|
</h2>
|
|
294
342
|
</div>
|
|
295
|
-
<div
|
|
296
|
-
|
|
343
|
+
<div
|
|
344
|
+
id="reporting-tour-body"
|
|
345
|
+
className="max-h-[min(42vh,18rem)] overflow-y-auto px-4 py-3"
|
|
346
|
+
>
|
|
347
|
+
<p className="whitespace-pre-wrap text-sm leading-relaxed text-zinc-700 dark:text-zinc-300">
|
|
348
|
+
{current.body}
|
|
349
|
+
</p>
|
|
297
350
|
</div>
|
|
298
351
|
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-zinc-200 px-4 py-3 dark:border-zinc-700">
|
|
299
352
|
<div className="flex gap-1.5" role="presentation" aria-hidden>
|
|
300
353
|
{steps.map((_, i) => (
|
|
301
354
|
<span
|
|
302
355
|
key={i}
|
|
303
|
-
className={`h-2 w-2 rounded-full ${
|
|
356
|
+
className={`h-2 w-2 rounded-full ${
|
|
357
|
+
i === step
|
|
358
|
+
? "bg-violet-500 dark:bg-violet-400"
|
|
359
|
+
: "bg-zinc-300 dark:bg-zinc-600"
|
|
360
|
+
}`}
|
|
304
361
|
/>
|
|
305
362
|
))}
|
|
306
363
|
</div>
|
|
@@ -308,17 +365,26 @@ export function ReportingTour({
|
|
|
308
365
|
<button
|
|
309
366
|
type="button"
|
|
310
367
|
className="text-sm text-zinc-500 underline-offset-2 hover:text-zinc-800 hover:underline dark:text-zinc-400 dark:hover:text-zinc-200"
|
|
311
|
-
onClick={finish}
|
|
368
|
+
onClick={() => finish("skip")}
|
|
312
369
|
>
|
|
313
370
|
{dt.tourSkipBtn}
|
|
314
371
|
</button>
|
|
315
372
|
{step > 0 ? (
|
|
316
|
-
<button
|
|
373
|
+
<button
|
|
374
|
+
type="button"
|
|
375
|
+
className={secondaryBtn}
|
|
376
|
+
onClick={() => setStep((s) => Math.max(0, s - 1))}
|
|
377
|
+
>
|
|
317
378
|
{dt.tourBackBtn}
|
|
318
379
|
</button>
|
|
319
380
|
) : null}
|
|
320
381
|
{last ? (
|
|
321
|
-
<button
|
|
382
|
+
<button
|
|
383
|
+
ref={primaryBtnRef}
|
|
384
|
+
type="button"
|
|
385
|
+
className={primaryBtn}
|
|
386
|
+
onClick={() => finish("done")}
|
|
387
|
+
>
|
|
322
388
|
{dt.tourDoneBtn}
|
|
323
389
|
</button>
|
|
324
390
|
) : (
|
|
@@ -335,7 +401,7 @@ export function ReportingTour({
|
|
|
335
401
|
</div>
|
|
336
402
|
</div>
|
|
337
403
|
</>,
|
|
338
|
-
document.body
|
|
404
|
+
document.body,
|
|
339
405
|
);
|
|
340
406
|
|
|
341
407
|
return node;
|
|
@@ -4,17 +4,21 @@ import { Fragment } from "react";
|
|
|
4
4
|
import { formatProjectDisplay, normalizeProjectKey } from "@/lib/taskParsing";
|
|
5
5
|
import {
|
|
6
6
|
PROJECT_INLINE_SUGGEST,
|
|
7
|
+
PROJECT_INLINE_SUGGEST_PERSONAL,
|
|
8
|
+
PROJECT_INLINE_SUGGEST_PERSONAL_SELECTED,
|
|
7
9
|
PROJECT_INLINE_SUGGEST_SELECTED,
|
|
8
10
|
} from "./taskFieldStyles";
|
|
9
11
|
import { useDescriptionPopoverAfterMs } from "./useDescriptionPopoverAfterMs";
|
|
10
12
|
|
|
11
13
|
function SuggestedProjectButton({
|
|
12
14
|
project,
|
|
15
|
+
personal,
|
|
13
16
|
selected,
|
|
14
17
|
onPick,
|
|
15
18
|
description,
|
|
16
19
|
}: {
|
|
17
20
|
project: string;
|
|
21
|
+
personal?: boolean;
|
|
18
22
|
selected: boolean;
|
|
19
23
|
onPick: () => void;
|
|
20
24
|
description: string | undefined;
|
|
@@ -22,9 +26,14 @@ function SuggestedProjectButton({
|
|
|
22
26
|
const { hasDescription, triggerProps, popoverLayer, anchorWrapperProps } =
|
|
23
27
|
useDescriptionPopoverAfterMs(description);
|
|
24
28
|
|
|
29
|
+
const baseSel = personal ? PROJECT_INLINE_SUGGEST_PERSONAL_SELECTED : PROJECT_INLINE_SUGGEST_SELECTED;
|
|
30
|
+
const baseUnsel = personal ? PROJECT_INLINE_SUGGEST_PERSONAL : PROJECT_INLINE_SUGGEST;
|
|
31
|
+
const ring = personal ? "focus-visible:ring-rose-500/35" : "focus-visible:ring-sky-500/35";
|
|
32
|
+
const ringUnsel = personal ? "focus-visible:ring-rose-500/30" : "focus-visible:ring-sky-500/30";
|
|
33
|
+
|
|
25
34
|
const chipClass = selected
|
|
26
|
-
? `${
|
|
27
|
-
: `${
|
|
35
|
+
? `${baseSel} cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-2 ${ring} focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-zinc-900`
|
|
36
|
+
: `${baseUnsel} cursor-pointer focus-visible:outline-none focus-visible:ring-2 ${ringUnsel} focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-zinc-900`;
|
|
28
37
|
|
|
29
38
|
const btn = (
|
|
30
39
|
<button
|
|
@@ -34,7 +43,7 @@ function SuggestedProjectButton({
|
|
|
34
43
|
{...triggerProps}
|
|
35
44
|
onClick={onPick}
|
|
36
45
|
>
|
|
37
|
-
{formatProjectDisplay(project)}
|
|
46
|
+
{formatProjectDisplay(project, personal ? { personal: true } : undefined)}
|
|
38
47
|
</button>
|
|
39
48
|
);
|
|
40
49
|
|
|
@@ -58,6 +67,7 @@ export function SavedProjectPicker({
|
|
|
58
67
|
onPickProject,
|
|
59
68
|
projectDescriptions,
|
|
60
69
|
noTopMargin,
|
|
70
|
+
personal,
|
|
61
71
|
}: {
|
|
62
72
|
knownProjects: string[];
|
|
63
73
|
selectedProject?: string | null;
|
|
@@ -65,6 +75,8 @@ export function SavedProjectPicker({
|
|
|
65
75
|
onPickProject: (name: string | null) => void;
|
|
66
76
|
projectDescriptions?: Record<string, string>;
|
|
67
77
|
noTopMargin?: boolean;
|
|
78
|
+
/** Liste de projets issus de `!` (styles rose). */
|
|
79
|
+
personal?: boolean;
|
|
68
80
|
}) {
|
|
69
81
|
const sel = selectedProject?.trim() ? normalizeProjectKey(selectedProject) : "";
|
|
70
82
|
const selLower = sel.toLowerCase();
|
|
@@ -80,6 +92,7 @@ export function SavedProjectPicker({
|
|
|
80
92
|
<SuggestedProjectButton
|
|
81
93
|
key={p}
|
|
82
94
|
project={p}
|
|
95
|
+
personal={personal}
|
|
83
96
|
selected={on}
|
|
84
97
|
description={projectDescriptions?.[normalizeProjectKey(p).toLowerCase()]}
|
|
85
98
|
onPick={() => onPickProject(on ? null : p)}
|