@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,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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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({
|
|
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 (
|
|
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
|
|
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={{
|
|
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={{
|
|
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={{
|
|
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={{
|
|
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
|
|
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
|
|
286
|
-
|
|
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 ${
|
|
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
|
|
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
|
|
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
|
))}
|