@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,183 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { DashboardConfirmModal } from "@/components/dashboard/DashboardSimpleModal";
|
|
4
|
+
import type { GlobalPauseActivationPreview } from "@/lib/globalPausePreview";
|
|
5
|
+
import type { DashboardStrings, Lang } from "@/lib/dashboardCopy";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_DASHBOARD_TIME_ZONE,
|
|
8
|
+
isValidIanaTimeZone,
|
|
9
|
+
} from "@/lib/dashboardTimeZone";
|
|
10
|
+
import { formatIsoInstantShort } from "@/lib/formatIsoShort";
|
|
11
|
+
import { formatDuration } from "@/lib/taskParsing";
|
|
12
|
+
|
|
13
|
+
function replaceTitleToken(s: string, title: string): string {
|
|
14
|
+
return s.split("{title}").join(title);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function GlobalPauseConfirmModal({
|
|
18
|
+
open,
|
|
19
|
+
preview,
|
|
20
|
+
lang,
|
|
21
|
+
displayTimeZone = DEFAULT_DASHBOARD_TIME_ZONE,
|
|
22
|
+
use24HourClock = true,
|
|
23
|
+
t,
|
|
24
|
+
onCancel,
|
|
25
|
+
onConfirm,
|
|
26
|
+
}: Readonly<{
|
|
27
|
+
open: boolean;
|
|
28
|
+
preview: GlobalPauseActivationPreview | null;
|
|
29
|
+
lang: Lang;
|
|
30
|
+
displayTimeZone?: string;
|
|
31
|
+
use24HourClock?: boolean;
|
|
32
|
+
t: DashboardStrings;
|
|
33
|
+
onCancel: () => void;
|
|
34
|
+
onConfirm: () => void;
|
|
35
|
+
}>) {
|
|
36
|
+
const tz =
|
|
37
|
+
displayTimeZone.trim() && isValidIanaTimeZone(displayTimeZone.trim())
|
|
38
|
+
? displayTimeZone.trim()
|
|
39
|
+
: DEFAULT_DASHBOARD_TIME_ZONE;
|
|
40
|
+
|
|
41
|
+
const summaryRows = preview
|
|
42
|
+
? (() => {
|
|
43
|
+
const startRaw = preview.sessionStartIso.trim();
|
|
44
|
+
const startLabel =
|
|
45
|
+
formatIsoInstantShort(startRaw, lang, tz, use24HourClock) ??
|
|
46
|
+
t.globalPauseConfirmSummaryUnknownInstant;
|
|
47
|
+
const endRaw = preview.sessionEndIso.trim();
|
|
48
|
+
const endLabel =
|
|
49
|
+
endRaw.length > 0
|
|
50
|
+
? formatIsoInstantShort(endRaw, lang, tz, use24HourClock) ??
|
|
51
|
+
t.globalPauseConfirmSummaryUnknownInstant
|
|
52
|
+
: t.globalPauseConfirmSummaryEndOngoing;
|
|
53
|
+
const wallLabel = formatDuration(
|
|
54
|
+
Math.max(0, preview.sessionWallMinutes),
|
|
55
|
+
);
|
|
56
|
+
const codingLabel =
|
|
57
|
+
preview.sessionCodingMinutes !== null
|
|
58
|
+
? formatDuration(preview.sessionCodingMinutes)
|
|
59
|
+
: t.globalPauseConfirmSummaryUnknownInstant;
|
|
60
|
+
const activeLabel =
|
|
61
|
+
preview.sessionActiveMinutes !== null
|
|
62
|
+
? formatDuration(preview.sessionActiveMinutes)
|
|
63
|
+
: t.globalPauseConfirmSummaryUnknownInstant;
|
|
64
|
+
const mainMin = preview.taskMainTimerMsExclusive / 60000;
|
|
65
|
+
const subMin = preview.subtasksTimerMsTotal / 60000;
|
|
66
|
+
const totalTaskMin = preview.taskTimersTotalMs / 60000;
|
|
67
|
+
|
|
68
|
+
return [
|
|
69
|
+
[t.globalPauseConfirmSummaryStart, startLabel],
|
|
70
|
+
[t.globalPauseConfirmSummaryEnd, endLabel],
|
|
71
|
+
[t.globalPauseConfirmSummaryWallDuration, wallLabel],
|
|
72
|
+
[t.globalPauseConfirmSummaryCodingSession, codingLabel],
|
|
73
|
+
[t.globalPauseConfirmSummaryActiveSession, activeLabel],
|
|
74
|
+
[t.globalPauseConfirmSummaryTasks, String(preview.taskCount)],
|
|
75
|
+
[t.globalPauseConfirmSummarySubtasks, String(preview.subtaskCount)],
|
|
76
|
+
[
|
|
77
|
+
t.globalPauseConfirmSummaryTaskTimersTotal,
|
|
78
|
+
formatDuration(Math.max(0, totalTaskMin)),
|
|
79
|
+
],
|
|
80
|
+
[
|
|
81
|
+
t.globalPauseConfirmSummaryMainTimer,
|
|
82
|
+
formatDuration(Math.max(0, mainMin)),
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
t.globalPauseConfirmSummarySubtaskTimers,
|
|
86
|
+
formatDuration(Math.max(0, subMin)),
|
|
87
|
+
],
|
|
88
|
+
] as const;
|
|
89
|
+
})()
|
|
90
|
+
: [];
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<DashboardConfirmModal
|
|
94
|
+
open={open && preview !== null}
|
|
95
|
+
title={t.globalPauseConfirmTitle}
|
|
96
|
+
message={t.globalPauseConfirmIntro}
|
|
97
|
+
cancelLabel={t.dialogCancelBtn}
|
|
98
|
+
confirmLabel={t.globalPauseConfirmConfirmBtn}
|
|
99
|
+
onCancel={onCancel}
|
|
100
|
+
onConfirm={onConfirm}
|
|
101
|
+
extra={
|
|
102
|
+
preview ? (
|
|
103
|
+
<div className="space-y-5 text-left text-sm text-zinc-700 dark:text-zinc-300">
|
|
104
|
+
<section aria-labelledby="global-pause-session-heading">
|
|
105
|
+
<h3
|
|
106
|
+
id="global-pause-session-heading"
|
|
107
|
+
className="text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500"
|
|
108
|
+
>
|
|
109
|
+
{t.globalPauseConfirmSessionHeading}
|
|
110
|
+
</h3>
|
|
111
|
+
<p className="mt-2 font-medium text-zinc-900 dark:text-zinc-100">
|
|
112
|
+
{preview.sessionName}
|
|
113
|
+
</p>
|
|
114
|
+
<p className="mt-1.5 text-sm leading-snug text-zinc-600 dark:text-zinc-400">
|
|
115
|
+
{preview.sessionWallWillPause
|
|
116
|
+
? t.globalPauseConfirmSessionWallWillPause
|
|
117
|
+
: t.globalPauseConfirmSessionWallAlreadyPaused}
|
|
118
|
+
</p>
|
|
119
|
+
|
|
120
|
+
<h4 className="mt-4 text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500">
|
|
121
|
+
{t.globalPauseConfirmSummaryHeading}
|
|
122
|
+
</h4>
|
|
123
|
+
<dl className="mt-2 space-y-1">
|
|
124
|
+
{summaryRows.map(([term, desc]) => (
|
|
125
|
+
<div
|
|
126
|
+
key={term}
|
|
127
|
+
className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5 rounded-md px-0 py-0.5 text-[0.85rem]"
|
|
128
|
+
>
|
|
129
|
+
<dt className="shrink-0 text-zinc-500 dark:text-zinc-400">
|
|
130
|
+
{term}
|
|
131
|
+
</dt>
|
|
132
|
+
<dd className="min-w-0 text-right tabular-nums font-medium text-zinc-900 dark:text-zinc-100">
|
|
133
|
+
{desc}
|
|
134
|
+
</dd>
|
|
135
|
+
</div>
|
|
136
|
+
))}
|
|
137
|
+
</dl>
|
|
138
|
+
</section>
|
|
139
|
+
<section aria-labelledby="global-pause-tasks-heading">
|
|
140
|
+
<h3
|
|
141
|
+
id="global-pause-tasks-heading"
|
|
142
|
+
className="text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-500"
|
|
143
|
+
>
|
|
144
|
+
{t.globalPauseConfirmTasksHeading}
|
|
145
|
+
</h3>
|
|
146
|
+
{preview.rows.length > 0 ? (
|
|
147
|
+
<ul className="mt-2 max-h-[min(40vh,18rem)] space-y-2 overflow-y-auto pr-0.5">
|
|
148
|
+
{preview.rows.map((row) => (
|
|
149
|
+
<li
|
|
150
|
+
key={row.taskId}
|
|
151
|
+
className="rounded-lg border border-zinc-200/90 bg-zinc-50/80 px-3 py-2 dark:border-zinc-600/80 dark:bg-zinc-950/40"
|
|
152
|
+
>
|
|
153
|
+
<p className="truncate font-medium text-zinc-900 dark:text-zinc-100">
|
|
154
|
+
{row.taskTitle}
|
|
155
|
+
</p>
|
|
156
|
+
<ul className="mt-1.5 list-disc space-y-0.5 pl-4 text-[0.85rem] leading-snug text-zinc-600 dark:text-zinc-400">
|
|
157
|
+
{row.pauseMainTimer ? (
|
|
158
|
+
<li>{t.globalPauseConfirmEffectMainTimer}</li>
|
|
159
|
+
) : null}
|
|
160
|
+
{row.activeSubtaskLabel !== undefined ? (
|
|
161
|
+
<li>
|
|
162
|
+
{replaceTitleToken(
|
|
163
|
+
t.globalPauseConfirmEffectSubtaskStop,
|
|
164
|
+
row.activeSubtaskLabel,
|
|
165
|
+
)}
|
|
166
|
+
</li>
|
|
167
|
+
) : null}
|
|
168
|
+
</ul>
|
|
169
|
+
</li>
|
|
170
|
+
))}
|
|
171
|
+
</ul>
|
|
172
|
+
) : (
|
|
173
|
+
<p className="mt-2 text-sm leading-snug text-zinc-600 dark:text-zinc-400">
|
|
174
|
+
{t.globalPauseConfirmNoTaskEffects}
|
|
175
|
+
</p>
|
|
176
|
+
)}
|
|
177
|
+
</section>
|
|
178
|
+
</div>
|
|
179
|
+
) : null
|
|
180
|
+
}
|
|
181
|
+
/>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
@@ -8,13 +8,16 @@ import {
|
|
|
8
8
|
useState,
|
|
9
9
|
} from "react";
|
|
10
10
|
import { createPortal } from "react-dom";
|
|
11
|
-
import { Calendar } from "lucide-react";
|
|
11
|
+
import { Calendar, Check } from "lucide-react";
|
|
12
12
|
import { DayPicker } from "react-day-picker";
|
|
13
13
|
import { enUS, fr } from "date-fns/locale";
|
|
14
14
|
import "react-day-picker/style.css";
|
|
15
15
|
|
|
16
16
|
import type { DashboardStrings, Lang } from "@/lib/dashboardCopy";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_DASHBOARD_TIME_ZONE,
|
|
19
|
+
isValidIanaTimeZone,
|
|
20
|
+
} from "@/lib/dashboardTimeZone";
|
|
18
21
|
import { TASK_PAST_DATETIME_TRIGGER_CLASS } from "./taskFieldStyles";
|
|
19
22
|
|
|
20
23
|
function pad2(n: number): string {
|
|
@@ -36,7 +39,8 @@ export function parseDatetimeLocalValue(s: string): Date | undefined {
|
|
|
36
39
|
return Number.isNaN(d.getTime()) ? undefined : d;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
/** Hauteur max pour le flip du popover (alignée sur le style du conteneur). */
|
|
43
|
+
const POPOVER_MAX_H = 480;
|
|
40
44
|
const GAP = 8;
|
|
41
45
|
|
|
42
46
|
type KronosysDatetimePopoverFieldProps = {
|
|
@@ -49,6 +53,8 @@ type KronosysDatetimePopoverFieldProps = {
|
|
|
49
53
|
displayTimeZone?: string;
|
|
50
54
|
/** Aperçu textuel : `true` = 24 h, `false` = 12 h (AM/PM). */
|
|
51
55
|
use24HourClock?: boolean;
|
|
56
|
+
/** Mode de préremplissage de l'heure quand la valeur est vide + bouton Today. */
|
|
57
|
+
defaultTimeMode?: "current-hour" | "next-half-hour";
|
|
52
58
|
t: Pick<
|
|
53
59
|
DashboardStrings,
|
|
54
60
|
| "taskPastDatetimePlaceholder"
|
|
@@ -68,8 +74,23 @@ export function KronosysDatetimePopoverField({
|
|
|
68
74
|
lang,
|
|
69
75
|
displayTimeZone = DEFAULT_DASHBOARD_TIME_ZONE,
|
|
70
76
|
use24HourClock = true,
|
|
77
|
+
defaultTimeMode = "current-hour",
|
|
71
78
|
t,
|
|
72
79
|
}: KronosysDatetimePopoverFieldProps) {
|
|
80
|
+
const resolveDefaultTimeParts = useCallback((): {
|
|
81
|
+
hour: number;
|
|
82
|
+
minute: number;
|
|
83
|
+
} => {
|
|
84
|
+
const now = new Date();
|
|
85
|
+
if (defaultTimeMode === "next-half-hour") {
|
|
86
|
+
if (now.getMinutes() < 30) {
|
|
87
|
+
return { hour: now.getHours(), minute: 30 };
|
|
88
|
+
}
|
|
89
|
+
return { hour: (now.getHours() + 1) % 24, minute: 0 };
|
|
90
|
+
}
|
|
91
|
+
return { hour: now.getHours(), minute: 0 };
|
|
92
|
+
}, [defaultTimeMode]);
|
|
93
|
+
|
|
73
94
|
const [open, setOpen] = useState(false);
|
|
74
95
|
const wasOpenRef = useRef(false);
|
|
75
96
|
const [pos, setPos] = useState({ top: 0, left: 0, maxW: 300 });
|
|
@@ -77,9 +98,12 @@ export function KronosysDatetimePopoverField({
|
|
|
77
98
|
const popoverRef = useRef<HTMLDivElement>(null);
|
|
78
99
|
|
|
79
100
|
const parsed = parseDatetimeLocalValue(value);
|
|
101
|
+
const defaultTime = resolveDefaultTimeParts();
|
|
80
102
|
const [selectedDay, setSelectedDay] = useState<Date | undefined>(parsed);
|
|
81
|
-
const [hour, setHour] = useState(parsed?.getHours() ??
|
|
82
|
-
const [minute, setMinute] = useState(
|
|
103
|
+
const [hour, setHour] = useState(parsed?.getHours() ?? defaultTime.hour);
|
|
104
|
+
const [minute, setMinute] = useState(
|
|
105
|
+
parsed?.getMinutes() ?? defaultTime.minute,
|
|
106
|
+
);
|
|
83
107
|
|
|
84
108
|
useEffect(() => {
|
|
85
109
|
const p = parseDatetimeLocalValue(value);
|
|
@@ -178,7 +202,8 @@ export function KronosysDatetimePopoverField({
|
|
|
178
202
|
dateStyle: "short",
|
|
179
203
|
timeStyle: "short",
|
|
180
204
|
hour12: !use24HourClock,
|
|
181
|
-
...(displayTimeZone.trim() &&
|
|
205
|
+
...(displayTimeZone.trim() &&
|
|
206
|
+
isValidIanaTimeZone(displayTimeZone.trim())
|
|
182
207
|
? { timeZone: displayTimeZone.trim() }
|
|
183
208
|
: {}),
|
|
184
209
|
}).format(parsed)
|
|
@@ -186,6 +211,8 @@ export function KronosysDatetimePopoverField({
|
|
|
186
211
|
|
|
187
212
|
const hours = Array.from({ length: 24 }, (_, i) => i);
|
|
188
213
|
const minutes = Array.from({ length: 60 }, (_, i) => i);
|
|
214
|
+
const acceptAriaLabel =
|
|
215
|
+
lang === "fr" ? "Accepter la date et l'heure" : "Accept date and time";
|
|
189
216
|
|
|
190
217
|
const popover =
|
|
191
218
|
open && typeof document !== "undefined"
|
|
@@ -195,130 +222,148 @@ export function KronosysDatetimePopoverField({
|
|
|
195
222
|
role="dialog"
|
|
196
223
|
aria-modal="true"
|
|
197
224
|
aria-label={ariaLabel}
|
|
198
|
-
className="kronosys-datetime-popover fixed z-[200] rounded-xl border border-violet-500/45 bg-zinc-50 p-3 shadow-2xl dark:border-violet-400/45 dark:bg-zinc-900"
|
|
225
|
+
className="kronosys-datetime-popover fixed z-[200] flex flex-col gap-3 overflow-hidden rounded-xl border border-violet-500/45 bg-zinc-50 p-3 shadow-2xl dark:border-violet-400/45 dark:bg-zinc-900"
|
|
199
226
|
style={{
|
|
200
227
|
top: pos.top,
|
|
201
228
|
left: pos.left,
|
|
202
229
|
width: pos.maxW,
|
|
203
|
-
maxHeight: POPOVER_MAX_H,
|
|
230
|
+
maxHeight: `min(${POPOVER_MAX_H}px, calc(100vh - 16px))`,
|
|
204
231
|
}}
|
|
205
232
|
>
|
|
206
|
-
<
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
locale={locale}
|
|
218
|
-
classNames={{
|
|
219
|
-
button_previous:
|
|
220
|
-
"rdp-button_previous rounded-lg border border-zinc-300 bg-white text-zinc-800 hover:bg-zinc-100 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700",
|
|
221
|
-
button_next:
|
|
222
|
-
"rdp-button_next rounded-lg border border-zinc-300 bg-white text-zinc-800 hover:bg-zinc-100 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700",
|
|
223
|
-
caption_label:
|
|
224
|
-
"rdp-caption_label text-sm font-semibold text-zinc-800 dark:text-zinc-100",
|
|
225
|
-
weekday: "rdp-weekday text-zinc-500 dark:text-zinc-400",
|
|
226
|
-
}}
|
|
227
|
-
components={{
|
|
228
|
-
Chevron: (props) =>
|
|
229
|
-
props.orientation === "left" ? (
|
|
230
|
-
<span
|
|
231
|
-
className="text-base leading-none font-semibold"
|
|
232
|
-
aria-hidden
|
|
233
|
-
>
|
|
234
|
-
‹
|
|
235
|
-
</span>
|
|
236
|
-
) : (
|
|
237
|
-
<span
|
|
238
|
-
className="text-base leading-none font-semibold"
|
|
239
|
-
aria-hidden
|
|
240
|
-
>
|
|
241
|
-
›
|
|
242
|
-
</span>
|
|
243
|
-
),
|
|
244
|
-
}}
|
|
245
|
-
/>
|
|
246
|
-
<div className="mt-3 flex flex-wrap items-center justify-center gap-2 border-t border-zinc-200 pt-3 dark:border-zinc-700/80">
|
|
247
|
-
<button
|
|
248
|
-
type="button"
|
|
249
|
-
className="rounded-lg border border-zinc-300 bg-white px-2.5 py-1 text-xs font-medium text-zinc-700 hover:bg-zinc-100 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700"
|
|
250
|
-
onClick={() => {
|
|
251
|
-
const now = new Date();
|
|
252
|
-
const today = new Date(
|
|
253
|
-
now.getFullYear(),
|
|
254
|
-
now.getMonth(),
|
|
255
|
-
now.getDate(),
|
|
256
|
-
hour,
|
|
257
|
-
minute,
|
|
258
|
-
0,
|
|
259
|
-
0,
|
|
260
|
-
);
|
|
261
|
-
setSelectedDay(today);
|
|
262
|
-
commitFrom(today, hour, minute);
|
|
233
|
+
<div className="min-h-0 min-w-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-y-contain">
|
|
234
|
+
<DayPicker
|
|
235
|
+
mode="single"
|
|
236
|
+
required={false}
|
|
237
|
+
selected={selectedDay}
|
|
238
|
+
defaultMonth={selectedDay ?? new Date()}
|
|
239
|
+
onSelect={(d) => {
|
|
240
|
+
setSelectedDay(d);
|
|
241
|
+
if (d) {
|
|
242
|
+
commitFrom(d, hour, minute);
|
|
243
|
+
}
|
|
263
244
|
}}
|
|
264
|
-
|
|
265
|
-
{
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const m = now.getMinutes();
|
|
274
|
-
setSelectedDay(now);
|
|
275
|
-
setHour(h);
|
|
276
|
-
setMinute(m);
|
|
277
|
-
commitFrom(now, h, m);
|
|
245
|
+
locale={locale}
|
|
246
|
+
classNames={{
|
|
247
|
+
button_previous:
|
|
248
|
+
"rdp-button_previous rounded-lg border border-zinc-300 bg-white text-zinc-800 hover:bg-zinc-100 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700",
|
|
249
|
+
button_next:
|
|
250
|
+
"rdp-button_next rounded-lg border border-zinc-300 bg-white text-zinc-800 hover:bg-zinc-100 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700",
|
|
251
|
+
caption_label:
|
|
252
|
+
"rdp-caption_label text-sm font-semibold text-zinc-800 dark:text-zinc-100",
|
|
253
|
+
weekday: "rdp-weekday text-zinc-500 dark:text-zinc-400",
|
|
278
254
|
}}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
255
|
+
components={{
|
|
256
|
+
Chevron: (props) =>
|
|
257
|
+
props.orientation === "left" ? (
|
|
258
|
+
<span
|
|
259
|
+
className="text-base leading-none font-semibold"
|
|
260
|
+
aria-hidden
|
|
261
|
+
>
|
|
262
|
+
‹
|
|
263
|
+
</span>
|
|
264
|
+
) : (
|
|
265
|
+
<span
|
|
266
|
+
className="text-base leading-none font-semibold"
|
|
267
|
+
aria-hidden
|
|
268
|
+
>
|
|
269
|
+
›
|
|
270
|
+
</span>
|
|
271
|
+
),
|
|
272
|
+
}}
|
|
273
|
+
/>
|
|
282
274
|
</div>
|
|
283
|
-
<div className="
|
|
284
|
-
<
|
|
285
|
-
<
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
275
|
+
<div className="relative z-[1] shrink-0 space-y-3 border-t border-zinc-200 bg-zinc-50 pt-3 dark:border-zinc-700/80 dark:bg-zinc-900">
|
|
276
|
+
<div className="flex flex-wrap items-center justify-center gap-2">
|
|
277
|
+
<button
|
|
278
|
+
type="button"
|
|
279
|
+
className="rounded-lg border border-zinc-300 bg-white px-2.5 py-1 text-xs font-medium text-zinc-700 hover:bg-zinc-100 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700"
|
|
280
|
+
onClick={() => {
|
|
281
|
+
const now = new Date();
|
|
282
|
+
const todayDefault = resolveDefaultTimeParts();
|
|
283
|
+
const today = new Date(
|
|
284
|
+
now.getFullYear(),
|
|
285
|
+
now.getMonth(),
|
|
286
|
+
now.getDate(),
|
|
287
|
+
todayDefault.hour,
|
|
288
|
+
todayDefault.minute,
|
|
289
|
+
0,
|
|
290
|
+
0,
|
|
291
|
+
);
|
|
292
|
+
setSelectedDay(today);
|
|
293
|
+
setHour(todayDefault.hour);
|
|
294
|
+
setMinute(todayDefault.minute);
|
|
295
|
+
commitFrom(today, todayDefault.hour, todayDefault.minute);
|
|
295
296
|
}}
|
|
296
|
-
aria-label={t.taskPastDatetimeHourAria}
|
|
297
297
|
>
|
|
298
|
-
{
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const m = Number(e.target.value);
|
|
298
|
+
{t.taskPastDatetimeTodayBtn}
|
|
299
|
+
</button>
|
|
300
|
+
<button
|
|
301
|
+
type="button"
|
|
302
|
+
className="rounded-lg border border-violet-500/45 bg-violet-50 px-2.5 py-1 text-xs font-medium text-violet-700 hover:bg-violet-100 dark:border-violet-400/55 dark:bg-violet-950/35 dark:text-violet-200 dark:hover:bg-violet-900/40"
|
|
303
|
+
onClick={() => {
|
|
304
|
+
const now = new Date();
|
|
305
|
+
const h = now.getHours();
|
|
306
|
+
const m = now.getMinutes();
|
|
307
|
+
setSelectedDay(now);
|
|
308
|
+
setHour(h);
|
|
310
309
|
setMinute(m);
|
|
311
|
-
commitFrom(
|
|
310
|
+
commitFrom(now, h, m);
|
|
311
|
+
}}
|
|
312
|
+
>
|
|
313
|
+
{t.taskPastDatetimeNowBtn}
|
|
314
|
+
</button>
|
|
315
|
+
</div>
|
|
316
|
+
<div className="flex flex-wrap items-center justify-start gap-2 sm:justify-center">
|
|
317
|
+
<label className="flex max-w-full flex-wrap items-center gap-1.5 text-xs font-medium text-zinc-600 dark:text-zinc-400">
|
|
318
|
+
<span className="whitespace-nowrap">
|
|
319
|
+
{t.taskPastDatetimeTimeLabel}
|
|
320
|
+
</span>
|
|
321
|
+
<select
|
|
322
|
+
className="h-8 w-16 min-w-0 rounded-lg border border-violet-500/45 bg-white px-2 text-xs font-mono text-zinc-900 outline-none dark:border-violet-400/50 dark:bg-zinc-800 dark:text-zinc-100"
|
|
323
|
+
value={hour}
|
|
324
|
+
onChange={(e) => {
|
|
325
|
+
const h = Number(e.target.value);
|
|
326
|
+
setHour(h);
|
|
327
|
+
commitFrom(selectedDay, h, minute);
|
|
328
|
+
}}
|
|
329
|
+
aria-label={t.taskPastDatetimeHourAria}
|
|
330
|
+
>
|
|
331
|
+
{hours.map((h) => (
|
|
332
|
+
<option key={h} value={h}>
|
|
333
|
+
{pad2(h)}
|
|
334
|
+
</option>
|
|
335
|
+
))}
|
|
336
|
+
</select>
|
|
337
|
+
<span className="font-mono text-zinc-500">:</span>
|
|
338
|
+
<select
|
|
339
|
+
className="h-8 w-16 min-w-0 rounded-lg border border-violet-500/45 bg-white px-2 text-xs font-mono text-zinc-900 outline-none dark:border-violet-400/50 dark:bg-zinc-800 dark:text-zinc-100"
|
|
340
|
+
value={minute}
|
|
341
|
+
onChange={(e) => {
|
|
342
|
+
const m = Number(e.target.value);
|
|
343
|
+
setMinute(m);
|
|
344
|
+
commitFrom(selectedDay, hour, m);
|
|
345
|
+
}}
|
|
346
|
+
aria-label={t.taskPastDatetimeMinuteAria}
|
|
347
|
+
>
|
|
348
|
+
{minutes.map((m) => (
|
|
349
|
+
<option key={m} value={m}>
|
|
350
|
+
{pad2(m)}
|
|
351
|
+
</option>
|
|
352
|
+
))}
|
|
353
|
+
</select>
|
|
354
|
+
</label>
|
|
355
|
+
<button
|
|
356
|
+
type="button"
|
|
357
|
+
className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-emerald-500/50 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 dark:border-emerald-400/60 dark:bg-emerald-950/35 dark:text-emerald-200 dark:hover:bg-emerald-900/40"
|
|
358
|
+
aria-label={acceptAriaLabel}
|
|
359
|
+
onClick={() => {
|
|
360
|
+
commitFrom(selectedDay, hour, minute);
|
|
361
|
+
setOpen(false);
|
|
312
362
|
}}
|
|
313
|
-
aria-label={t.taskPastDatetimeMinuteAria}
|
|
314
363
|
>
|
|
315
|
-
{
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
</option>
|
|
319
|
-
))}
|
|
320
|
-
</select>
|
|
321
|
-
</label>
|
|
364
|
+
<Check className="h-4 w-4" strokeWidth={2.5} aria-hidden />
|
|
365
|
+
</button>
|
|
366
|
+
</div>
|
|
322
367
|
</div>
|
|
323
368
|
</div>,
|
|
324
369
|
document.body,
|
|
@@ -330,24 +375,24 @@ export function KronosysDatetimePopoverField({
|
|
|
330
375
|
<button
|
|
331
376
|
ref={triggerRef}
|
|
332
377
|
type="button"
|
|
333
|
-
className={`${TASK_PAST_DATETIME_TRIGGER_CLASS}
|
|
378
|
+
className={`${TASK_PAST_DATETIME_TRIGGER_CLASS} max-w-full text-left`}
|
|
334
379
|
aria-label={ariaLabel}
|
|
335
380
|
aria-expanded={open}
|
|
336
381
|
aria-haspopup="dialog"
|
|
337
382
|
onClick={() => setOpen((o) => !o)}
|
|
338
383
|
>
|
|
339
384
|
<span
|
|
340
|
-
className={`inline-block min-w-0 max-w-full flex-1 truncate text-left
|
|
385
|
+
className={`inline-block min-w-0 max-w-full flex-1 truncate text-left ${
|
|
341
386
|
display
|
|
342
|
-
? "text-zinc-
|
|
343
|
-
: "text-zinc-
|
|
387
|
+
? "text-zinc-500 dark:text-zinc-500"
|
|
388
|
+
: "text-zinc-400 dark:text-zinc-500/75"
|
|
344
389
|
}`}
|
|
345
390
|
>
|
|
346
391
|
{display ?? t.taskPastDatetimePlaceholder}
|
|
347
392
|
</span>
|
|
348
393
|
<Calendar
|
|
349
|
-
className="h-3
|
|
350
|
-
strokeWidth={
|
|
394
|
+
className="h-3 w-3 shrink-0 text-zinc-400/70 opacity-80 transition-[opacity,color] group-hover/datetime-trigger:text-zinc-500 group-hover/datetime-trigger:opacity-100 dark:text-zinc-500/55 dark:group-hover/datetime-trigger:text-zinc-400"
|
|
395
|
+
strokeWidth={1.5}
|
|
351
396
|
aria-hidden
|
|
352
397
|
/>
|
|
353
398
|
</button>
|