@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.
Files changed (112) hide show
  1. package/README.md +28 -1
  2. package/app/api/action/route.ts +39 -3
  3. package/app/api/action-logs/route.ts +24 -0
  4. package/app/api/backup/route.ts +1 -1
  5. package/app/api/restore/route.ts +145 -0
  6. package/app/changelog/page.tsx +71 -4
  7. package/app/globals.css +127 -0
  8. package/app/guide/page.tsx +61 -15
  9. package/app/implementation/page.tsx +700 -0
  10. package/app/layout.tsx +14 -3
  11. package/app/licenses/page.tsx +99 -37
  12. package/app/logs/page.tsx +258 -0
  13. package/app/manifest.ts +5 -5
  14. package/app/page.tsx +784 -229
  15. package/app/reporting/page.tsx +1266 -474
  16. package/app/settings/page.tsx +252 -18
  17. package/bin/kronosys.mjs +140 -15
  18. package/components/KronosysPayloadProvider.tsx +2 -0
  19. package/components/RouteTransition.tsx +18 -0
  20. package/components/dashboard/AppShellCommandCenterPlaceholder.tsx +17 -0
  21. package/components/dashboard/AppShellHeaderSessionMeta.tsx +210 -0
  22. package/components/dashboard/AppShellHeaderWallClock.tsx +54 -0
  23. package/components/dashboard/AppShellLiveSessionDrawer.tsx +154 -38
  24. package/components/dashboard/AppShellRouteNav.tsx +323 -48
  25. package/components/dashboard/DashboardPauseBackdrop.tsx +50 -0
  26. package/components/dashboard/DashboardSimpleModal.tsx +168 -25
  27. package/components/dashboard/DashboardTour.tsx +115 -29
  28. package/components/dashboard/GlobalPauseConfirmModal.tsx +183 -0
  29. package/components/dashboard/KronosysDatetimePopoverField.tsx +167 -122
  30. package/components/dashboard/KronosysTimePopoverField.tsx +54 -12
  31. package/components/dashboard/NewSessionScopeModal.tsx +211 -20
  32. package/components/dashboard/PlannedTaskBoundaryConflictWatcher.tsx +275 -0
  33. package/components/dashboard/ReportingTour.tsx +87 -21
  34. package/components/dashboard/SavedProjectPicker.tsx +16 -3
  35. package/components/dashboard/SelectedSessionSidebarBlock.tsx +512 -142
  36. package/components/dashboard/SessionListPanel.tsx +327 -44
  37. package/components/dashboard/SettingsTagsProjectsSection.tsx +1073 -264
  38. package/components/dashboard/SettingsTaskTemplatesSection.tsx +316 -0
  39. package/components/dashboard/SettingsTour.tsx +86 -21
  40. package/components/dashboard/TagPills.tsx +14 -1
  41. package/components/dashboard/TaskFocusPanel.tsx +1081 -478
  42. package/components/dashboard/TaskSessionLiveCard.tsx +650 -135
  43. package/components/dashboard/TaskTimelineGanttModal.tsx +601 -0
  44. package/components/dashboard/taskFieldStyles.ts +20 -4
  45. package/components/dashboard/useReportingInteractionState.ts +80 -0
  46. package/lib/appShellHeaderClasses.ts +13 -0
  47. package/lib/businessRulesMatrix.ts +210 -0
  48. package/lib/copyToClipboard.ts +43 -0
  49. package/lib/dashboardCopy.ts +494 -84
  50. package/lib/dashboardQuickSearch.ts +54 -2
  51. package/lib/dashboardTimeZone.ts +109 -0
  52. package/lib/formatAppShellWallClock.ts +66 -0
  53. package/lib/formatSessionNameTemplate.ts +141 -0
  54. package/lib/generatedUserChangelog.ts +177 -6
  55. package/lib/globalPausePreview.ts +292 -0
  56. package/lib/implementationNotes.ts +1188 -0
  57. package/lib/kronosysApi.ts +6 -0
  58. package/lib/kronosysDashboardModalGates.ts +24 -0
  59. package/lib/plannedBoundaryAttention.ts +9 -0
  60. package/lib/plannedBoundaryConflict.ts +23 -0
  61. package/lib/reportingAggregate.ts +517 -75
  62. package/lib/reportingMetricHelp.ts +8 -0
  63. package/lib/reportingStrings.ts +37 -3
  64. package/lib/sessionListMerge.ts +4 -0
  65. package/lib/sessionTaskSidebarStats.ts +182 -21
  66. package/lib/settingsCopy.ts +178 -4
  67. package/lib/taskParsing.ts +360 -103
  68. package/lib/taskTemplateDraft.ts +135 -0
  69. package/lib/taskTimelineGantt.ts +265 -0
  70. package/lib/temporalDisplayPlanned.ts +71 -0
  71. package/lib/userGuideCopy.ts +121 -47
  72. package/next.config.ts +7 -0
  73. package/package.json +12 -24
  74. package/server/actionDispatch.ts +1000 -77
  75. package/server/actionTaskSession.ts +337 -24
  76. package/server/db.ts +7 -15
  77. package/server/dbSchema.ts +24 -0
  78. package/server/defaultCfg.ts +5 -0
  79. package/server/gitlabTokenStore.ts +0 -12
  80. package/server/liveHistorySync.ts +53 -0
  81. package/server/mainTimerHydrate.ts +38 -2
  82. package/server/payloadStore.ts +33 -11
  83. package/server/sessionWallHydrate.ts +66 -3
  84. package/server/userActionLog.ts +126 -0
  85. package/sonar-project.properties +11 -0
  86. package/tsconfig.json +2 -1
  87. package/components/dashboard/IssuePickerModal.tsx +0 -168
  88. package/components/dashboard/ThemeToggle.test.tsx +0 -26
  89. package/lib/backupCsvExport.test.ts +0 -149
  90. package/lib/dashboardQuickSearchQuery.test.ts +0 -63
  91. package/lib/dataDir.test.ts +0 -87
  92. package/lib/formatIsoShort.test.ts +0 -46
  93. package/lib/kronoFocusRhythm.test.ts +0 -130
  94. package/lib/kronoFocusTimerUrgency.test.ts +0 -74
  95. package/lib/legacyKronoFocusStorageKeys.test.ts +0 -29
  96. package/lib/reportingAggregate.test.ts +0 -325
  97. package/lib/reportingNonFinalIndicators.test.ts +0 -157
  98. package/lib/reportingTagWeekBreakdown.test.ts +0 -141
  99. package/lib/reportingWeekLayout.test.ts +0 -239
  100. package/lib/sessionAssiduity.test.ts +0 -25
  101. package/lib/sessionEndWarnings.test.ts +0 -200
  102. package/lib/sessionListMerge.test.ts +0 -101
  103. package/lib/sessionTaskSidebarStats.test.ts +0 -24
  104. package/lib/taskParsing.test.ts +0 -153
  105. package/lib/usageProfile.test.ts +0 -84
  106. package/server/actionDispatch.test.ts +0 -723
  107. package/server/actionTaskSession.test.ts +0 -713
  108. package/server/kronoFocusHydrate.test.ts +0 -142
  109. package/server/kronoFocusMigrate.test.ts +0 -53
  110. package/server/mainTimerHydrate.test.ts +0 -65
  111. package/server/payloadStore.test.ts +0 -78
  112. 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({ title: dt.reportingTourStep6Title, body: dt.reportingTourStep6Body });
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
- markReportingTourCompleted();
106
- onOpenChange(false);
107
- }, [onOpenChange]);
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({ block: "center", inline: "nearest", behavior: "auto" });
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 (top + ph > vh - VIEW_MARGIN && hole.top - TOOLTIP_GAP - ph >= VIEW_MARGIN) {
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={{ top: 0, left: 0, width: vw, height: topH, zIndex: 210 }}
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={{ top: bottomTop, left: 0, width: vw, height: bottomH, zIndex: 210 }}
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={{ top: t, left: 0, width: leftW, height: h, zIndex: 210 }}
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={{ top: t, left: rightLeft, width: rightW, height: h, zIndex: 210 }}
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 id="reporting-tour-title" className="mt-1 text-base font-semibold text-zinc-900 dark:text-zinc-100">
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 id="reporting-tour-body" className="max-h-[min(42vh,18rem)] overflow-y-auto px-4 py-3">
296
- <p className="whitespace-pre-wrap text-sm leading-relaxed text-zinc-700 dark:text-zinc-300">{current.body}</p>
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 ${i === step ? "bg-violet-500 dark:bg-violet-400" : "bg-zinc-300 dark:bg-zinc-600"}`}
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 type="button" className={secondaryBtn} onClick={() => setStep((s) => Math.max(0, s - 1))}>
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 ref={primaryBtnRef} type="button" className={primaryBtn} onClick={finish}>
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
- ? `${PROJECT_INLINE_SUGGEST_SELECTED} cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/35 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-zinc-900`
27
- : `${PROJECT_INLINE_SUGGEST} cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/30 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-zinc-900`;
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)}