@nightkatana/kronosys-app 1.0.0-beta.21 → 1.0.0-beta.22
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 +1 -1
- package/app/changelog/page.tsx +87 -19
- package/app/globals.css +10 -8
- package/app/guide/page.tsx +71 -34
- package/app/implementation/page.tsx +70 -60
- package/app/licenses/page.tsx +79 -47
- package/app/logs/page.tsx +103 -47
- package/app/page.tsx +104 -169
- package/app/reporting/page.tsx +1918 -1436
- package/app/settings/page.tsx +66 -44
- package/components/KronosysPayloadProvider.tsx +19 -5
- package/components/dashboard/AppShellHeaderKronoFocus.tsx +78 -0
- package/components/dashboard/AppShellHeaderToolbarLayout.tsx +36 -0
- package/components/dashboard/AppShellHeaderUtilityRibbon.tsx +19 -0
- package/components/dashboard/AppShellHeaderWallClock.tsx +23 -17
- package/components/dashboard/AppShellRouteNav.tsx +336 -209
- package/components/dashboard/AppShellToolbarCommandCenter.tsx +225 -0
- package/components/dashboard/AppShellToolbarRouteNav.tsx +204 -0
- package/components/dashboard/DashboardCommandCenter.tsx +119 -30
- package/components/dashboard/KronoFocusPanel.tsx +287 -260
- package/components/dashboard/LanguageMenu.tsx +23 -7
- package/components/dashboard/PageRefreshButton.tsx +42 -16
- package/components/dashboard/ReportingTour.tsx +20 -2
- package/components/dashboard/SessionListPanel.tsx +4 -4
- package/components/dashboard/ThemeToggle.tsx +4 -3
- package/components/dashboard/useAnchoredFloatingPortalStyle.ts +9 -2
- package/components/dashboard/useKronoFocusLiveSeconds.ts +4 -2
- package/lib/appShellHeaderClasses.ts +22 -3
- package/lib/appShellToolbarChrome.ts +112 -0
- package/lib/appShellToolbarDeferredIntents.ts +112 -0
- package/lib/appShellToolbarSessionSlices.ts +67 -0
- package/lib/dashboardCopy.ts +78 -29
- package/lib/dashboardQuickSearch.ts +37 -6
- package/lib/dashboardUrlSession.ts +36 -0
- package/lib/generatedUserChangelog.ts +14 -0
- package/lib/implementationNotes.ts +18 -14
- package/lib/reportingAggregate.ts +68 -9
- package/lib/reportingMetricHelp.ts +8 -8
- package/lib/reportingStrings.ts +118 -9
- package/lib/reportingTagWeekBreakdown.ts +55 -13
- package/lib/settingsCopy.ts +6 -7
- package/lib/userGuideCopy.ts +29 -26
- package/package.json +7 -5
- package/server/db.ts +6 -4
- package/server/dbSchema.ts +2 -2
- package/components/dashboard/AppShellCommandCenterPlaceholder.tsx +0 -17
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useId, useRef, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
4
5
|
import { Play, Pause, RotateCcw, Check, X, CircleHelp } from "lucide-react";
|
|
6
|
+
import { useAnchoredFloatingPortalStyle } from "@/components/dashboard/useAnchoredFloatingPortalStyle";
|
|
5
7
|
import type { DashboardStrings } from "@/lib/dashboardCopy";
|
|
6
8
|
import {
|
|
7
9
|
clearKronoFocusDurationHistory,
|
|
@@ -15,6 +17,13 @@ import {
|
|
|
15
17
|
KRONO_FOCUS_RHYTHM_PRESETS,
|
|
16
18
|
} from "@/lib/kronoFocusRhythm";
|
|
17
19
|
import { getKronoFocusTimerUrgency } from "@/lib/kronoFocusTimerUrgency";
|
|
20
|
+
import {
|
|
21
|
+
appShellToolbarIconActiveClass,
|
|
22
|
+
appShellToolbarIconLinkClass,
|
|
23
|
+
appShellToolbarInsetCellH10ButtonClass,
|
|
24
|
+
appShellToolbarInsetCellH10Class,
|
|
25
|
+
appShellToolbarRibbonTrayWideClass,
|
|
26
|
+
} from "@/lib/appShellToolbarChrome";
|
|
18
27
|
|
|
19
28
|
import { useKronoFocusLiveSeconds } from "./useKronoFocusLiveSeconds";
|
|
20
29
|
|
|
@@ -41,19 +50,10 @@ const MAX_WORK_SEC = 8 * 3600;
|
|
|
41
50
|
/** Durée de travail KronoFocus par défaut (25 min). */
|
|
42
51
|
const DEFAULT_KRONO_FOCUS_WORK_SEC = 25 * 60;
|
|
43
52
|
|
|
44
|
-
/**
|
|
45
|
-
* Bandeau entête : repères proches des boutons du header, avec variantes clair / sombre.
|
|
46
|
-
*/
|
|
47
|
-
/** Même gabarit que `AppShellRouteNav` (`size-10`) pour rester sur une ligne avec la nav. */
|
|
48
|
-
const kronoFocusControlBtnBaseHeader =
|
|
49
|
-
"box-border inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-lg border border-zinc-300 bg-white p-0 leading-none outline-none transition hover:border-zinc-400 [&_svg]:pointer-events-none [&_svg]:block [&_svg]:!h-5 [&_svg]:!w-5 [&_svg]:max-h-5 [&_svg]:max-w-5 [&_svg]:shrink-0 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-violet-500/55 dark:border-zinc-700 dark:bg-zinc-900 dark:hover:border-zinc-500";
|
|
50
|
-
const kronoFocusControlNeutralHeader = `${kronoFocusControlBtnBaseHeader} text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800/80`;
|
|
51
|
-
const kronoFocusControlAccentHeader = `${kronoFocusControlBtnBaseHeader} text-violet-800 hover:bg-violet-50 dark:text-violet-200 dark:hover:bg-violet-950/55`;
|
|
52
|
-
|
|
53
53
|
const kronoFocusControlBtnBaseCard =
|
|
54
54
|
"box-border inline-flex h-9 w-9 shrink-0 cursor-pointer items-center justify-center rounded-md border border-zinc-300 bg-white p-0 leading-none outline-none transition hover:border-zinc-400 [&_svg]:pointer-events-none [&_svg]:block [&_svg]:!h-[18px] [&_svg]:!w-[18px] [&_svg]:max-h-[18px] [&_svg]:max-w-[18px] [&_svg]:shrink-0 sm:h-10 sm:w-10 sm:[&_svg]:!h-5 sm:[&_svg]:!w-5 sm:[&_svg]:max-h-5 sm:[&_svg]:max-w-5 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-violet-500/55 dark:border-zinc-700 dark:bg-zinc-900 dark:hover:border-zinc-500";
|
|
55
|
-
const kronoFocusControlNeutralCard = `${kronoFocusControlBtnBaseCard} text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800/80`;
|
|
56
|
-
const kronoFocusControlAccentCard = `${kronoFocusControlBtnBaseCard} text-violet-800 hover:bg-violet-50 dark:text-violet-200 dark:hover:bg-violet-950/55`;
|
|
55
|
+
const kronoFocusControlNeutralCard = `${kronoFocusControlBtnBaseCard} text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800/80 disabled:pointer-events-none disabled:opacity-45 disabled:cursor-not-allowed disabled:hover:bg-white dark:disabled:hover:bg-zinc-900`;
|
|
56
|
+
const kronoFocusControlAccentCard = `${kronoFocusControlBtnBaseCard} text-violet-800 hover:bg-violet-50 dark:text-violet-200 dark:hover:bg-violet-950/55 disabled:pointer-events-none disabled:opacity-45 disabled:cursor-not-allowed disabled:hover:bg-white dark:disabled:hover:bg-zinc-900`;
|
|
57
57
|
|
|
58
58
|
/** Affichage minuteur et champ de durée : toujours `HH:MM:SS` (temps écoulé, pas une heure du jour). */
|
|
59
59
|
function formatSecondsAsHMS(totalSec: number): string {
|
|
@@ -61,7 +61,9 @@ function formatSecondsAsHMS(totalSec: number): string {
|
|
|
61
61
|
const h = Math.floor(s / 3600);
|
|
62
62
|
const m = Math.floor((s % 3600) / 60);
|
|
63
63
|
const sec = s % 60;
|
|
64
|
-
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(
|
|
64
|
+
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(
|
|
65
|
+
sec,
|
|
66
|
+
).padStart(2, "0")}`;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/**
|
|
@@ -73,7 +75,8 @@ function parseTimeInputToSeconds(value: string): number | null {
|
|
|
73
75
|
if (parts.length === 2) {
|
|
74
76
|
const h = Number.parseInt(parts[0], 10);
|
|
75
77
|
const m = Number.parseInt(parts[1], 10);
|
|
76
|
-
if (Number.isNaN(h) || Number.isNaN(m) || m < 0 || m > 59 || h < 0)
|
|
78
|
+
if (Number.isNaN(h) || Number.isNaN(m) || m < 0 || m > 59 || h < 0)
|
|
79
|
+
return null;
|
|
77
80
|
return h * 3600 + m * 60;
|
|
78
81
|
}
|
|
79
82
|
if (parts.length === 3) {
|
|
@@ -215,7 +218,9 @@ function KronoFocusDurationPopoverFields({
|
|
|
215
218
|
/>
|
|
216
219
|
</div>
|
|
217
220
|
</div>
|
|
218
|
-
<p className="mt-1.5 text-[0.6rem] leading-snug text-zinc-500 dark:text-zinc-500">
|
|
221
|
+
<p className="mt-1.5 text-[0.6rem] leading-snug text-zinc-500 dark:text-zinc-500">
|
|
222
|
+
{t.kronoFocusRhythmBreaksMinutesHint}
|
|
223
|
+
</p>
|
|
219
224
|
<button
|
|
220
225
|
type="button"
|
|
221
226
|
className="mt-2 w-full rounded-md border border-zinc-300 px-2 py-1.5 text-left text-[0.7rem] font-medium text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900 dark:border-zinc-600 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
|
|
@@ -250,7 +255,10 @@ function KronoFocusDurationPopoverFields({
|
|
|
250
255
|
type="button"
|
|
251
256
|
className={kronoFocusDurationHistoryChipClass}
|
|
252
257
|
title={label}
|
|
253
|
-
aria-label={t.kronoFocusDurationHistoryPickAria.replace(
|
|
258
|
+
aria-label={t.kronoFocusDurationHistoryPickAria.replace(
|
|
259
|
+
"{time}",
|
|
260
|
+
label,
|
|
261
|
+
)}
|
|
254
262
|
onClick={() => setDraftTime(label)}
|
|
255
263
|
>
|
|
256
264
|
{label}
|
|
@@ -284,67 +292,6 @@ function KronoFocusDurationPopoverFields({
|
|
|
284
292
|
);
|
|
285
293
|
}
|
|
286
294
|
|
|
287
|
-
function KronoFocusPanelHelpTrigger({ t }: { t: DashboardStrings }) {
|
|
288
|
-
const subtitle = (t.kronoFocusStandaloneSubtitle ?? "").trim();
|
|
289
|
-
const note = (t.kronoFocusAutoRefreshNote ?? "").trim();
|
|
290
|
-
const hasBody = subtitle.length > 0 || note.length > 0;
|
|
291
|
-
|
|
292
|
-
const [open, setOpen] = useState(false);
|
|
293
|
-
const rootRef = useRef<HTMLDivElement>(null);
|
|
294
|
-
const id = useId();
|
|
295
|
-
|
|
296
|
-
useEffect(() => {
|
|
297
|
-
if (!open) {
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
const onDoc = (e: MouseEvent) => {
|
|
301
|
-
if (!rootRef.current?.contains(e.target as Node)) {
|
|
302
|
-
setOpen(false);
|
|
303
|
-
}
|
|
304
|
-
};
|
|
305
|
-
document.addEventListener("mousedown", onDoc);
|
|
306
|
-
return () => document.removeEventListener("mousedown", onDoc);
|
|
307
|
-
}, [open]);
|
|
308
|
-
|
|
309
|
-
if (!hasBody) {
|
|
310
|
-
return null;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return (
|
|
314
|
-
<div className="relative inline-flex shrink-0" ref={rootRef}>
|
|
315
|
-
<button
|
|
316
|
-
type="button"
|
|
317
|
-
className="rounded p-0.5 text-zinc-500 hover:bg-zinc-200/90 hover:text-zinc-800 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
|
|
318
|
-
aria-label={t.kronoFocusPanelHelpAriaLabel}
|
|
319
|
-
aria-expanded={open ? "true" : "false"}
|
|
320
|
-
aria-controls={`${id}-kronoFocus-panel-help`}
|
|
321
|
-
onClick={() => setOpen((o) => !o)}
|
|
322
|
-
>
|
|
323
|
-
<CircleHelp size={15} strokeWidth={1.75} aria-hidden />
|
|
324
|
-
</button>
|
|
325
|
-
{open ? (
|
|
326
|
-
<div
|
|
327
|
-
id={`${id}-kronoFocus-panel-help`}
|
|
328
|
-
className="absolute left-0 top-full z-[60] mt-1 w-[min(calc(100vw-2rem),18rem)] rounded-lg border border-zinc-200 bg-white p-2.5 text-left shadow-lg dark:border-zinc-600 dark:bg-zinc-900"
|
|
329
|
-
role="region"
|
|
330
|
-
aria-label={t.kronoFocusPanelHelpAriaLabel}
|
|
331
|
-
>
|
|
332
|
-
{subtitle ? (
|
|
333
|
-
<p className="text-[0.7rem] leading-snug text-zinc-700 dark:text-zinc-300">{subtitle}</p>
|
|
334
|
-
) : null}
|
|
335
|
-
{note ? (
|
|
336
|
-
<p
|
|
337
|
-
className={`text-[0.7rem] leading-snug text-zinc-600 dark:text-zinc-400 ${subtitle ? "mt-2" : ""}`}
|
|
338
|
-
>
|
|
339
|
-
{note}
|
|
340
|
-
</p>
|
|
341
|
-
) : null}
|
|
342
|
-
</div>
|
|
343
|
-
) : null}
|
|
344
|
-
</div>
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
295
|
function KronoFocusDurationHelpTrigger({ t }: { t: DashboardStrings }) {
|
|
349
296
|
const [open, setOpen] = useState(false);
|
|
350
297
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
@@ -382,7 +329,9 @@ function KronoFocusDurationHelpTrigger({ t }: { t: DashboardStrings }) {
|
|
|
382
329
|
role="region"
|
|
383
330
|
aria-label={t.kronoFocusDurationHelpAriaLabel}
|
|
384
331
|
>
|
|
385
|
-
<p className="text-[0.7rem] leading-snug text-zinc-700 dark:text-zinc-300">
|
|
332
|
+
<p className="text-[0.7rem] leading-snug text-zinc-700 dark:text-zinc-300">
|
|
333
|
+
{t.kronoFocusDurationHelpBody}
|
|
334
|
+
</p>
|
|
386
335
|
</div>
|
|
387
336
|
) : null}
|
|
388
337
|
</div>
|
|
@@ -407,25 +356,44 @@ export function KronoFocusPanel({
|
|
|
407
356
|
/** `headerBar` : bandeau compact pour l’entête (grands écrans) ; `default` : carte complète */
|
|
408
357
|
variant?: "default" | "headerBar";
|
|
409
358
|
}) {
|
|
410
|
-
const serverSecs =
|
|
359
|
+
const serverSecs =
|
|
360
|
+
kronoFocus?.timeLeftSeconds ?? DEFAULT_KRONO_FOCUS_WORK_SEC;
|
|
411
361
|
const mode = kronoFocus?.mode ?? "work";
|
|
412
362
|
const status = kronoFocus?.status ?? "idle";
|
|
413
|
-
const displaySecs = useKronoFocusLiveSeconds(
|
|
363
|
+
const displaySecs = useKronoFocusLiveSeconds(
|
|
364
|
+
serverSecs,
|
|
365
|
+
status,
|
|
366
|
+
kronoFocus?.kronoFocusDeadlineAtMs,
|
|
367
|
+
);
|
|
414
368
|
const clockDisplay = formatSecondsAsHMS(displaySecs);
|
|
415
369
|
const canEditWorkDuration =
|
|
416
370
|
!viewingArchive && status === "idle" && mode === "work";
|
|
417
371
|
|
|
418
372
|
const [durationPopoverOpen, setDurationPopoverOpen] = useState(false);
|
|
419
|
-
const [draftTime, setDraftTime] = useState(() =>
|
|
373
|
+
const [draftTime, setDraftTime] = useState(() =>
|
|
374
|
+
formatSecondsAsHMS(serverSecs),
|
|
375
|
+
);
|
|
420
376
|
const [draftShortBreakMin, setDraftShortBreakMin] = useState("5");
|
|
421
377
|
const [draftLongBreakMin, setDraftLongBreakMin] = useState("15");
|
|
422
|
-
const [customDurationHistory, setCustomDurationHistory] = useState<number[]>(
|
|
423
|
-
|
|
378
|
+
const [customDurationHistory, setCustomDurationHistory] = useState<number[]>(
|
|
379
|
+
[],
|
|
380
|
+
);
|
|
381
|
+
const timeButtonRef = useRef<HTMLButtonElement>(null);
|
|
382
|
+
const durationPopoverPanelRef = useRef<HTMLDivElement>(null);
|
|
424
383
|
const prevKronoFocusStatusRef = useRef(status);
|
|
425
384
|
const [showStartPulse, setShowStartPulse] = useState(false);
|
|
426
385
|
|
|
386
|
+
const durationPopoverStyle = useAnchoredFloatingPortalStyle(
|
|
387
|
+
durationPopoverOpen && canEditWorkDuration,
|
|
388
|
+
timeButtonRef,
|
|
389
|
+
durationPopoverPanelRef,
|
|
390
|
+
{ align: "center", maxWidthRem: 20 },
|
|
391
|
+
);
|
|
392
|
+
|
|
427
393
|
useEffect(() => {
|
|
428
|
-
setCustomDurationHistory(
|
|
394
|
+
setCustomDurationHistory(
|
|
395
|
+
loadKronoFocusDurationHistory(DEFAULT_KRONO_FOCUS_WORK_SEC),
|
|
396
|
+
);
|
|
429
397
|
}, []);
|
|
430
398
|
|
|
431
399
|
useEffect(() => {
|
|
@@ -439,7 +407,8 @@ export function KronoFocusPanel({
|
|
|
439
407
|
return;
|
|
440
408
|
}
|
|
441
409
|
const workSec =
|
|
442
|
-
typeof kronoFocus?.workDurationSeconds === "number" &&
|
|
410
|
+
typeof kronoFocus?.workDurationSeconds === "number" &&
|
|
411
|
+
Number.isFinite(kronoFocus.workDurationSeconds)
|
|
443
412
|
? kronoFocus.workDurationSeconds
|
|
444
413
|
: serverSecs;
|
|
445
414
|
setDraftTime(formatSecondsAsHMS(workSec));
|
|
@@ -474,8 +443,12 @@ export function KronoFocusPanel({
|
|
|
474
443
|
useEffect(() => {
|
|
475
444
|
if (!durationPopoverOpen) return;
|
|
476
445
|
const onDocMouseDown = (e: MouseEvent) => {
|
|
477
|
-
const
|
|
478
|
-
|
|
446
|
+
const target = e.target as Node;
|
|
447
|
+
const trigger = timeButtonRef.current;
|
|
448
|
+
const panel = durationPopoverPanelRef.current;
|
|
449
|
+
const insideTrigger = trigger ? trigger.contains(target) : false;
|
|
450
|
+
const insidePanel = panel ? panel.contains(target) : false;
|
|
451
|
+
if (!insideTrigger && !insidePanel) {
|
|
479
452
|
setDurationPopoverOpen(false);
|
|
480
453
|
}
|
|
481
454
|
};
|
|
@@ -490,7 +463,9 @@ export function KronoFocusPanel({
|
|
|
490
463
|
};
|
|
491
464
|
}, [durationPopoverOpen]);
|
|
492
465
|
|
|
493
|
-
const applyRhythmPreset = (
|
|
466
|
+
const applyRhythmPreset = (
|
|
467
|
+
preset: (typeof KRONO_FOCUS_RHYTHM_PRESETS)[number],
|
|
468
|
+
) => {
|
|
494
469
|
void post({
|
|
495
470
|
type: "setKronoFocusDurations",
|
|
496
471
|
workSeconds: preset.workSeconds,
|
|
@@ -498,7 +473,11 @@ export function KronoFocusPanel({
|
|
|
498
473
|
longBreakSeconds: preset.longBreakSeconds,
|
|
499
474
|
});
|
|
500
475
|
setCustomDurationHistory((prev) => {
|
|
501
|
-
const next = pushKronoFocusDurationHistory(
|
|
476
|
+
const next = pushKronoFocusDurationHistory(
|
|
477
|
+
prev,
|
|
478
|
+
preset.workSeconds,
|
|
479
|
+
DEFAULT_KRONO_FOCUS_WORK_SEC,
|
|
480
|
+
);
|
|
502
481
|
persistKronoFocusDurationHistory(next);
|
|
503
482
|
return next;
|
|
504
483
|
});
|
|
@@ -511,17 +490,22 @@ export function KronoFocusPanel({
|
|
|
511
490
|
parsed !== null
|
|
512
491
|
? clampWorkDurationSeconds(parsed)
|
|
513
492
|
: clampWorkDurationSeconds(
|
|
514
|
-
typeof kronoFocus?.workDurationSeconds === "number" &&
|
|
493
|
+
typeof kronoFocus?.workDurationSeconds === "number" &&
|
|
494
|
+
Number.isFinite(kronoFocus.workDurationSeconds)
|
|
515
495
|
? kronoFocus.workDurationSeconds
|
|
516
496
|
: serverSecs,
|
|
517
497
|
);
|
|
518
498
|
const sm = Number.parseInt(draftShortBreakMin.trim(), 10);
|
|
519
499
|
const lm = Number.parseInt(draftLongBreakMin.trim(), 10);
|
|
520
500
|
const shortBreakSeconds = clampBreakDurationSeconds(
|
|
521
|
-
Number.isFinite(sm) && sm >= 1
|
|
501
|
+
Number.isFinite(sm) && sm >= 1
|
|
502
|
+
? sm * 60
|
|
503
|
+
: kronoFocus?.shortBreakDurationSeconds ?? 5 * 60,
|
|
522
504
|
);
|
|
523
505
|
const longBreakSeconds = clampBreakDurationSeconds(
|
|
524
|
-
Number.isFinite(lm) && lm >= 1
|
|
506
|
+
Number.isFinite(lm) && lm >= 1
|
|
507
|
+
? lm * 60
|
|
508
|
+
: kronoFocus?.longBreakDurationSeconds ?? 15 * 60,
|
|
525
509
|
);
|
|
526
510
|
void post({
|
|
527
511
|
type: "setKronoFocusDurations",
|
|
@@ -530,7 +514,11 @@ export function KronoFocusPanel({
|
|
|
530
514
|
longBreakSeconds,
|
|
531
515
|
});
|
|
532
516
|
setCustomDurationHistory((prev) => {
|
|
533
|
-
const next = pushKronoFocusDurationHistory(
|
|
517
|
+
const next = pushKronoFocusDurationHistory(
|
|
518
|
+
prev,
|
|
519
|
+
workSeconds,
|
|
520
|
+
DEFAULT_KRONO_FOCUS_WORK_SEC,
|
|
521
|
+
);
|
|
534
522
|
persistKronoFocusDurationHistory(next);
|
|
535
523
|
return next;
|
|
536
524
|
});
|
|
@@ -543,7 +531,11 @@ export function KronoFocusPanel({
|
|
|
543
531
|
};
|
|
544
532
|
|
|
545
533
|
const modeLabel =
|
|
546
|
-
mode === "work"
|
|
534
|
+
mode === "work"
|
|
535
|
+
? t.workMode
|
|
536
|
+
: mode === "break"
|
|
537
|
+
? t.breakMode
|
|
538
|
+
: t.longBreakMode;
|
|
547
539
|
|
|
548
540
|
const linkedId = kronoFocus?.linkedTaskId;
|
|
549
541
|
const linkedName = kronoFocus?.linkedTaskName?.trim();
|
|
@@ -555,20 +547,23 @@ export function KronoFocusPanel({
|
|
|
555
547
|
? `#kronosys-active-task-${linkedId}`
|
|
556
548
|
: "#kronosys-task-focus";
|
|
557
549
|
|
|
558
|
-
const { blink: timerBlink, urgentHighlight: timerUrgentHighlight } =
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
550
|
+
const { blink: timerBlink, urgentHighlight: timerUrgentHighlight } =
|
|
551
|
+
getKronoFocusTimerUrgency({
|
|
552
|
+
timeLeftSeconds: displaySecs,
|
|
553
|
+
mode,
|
|
554
|
+
status,
|
|
555
|
+
});
|
|
563
556
|
|
|
564
557
|
const clearStartPulse = () => setShowStartPulse(false);
|
|
565
558
|
|
|
566
559
|
const timeSize =
|
|
567
|
-
variant === "headerBar"
|
|
568
|
-
|
|
560
|
+
variant === "headerBar"
|
|
561
|
+
? "text-sm font-bold sm:text-base"
|
|
562
|
+
: "text-3xl sm:text-4xl";
|
|
563
|
+
/** Libellé de phase : bandeau entête = cellule h10 ; carte = grande échelle. */
|
|
569
564
|
const phaseLabelClassName =
|
|
570
565
|
variant === "headerBar"
|
|
571
|
-
?
|
|
566
|
+
? "block min-w-0 truncate text-center text-xs font-bold leading-none tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-sm"
|
|
572
567
|
: `${timeSize} max-w-[min(100%,24rem)] text-center font-bold leading-none tracking-tight text-zinc-800 dark:text-zinc-100 sm:max-w-[30rem]`;
|
|
573
568
|
/** Largeur stable pour `HH:MM:SS` jusqu’à 8 h (08:00:00). */
|
|
574
569
|
const timeSlotClass =
|
|
@@ -583,39 +578,72 @@ export function KronoFocusPanel({
|
|
|
583
578
|
showStartPulse
|
|
584
579
|
? "kronosys-krono-focus-start-pulse"
|
|
585
580
|
: timerBlink
|
|
586
|
-
|
|
587
|
-
|
|
581
|
+
? "kronosys-krono-focus-time-blink"
|
|
582
|
+
: ""
|
|
588
583
|
}`;
|
|
589
584
|
|
|
590
|
-
const durationPopoverAlign =
|
|
591
|
-
"left-1/2 top-full z-50 mt-2 w-[min(100vw-2rem,20rem)] -translate-x-1/2";
|
|
592
|
-
|
|
593
585
|
if (variant === "headerBar") {
|
|
586
|
+
const headerBarCtrlDisabled =
|
|
587
|
+
"disabled:pointer-events-none disabled:opacity-45 disabled:cursor-not-allowed";
|
|
588
|
+
const headerBarCtrlNeutral = `${appShellToolbarIconLinkClass} ${headerBarCtrlDisabled} [&_svg]:pointer-events-none [&_svg]:block [&_svg]:shrink-0`;
|
|
589
|
+
const headerBarCtrlAccent = `${appShellToolbarIconActiveClass} ${headerBarCtrlDisabled} [&_svg]:pointer-events-none [&_svg]:block [&_svg]:shrink-0`;
|
|
590
|
+
|
|
591
|
+
const ctrlsReadOnly = viewingArchive;
|
|
592
|
+
const ctrlRoTitle = ctrlsReadOnly
|
|
593
|
+
? t.kronoFocusControlsReadOnlyTooltip
|
|
594
|
+
: undefined;
|
|
595
|
+
|
|
594
596
|
return (
|
|
595
597
|
<section
|
|
596
|
-
className=
|
|
598
|
+
className={`relative ${appShellToolbarRibbonTrayWideClass}`}
|
|
597
599
|
aria-label={t.kronoFocusTitle}
|
|
598
600
|
>
|
|
599
|
-
<div
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
601
|
+
<div className="relative flex min-h-0 min-w-0 max-w-full flex-nowrap items-center gap-x-2">
|
|
602
|
+
<div
|
|
603
|
+
className={`${appShellToolbarInsetCellH10Class} max-w-[8.5rem] min-w-0 shrink px-1.5 sm:max-w-[11rem]`}
|
|
604
|
+
>
|
|
605
|
+
<span className={phaseLabelClassName}>{modeLabel}</span>
|
|
606
|
+
</div>
|
|
607
|
+
<div className="flex shrink-0 items-center gap-x-2">
|
|
608
|
+
{status === "running" ? (
|
|
609
|
+
<button
|
|
610
|
+
type="button"
|
|
611
|
+
className={headerBarCtrlNeutral}
|
|
612
|
+
disabled={ctrlsReadOnly}
|
|
613
|
+
title={ctrlsReadOnly ? ctrlRoTitle : t.kronoFocusPause}
|
|
614
|
+
aria-label={t.kronoFocusPause}
|
|
615
|
+
onClick={() => void post({ type: "pauseKronoFocus" })}
|
|
616
|
+
>
|
|
617
|
+
<Pause strokeWidth={2} aria-hidden />
|
|
618
|
+
</button>
|
|
619
|
+
) : (
|
|
620
|
+
<button
|
|
621
|
+
type="button"
|
|
622
|
+
className={headerBarCtrlAccent}
|
|
623
|
+
disabled={ctrlsReadOnly}
|
|
624
|
+
title={ctrlsReadOnly ? ctrlRoTitle : t.kronoFocusStart}
|
|
625
|
+
aria-label={t.kronoFocusStart}
|
|
626
|
+
onClick={() => void post({ type: "startKronoFocus" })}
|
|
627
|
+
>
|
|
628
|
+
<Play strokeWidth={2} aria-hidden />
|
|
629
|
+
</button>
|
|
630
|
+
)}
|
|
631
|
+
<button
|
|
632
|
+
type="button"
|
|
633
|
+
className={headerBarCtrlNeutral}
|
|
634
|
+
disabled={ctrlsReadOnly}
|
|
635
|
+
title={ctrlsReadOnly ? ctrlRoTitle : t.kronoFocusReset}
|
|
636
|
+
aria-label={t.kronoFocusReset}
|
|
637
|
+
onClick={() => void post({ type: "resetKronoFocus" })}
|
|
607
638
|
>
|
|
608
|
-
{
|
|
609
|
-
</
|
|
610
|
-
) : null}
|
|
611
|
-
<div className="shrink-0">
|
|
612
|
-
<KronoFocusPanelHelpTrigger t={t} />
|
|
639
|
+
<RotateCcw strokeWidth={2} aria-hidden />
|
|
640
|
+
</button>
|
|
613
641
|
</div>
|
|
614
|
-
<div className={phaseLabelClassName}>{modeLabel}</div>
|
|
615
642
|
{canEditWorkDuration ? (
|
|
616
643
|
<button
|
|
644
|
+
ref={timeButtonRef}
|
|
617
645
|
type="button"
|
|
618
|
-
className={`${timeSlotClass}
|
|
646
|
+
className={`${timeSlotClass} ${appShellToolbarInsetCellH10ButtonClass} min-w-[9ch] px-1 font-mono tabular-nums ${timeClassName}`}
|
|
619
647
|
title={t.kronoFocusEditDurationTitle}
|
|
620
648
|
aria-label={t.kronoFocusEditDurationTitle}
|
|
621
649
|
aria-expanded={durationPopoverOpen ? "true" : "false"}
|
|
@@ -625,51 +653,22 @@ export function KronoFocusPanel({
|
|
|
625
653
|
{clockDisplay}
|
|
626
654
|
</button>
|
|
627
655
|
) : (
|
|
628
|
-
<div
|
|
656
|
+
<div
|
|
657
|
+
className={`${timeSlotClass} ${appShellToolbarInsetCellH10Class} min-w-[9ch] px-1 font-mono tabular-nums leading-none ${timeClassName}`}
|
|
658
|
+
onAnimationEnd={clearStartPulse}
|
|
659
|
+
>
|
|
629
660
|
{clockDisplay}
|
|
630
661
|
</div>
|
|
631
662
|
)}
|
|
632
|
-
{!viewingArchive ? (
|
|
633
|
-
<div className="flex shrink-0 items-center gap-1.5">
|
|
634
|
-
{status === "running" ? (
|
|
635
|
-
<button
|
|
636
|
-
type="button"
|
|
637
|
-
className={kronoFocusControlNeutralHeader}
|
|
638
|
-
title={t.kronoFocusPause}
|
|
639
|
-
aria-label={t.kronoFocusPause}
|
|
640
|
-
onClick={() => void post({ type: "pauseKronoFocus" })}
|
|
641
|
-
>
|
|
642
|
-
<Pause strokeWidth={2} aria-hidden />
|
|
643
|
-
</button>
|
|
644
|
-
) : (
|
|
645
|
-
<button
|
|
646
|
-
type="button"
|
|
647
|
-
className={kronoFocusControlAccentHeader}
|
|
648
|
-
title={t.kronoFocusStart}
|
|
649
|
-
aria-label={t.kronoFocusStart}
|
|
650
|
-
onClick={() => void post({ type: "startKronoFocus" })}
|
|
651
|
-
>
|
|
652
|
-
<Play strokeWidth={2} aria-hidden />
|
|
653
|
-
</button>
|
|
654
|
-
)}
|
|
655
|
-
<button
|
|
656
|
-
type="button"
|
|
657
|
-
className={kronoFocusControlNeutralHeader}
|
|
658
|
-
title={t.kronoFocusReset}
|
|
659
|
-
aria-label={t.kronoFocusReset}
|
|
660
|
-
onClick={() => void post({ type: "resetKronoFocus" })}
|
|
661
|
-
>
|
|
662
|
-
<RotateCcw strokeWidth={2} aria-hidden />
|
|
663
|
-
</button>
|
|
664
|
-
</div>
|
|
665
|
-
) : null}
|
|
666
663
|
{showTaskLink ? (
|
|
667
|
-
<div className="hidden min-w-0 max-w-[min(38vw,12rem)] shrink border-l border-
|
|
664
|
+
<div className="hidden h-10 min-w-0 max-w-[min(38vw,12rem)] shrink items-center border-l border-zinc-300/70 pl-2 sm:flex dark:border-zinc-600/70">
|
|
668
665
|
<p
|
|
669
666
|
className="min-w-0 truncate text-left text-[0.65rem] leading-none text-zinc-600 dark:text-zinc-400"
|
|
670
667
|
title={`${t.kronoFocusLinkedTaskIntro} ${linkedName || "—"}`}
|
|
671
668
|
>
|
|
672
|
-
<span className="text-zinc-500">
|
|
669
|
+
<span className="text-zinc-500">
|
|
670
|
+
{t.kronoFocusLinkedTaskIntro}{" "}
|
|
671
|
+
</span>
|
|
673
672
|
<a
|
|
674
673
|
href={taskHref}
|
|
675
674
|
className="font-medium text-violet-800 underline decoration-violet-500/45 underline-offset-2 hover:text-violet-700 dark:text-violet-400/95 dark:decoration-violet-500/50 dark:hover:text-violet-300"
|
|
@@ -680,34 +679,42 @@ export function KronoFocusPanel({
|
|
|
680
679
|
</div>
|
|
681
680
|
) : null}
|
|
682
681
|
|
|
683
|
-
{durationPopoverOpen &&
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
682
|
+
{durationPopoverOpen &&
|
|
683
|
+
canEditWorkDuration &&
|
|
684
|
+
typeof document !== "undefined" &&
|
|
685
|
+
createPortal(
|
|
686
|
+
<div
|
|
687
|
+
ref={durationPopoverPanelRef}
|
|
688
|
+
style={durationPopoverStyle}
|
|
689
|
+
className="rounded-lg border border-zinc-200 bg-white p-3 text-left shadow-xl dark:border-zinc-600 dark:bg-zinc-900"
|
|
690
|
+
role="dialog"
|
|
691
|
+
aria-label={t.kronoFocusDurationPickerLabel}
|
|
692
|
+
>
|
|
693
|
+
<KronoFocusDurationPopoverFields
|
|
694
|
+
inputId="kronosys-krono-focus-duration-hb"
|
|
695
|
+
t={t}
|
|
696
|
+
draftTime={draftTime}
|
|
697
|
+
setDraftTime={setDraftTime}
|
|
698
|
+
draftShortBreakMin={draftShortBreakMin}
|
|
699
|
+
setDraftShortBreakMin={setDraftShortBreakMin}
|
|
700
|
+
draftLongBreakMin={draftLongBreakMin}
|
|
701
|
+
setDraftLongBreakMin={setDraftLongBreakMin}
|
|
702
|
+
onPickPreset={applyRhythmPreset}
|
|
703
|
+
customHistory={customDurationHistory}
|
|
704
|
+
onPickDefault={() => {
|
|
705
|
+
setDraftTime(
|
|
706
|
+
formatSecondsAsHMS(DEFAULT_KRONO_FOCUS_WORK_SEC),
|
|
707
|
+
);
|
|
708
|
+
setDraftShortBreakMin("5");
|
|
709
|
+
setDraftLongBreakMin("15");
|
|
710
|
+
}}
|
|
711
|
+
onApply={applyDraftDuration}
|
|
712
|
+
onCancel={() => setDurationPopoverOpen(false)}
|
|
713
|
+
onClearHistory={clearDurationHistory}
|
|
714
|
+
/>
|
|
715
|
+
</div>,
|
|
716
|
+
document.body,
|
|
717
|
+
)}
|
|
711
718
|
</div>
|
|
712
719
|
</section>
|
|
713
720
|
);
|
|
@@ -719,58 +726,65 @@ export function KronoFocusPanel({
|
|
|
719
726
|
aria-label={t.kronoFocusTitle}
|
|
720
727
|
>
|
|
721
728
|
<div className="mx-auto flex w-full max-w-5xl flex-col items-center gap-4">
|
|
722
|
-
<div className="flex w-full flex-wrap items-center justify-center gap-3 sm:justify-
|
|
723
|
-
<div className="flex
|
|
724
|
-
{
|
|
725
|
-
<p className="max-w-[min(100%,22rem)] text-center text-[0.7rem] leading-snug text-violet-900/85 dark:text-violet-200/75">
|
|
726
|
-
{t.kronoFocusLiveWhileViewingArchive}
|
|
727
|
-
</p>
|
|
728
|
-
) : null}
|
|
729
|
-
<KronoFocusPanelHelpTrigger t={t} />
|
|
730
|
-
</div>
|
|
731
|
-
{!viewingArchive ? (
|
|
732
|
-
<div className="flex shrink-0 items-center gap-2">
|
|
733
|
-
{status === "running" ? (
|
|
734
|
-
<button
|
|
735
|
-
type="button"
|
|
736
|
-
className={kronoFocusControlNeutralCard}
|
|
737
|
-
title={t.kronoFocusPause}
|
|
738
|
-
aria-label={t.kronoFocusPause}
|
|
739
|
-
onClick={() => void post({ type: "pauseKronoFocus" })}
|
|
740
|
-
>
|
|
741
|
-
<Pause strokeWidth={2} aria-hidden />
|
|
742
|
-
</button>
|
|
743
|
-
) : (
|
|
744
|
-
<button
|
|
745
|
-
type="button"
|
|
746
|
-
className={kronoFocusControlAccentCard}
|
|
747
|
-
title={t.kronoFocusStart}
|
|
748
|
-
aria-label={t.kronoFocusStart}
|
|
749
|
-
onClick={() => void post({ type: "startKronoFocus" })}
|
|
750
|
-
>
|
|
751
|
-
<Play strokeWidth={2} aria-hidden />
|
|
752
|
-
</button>
|
|
753
|
-
)}
|
|
729
|
+
<div className="flex w-full flex-wrap items-center justify-center gap-3 sm:justify-end">
|
|
730
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
731
|
+
{status === "running" ? (
|
|
754
732
|
<button
|
|
755
733
|
type="button"
|
|
756
734
|
className={kronoFocusControlNeutralCard}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
735
|
+
disabled={viewingArchive}
|
|
736
|
+
title={
|
|
737
|
+
viewingArchive
|
|
738
|
+
? t.kronoFocusControlsReadOnlyTooltip
|
|
739
|
+
: t.kronoFocusPause
|
|
740
|
+
}
|
|
741
|
+
aria-label={t.kronoFocusPause}
|
|
742
|
+
onClick={() => void post({ type: "pauseKronoFocus" })}
|
|
760
743
|
>
|
|
761
|
-
<
|
|
744
|
+
<Pause strokeWidth={2} aria-hidden />
|
|
762
745
|
</button>
|
|
763
|
-
|
|
764
|
-
|
|
746
|
+
) : (
|
|
747
|
+
<button
|
|
748
|
+
type="button"
|
|
749
|
+
className={kronoFocusControlAccentCard}
|
|
750
|
+
disabled={viewingArchive}
|
|
751
|
+
title={
|
|
752
|
+
viewingArchive
|
|
753
|
+
? t.kronoFocusControlsReadOnlyTooltip
|
|
754
|
+
: t.kronoFocusStart
|
|
755
|
+
}
|
|
756
|
+
aria-label={t.kronoFocusStart}
|
|
757
|
+
onClick={() => void post({ type: "startKronoFocus" })}
|
|
758
|
+
>
|
|
759
|
+
<Play strokeWidth={2} aria-hidden />
|
|
760
|
+
</button>
|
|
761
|
+
)}
|
|
762
|
+
<button
|
|
763
|
+
type="button"
|
|
764
|
+
className={kronoFocusControlNeutralCard}
|
|
765
|
+
disabled={viewingArchive}
|
|
766
|
+
title={
|
|
767
|
+
viewingArchive
|
|
768
|
+
? t.kronoFocusControlsReadOnlyTooltip
|
|
769
|
+
: t.kronoFocusReset
|
|
770
|
+
}
|
|
771
|
+
aria-label={t.kronoFocusReset}
|
|
772
|
+
onClick={() => void post({ type: "resetKronoFocus" })}
|
|
773
|
+
>
|
|
774
|
+
<RotateCcw strokeWidth={2} aria-hidden />
|
|
775
|
+
</button>
|
|
776
|
+
</div>
|
|
765
777
|
</div>
|
|
766
778
|
|
|
767
|
-
<div
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
779
|
+
<div className="relative flex w-full min-w-0 flex-wrap items-center justify-center gap-x-5 gap-y-3">
|
|
780
|
+
<div
|
|
781
|
+
className={`${phaseLabelClassName} text-violet-950 dark:text-violet-100/95`}
|
|
782
|
+
>
|
|
783
|
+
{modeLabel}
|
|
784
|
+
</div>
|
|
772
785
|
{canEditWorkDuration ? (
|
|
773
786
|
<button
|
|
787
|
+
ref={timeButtonRef}
|
|
774
788
|
type="button"
|
|
775
789
|
className={`${timeSlotClass} rounded-md px-1 py-0.5 ${timeClassName} cursor-pointer outline-none transition-colors hover:bg-zinc-200/60 dark:hover:bg-zinc-800/50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-violet-500/55`}
|
|
776
790
|
title={t.kronoFocusEditDurationTitle}
|
|
@@ -782,44 +796,57 @@ export function KronoFocusPanel({
|
|
|
782
796
|
{clockDisplay}
|
|
783
797
|
</button>
|
|
784
798
|
) : (
|
|
785
|
-
<div className={`${timeSlotClass} ${timeClassName}`} onAnimationEnd={clearStartPulse}>
|
|
786
|
-
{clockDisplay}
|
|
787
|
-
</div>
|
|
788
|
-
)}
|
|
789
|
-
|
|
790
|
-
{durationPopoverOpen && canEditWorkDuration && (
|
|
791
799
|
<div
|
|
792
|
-
className={
|
|
793
|
-
|
|
794
|
-
aria-label={t.kronoFocusDurationPickerLabel}
|
|
800
|
+
className={`${timeSlotClass} ${timeClassName}`}
|
|
801
|
+
onAnimationEnd={clearStartPulse}
|
|
795
802
|
>
|
|
796
|
-
|
|
797
|
-
inputId="kronosys-krono-focus-duration"
|
|
798
|
-
t={t}
|
|
799
|
-
draftTime={draftTime}
|
|
800
|
-
setDraftTime={setDraftTime}
|
|
801
|
-
draftShortBreakMin={draftShortBreakMin}
|
|
802
|
-
setDraftShortBreakMin={setDraftShortBreakMin}
|
|
803
|
-
draftLongBreakMin={draftLongBreakMin}
|
|
804
|
-
setDraftLongBreakMin={setDraftLongBreakMin}
|
|
805
|
-
onPickPreset={applyRhythmPreset}
|
|
806
|
-
customHistory={customDurationHistory}
|
|
807
|
-
onPickDefault={() => {
|
|
808
|
-
setDraftTime(formatSecondsAsHMS(DEFAULT_KRONO_FOCUS_WORK_SEC));
|
|
809
|
-
setDraftShortBreakMin("5");
|
|
810
|
-
setDraftLongBreakMin("15");
|
|
811
|
-
}}
|
|
812
|
-
onApply={applyDraftDuration}
|
|
813
|
-
onCancel={() => setDurationPopoverOpen(false)}
|
|
814
|
-
onClearHistory={clearDurationHistory}
|
|
815
|
-
/>
|
|
803
|
+
{clockDisplay}
|
|
816
804
|
</div>
|
|
817
805
|
)}
|
|
806
|
+
|
|
807
|
+
{durationPopoverOpen &&
|
|
808
|
+
canEditWorkDuration &&
|
|
809
|
+
typeof document !== "undefined" &&
|
|
810
|
+
createPortal(
|
|
811
|
+
<div
|
|
812
|
+
ref={durationPopoverPanelRef}
|
|
813
|
+
style={durationPopoverStyle}
|
|
814
|
+
className="rounded-lg border border-zinc-200 bg-white p-3 text-left shadow-xl dark:border-zinc-600 dark:bg-zinc-900"
|
|
815
|
+
role="dialog"
|
|
816
|
+
aria-label={t.kronoFocusDurationPickerLabel}
|
|
817
|
+
>
|
|
818
|
+
<KronoFocusDurationPopoverFields
|
|
819
|
+
inputId="kronosys-krono-focus-duration"
|
|
820
|
+
t={t}
|
|
821
|
+
draftTime={draftTime}
|
|
822
|
+
setDraftTime={setDraftTime}
|
|
823
|
+
draftShortBreakMin={draftShortBreakMin}
|
|
824
|
+
setDraftShortBreakMin={setDraftShortBreakMin}
|
|
825
|
+
draftLongBreakMin={draftLongBreakMin}
|
|
826
|
+
setDraftLongBreakMin={setDraftLongBreakMin}
|
|
827
|
+
onPickPreset={applyRhythmPreset}
|
|
828
|
+
customHistory={customDurationHistory}
|
|
829
|
+
onPickDefault={() => {
|
|
830
|
+
setDraftTime(
|
|
831
|
+
formatSecondsAsHMS(DEFAULT_KRONO_FOCUS_WORK_SEC),
|
|
832
|
+
);
|
|
833
|
+
setDraftShortBreakMin("5");
|
|
834
|
+
setDraftLongBreakMin("15");
|
|
835
|
+
}}
|
|
836
|
+
onApply={applyDraftDuration}
|
|
837
|
+
onCancel={() => setDurationPopoverOpen(false)}
|
|
838
|
+
onClearHistory={clearDurationHistory}
|
|
839
|
+
/>
|
|
840
|
+
</div>,
|
|
841
|
+
document.body,
|
|
842
|
+
)}
|
|
818
843
|
</div>
|
|
819
844
|
|
|
820
845
|
{showTaskLink ? (
|
|
821
846
|
<p className="w-full max-w-3xl text-center text-[0.8rem] leading-snug text-zinc-600 dark:text-zinc-400">
|
|
822
|
-
<span className="text-zinc-500">
|
|
847
|
+
<span className="text-zinc-500">
|
|
848
|
+
{t.kronoFocusLinkedTaskIntro}{" "}
|
|
849
|
+
</span>
|
|
823
850
|
<a
|
|
824
851
|
href={taskHref}
|
|
825
852
|
className="font-medium text-violet-800 underline decoration-violet-500/45 underline-offset-2 hover:text-violet-700 dark:text-violet-400/95 dark:decoration-violet-500/50 dark:hover:text-violet-300"
|