@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,80 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
readWeekStartsOnFromStorage,
|
|
7
|
+
type ReportingWeekStartsOn,
|
|
8
|
+
} from "@/lib/reportingWeekLayout";
|
|
9
|
+
|
|
10
|
+
type ModalAnchor = { x: number; y: number };
|
|
11
|
+
type ModalRect = { left: number; top: number; width: number; height: number };
|
|
12
|
+
|
|
13
|
+
export function useReportingInteractionState<TScope>() {
|
|
14
|
+
const [weekStartsOn, setWeekStartsOn] = useState<ReportingWeekStartsOn>("monday");
|
|
15
|
+
const [chartWeekNavIndex, setChartWeekNavIndex] = useState(-1);
|
|
16
|
+
const [tagWeekRollupOpenKeys, setTagWeekRollupOpenKeys] = useState<Set<string>>(
|
|
17
|
+
() => new Set<string>(),
|
|
18
|
+
);
|
|
19
|
+
const [projectWeekTagBreakdownOpenKeys, setProjectWeekTagBreakdownOpenKeys] =
|
|
20
|
+
useState<Set<string>>(() => new Set<string>());
|
|
21
|
+
const [reportingTourOpen, setReportingTourOpen] = useState(false);
|
|
22
|
+
const [taskInspectScope, setTaskInspectScope] = useState<TScope | null>(null);
|
|
23
|
+
const [taskInspectAnchor, setTaskInspectAnchor] = useState<ModalAnchor | null>(null);
|
|
24
|
+
const [taskInspectModalRect, setTaskInspectModalRect] = useState<ModalRect | null>(null);
|
|
25
|
+
const [portalReady, setPortalReady] = useState(false);
|
|
26
|
+
const taskInspectModalRef = useRef<HTMLDivElement | null>(null);
|
|
27
|
+
|
|
28
|
+
const toggleTagWeekRollup = useCallback((key: string) => {
|
|
29
|
+
setTagWeekRollupOpenKeys((prev) => {
|
|
30
|
+
const next = new Set(prev);
|
|
31
|
+
if (next.has(key)) {
|
|
32
|
+
next.delete(key);
|
|
33
|
+
} else {
|
|
34
|
+
next.add(key);
|
|
35
|
+
}
|
|
36
|
+
return next;
|
|
37
|
+
});
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const toggleProjectWeekTagBreakdown = useCallback((key: string) => {
|
|
41
|
+
setProjectWeekTagBreakdownOpenKeys((prev) => {
|
|
42
|
+
const next = new Set(prev);
|
|
43
|
+
if (next.has(key)) {
|
|
44
|
+
next.delete(key);
|
|
45
|
+
} else {
|
|
46
|
+
next.add(key);
|
|
47
|
+
}
|
|
48
|
+
return next;
|
|
49
|
+
});
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
setWeekStartsOn(readWeekStartsOnFromStorage());
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
setPortalReady(true);
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
weekStartsOn,
|
|
62
|
+
setWeekStartsOn,
|
|
63
|
+
chartWeekNavIndex,
|
|
64
|
+
setChartWeekNavIndex,
|
|
65
|
+
tagWeekRollupOpenKeys,
|
|
66
|
+
toggleTagWeekRollup,
|
|
67
|
+
projectWeekTagBreakdownOpenKeys,
|
|
68
|
+
toggleProjectWeekTagBreakdown,
|
|
69
|
+
reportingTourOpen,
|
|
70
|
+
setReportingTourOpen,
|
|
71
|
+
taskInspectScope,
|
|
72
|
+
setTaskInspectScope,
|
|
73
|
+
taskInspectAnchor,
|
|
74
|
+
setTaskInspectAnchor,
|
|
75
|
+
taskInspectModalRect,
|
|
76
|
+
setTaskInspectModalRect,
|
|
77
|
+
portalReady,
|
|
78
|
+
taskInspectModalRef,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -10,3 +10,16 @@ export const appShellHeaderClassName =
|
|
|
10
10
|
*/
|
|
11
11
|
export const appShellHeaderToolRowClassName =
|
|
12
12
|
"flex w-full flex-col gap-3 sm:flex-row sm:items-start sm:justify-between";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Première ligne d’en-tête : bloc titre (gauche) et colonne contexte session (droite), comme sur le tableau de bord.
|
|
16
|
+
*/
|
|
17
|
+
export const appShellHeaderTitleMetaRowClassName =
|
|
18
|
+
"mb-3 flex w-full flex-col gap-3 sm:mb-4 sm:flex-row sm:items-start sm:justify-between sm:gap-6";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Barre d’actions à droite : même empilement que le tableau de bord (recherche / raccourcis,
|
|
22
|
+
* navigation, thème, rafraîchissement, langue) pour éviter les décalages entre routes.
|
|
23
|
+
*/
|
|
24
|
+
export const appShellHeaderToolbarClassName =
|
|
25
|
+
"flex min-h-10 shrink-0 flex-wrap items-center justify-end gap-1.5";
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type { Lang } from "./dashboardCopy";
|
|
2
|
+
|
|
3
|
+
export type BusinessRuleStatus = "todo" | "validated" | "partial";
|
|
4
|
+
|
|
5
|
+
export type BusinessRuleItem = {
|
|
6
|
+
id: string;
|
|
7
|
+
domain: string;
|
|
8
|
+
title: string;
|
|
9
|
+
status: BusinessRuleStatus;
|
|
10
|
+
codeRefs: string[];
|
|
11
|
+
testRefs: string[];
|
|
12
|
+
note?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type BusinessRulesMatrixBundle = {
|
|
16
|
+
heading: string;
|
|
17
|
+
subtitle: string;
|
|
18
|
+
statusLegend: string;
|
|
19
|
+
columns: {
|
|
20
|
+
id: string;
|
|
21
|
+
domain: string;
|
|
22
|
+
rule: string;
|
|
23
|
+
status: string;
|
|
24
|
+
code: string;
|
|
25
|
+
tests: string;
|
|
26
|
+
note: string;
|
|
27
|
+
};
|
|
28
|
+
statuses: Record<BusinessRuleStatus, string>;
|
|
29
|
+
rows: BusinessRuleItem[];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const rowsFr: BusinessRuleItem[] = [
|
|
33
|
+
{
|
|
34
|
+
id: "BR-001",
|
|
35
|
+
domain: "Session",
|
|
36
|
+
title: "Une session live cumule une durée murale tant qu'elle n'est ni en pause ni terminée.",
|
|
37
|
+
status: "todo",
|
|
38
|
+
codeRefs: ["server/sessionWallHydrate.ts", "server/actionDispatch.ts"],
|
|
39
|
+
testRefs: ["server/sessionWallHydrate.test.ts", "server/actionDispatch.test.ts"],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "BR-002",
|
|
43
|
+
domain: "Session",
|
|
44
|
+
title: "La pause session gèle la durée murale sans modifier directement les durées de tâches déjà persistées.",
|
|
45
|
+
status: "todo",
|
|
46
|
+
codeRefs: ["server/actionDispatch.ts"],
|
|
47
|
+
testRefs: ["server/actionDispatch.test.ts"],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "BR-003",
|
|
51
|
+
domain: "Session",
|
|
52
|
+
title: "La fin de session ferme les segments actifs et garde un état cohérent pour l'historique.",
|
|
53
|
+
status: "todo",
|
|
54
|
+
codeRefs: ["server/actionDispatch.ts"],
|
|
55
|
+
testRefs: ["server/actionDispatch.test.ts"],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "BR-004",
|
|
59
|
+
domain: "Tâches",
|
|
60
|
+
title: "Le minuteur tâche cumule en durationMs via segments, distinct du mur session.",
|
|
61
|
+
status: "todo",
|
|
62
|
+
codeRefs: ["server/actionTaskSession.ts", "server/mainTimerHydrate.ts"],
|
|
63
|
+
testRefs: ["server/actionTaskSession.test.ts", "server/mainTimerHydrate.test.ts"],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "BR-005",
|
|
67
|
+
domain: "Tâches",
|
|
68
|
+
title: "Une sous-tâche active exclut le cumul parallèle du minuteur principal.",
|
|
69
|
+
status: "todo",
|
|
70
|
+
codeRefs: ["server/actionTaskSession.ts"],
|
|
71
|
+
testRefs: ["server/actionTaskSession.test.ts"],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "BR-006",
|
|
75
|
+
domain: "Tâches",
|
|
76
|
+
title: "Les bornes temporelles tâche respectent les modes keep/manual/from_bounds.",
|
|
77
|
+
status: "todo",
|
|
78
|
+
codeRefs: ["server/actionTaskSession.ts", "server/actionDispatch.ts"],
|
|
79
|
+
testRefs: ["server/actionTaskSession.test.ts", "server/actionDispatch.test.ts"],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "BR-007",
|
|
83
|
+
domain: "Pause globale",
|
|
84
|
+
title: "Le contexte de pause globale restaure correctement les minuteurs concernés.",
|
|
85
|
+
status: "todo",
|
|
86
|
+
codeRefs: ["server/actionDispatch.ts"],
|
|
87
|
+
testRefs: ["server/actionDispatch.test.ts"],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "BR-008",
|
|
91
|
+
domain: "Planning futur",
|
|
92
|
+
title: "Le statut planifié (futur) reste un comportement UI sans casser les invariants serveur.",
|
|
93
|
+
status: "todo",
|
|
94
|
+
codeRefs: ["lib/temporalDisplayPlanned.ts", "lib/sessionTaskSidebarStats.ts", "app/page.tsx"],
|
|
95
|
+
testRefs: ["lib/temporalDisplayPlanned.test.ts", "lib/sessionTaskSidebarStats.test.ts"],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "BR-009",
|
|
99
|
+
domain: "Reporting",
|
|
100
|
+
title: "Les agrégats distinguent explicitement temps mur session et temps tâches.",
|
|
101
|
+
status: "todo",
|
|
102
|
+
codeRefs: ["lib/reportingAggregate.ts"],
|
|
103
|
+
testRefs: ["lib/reportingAggregate.test.ts"],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "BR-010",
|
|
107
|
+
domain: "Reporting",
|
|
108
|
+
title: "Les filtres tags/projets et l'exclusion d'archives respectent la logique attendue.",
|
|
109
|
+
status: "todo",
|
|
110
|
+
codeRefs: ["lib/reportingAggregate.ts", "app/reporting/page.tsx"],
|
|
111
|
+
testRefs: ["lib/reportingTagWeekBreakdown.test.ts", "lib/reportingAggregate.test.ts"],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "BR-011",
|
|
115
|
+
domain: "Métadonnées",
|
|
116
|
+
title: "Renommer/purger tag ou projet propage les effets partout.",
|
|
117
|
+
status: "todo",
|
|
118
|
+
codeRefs: ["server/actionDispatch.ts", "server/actionTaskSession.ts"],
|
|
119
|
+
testRefs: ["server/actionDispatch.test.ts"],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "BR-012",
|
|
123
|
+
domain: "API état",
|
|
124
|
+
title: "/api/state renvoie un contrat stable (type, payload, headers cache).",
|
|
125
|
+
status: "todo",
|
|
126
|
+
codeRefs: ["app/api/state/route.ts"],
|
|
127
|
+
testRefs: ["test/integration/apiState.test.ts"],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: "BR-013",
|
|
131
|
+
domain: "API action",
|
|
132
|
+
title: "/api/action valide l'entrée minimale et homogénéise les erreurs d'entrée.",
|
|
133
|
+
status: "todo",
|
|
134
|
+
codeRefs: ["app/api/action/route.ts"],
|
|
135
|
+
testRefs: ["test/integration/apiActionValidation.test.ts"],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: "BR-014",
|
|
139
|
+
domain: "API restore",
|
|
140
|
+
title: "/api/restore valide format, taille et forme des données importées.",
|
|
141
|
+
status: "todo",
|
|
142
|
+
codeRefs: ["app/api/restore/route.ts"],
|
|
143
|
+
testRefs: ["test/integration/apiRestore.test.ts"],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "BR-015",
|
|
147
|
+
domain: "Persistance",
|
|
148
|
+
title: "Les mutations payload sensibles sont sérialisées/transactionnelles.",
|
|
149
|
+
status: "partial",
|
|
150
|
+
codeRefs: ["server/payloadStore.ts", "server/actionDispatch.ts"],
|
|
151
|
+
testRefs: ["server/actionDispatch.test.ts"],
|
|
152
|
+
note: "Ajouter des tests de concurrence dédiés.",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: "BR-016",
|
|
156
|
+
domain: "Schéma DB",
|
|
157
|
+
title: "Le schéma centralisé évite la duplication de DDL et les divergences.",
|
|
158
|
+
status: "todo",
|
|
159
|
+
codeRefs: ["server/dbSchema.ts", "server/db.ts"],
|
|
160
|
+
testRefs: ["test/integration/apiState.test.ts", "test/integration/apiRestore.test.ts"],
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
const frBundle: BusinessRulesMatrixBundle = {
|
|
165
|
+
heading: "Matrice de validation métier",
|
|
166
|
+
subtitle:
|
|
167
|
+
"Référentiel de vérification des règles métier critiques. Source dépôt: docs/BUSINESS_RULES_VALIDATION_MATRIX.md.",
|
|
168
|
+
statusLegend: "Statuts: À vérifier / À compléter / Validé.",
|
|
169
|
+
columns: {
|
|
170
|
+
id: "ID",
|
|
171
|
+
domain: "Domaine",
|
|
172
|
+
rule: "Règle métier",
|
|
173
|
+
status: "Statut",
|
|
174
|
+
code: "Code",
|
|
175
|
+
tests: "Tests",
|
|
176
|
+
note: "Note",
|
|
177
|
+
},
|
|
178
|
+
statuses: {
|
|
179
|
+
todo: "À vérifier",
|
|
180
|
+
partial: "À compléter",
|
|
181
|
+
validated: "Validé",
|
|
182
|
+
},
|
|
183
|
+
rows: rowsFr,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const enBundle: BusinessRulesMatrixBundle = {
|
|
187
|
+
heading: "Business validation matrix",
|
|
188
|
+
subtitle:
|
|
189
|
+
"Verification register for critical business rules. Repo source: docs/BUSINESS_RULES_VALIDATION_MATRIX.md.",
|
|
190
|
+
statusLegend: "Statuses: To verify / Partial / Validated.",
|
|
191
|
+
columns: {
|
|
192
|
+
id: "ID",
|
|
193
|
+
domain: "Domain",
|
|
194
|
+
rule: "Business rule",
|
|
195
|
+
status: "Status",
|
|
196
|
+
code: "Code",
|
|
197
|
+
tests: "Tests",
|
|
198
|
+
note: "Note",
|
|
199
|
+
},
|
|
200
|
+
statuses: {
|
|
201
|
+
todo: "To verify",
|
|
202
|
+
partial: "Partial",
|
|
203
|
+
validated: "Validated",
|
|
204
|
+
},
|
|
205
|
+
rows: rowsFr.map((row) => ({ ...row })),
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export function businessRulesMatrixBundle(lang: Lang): BusinessRulesMatrixBundle {
|
|
209
|
+
return lang === "fr" ? frBundle : enBundle;
|
|
210
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copie du texte dans le presse-papiers.
|
|
3
|
+
* Utilise l’API Clipboard lorsque le contexte est sécurisé ; sinon (HTTP hors localhost)
|
|
4
|
+
* ou en cas d’échec, retombe sur une sélection temporaire + execCommand.
|
|
5
|
+
*/
|
|
6
|
+
export async function copyTextToClipboard(text: string): Promise<boolean> {
|
|
7
|
+
if (typeof document === "undefined") {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const fallbackCopy = (): boolean => {
|
|
12
|
+
try {
|
|
13
|
+
const ta = document.createElement("textarea");
|
|
14
|
+
ta.value = text;
|
|
15
|
+
ta.setAttribute("readonly", "");
|
|
16
|
+
ta.style.position = "fixed";
|
|
17
|
+
ta.style.left = "-9999px";
|
|
18
|
+
ta.style.top = "0";
|
|
19
|
+
ta.setAttribute("tabindex", "-1");
|
|
20
|
+
document.body.appendChild(ta);
|
|
21
|
+
ta.focus();
|
|
22
|
+
ta.select();
|
|
23
|
+
ta.setSelectionRange(0, text.length);
|
|
24
|
+
/* Fallback intentionnel : Clipboard API indisponible (HTTP, permissions). */
|
|
25
|
+
const ok = document.execCommand("copy"); // NOSONAR — repli navigateur hors Clipboard API
|
|
26
|
+
ta.remove();
|
|
27
|
+
return ok;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (globalThis.window?.isSecureContext && navigator.clipboard?.writeText) {
|
|
34
|
+
try {
|
|
35
|
+
await navigator.clipboard.writeText(text);
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return fallbackCopy();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return fallbackCopy();
|
|
43
|
+
}
|