@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,316 @@
1
+ "use client";
2
+
3
+ import { useCallback, useMemo, useState } from "react";
4
+ import { Pencil, Save, Trash2, X } from "lucide-react";
5
+ import { postKronosysAction } from "@/lib/kronosysApi";
6
+ import type { KronosysUpdatePayload } from "@/lib/kronosysApi";
7
+ import type { Lang } from "@/lib/dashboardCopy";
8
+ import type { SettingsCopy } from "@/lib/settingsCopy";
9
+ import { buildStartTaskFromDraft } from "@/lib/taskParsing";
10
+ import {
11
+ formatTaskTemplateDraftLine,
12
+ parseTaskTemplatesFromPayload,
13
+ type ParsedTaskTemplateRow,
14
+ } from "@/lib/taskTemplateDraft";
15
+ import { DashboardConfirmModal } from "./DashboardSimpleModal";
16
+ import { useDashboardToast } from "./DashboardToastProvider";
17
+ import { InlineMetricHelpTrigger } from "./InlineMetricHelpTrigger";
18
+
19
+ const CARD_CLASS =
20
+ "scroll-mt-24 space-y-4 rounded-xl border border-zinc-200 bg-zinc-50/90 p-4 dark:border-zinc-800 dark:bg-zinc-900/40";
21
+
22
+ const INPUT_CLASS =
23
+ "w-full min-w-0 rounded-lg border border-zinc-300 bg-white px-2.5 py-2 text-sm text-zinc-900 outline-none ring-violet-500/30 focus:ring-2 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100";
24
+
25
+ const ICON_BTN =
26
+ "inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-zinc-300 bg-white text-zinc-700 shadow-sm transition hover:border-zinc-400 hover:bg-zinc-50 disabled:opacity-40 dark:border-zinc-600 dark:bg-zinc-800/90 dark:text-zinc-200 dark:hover:border-zinc-500 dark:hover:bg-zinc-800";
27
+
28
+ const ICON_BTN_DANGER =
29
+ "inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-red-700/55 bg-white text-red-800 shadow-sm transition hover:border-red-700 hover:bg-red-50 disabled:opacity-40 dark:border-red-800/55 dark:bg-red-950/30 dark:text-red-200 dark:hover:bg-red-950/50";
30
+
31
+ function formatUpdatedShort(iso: string | undefined, lang: Lang): string {
32
+ if (!iso || !iso.trim()) {
33
+ return "—";
34
+ }
35
+ const d = new Date(iso);
36
+ if (Number.isNaN(d.getTime())) {
37
+ return iso.slice(0, 10);
38
+ }
39
+ return d.toLocaleString(lang === "fr" ? "fr-CA" : "en-CA", {
40
+ dateStyle: "short",
41
+ timeStyle: "short",
42
+ });
43
+ }
44
+
45
+ export function SettingsTaskTemplatesSection({
46
+ s,
47
+ lang,
48
+ payload,
49
+ saving,
50
+ refresh,
51
+ }: {
52
+ s: SettingsCopy;
53
+ lang: Lang;
54
+ payload: KronosysUpdatePayload;
55
+ saving: boolean;
56
+ refresh: () => Promise<boolean | void>;
57
+ }) {
58
+ const { pushToast } = useDashboardToast();
59
+ const rows = useMemo(
60
+ () =>
61
+ parseTaskTemplatesFromPayload(
62
+ (payload as Record<string, unknown>).taskTemplates,
63
+ ),
64
+ [payload],
65
+ );
66
+
67
+ const [newDraft, setNewDraft] = useState("");
68
+ const [editingId, setEditingId] = useState<string | null>(null);
69
+ const [editDraft, setEditDraft] = useState("");
70
+ const [deleteId, setDeleteId] = useState<string | null>(null);
71
+
72
+ const post = useCallback(
73
+ async (body: Record<string, unknown>) => {
74
+ await postKronosysAction(body);
75
+ await refresh();
76
+ },
77
+ [refresh],
78
+ );
79
+
80
+ const submitNew = useCallback(async () => {
81
+ const d = buildStartTaskFromDraft(newDraft, [], undefined);
82
+ if (!d.name.trim() && d.tags.length === 0 && !d.project) {
83
+ return;
84
+ }
85
+ await post({
86
+ type: "saveTaskTemplate",
87
+ name: d.name,
88
+ tags: d.tags,
89
+ ...(d.project ? { project: d.project } : {}),
90
+ ...(d.project ? { personalProject: d.personalProject === true } : {}),
91
+ });
92
+ setNewDraft("");
93
+ pushToast(s.taskTemplatesSavedToast);
94
+ }, [newDraft, post, pushToast, s.taskTemplatesSavedToast]);
95
+
96
+ const beginEdit = useCallback((tpl: ParsedTaskTemplateRow) => {
97
+ setEditingId(tpl.id);
98
+ setEditDraft(formatTaskTemplateDraftLine(tpl));
99
+ }, []);
100
+
101
+ const cancelEdit = useCallback(() => {
102
+ setEditingId(null);
103
+ setEditDraft("");
104
+ }, []);
105
+
106
+ const saveEdit = useCallback(async () => {
107
+ if (!editingId) {
108
+ return;
109
+ }
110
+ const d = buildStartTaskFromDraft(editDraft, [], undefined);
111
+ if (!d.name.trim() && d.tags.length === 0 && !d.project) {
112
+ return;
113
+ }
114
+ await post({
115
+ type: "saveTaskTemplate",
116
+ id: editingId,
117
+ name: d.name,
118
+ tags: d.tags,
119
+ ...(d.project ? { project: d.project } : {}),
120
+ ...(d.project ? { personalProject: d.personalProject === true } : {}),
121
+ });
122
+ setEditingId(null);
123
+ setEditDraft("");
124
+ pushToast(s.taskTemplatesUpdatedToast);
125
+ }, [editDraft, editingId, post, pushToast, s.taskTemplatesUpdatedToast]);
126
+
127
+ const confirmDelete = useCallback(async () => {
128
+ const id = deleteId;
129
+ setDeleteId(null);
130
+ if (!id) {
131
+ return;
132
+ }
133
+ await post({ type: "removeTaskTemplate", taskTemplateId: id });
134
+ pushToast(s.taskTemplatesRemovedToast);
135
+ if (editingId === id) {
136
+ cancelEdit();
137
+ }
138
+ }, [
139
+ cancelEdit,
140
+ deleteId,
141
+ editingId,
142
+ post,
143
+ pushToast,
144
+ s.taskTemplatesRemovedToast,
145
+ ]);
146
+
147
+ const disabled = saving;
148
+
149
+ return (
150
+ <section
151
+ id="settings-task-templates"
152
+ className={CARD_CLASS}
153
+ aria-labelledby="settings-task-templates-heading"
154
+ >
155
+ <div className="flex flex-wrap items-start justify-between gap-3">
156
+ <div className="min-w-0">
157
+ <h2
158
+ id="settings-task-templates-heading"
159
+ className="text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400"
160
+ >
161
+ {s.sectionTaskTemplates}
162
+ </h2>
163
+ <p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
164
+ {s.taskTemplatesIntro}
165
+ </p>
166
+ </div>
167
+ <InlineMetricHelpTrigger
168
+ ariaLabel={s.taskTemplatesHelpAria}
169
+ body={s.taskTemplatesHelpBody}
170
+ panelClassName="w-[min(calc(100vw-2rem),24rem)]"
171
+ />
172
+ </div>
173
+
174
+ <div className="space-y-2">
175
+ <div className="flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center">
176
+ <input
177
+ type="text"
178
+ className={INPUT_CLASS}
179
+ value={newDraft}
180
+ onChange={(e) => setNewDraft(e.target.value)}
181
+ placeholder={s.taskTemplatesDraftPlaceholder}
182
+ aria-label={s.taskTemplatesDraftPlaceholder}
183
+ disabled={disabled}
184
+ spellCheck={false}
185
+ onKeyDown={(e) => {
186
+ if (e.key === "Enter") {
187
+ e.preventDefault();
188
+ void submitNew();
189
+ }
190
+ }}
191
+ />
192
+ <button
193
+ type="button"
194
+ className="shrink-0 rounded-lg bg-violet-600 px-3 py-2 text-sm font-medium text-white hover:bg-violet-500 disabled:opacity-50"
195
+ disabled={disabled || !newDraft.trim()}
196
+ onClick={() => void submitNew()}
197
+ >
198
+ {s.taskTemplatesAddBtn}
199
+ </button>
200
+ </div>
201
+ </div>
202
+
203
+ {rows.length === 0 ? (
204
+ <p className="text-sm text-zinc-500 dark:text-zinc-400">
205
+ {s.taskTemplatesEmpty}
206
+ </p>
207
+ ) : (
208
+ <div className="overflow-x-auto">
209
+ <table className="w-full min-w-[20rem] border-collapse text-left text-sm">
210
+ <thead>
211
+ <tr className="border-b border-zinc-200 dark:border-zinc-700">
212
+ <th className="pb-2 pr-3 font-semibold text-zinc-600 dark:text-zinc-400">
213
+ {s.taskTemplatesColDraft}
214
+ </th>
215
+ </tr>
216
+ </thead>
217
+ <tbody>
218
+ {rows.map((tpl) => {
219
+ const isEditing = editingId === tpl.id;
220
+ return (
221
+ <tr
222
+ key={tpl.id}
223
+ className="border-b border-zinc-100 dark:border-zinc-800/90"
224
+ >
225
+ <td className="py-3 pr-0 align-top">
226
+ {isEditing ? (
227
+ <input
228
+ type="text"
229
+ className={INPUT_CLASS}
230
+ value={editDraft}
231
+ onChange={(e) => setEditDraft(e.target.value)}
232
+ disabled={disabled}
233
+ spellCheck={false}
234
+ aria-label={s.taskTemplatesColDraft}
235
+ />
236
+ ) : (
237
+ <code className="block max-w-xl whitespace-pre-wrap break-all text-xs text-zinc-800 dark:text-zinc-200">
238
+ {formatTaskTemplateDraftLine(tpl)}
239
+ </code>
240
+ )}
241
+ <div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-2">
242
+ <span className="text-xs tabular-nums text-zinc-600 dark:text-zinc-400">
243
+ {s.taskTemplatesColUpdated}:{" "}
244
+ {formatUpdatedShort(tpl.updatedAt, lang)}
245
+ </span>
246
+ <div className="flex flex-wrap items-center gap-1">
247
+ {isEditing ? (
248
+ <>
249
+ <button
250
+ type="button"
251
+ className={ICON_BTN}
252
+ title={s.taskTemplatesSaveBtn}
253
+ aria-label={s.taskTemplatesSaveBtn}
254
+ disabled={disabled || !editDraft.trim()}
255
+ onClick={() => void saveEdit()}
256
+ >
257
+ <Save size={16} />
258
+ </button>
259
+ <button
260
+ type="button"
261
+ className={ICON_BTN}
262
+ title={s.taskTemplatesCancelBtn}
263
+ aria-label={s.taskTemplatesCancelBtn}
264
+ disabled={disabled}
265
+ onClick={cancelEdit}
266
+ >
267
+ <X size={16} />
268
+ </button>
269
+ </>
270
+ ) : (
271
+ <>
272
+ <button
273
+ type="button"
274
+ className={ICON_BTN}
275
+ title={s.taskTemplatesEditBtn}
276
+ aria-label={s.taskTemplatesEditBtn}
277
+ disabled={disabled}
278
+ onClick={() => beginEdit(tpl)}
279
+ >
280
+ <Pencil size={16} />
281
+ </button>
282
+ <button
283
+ type="button"
284
+ className={ICON_BTN_DANGER}
285
+ title={s.taskTemplatesDeleteBtn}
286
+ aria-label={s.taskTemplatesDeleteBtn}
287
+ disabled={disabled}
288
+ onClick={() => setDeleteId(tpl.id)}
289
+ >
290
+ <Trash2 size={16} />
291
+ </button>
292
+ </>
293
+ )}
294
+ </div>
295
+ </div>
296
+ </td>
297
+ </tr>
298
+ );
299
+ })}
300
+ </tbody>
301
+ </table>
302
+ </div>
303
+ )}
304
+
305
+ <DashboardConfirmModal
306
+ open={deleteId !== null}
307
+ message={s.taskTemplatesDeleteConfirm}
308
+ cancelLabel={s.dialogCancelBtn}
309
+ confirmLabel={s.dialogConfirmBtn}
310
+ confirmVariant="danger"
311
+ onCancel={() => setDeleteId(null)}
312
+ onConfirm={() => void confirmDelete()}
313
+ />
314
+ </section>
315
+ );
316
+ }
@@ -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 { markSettingsTourCompleted } from "@/lib/dashboardTourStorage";
14
15
  import type { DashboardStrings } from "@/lib/dashboardCopy";
15
16
 
@@ -79,7 +80,7 @@ export function SettingsTour({
79
80
  { title: dt.settingsTourStep5Title, body: dt.settingsTourStep5Body },
80
81
  { title: dt.settingsTourStep6Title, body: dt.settingsTourStep6Body },
81
82
  ],
82
- [dt]
83
+ [dt],
83
84
  );
84
85
 
85
86
  const total = steps.length;
@@ -93,12 +94,22 @@ export function SettingsTour({
93
94
  }
94
95
  }, [open]);
95
96
 
96
- const finish = useCallback(() => {
97
- markSettingsTourCompleted();
98
- onOpenChange(false);
99
- }, [onOpenChange]);
97
+ const finish = useCallback(
98
+ (reason: "skip" | "done" | "escape") => {
99
+ void postKronosysAction({
100
+ type: "tourInteraction",
101
+ tour: "settings_spotlight",
102
+ action: reason,
103
+ stepIndex: step,
104
+ stepTotal: total,
105
+ }).catch(() => undefined);
106
+ markSettingsTourCompleted();
107
+ onOpenChange(false);
108
+ },
109
+ [onOpenChange, step, total],
110
+ );
100
111
 
101
- useEscapeDismiss(open, finish);
112
+ useEscapeDismiss(open, () => finish("escape"));
102
113
 
103
114
  const updateHoleFromDom = useCallback(() => {
104
115
  if (!open) {
@@ -120,7 +131,11 @@ export function SettingsTour({
120
131
  }
121
132
  const el = document.querySelector(selector);
122
133
  if (el instanceof HTMLElement) {
123
- el.scrollIntoView({ block: "center", inline: "nearest", behavior: "auto" });
134
+ el.scrollIntoView({
135
+ block: "center",
136
+ inline: "nearest",
137
+ behavior: "auto",
138
+ });
124
139
  }
125
140
  updateHoleFromDom();
126
141
  const raf = requestAnimationFrame(updateHoleFromDom);
@@ -167,7 +182,10 @@ export function SettingsTour({
167
182
  const ph = panel?.getBoundingClientRect().height ?? 220;
168
183
 
169
184
  let top = hole.top + hole.height + TOOLTIP_GAP;
170
- if (top + ph > vh - VIEW_MARGIN && hole.top - TOOLTIP_GAP - ph >= VIEW_MARGIN) {
185
+ if (
186
+ top + ph > vh - VIEW_MARGIN &&
187
+ hole.top - TOOLTIP_GAP - ph >= VIEW_MARGIN
188
+ ) {
171
189
  top = hole.top - TOOLTIP_GAP - ph;
172
190
  }
173
191
  top = Math.max(VIEW_MARGIN, Math.min(top, vh - ph - VIEW_MARGIN));
@@ -198,7 +216,9 @@ export function SettingsTour({
198
216
  return null;
199
217
  }
200
218
 
201
- const progressLabel = dt.settingsTourProgressLabel.replace("{n}", String(step + 1)).replace("{total}", String(total));
219
+ const progressLabel = dt.settingsTourProgressLabel
220
+ .replace("{n}", String(step + 1))
221
+ .replace("{total}", String(total));
202
222
 
203
223
  const secondaryBtn =
204
224
  "rounded-lg border border-zinc-300 bg-white px-4 py-2 text-sm font-medium text-zinc-800 transition hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700";
@@ -226,22 +246,46 @@ export function SettingsTour({
226
246
  <>
227
247
  <div
228
248
  className="fixed bg-black/55"
229
- style={{ top: 0, left: 0, width: vw, height: topH, zIndex: 210 }}
249
+ style={{
250
+ top: 0,
251
+ left: 0,
252
+ width: vw,
253
+ height: topH,
254
+ zIndex: 210,
255
+ }}
230
256
  aria-hidden
231
257
  />
232
258
  <div
233
259
  className="fixed bg-black/55"
234
- style={{ top: bottomTop, left: 0, width: vw, height: bottomH, zIndex: 210 }}
260
+ style={{
261
+ top: bottomTop,
262
+ left: 0,
263
+ width: vw,
264
+ height: bottomH,
265
+ zIndex: 210,
266
+ }}
235
267
  aria-hidden
236
268
  />
237
269
  <div
238
270
  className="fixed bg-black/55"
239
- style={{ top: t, left: 0, width: leftW, height: h, zIndex: 210 }}
271
+ style={{
272
+ top: t,
273
+ left: 0,
274
+ width: leftW,
275
+ height: h,
276
+ zIndex: 210,
277
+ }}
240
278
  aria-hidden
241
279
  />
242
280
  <div
243
281
  className="fixed bg-black/55"
244
- style={{ top: t, left: rightLeft, width: rightW, height: h, zIndex: 210 }}
282
+ style={{
283
+ top: t,
284
+ left: rightLeft,
285
+ width: rightW,
286
+ height: h,
287
+ zIndex: 210,
288
+ }}
245
289
  aria-hidden
246
290
  />
247
291
  <div
@@ -278,19 +322,31 @@ export function SettingsTour({
278
322
  <p className="text-xs font-medium uppercase tracking-wide text-violet-600 dark:text-violet-300">
279
323
  {progressLabel}
280
324
  </p>
281
- <h2 id="settings-tour-title" className="mt-1 text-base font-semibold text-zinc-900 dark:text-zinc-100">
325
+ <h2
326
+ id="settings-tour-title"
327
+ className="mt-1 text-base font-semibold text-zinc-900 dark:text-zinc-100"
328
+ >
282
329
  {current.title}
283
330
  </h2>
284
331
  </div>
285
- <div id="settings-tour-body" className="max-h-[min(42vh,18rem)] overflow-y-auto px-4 py-3">
286
- <p className="whitespace-pre-wrap text-sm leading-relaxed text-zinc-700 dark:text-zinc-300">{current.body}</p>
332
+ <div
333
+ id="settings-tour-body"
334
+ className="max-h-[min(42vh,18rem)] overflow-y-auto px-4 py-3"
335
+ >
336
+ <p className="whitespace-pre-wrap text-sm leading-relaxed text-zinc-700 dark:text-zinc-300">
337
+ {current.body}
338
+ </p>
287
339
  </div>
288
340
  <div className="flex flex-wrap items-center justify-between gap-2 border-t border-zinc-200 px-4 py-3 dark:border-zinc-700">
289
341
  <div className="flex gap-1.5" role="presentation" aria-hidden>
290
342
  {steps.map((_, i) => (
291
343
  <span
292
344
  key={i}
293
- className={`h-2 w-2 rounded-full ${i === step ? "bg-violet-500 dark:bg-violet-400" : "bg-zinc-300 dark:bg-zinc-600"}`}
345
+ className={`h-2 w-2 rounded-full ${
346
+ i === step
347
+ ? "bg-violet-500 dark:bg-violet-400"
348
+ : "bg-zinc-300 dark:bg-zinc-600"
349
+ }`}
294
350
  />
295
351
  ))}
296
352
  </div>
@@ -298,17 +354,26 @@ export function SettingsTour({
298
354
  <button
299
355
  type="button"
300
356
  className="text-sm text-zinc-500 underline-offset-2 hover:text-zinc-800 hover:underline dark:text-zinc-400 dark:hover:text-zinc-200"
301
- onClick={finish}
357
+ onClick={() => finish("skip")}
302
358
  >
303
359
  {dt.settingsTourSkipBtn}
304
360
  </button>
305
361
  {step > 0 ? (
306
- <button type="button" className={secondaryBtn} onClick={() => setStep((s) => Math.max(0, s - 1))}>
362
+ <button
363
+ type="button"
364
+ className={secondaryBtn}
365
+ onClick={() => setStep((s) => Math.max(0, s - 1))}
366
+ >
307
367
  {dt.settingsTourBackBtn}
308
368
  </button>
309
369
  ) : null}
310
370
  {last ? (
311
- <button ref={primaryBtnRef} type="button" className={primaryBtn} onClick={finish}>
371
+ <button
372
+ ref={primaryBtnRef}
373
+ type="button"
374
+ className={primaryBtn}
375
+ onClick={() => finish("done")}
376
+ >
312
377
  {dt.settingsTourDoneBtn}
313
378
  </button>
314
379
  ) : (
@@ -325,7 +390,7 @@ export function SettingsTour({
325
390
  </div>
326
391
  </div>
327
392
  </>,
328
- document.body
393
+ document.body,
329
394
  );
330
395
 
331
396
  return node;
@@ -18,12 +18,16 @@ function TagPillItem({
18
18
  onRemove,
19
19
  description,
20
20
  defaultTagBucketLabel,
21
+ taskProject,
22
+ taskPersonalProject,
21
23
  }: {
22
24
  tag: string;
23
25
  chip: string;
24
26
  onRemove?: (tag: string) => void;
25
27
  description: string | undefined;
26
28
  defaultTagBucketLabel?: string;
29
+ taskProject?: string | null;
30
+ taskPersonalProject?: boolean;
27
31
  }) {
28
32
  const { hasDescription, triggerProps, popoverLayer, anchorWrapperProps } =
29
33
  useDescriptionPopoverAfterMs(description);
@@ -51,7 +55,10 @@ function TagPillItem({
51
55
  : {})}
52
56
  >
53
57
  <span className="pointer-events-none break-all">
54
- {formatTagDisplayForTask(tag, defaultTagBucketLabel)}
58
+ {formatTagDisplayForTask(tag, defaultTagBucketLabel, {
59
+ taskProject,
60
+ personalProject: taskPersonalProject,
61
+ })}
55
62
  </span>
56
63
  {interactive ? (
57
64
  <span className="pointer-events-none text-[0.7rem] font-normal text-zinc-400 dark:text-zinc-500">×</span>
@@ -86,6 +93,8 @@ export function TagPills({
86
93
  tagDescriptions,
87
94
  /** Libellé lisible pour l’étiquette réservée `default` (ex. « défaut » en français). */
88
95
  defaultTagBucketLabel,
96
+ taskProject,
97
+ taskPersonalProject,
89
98
  }: {
90
99
  tags: string[];
91
100
  onRemove?: (tag: string) => void;
@@ -96,6 +105,8 @@ export function TagPills({
96
105
  differentiateProjectScopedTags?: boolean;
97
106
  tagDescriptions?: Record<string, string>;
98
107
  defaultTagBucketLabel?: string;
108
+ taskProject?: string | null;
109
+ taskPersonalProject?: boolean;
99
110
  }) {
100
111
  const chipBase = (tag: string) => {
101
112
  const scoped = differentiateProjectScopedTags && isProjectScopedTag(normalizeTagKey(tag));
@@ -141,6 +152,8 @@ export function TagPills({
141
152
  onRemove={onRemove}
142
153
  description={describe(tag)}
143
154
  defaultTagBucketLabel={defaultTagBucketLabel}
155
+ taskProject={taskProject}
156
+ taskPersonalProject={taskPersonalProject}
144
157
  />
145
158
  </Fragment>
146
159
  ))}