@nightkatana/kronosys-app 1.0.0-beta.18 → 1.0.0-beta.19
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 +18 -24
- package/app/changelog/page.tsx +2 -0
- package/app/globals.css +33 -4
- package/app/guide/page.tsx +2 -0
- package/app/implementation/page.tsx +450 -78
- package/app/layout.tsx +4 -1
- package/app/licenses/page.tsx +2 -0
- package/app/logs/page.tsx +258 -0
- package/app/manifest.ts +5 -5
- package/app/page.tsx +44 -1
- package/app/reporting/page.tsx +28 -62
- package/app/settings/page.tsx +2 -0
- package/components/dashboard/AppShellHeaderWallClock.tsx +54 -0
- package/components/dashboard/AppShellLiveSessionDrawer.tsx +154 -38
- package/components/dashboard/AppShellRouteNav.tsx +94 -8
- package/components/dashboard/DashboardPauseBackdrop.tsx +50 -0
- package/components/dashboard/DashboardTour.tsx +115 -29
- package/components/dashboard/KronosysDatetimePopoverField.tsx +42 -6
- package/components/dashboard/KronosysTimePopoverField.tsx +54 -12
- package/components/dashboard/NewSessionScopeModal.tsx +107 -35
- 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 +84 -6
- package/components/dashboard/SessionListPanel.tsx +21 -2
- package/components/dashboard/SettingsTaskTemplatesSection.tsx +2 -0
- package/components/dashboard/SettingsTour.tsx +86 -21
- package/components/dashboard/TagPills.tsx +14 -1
- package/components/dashboard/TaskFocusPanel.tsx +206 -26
- package/components/dashboard/TaskSessionLiveCard.tsx +238 -16
- package/components/dashboard/TaskTimelineGanttModal.tsx +47 -3
- package/components/dashboard/taskFieldStyles.ts +17 -2
- package/components/dashboard/useReportingInteractionState.ts +80 -0
- package/lib/businessRulesMatrix.ts +210 -0
- package/lib/copyToClipboard.ts +43 -0
- package/lib/dashboardCopy.ts +117 -6
- package/lib/dashboardTimeZone.ts +109 -0
- package/lib/formatAppShellWallClock.ts +66 -0
- package/lib/generatedUserChangelog.ts +35 -1
- package/lib/implementationNotes.ts +508 -29
- package/lib/kronosysApi.ts +4 -0
- package/lib/kronosysDashboardModalGates.ts +24 -0
- package/lib/plannedBoundaryAttention.ts +9 -0
- package/lib/plannedBoundaryConflict.ts +23 -0
- package/lib/reportingAggregate.ts +450 -81
- package/lib/reportingStrings.ts +9 -0
- package/lib/sessionListMerge.ts +4 -0
- package/lib/sessionTaskSidebarStats.ts +59 -13
- package/lib/settingsCopy.ts +4 -4
- package/lib/taskParsing.ts +332 -103
- package/lib/taskTemplateDraft.ts +18 -4
- package/lib/taskTimelineGantt.ts +103 -3
- package/lib/temporalDisplayPlanned.ts +71 -0
- package/lib/userGuideCopy.ts +75 -33
- package/next-env.d.ts +1 -1
- package/package.json +6 -19
- package/server/actionDispatch.ts +438 -75
- package/server/actionTaskSession.ts +134 -34
- package/server/db.ts +7 -15
- package/server/dbSchema.ts +24 -0
- package/server/gitlabTokenStore.ts +0 -12
- package/server/liveHistorySync.ts +53 -0
- package/server/mainTimerHydrate.ts +37 -16
- package/server/payloadStore.ts +33 -11
- package/server/sessionWallHydrate.ts +46 -6
- package/server/userActionLog.ts +126 -0
- package/sonar-project.properties +11 -0
- package/components/dashboard/TaskNotesDisplay.test.tsx +0 -110
- 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/formatSessionNameTemplate.test.ts +0 -53
- package/lib/globalPausePreview.test.ts +0 -170
- 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 -352
- package/lib/reportingNonFinalIndicators.test.ts +0 -157
- package/lib/reportingTagWeekBreakdown.test.ts +0 -142
- 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 -50
- package/lib/taskParsing.test.ts +0 -153
- package/lib/taskTemplateDraft.test.ts +0 -52
- package/lib/taskTimelineGantt.test.ts +0 -50
- package/lib/usageProfile.test.ts +0 -84
- package/server/actionDispatch.test.ts +0 -1083
- package/server/actionTaskSession.test.ts +0 -723
- package/server/kronoFocusHydrate.test.ts +0 -142
- package/server/kronoFocusMigrate.test.ts +0 -53
- package/server/mainTimerHydrate.test.ts +0 -93
- package/server/payloadStore.test.ts +0 -78
- package/server/sessionWallHydrate.test.ts +0 -64
package/README.md
CHANGED
|
@@ -12,11 +12,38 @@ Stack : **Next 16** (App Router), **React 19**, **Tailwind 4**, **better-sqlite3
|
|
|
12
12
|
| `npm run build` / `npm run start` | Production |
|
|
13
13
|
| `npm run test` | Vitest |
|
|
14
14
|
| `npm run test:coverage` | Vitest avec couverture LCOV pour SonarQube Cloud |
|
|
15
|
+
| `npm run test:e2e` | Playwright — voir **Tests E2E** ci‑dessous ; démarre Next sur **5555** si besoin (`playwright.config.mjs`) |
|
|
16
|
+
| `npm run test:e2e:install` | Télécharge Chromium, Chrome Headless Shell et ffmpeg pour la version installée de Playwright |
|
|
17
|
+
| `npm run test:e2e:podman` | E2E dans une image Ubuntu officielle Playwright via Podman (voir ci‑dessous) |
|
|
15
18
|
| `npm run hooks:install` | Installe Husky et active les hooks Git du dépôt |
|
|
16
19
|
| `npm run release:dry` | Simule bump semver + `CHANGELOG.md` + tag (aucune écriture) |
|
|
17
20
|
| `npm run release` | Bump `package.json` / `package-lock.json`, met à jour le journal, commit, tag `v…` |
|
|
18
21
|
| `npm run release:first` | Une fois : journal complet pour la version courante **sans** bump (puis tag `v…`) |
|
|
19
22
|
|
|
23
|
+
### Tests E2E (Playwright)
|
|
24
|
+
|
|
25
|
+
Après **`npm install`** ou une mise à jour de **`@playwright/test`**, exécuter une fois :
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm run test:e2e:install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Sans cela, les tests échouent souvent avec **`Executable doesn't exist`** ou **`browserType.launch`** : Playwright doit télécharger **Chromium**, **Chrome Headless Shell** (utilisé pour le projet « chromium ») et **ffmpeg**.
|
|
32
|
+
|
|
33
|
+
Sous **Fedora**, ne pas utiliser `npx playwright install-deps` (script prévu pour Debian/Ubuntu). Si la validation « host » bloque le téléchargement :
|
|
34
|
+
`PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=1 npm run test:e2e:install`.
|
|
35
|
+
|
|
36
|
+
Sous **Fedora** (ou si les binaires Playwright hôtent sur un fallback Ubuntu pénible), on peut lancer les E2E dans la **même image** que le pipeline GitLab (Ubuntu Noble, navigateurs inclus) avec **Podman** :
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm run test:e2e:podman
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
(équivalent : `bash scripts/e2e-podman.sh` — exécute `npm ci` puis `npm run test:e2e` *dans* le conteneur : ne pas réutiliser le `node_modules` de l’hôte).
|
|
43
|
+
Image par défaut : `mcr.microsoft.com/playwright:v1.59.1-noble` (voir `PLAYWRIGHT_IMAGE=…` pour surcharger). Le montage utilise le suffixe **`:Z`** pour SELinux.
|
|
44
|
+
|
|
45
|
+
En cas de conflit avec un `.next` déjà présent sur l’hôte (autre `next dev`), le script utilise un **volume Podman** pour `/work/.next` (`kronosys-e2e-dot-next` par défaut, surcharge avec `PODMAN_E2E_NEXT_VOLUME`). Pour repartir à zéro : `podman volume rm kronosys-e2e-dot-next`.
|
|
46
|
+
|
|
20
47
|
Les messages de commit doivent suivre les [Conventional Commits](https://www.conventionalcommits.org/) (`feat`, `fix`, etc.) pour que le journal regroupe correctement **nouveautés**, **corrections** et autres rubriques. Après `npm run release`, pousser avec `git push --follow-tags`. Si le dépôt n’a pas encore de tag pour la dernière entrée du `CHANGELOG.md`, créez `git tag vX.Y.Z <commit>` avant le prochain `release`, sinon l’historique serait dupliqué (voir l’en-tête du journal). Sur **GitHub**, le workflow **Release** crée une **GitHub Release** à chaque tag `v*.*.*`. Sur **GitLab**, créez une release à partir du même tag (interface ou `glab release create`) en réutilisant la section extraite : `bash scripts/extract-changelog-for-tag.sh CHANGELOG.md X.Y.Z`.
|
|
21
48
|
|
|
22
49
|
Depuis la **racine** du monorepo : `npm run dev` délègue ici.
|
|
@@ -72,7 +99,7 @@ Sinon : `NODE_EXTRA_CA_CERTS` pointant vers le PEM racine de l’organisation, o
|
|
|
72
99
|
|
|
73
100
|
## Publication npm (`@nightkatana/kronosys-app`)
|
|
74
101
|
|
|
75
|
-
La version est définie dans `package.json` (
|
|
102
|
+
La version est définie dans `package.json` (référence actuelle : **`1.0.0-beta.19`**). Avant la première publication : créer le scope **@nightkatana** sur [npmjs.com](https://www.npmjs.com/) si besoin, puis `npm login`. Depuis ce dossier :
|
|
76
103
|
|
|
77
104
|
```bash
|
|
78
105
|
npm publish --access public
|
package/app/api/action/route.ts
CHANGED
|
@@ -1,16 +1,52 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
|
|
3
3
|
import { dispatchKronosysAction } from "@/server/actionDispatch";
|
|
4
|
+
import { clearUserActionLogs, logUserAction } from "@/server/userActionLog";
|
|
4
5
|
|
|
5
6
|
export const runtime = "nodejs";
|
|
6
7
|
|
|
8
|
+
function badRequest(error: string, status = 400) {
|
|
9
|
+
return NextResponse.json({ ok: false, error }, { status });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
13
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
export async function POST(req: Request) {
|
|
8
|
-
let
|
|
17
|
+
let parsedBody: unknown;
|
|
9
18
|
try {
|
|
10
|
-
|
|
19
|
+
parsedBody = await req.json();
|
|
11
20
|
} catch {
|
|
12
|
-
return
|
|
21
|
+
return badRequest("invalid_json");
|
|
22
|
+
}
|
|
23
|
+
if (!isRecord(parsedBody)) {
|
|
24
|
+
return badRequest("invalid_action_request");
|
|
13
25
|
}
|
|
26
|
+
const actionType = typeof parsedBody.type === "string" ? parsedBody.type.trim() : "";
|
|
27
|
+
if (!actionType) {
|
|
28
|
+
return badRequest("missing_action_type");
|
|
29
|
+
}
|
|
30
|
+
const body = parsedBody;
|
|
14
31
|
const out = await dispatchKronosysAction(body);
|
|
32
|
+
try {
|
|
33
|
+
if (actionType === "clearHistory") {
|
|
34
|
+
clearUserActionLogs();
|
|
35
|
+
return NextResponse.json({ ok: out.ok, result: out.result ?? {} });
|
|
36
|
+
}
|
|
37
|
+
const sourceIp =
|
|
38
|
+
req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
|
|
39
|
+
req.headers.get("x-real-ip") ||
|
|
40
|
+
null;
|
|
41
|
+
const userAgent = req.headers.get("user-agent");
|
|
42
|
+
logUserAction({
|
|
43
|
+
actionType,
|
|
44
|
+
ok: out.ok,
|
|
45
|
+
body,
|
|
46
|
+
context: { sourceIp, userAgent },
|
|
47
|
+
});
|
|
48
|
+
} catch {
|
|
49
|
+
/* ignore logging failure */
|
|
50
|
+
}
|
|
15
51
|
return NextResponse.json({ ok: out.ok, result: out.result ?? {} });
|
|
16
52
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
import { readUserActionLogs } from "@/server/userActionLog";
|
|
4
|
+
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
function parsePositiveInt(raw: string | null, fallback: number): number {
|
|
8
|
+
if (!raw) {
|
|
9
|
+
return fallback;
|
|
10
|
+
}
|
|
11
|
+
const n = Number.parseInt(raw, 10);
|
|
12
|
+
if (!Number.isFinite(n)) {
|
|
13
|
+
return fallback;
|
|
14
|
+
}
|
|
15
|
+
return n;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function GET(req: Request) {
|
|
19
|
+
const url = new URL(req.url);
|
|
20
|
+
const limit = parsePositiveInt(url.searchParams.get("limit"), 100);
|
|
21
|
+
const offset = parsePositiveInt(url.searchParams.get("offset"), 0);
|
|
22
|
+
const logs = readUserActionLogs(limit, offset);
|
|
23
|
+
return NextResponse.json({ ok: true, logs });
|
|
24
|
+
}
|
package/app/api/backup/route.ts
CHANGED
package/app/api/restore/route.ts
CHANGED
|
@@ -12,6 +12,10 @@ export const runtime = "nodejs";
|
|
|
12
12
|
const DB_NAME = "kronosys.sqlite";
|
|
13
13
|
const MAX_UPLOAD_BYTES = 64 * 1024 * 1024;
|
|
14
14
|
|
|
15
|
+
function badRequest(error: string, extra?: Record<string, unknown>, status = 400) {
|
|
16
|
+
return NextResponse.json({ ok: false, error, ...(extra ?? {}) }, { status });
|
|
17
|
+
}
|
|
18
|
+
|
|
15
19
|
function nowStamp(): string {
|
|
16
20
|
return new Date().toISOString().replace(/\.\d{3}Z$/, "Z").replaceAll(":", "-");
|
|
17
21
|
}
|
|
@@ -58,7 +62,7 @@ function restoreFromSqliteBuffer(buf: Buffer): { backupPath: string | null } {
|
|
|
58
62
|
|
|
59
63
|
try {
|
|
60
64
|
const db = getSqlite();
|
|
61
|
-
db.
|
|
65
|
+
db.exec("PRAGMA integrity_check;");
|
|
62
66
|
db.exec("SELECT 1;");
|
|
63
67
|
} catch (error) {
|
|
64
68
|
resetSqliteConnection();
|
|
@@ -75,25 +79,24 @@ export async function POST(req: Request) {
|
|
|
75
79
|
const ctype = req.headers.get("content-type") ?? "";
|
|
76
80
|
const isMultipart = ctype.toLowerCase().includes("multipart/form-data");
|
|
77
81
|
if (!isMultipart) {
|
|
78
|
-
return
|
|
79
|
-
{ ok: false, error: "unsupported_content_type" },
|
|
80
|
-
{ status: 415 },
|
|
81
|
-
);
|
|
82
|
+
return badRequest("unsupported_content_type", undefined, 415);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
let form: FormData;
|
|
86
|
+
try {
|
|
87
|
+
form = await req.formData();
|
|
88
|
+
} catch {
|
|
89
|
+
return badRequest("invalid_form_data");
|
|
90
|
+
}
|
|
85
91
|
const file = form.get("file");
|
|
86
92
|
const formatField = form.get("format");
|
|
87
93
|
const formatRaw =
|
|
88
94
|
typeof formatField === "string" ? formatField.trim().toLowerCase() : "";
|
|
89
95
|
if (!(file instanceof File)) {
|
|
90
|
-
return
|
|
96
|
+
return badRequest("file_required");
|
|
91
97
|
}
|
|
92
98
|
if (file.size <= 0 || file.size > MAX_UPLOAD_BYTES) {
|
|
93
|
-
return
|
|
94
|
-
{ ok: false, error: "invalid_file_size", maxBytes: MAX_UPLOAD_BYTES },
|
|
95
|
-
{ status: 400 },
|
|
96
|
-
);
|
|
99
|
+
return badRequest("invalid_file_size", { maxBytes: MAX_UPLOAD_BYTES });
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
const lowerName = file.name.toLowerCase();
|
|
@@ -110,13 +113,10 @@ export async function POST(req: Request) {
|
|
|
110
113
|
try {
|
|
111
114
|
parsed = JSON.parse(text);
|
|
112
115
|
} catch {
|
|
113
|
-
return
|
|
116
|
+
return badRequest("invalid_json");
|
|
114
117
|
}
|
|
115
118
|
if (!validatePayloadShape(parsed)) {
|
|
116
|
-
return
|
|
117
|
-
{ ok: false, error: "invalid_kronosys_payload" },
|
|
118
|
-
{ status: 400 },
|
|
119
|
-
);
|
|
119
|
+
return badRequest("invalid_kronosys_payload");
|
|
120
120
|
}
|
|
121
121
|
writePayload(parsed);
|
|
122
122
|
return NextResponse.json({ ok: true, restored: "json" });
|
|
@@ -125,17 +125,11 @@ export async function POST(req: Request) {
|
|
|
125
125
|
const bytes = await file.arrayBuffer();
|
|
126
126
|
const buf = Buffer.from(bytes);
|
|
127
127
|
if (buf.length < 1024) {
|
|
128
|
-
return
|
|
129
|
-
{ ok: false, error: "sqlite_too_small" },
|
|
130
|
-
{ status: 400 },
|
|
131
|
-
);
|
|
128
|
+
return badRequest("sqlite_too_small");
|
|
132
129
|
}
|
|
133
130
|
const header = buf.subarray(0, 16).toString("utf8");
|
|
134
131
|
if (!header.startsWith("SQLite format 3")) {
|
|
135
|
-
return
|
|
136
|
-
{ ok: false, error: "invalid_sqlite_file" },
|
|
137
|
-
{ status: 400 },
|
|
138
|
-
);
|
|
132
|
+
return badRequest("invalid_sqlite_file");
|
|
139
133
|
}
|
|
140
134
|
|
|
141
135
|
try {
|
package/app/changelog/page.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import Link from "next/link";
|
|
|
5
5
|
import { useSearchParams } from "next/navigation";
|
|
6
6
|
import ReactMarkdown, { type Components } from "react-markdown";
|
|
7
7
|
import { AppVersionStamp } from "@/components/dashboard/AppVersionStamp";
|
|
8
|
+
import { AppShellHeaderWallClock } from "@/components/dashboard/AppShellHeaderWallClock";
|
|
8
9
|
import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
|
|
9
10
|
import { PageRefreshButton } from "@/components/dashboard/PageRefreshButton";
|
|
10
11
|
import { ScrollToTopFab } from "@/components/dashboard/ScrollToTopFab";
|
|
@@ -105,6 +106,7 @@ function ChangelogBody() {
|
|
|
105
106
|
</p>
|
|
106
107
|
</div>
|
|
107
108
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
109
|
+
<AppShellHeaderWallClock lang={lang} dt={dt} />
|
|
108
110
|
<ThemeToggle lang={lang} />
|
|
109
111
|
<PageRefreshButton
|
|
110
112
|
title={dt.pageRefreshTitle}
|
package/app/globals.css
CHANGED
|
@@ -51,17 +51,15 @@ body {
|
|
|
51
51
|
@keyframes kronosys-route-transition-in-kf {
|
|
52
52
|
from {
|
|
53
53
|
opacity: 0;
|
|
54
|
-
transform: translateY(6px);
|
|
55
54
|
}
|
|
56
55
|
to {
|
|
57
56
|
opacity: 1;
|
|
58
|
-
transform: translateY(0);
|
|
59
57
|
}
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
.kronosys-route-transition-in {
|
|
63
61
|
animation: kronosys-route-transition-in-kf 170ms ease-out both;
|
|
64
|
-
will-change: opacity
|
|
62
|
+
will-change: opacity;
|
|
65
63
|
}
|
|
66
64
|
|
|
67
65
|
/* Minuteur focus : clignotement sous 5 min (travail / longue pause), accent violet sous 30 s */
|
|
@@ -99,6 +97,32 @@ html.dark .kronosys-session-duration-alert {
|
|
|
99
97
|
color: rgb(248 113 113);
|
|
100
98
|
}
|
|
101
99
|
|
|
100
|
+
/* Fin planifiée imminente (sessions/tâches): clignotement progressif avant échéance. */
|
|
101
|
+
@keyframes kronosys-end-time-alert-blink-kf {
|
|
102
|
+
0%,
|
|
103
|
+
100% {
|
|
104
|
+
opacity: 1;
|
|
105
|
+
}
|
|
106
|
+
50% {
|
|
107
|
+
opacity: 0.42;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.kronosys-end-time-alert-slow {
|
|
112
|
+
color: rgb(220 38 38);
|
|
113
|
+
animation: kronosys-end-time-alert-blink-kf 2.2s ease-in-out infinite;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.kronosys-end-time-alert-fast {
|
|
117
|
+
color: rgb(220 38 38);
|
|
118
|
+
animation: kronosys-end-time-alert-blink-kf 0.95s ease-in-out infinite;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
html.dark .kronosys-end-time-alert-slow,
|
|
122
|
+
html.dark .kronosys-end-time-alert-fast {
|
|
123
|
+
color: rgb(248 113 113);
|
|
124
|
+
}
|
|
125
|
+
|
|
102
126
|
/* Saisie tâche « passé » : exactement deux assombrissements (pas de 3e pic dû au easing) */
|
|
103
127
|
@keyframes kronosys-past-datetime-blink-twice-kf {
|
|
104
128
|
0%,
|
|
@@ -271,7 +295,6 @@ html.dark .kronosys-datetime-popover select option {
|
|
|
271
295
|
|
|
272
296
|
.kronosys-route-transition-in {
|
|
273
297
|
animation: none;
|
|
274
|
-
transform: none;
|
|
275
298
|
}
|
|
276
299
|
|
|
277
300
|
.kronosys-krono-focus-time-blink {
|
|
@@ -285,6 +308,12 @@ html.dark .kronosys-datetime-popover select option {
|
|
|
285
308
|
transform: none;
|
|
286
309
|
}
|
|
287
310
|
|
|
311
|
+
.kronosys-end-time-alert-slow,
|
|
312
|
+
.kronosys-end-time-alert-fast {
|
|
313
|
+
animation: none;
|
|
314
|
+
opacity: 1;
|
|
315
|
+
}
|
|
316
|
+
|
|
288
317
|
.kronosys-past-datetime-blink-twice {
|
|
289
318
|
animation: none;
|
|
290
319
|
opacity: 1;
|
package/app/guide/page.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "@/lib/appShellHeaderClasses";
|
|
15
15
|
import { AppShellCommandCenterPlaceholder } from "@/components/dashboard/AppShellCommandCenterPlaceholder";
|
|
16
16
|
import { AppShellHeaderSessionMeta } from "@/components/dashboard/AppShellHeaderSessionMeta";
|
|
17
|
+
import { AppShellHeaderWallClock } from "@/components/dashboard/AppShellHeaderWallClock";
|
|
17
18
|
import { AppShellRouteNav } from "@/components/dashboard/AppShellRouteNav";
|
|
18
19
|
import { UserGuideBodyText } from "@/components/dashboard/UserGuideBodyText";
|
|
19
20
|
import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
|
|
@@ -121,6 +122,7 @@ function GuideContent() {
|
|
|
121
122
|
</div>
|
|
122
123
|
<div className="flex w-full justify-end">
|
|
123
124
|
<div className={appShellHeaderToolbarClassName}>
|
|
125
|
+
<AppShellHeaderWallClock lang={lang} dt={dt} />
|
|
124
126
|
<AppShellCommandCenterPlaceholder />
|
|
125
127
|
<AppShellRouteNav
|
|
126
128
|
current="guide"
|