@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,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 { DEFAULT_DASHBOARD_TIME_ZONE, isValidIanaTimeZone } from "@/lib/dashboardTimeZone";
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
- const POPOVER_MAX_H = 400;
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() ?? 12);
82
- const [minute, setMinute] = useState(parsed?.getMinutes() ?? 0);
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() && isValidIanaTimeZone(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
- <DayPicker
207
- mode="single"
208
- required={false}
209
- selected={selectedDay}
210
- defaultMonth={selectedDay ?? new Date()}
211
- onSelect={(d) => {
212
- setSelectedDay(d);
213
- if (d) {
214
- commitFrom(d, hour, minute);
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
- {t.taskPastDatetimeTodayBtn}
266
- </button>
267
- <button
268
- type="button"
269
- 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"
270
- onClick={() => {
271
- const now = new Date();
272
- const h = now.getHours();
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
- {t.taskPastDatetimeNowBtn}
281
- </button>
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="mt-3 flex flex-wrap items-center justify-center gap-2 border-t border-zinc-200 pt-3 dark:border-zinc-700/80">
284
- <label className="flex items-center gap-1.5 text-xs font-medium text-zinc-600 dark:text-zinc-400">
285
- <span className="whitespace-nowrap">
286
- {t.taskPastDatetimeTimeLabel}
287
- </span>
288
- <select
289
- className="h-8 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"
290
- value={hour}
291
- onChange={(e) => {
292
- const h = Number(e.target.value);
293
- setHour(h);
294
- commitFrom(selectedDay, h, minute);
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
- {hours.map((h) => (
299
- <option key={h} value={h}>
300
- {pad2(h)}
301
- </option>
302
- ))}
303
- </select>
304
- <span className="font-mono text-zinc-500">:</span>
305
- <select
306
- className="h-8 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"
307
- value={minute}
308
- onChange={(e) => {
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(selectedDay, hour, m);
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
- {minutes.map((m) => (
316
- <option key={m} value={m}>
317
- {pad2(m)}
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} inline-flex max-w-full cursor-pointer items-center gap-2 text-left`}
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 font-mono text-xs tabular-nums ${
385
+ className={`inline-block min-w-0 max-w-full flex-1 truncate text-left ${
341
386
  display
342
- ? "text-zinc-800 dark:text-zinc-100"
343
- : "text-zinc-500 dark:text-zinc-500"
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.5 w-3.5 shrink-0 opacity-70"
350
- strokeWidth={2}
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>