structural-codes-viewer 0.1.0-alpha.1 → 0.1.0-alpha.3

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 (63) hide show
  1. package/LICENSE +501 -0
  2. package/NOTICE +16 -0
  3. package/README.md +57 -68
  4. package/package-dist/CorpusContent.d.ts +44 -9
  5. package/package-dist/CorpusContent.js +452 -26
  6. package/package-dist/CorpusContent.js.map +1 -1
  7. package/package-dist/NormativeViewer.d.ts +23 -2
  8. package/package-dist/NormativeViewer.js +1699 -234
  9. package/package-dist/NormativeViewer.js.map +1 -1
  10. package/package-dist/ReferenceTools.d.ts +49 -0
  11. package/package-dist/ReferenceTools.js +14 -0
  12. package/package-dist/ReferenceTools.js.map +1 -0
  13. package/package-dist/annotations/AnnotationUi.d.ts +57 -0
  14. package/package-dist/annotations/AnnotationUi.js +114 -0
  15. package/package-dist/annotations/AnnotationUi.js.map +1 -0
  16. package/package-dist/annotations/annotationTargets.d.ts +39 -0
  17. package/package-dist/annotations/annotationTargets.js +94 -0
  18. package/package-dist/annotations/annotationTargets.js.map +1 -0
  19. package/package-dist/annotations/exportImport.d.ts +6 -0
  20. package/package-dist/annotations/exportImport.js +30 -0
  21. package/package-dist/annotations/exportImport.js.map +1 -0
  22. package/package-dist/annotations/index.d.ts +4 -0
  23. package/package-dist/annotations/index.js +5 -0
  24. package/package-dist/annotations/index.js.map +1 -0
  25. package/package-dist/annotations/indexedDb.d.ts +21 -0
  26. package/package-dist/annotations/indexedDb.js +210 -0
  27. package/package-dist/annotations/indexedDb.js.map +1 -0
  28. package/package-dist/annotations/schema.d.ts +8 -0
  29. package/package-dist/annotations/schema.js +102 -0
  30. package/package-dist/annotations/schema.js.map +1 -0
  31. package/package-dist/annotations/types.d.ts +52 -0
  32. package/package-dist/annotations/types.js +18 -0
  33. package/package-dist/annotations/types.js.map +1 -0
  34. package/package-dist/chunkNavigation.d.ts +12 -0
  35. package/package-dist/chunkNavigation.js +44 -0
  36. package/package-dist/chunkNavigation.js.map +1 -0
  37. package/package-dist/corpusData.d.ts +176 -33
  38. package/package-dist/corpusData.js +52 -0
  39. package/package-dist/corpusData.js.map +1 -1
  40. package/package-dist/crossReferences.d.ts +46 -0
  41. package/package-dist/crossReferences.js +198 -0
  42. package/package-dist/crossReferences.js.map +1 -0
  43. package/package-dist/generate-artifacts.mjs +47 -12
  44. package/package-dist/globalScrubber.d.ts +10 -0
  45. package/package-dist/globalScrubber.js +85 -0
  46. package/package-dist/globalScrubber.js.map +1 -0
  47. package/package-dist/permalinks.d.ts +22 -0
  48. package/package-dist/permalinks.js +36 -0
  49. package/package-dist/permalinks.js.map +1 -0
  50. package/package-dist/searchClient.d.ts +22 -0
  51. package/package-dist/searchClient.js +133 -0
  52. package/package-dist/searchClient.js.map +1 -0
  53. package/package-dist/searchEngine.d.ts +68 -0
  54. package/package-dist/searchEngine.js +558 -0
  55. package/package-dist/searchEngine.js.map +1 -0
  56. package/package-dist/searchWorker.d.ts +1 -0
  57. package/package-dist/searchWorker.js +32 -0
  58. package/package-dist/searchWorker.js.map +1 -0
  59. package/package-dist/styles.css +762 -182
  60. package/package-dist/tableCaptions.d.mts +3 -0
  61. package/package-dist/tableCaptions.mjs +35 -0
  62. package/package-dist/tableCaptions.mjs.map +1 -0
  63. package/package.json +18 -14
@@ -1,37 +1,219 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useEffect, useMemo, useRef, useState } from "react";
4
- import { documentForMode, loadChunk, loadDocumentIndex, loadManifest, loadRelations, loadSearchIndex, } from "./corpusData";
5
- import { BlockContent, isRepeatedUnitTitle } from "./CorpusContent";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { memo, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
4
+ import { adjacentChunkPaths, createDocumentLookup, documentForMode, initialUnitForIndex, loadChunk, loadCrossReferenceIndex, loadDocumentIndex, loadManifest, loadRelations, } from "./corpusData";
5
+ import { AlignedLabelList, BlockContent, groupAlignedLabelBlocks, hasAlphabeticListMarker, hasAlphaRatioListLayout, hasInferredAlphaRatioListMarker, hasLeadingEmphasisLabel, hasLeadingMath, hasNoListMarker, hasOfficialListMarker, hasSimpleDashMarker, hasTrailingMath, hasTrailingStrong, indentLevelClass, isRepeatedUnitTitle, listLevelClass, listMarkerClass, renderInlineSegments } from "./CorpusContent";
6
+ import { useViewerSearch } from "./searchClient";
7
+ import { createCrossReferenceLookup, resolveCrossReference } from "./crossReferences.js";
8
+ import { ReferencePreview } from "./ReferenceTools";
9
+ import { targetFromUrl, urlForViewerTarget } from "./permalinks";
10
+ import { isChunkNearRenderedWindow, navigationChunkWindow, progressiveChunkTargetsForVisibleUnit } from "./chunkNavigation.js";
11
+ import { buildGlobalScrubberEntries, createGlobalScrubberDragSession, globalScrubberKeyboardIndex, globalScrubberRatioForId, resolveGlobalScrubberActiveId } from "./globalScrubber.js";
12
+ import { AnnotationContextMenu, AnnotationEditor, AnnotationMarginMarker } from "./annotations/AnnotationUi";
13
+ import { annotationPositionRanks, annotationsForTarget, annotationTargetFromElement, automaticAnnotationLabel, clusterAnnotationMarkers, createLongPressSession } from "./annotations/annotationTargets";
14
+ import { annotationTargetKey } from "./annotations/schema";
6
15
  const modeOptions = [
7
- { id: "combined", label: "NTC 2018 + Circolare 7/2019" },
8
16
  { id: "ntc", label: "Solo NTC 2018" },
9
17
  { id: "circ", label: "Solo Circolare 7/2019" },
18
+ { id: "combined", label: "NTC 2018 + Circolare 7/2019" },
10
19
  ];
11
- const hierarchyLabels = ["Capitoli", "Paragrafi", "Sottoparagrafi", "Dettagli"];
12
- const maxContinuousChapterBytes = 2500000;
13
- const maxContinuousChapterUnits = 220;
20
+ const ModeSegmentedControl = memo(function ModeSegmentedControl({ mode, onChange }) {
21
+ const tooltipBaseId = useId();
22
+ return _jsx("div", { className: "scv-mode-switch", role: "group", "aria-label": "Modalit\u00E0 documento", children: modeOptions.map((option) => {
23
+ const tooltipId = `${tooltipBaseId}-${option.id}`;
24
+ return _jsxs("button", { type: "button", className: `scv-mode-button ${mode === option.id ? "active" : ""}`, onClick: () => onChange(option.id), "aria-label": option.label, "aria-describedby": tooltipId, "aria-pressed": mode === option.id, children: [_jsx("span", { className: "scv-mode-label", children: option.id === "ntc" ? _jsxs(_Fragment, { children: ["NTC", _jsx("br", {}), "2018"] }) : option.id === "circ" ? _jsxs(_Fragment, { children: ["CIRC.", _jsx("br", {}), "2019"] }) : _jsxs(_Fragment, { children: ["NTC", _jsx("br", {}), "CIRC."] }) }), _jsx("span", { id: tooltipId, className: "scv-control-tooltip scv-mode-tooltip", role: "tooltip", children: option.label })] }, option.id);
25
+ }) });
26
+ });
27
+ function scvBlockClass(block, sourceUnitId) {
28
+ const catenaryLabel = (sourceUnitId === null || sourceUnitId === void 0 ? void 0 : sourceUnitId.endsWith(":5.2.2.9.1")) && block.kind === "list-item" && block.listMarker === "none";
29
+ const alphaRatioLayout = hasAlphaRatioListLayout(block, sourceUnitId);
30
+ const inferredAlphaRatioMarker = hasInferredAlphaRatioListMarker(block, sourceUnitId);
31
+ const officialMarker = hasOfficialListMarker(block) || inferredAlphaRatioMarker;
32
+ const levelClass = listLevelClass(block) || (alphaRatioLayout ? "list-item-level-1" : "");
33
+ return `scv-block scv-block-${block.kind} ${officialMarker ? "list-item-with-official-marker" : ""} ${hasAlphabeticListMarker(block) ? "list-item-with-alphabetic-marker" : ""} ${hasSimpleDashMarker(block) && !inferredAlphaRatioMarker ? "list-item-with-simple-dash" : ""} ${hasNoListMarker(block) ? "list-item-without-marker" : ""} ${listMarkerClass(block)} ${levelClass} ${indentLevelClass(block)} ${hasLeadingMath(block) ? "list-item-with-leading-symbol" : ""} ${hasLeadingEmphasisLabel(block) ? "block-with-leading-label" : ""} ${hasTrailingStrong(block) ? "list-item-with-trailing-siglum" : ""} ${hasTrailingMath(block) ? "list-item-with-trailing-symbol" : ""} ${catenaryLabel ? "list-item-with-catenary-label" : ""} ${alphaRatioLayout ? "list-item-with-alpha-ratio" : ""}`;
34
+ }
35
+ function blockAssetKind(block, assets) {
36
+ if (!block.assetId)
37
+ return undefined;
38
+ if (assets.formulas[block.assetId])
39
+ return "formula";
40
+ if (assets.tables[block.assetId])
41
+ return "table";
42
+ if (assets.figures[block.assetId])
43
+ return "figure";
44
+ return undefined;
45
+ }
46
+ const ScvBlockFlow = memo(function ScvBlockFlow({ blocks, assets, assetsBaseUrl, sourceUnitId, sourceDocument, notesByTarget, onOpenAnnotation }) {
47
+ return _jsx("div", { className: "scv-unit-blocks", children: groupAlignedLabelBlocks(blocks).map((group) => {
48
+ var _a;
49
+ return group.kind === "label-list"
50
+ ? _jsx(AlignedLabelList, { blocks: group.blocks, assets: assets, assetsBaseUrl: assetsBaseUrl, sourceUnitId: sourceUnitId, sourceDocument: sourceDocument, renderAccessory: (block) => { var _a; return _jsx(AnnotationMarginMarker, { notes: (_a = notesByTarget.get(annotationTargetKey({ kind: "block", unitId: sourceUnitId, blockId: block.blockId }))) !== null && _a !== void 0 ? _a : [], onOpen: onOpenAnnotation }); } }, group.blocks[0].blockId)
51
+ : _jsxs("div", { tabIndex: 0, className: scvBlockClass(group.block, sourceUnitId), "data-scv-citation-target": group.block.assetId ? "asset" : "block", "data-scv-source-unit-id": sourceUnitId, "data-scv-block-id": group.block.blockId, "data-scv-asset-id": group.block.assetId, "data-scv-asset-kind": blockAssetKind(group.block, assets), children: [_jsx(BlockContent, { block: group.block, assets: assets, assetsBaseUrl: assetsBaseUrl, sourceUnitId: sourceUnitId, sourceDocument: sourceDocument }), _jsx(AnnotationMarginMarker, { notes: (_a = notesByTarget.get(annotationTargetKey(group.block.assetId ? { kind: "asset", unitId: sourceUnitId, assetId: group.block.assetId, assetKind: blockAssetKind(group.block, assets) } : { kind: "block", unitId: sourceUnitId, blockId: group.block.blockId }))) !== null && _a !== void 0 ? _a : [], onOpen: onOpenAnnotation })] }, group.block.blockId);
52
+ }) });
53
+ });
54
+ function AuxiliaryContent({ panel, context }) {
55
+ return typeof panel === "function" ? panel(context) : panel;
56
+ }
57
+ function viewerHistoryEntry(state) {
58
+ if (!state || typeof state !== "object" || !("scvNavigation" in state))
59
+ return null;
60
+ const entry = state.scvNavigation;
61
+ return entry && typeof entry === "object" && "session" in entry && "index" in entry && "scrollTop" in entry
62
+ && typeof entry.session === "string" && typeof entry.index === "number" && typeof entry.scrollTop === "number" ? entry : null;
63
+ }
64
+ function withViewerHistoryEntry(state, entry) {
65
+ return Object.assign(Object.assign({}, (state && typeof state === "object" ? state : {})), { scvNavigation: entry });
66
+ }
67
+ const chunkUnitMaps = new WeakMap();
68
+ const emptyRelatedRecords = [];
69
+ const emptyNavigationEntries = [];
70
+ function unitsForChunk(chunk) {
71
+ let map = chunkUnitMaps.get(chunk);
72
+ if (!map) {
73
+ map = new Map(chunk.units.map((unit) => [unit.id, unit]));
74
+ chunkUnitMaps.set(chunk, map);
75
+ }
76
+ return map;
77
+ }
14
78
  function depth(unit) {
15
79
  return unit.hierarchy.ancestorIds.length;
16
80
  }
17
- function ancestorIdAtDepth(unit, targetDepth) {
81
+ function hasUnitContent(unit) {
82
+ return unit.blocks.some((block) => {
83
+ var _a, _b;
84
+ if (block.kind === "heading" || block.blockId === unit.titleBlockId)
85
+ return false;
86
+ return Boolean(block.assetId || ((_b = (_a = block.text) === null || _a === void 0 ? void 0 : _a.normalized) === null || _b === void 0 ? void 0 : _b.trim()));
87
+ });
88
+ }
89
+ function baseNumbering(value) {
90
+ return value.replace(/^C/iu, "");
91
+ }
92
+ function compareNumbering(left, right) {
93
+ return left.numbering.sortKey.localeCompare(right.numbering.sortKey, "it", { numeric: true });
94
+ }
95
+ function compareBaseNumbering(left, right) {
96
+ return baseNumbering(left.numbering.official).localeCompare(baseNumbering(right.numbering.official), "it", { numeric: true });
97
+ }
98
+ function parentNumbering(value) {
99
+ const parts = value.split(".");
100
+ parts.pop();
101
+ return parts.join(".") || null;
102
+ }
103
+ function findTextUnit(root, unitId) {
18
104
  var _a;
19
- return depth(unit) === targetDepth ? unit.id : (_a = unit.hierarchy.ancestorIds[targetDepth]) !== null && _a !== void 0 ? _a : null;
105
+ return (_a = root === null || root === void 0 ? void 0 : root.querySelector(`[data-scv-text-unit="${CSS.escape(unitId)}"]`)) !== null && _a !== void 0 ? _a : null;
20
106
  }
21
- function containsUnit(unit, ancestorId) {
22
- return unit.id === ancestorId || unit.hierarchy.ancestorIds.includes(ancestorId);
107
+ function scrollElementIntoPane(root, target) {
108
+ var _a;
109
+ const top = root.scrollTop + target.getBoundingClientRect().top - root.getBoundingClientRect().top;
110
+ const contextBar = (_a = root.parentElement) === null || _a === void 0 ? void 0 : _a.querySelector(".scv-mobile-context");
111
+ const offset = contextBar && window.getComputedStyle(contextBar).display !== "none"
112
+ ? Math.max(18, contextBar.getBoundingClientRect().bottom - root.getBoundingClientRect().top + 10) : 18;
113
+ root.scrollTo({ top: Math.max(0, top - offset), behavior: "auto" });
23
114
  }
24
- function normalizedQuery(value) {
25
- return value.normalize("NFKC").trim().toLocaleLowerCase("it");
115
+ function scrollTextUnit(root, unitId) {
116
+ const target = findTextUnit(root, unitId);
117
+ if (!root || !target)
118
+ return false;
119
+ scrollElementIntoPane(root, target);
120
+ return true;
26
121
  }
27
- function updateDeepLink(mode, unitId, defaultMode) {
28
- const url = new URL(window.location.href);
29
- if (mode === defaultMode)
30
- url.searchParams.delete("mode");
31
- else
32
- url.searchParams.set("mode", mode);
33
- url.searchParams.set("unit", unitId);
34
- window.history.replaceState(null, "", url);
122
+ function findViewerTarget(root, target) {
123
+ if (!root)
124
+ return null;
125
+ const unitId = CSS.escape(target.unitId);
126
+ if (target.kind === "unit")
127
+ return root.querySelector(`[data-scv-text-unit="${unitId}"], [data-scv-related-unit="${unitId}"]`);
128
+ if (target.kind === "block")
129
+ return root.querySelector(`[data-scv-source-unit-id="${unitId}"][data-scv-block-id="${CSS.escape(target.blockId)}"]`);
130
+ return root.querySelector(`[data-scv-source-unit-id="${unitId}"][data-scv-asset-id="${CSS.escape(target.assetId)}"]`);
131
+ }
132
+ function scrollViewerTarget(root, target) {
133
+ const element = findViewerTarget(root, target);
134
+ if (!root || !element)
135
+ return false;
136
+ scrollElementIntoPane(root, element);
137
+ return true;
138
+ }
139
+ async function writeTextClipboard(value) {
140
+ var _a;
141
+ if ((_a = navigator.clipboard) === null || _a === void 0 ? void 0 : _a.writeText) {
142
+ await navigator.clipboard.writeText(value);
143
+ return;
144
+ }
145
+ const input = document.createElement("textarea");
146
+ input.value = value;
147
+ input.style.cssText = "position:fixed;left:-10000px;top:-10000px";
148
+ document.body.append(input);
149
+ try {
150
+ input.select();
151
+ if (!document.execCommand("copy"))
152
+ throw new Error("Copia non consentita.");
153
+ }
154
+ finally {
155
+ input.remove();
156
+ }
157
+ }
158
+ function updateDeepLink(mode, target, defaultMode, action = "replace", state = window.history.state) {
159
+ const url = urlForViewerTarget(window.location.href, mode, defaultMode, target);
160
+ window.history[`${action}State`](state, "", url);
161
+ }
162
+ function documentFromUnitId(unitId) {
163
+ return unitId.includes(":circ2019:") ? "circ2019" : "ntc2018";
164
+ }
165
+ function referenceDescriptor(element) {
166
+ const kind = element.dataset.scvReferenceKind;
167
+ const number = element.dataset.scvReferenceNumber;
168
+ const sourceUnitId = element.dataset.scvSourceUnitId;
169
+ const sourceDocument = element.dataset.scvSourceDocument;
170
+ if (!kind || !number || !sourceUnitId || !sourceDocument)
171
+ return null;
172
+ const hint = element.dataset.scvReferenceDocument;
173
+ return { kind, number, sourceUnitId, sourceDocument, documentHint: hint === "ntc2018" || hint === "circ2019" ? hint : null };
174
+ }
175
+ function citationSelectionFromElement(element) {
176
+ const kind = element.dataset.scvCitationTarget;
177
+ const unitId = element.dataset.scvSourceUnitId;
178
+ if (!unitId)
179
+ return null;
180
+ if (kind === "asset" && element.dataset.scvAssetId) {
181
+ const assetKind = element.dataset.scvAssetKind;
182
+ return { kind: "asset", unitId, assetId: element.dataset.scvAssetId, assetKind: assetKind === "formula" || assetKind === "table" || assetKind === "figure" ? assetKind : undefined };
183
+ }
184
+ if (kind === "block" && element.dataset.scvBlockId)
185
+ return { kind: "block", unitId, blockId: element.dataset.scvBlockId };
186
+ if (kind === "unit")
187
+ return { kind: "unit", unitId };
188
+ return null;
189
+ }
190
+ function viewerTargetForReference(reference) {
191
+ if (!reference.asset)
192
+ return { kind: "unit", unitId: reference.unit.id };
193
+ return { kind: "asset", unitId: reference.unit.id, assetId: reference.asset.id, assetKind: reference.asset.kind };
194
+ }
195
+ function viewerTargetForSearchResult(result) {
196
+ if (result.assetId)
197
+ return { kind: "asset", unitId: result.id, assetId: result.assetId, assetKind: result.assetKind };
198
+ if (result.blockId)
199
+ return { kind: "block", unitId: result.id, blockId: result.blockId };
200
+ return { kind: "unit", unitId: result.id };
201
+ }
202
+ function sameViewerTarget(left, right) {
203
+ if (!left || left.kind !== right.kind || left.unitId !== right.unitId)
204
+ return false;
205
+ if (left.kind === "block" && right.kind === "block")
206
+ return left.blockId === right.blockId;
207
+ if (left.kind === "asset" && right.kind === "asset")
208
+ return left.assetId === right.assetId;
209
+ return left.kind === "unit" && right.kind === "unit";
210
+ }
211
+ function referenceLabel(reference) {
212
+ const document = reference.unit.document === "ntc2018" ? "NTC 2018" : "Circolare 7/2019";
213
+ if (!reference.asset)
214
+ return `${document} · § ${reference.unit.numbering}`;
215
+ const prefix = reference.asset.kind === "formula" ? "Formula" : reference.asset.kind === "table" ? "Tab." : "Fig.";
216
+ return `${document} · ${prefix} ${reference.asset.officialNumber}`;
35
217
  }
36
218
  function evidencePages(unit, chunk) {
37
219
  const pages = unit.blocks.flatMap((block) => {
@@ -45,280 +227,1563 @@ function evidencePages(unit, chunk) {
45
227
  });
46
228
  return [...new Set(pages)].sort((left, right) => left - right);
47
229
  }
48
- function snippet(value) {
49
- const compact = value.replace(/\s+/gu, " ").trim();
50
- return compact.length > 145 ? `${compact.slice(0, 142)}…` : compact;
51
- }
52
230
  function relationTargets(resultId, relations) {
53
231
  return relations
54
232
  .filter((edge) => edge.sourceUnitId === resultId)
55
233
  .sort((left, right) => left.targetUnitId.localeCompare(right.targetUnitId, "it", { numeric: true }));
56
234
  }
57
- export function NormativeViewer({ defaultMode = "combined", dataBaseUrl = "/data/codes", assetsBaseUrl = "/assets", auxiliaryPanel, auxiliaryPanelLabel = "PDF ufficiale", auxiliaryPanelDefaultVisible = false, analyticalHref, className, }) {
58
- var _a, _b, _c, _d, _e;
235
+ function matchingPrimarySummary(summary, lookup) {
236
+ var _a;
237
+ return (_a = lookup.unitByNumbering.get(baseNumbering(summary.numbering.official))) !== null && _a !== void 0 ? _a : null;
238
+ }
239
+ function recordsForPaths(index, lookup, paths, chunks) {
240
+ var _a;
241
+ if (!index || !lookup)
242
+ return [];
243
+ const records = [];
244
+ for (const path of lookup.chunkPaths) {
245
+ if (!paths.has(path))
246
+ continue;
247
+ const chunk = chunks.get(path);
248
+ if (!chunk)
249
+ continue;
250
+ const units = unitsForChunk(chunk);
251
+ for (const summary of (_a = lookup.unitsByChunkPath.get(path)) !== null && _a !== void 0 ? _a : []) {
252
+ const unit = units.get(summary.id);
253
+ if (unit)
254
+ records.push({ summary, unit, chunk });
255
+ }
256
+ }
257
+ return records;
258
+ }
259
+ function buildCombinedPlan(primaryIndex, circIndex, relations) {
260
+ var _a, _b;
261
+ const empty = { fallbackSummaries: [], circPathsByPrimaryPath: new Map(), primaryAnchorByFallbackId: new Map() };
262
+ if (!primaryIndex || !circIndex)
263
+ return empty;
264
+ const relatedSourceIds = new Set(relations.map((edge) => edge.sourceUnitId));
265
+ const fallbackSummaries = circIndex.units.filter((summary) => !relatedSourceIds.has(summary.id));
266
+ const primary = [...primaryIndex.units].sort(compareBaseNumbering);
267
+ const circPathsByPrimaryPath = new Map();
268
+ const primaryAnchorByFallbackId = new Map();
269
+ for (const fallback of fallbackSummaries) {
270
+ let low = 0;
271
+ let high = primary.length;
272
+ while (low < high) {
273
+ const middle = (low + high) >>> 1;
274
+ if (compareBaseNumbering(primary[middle], fallback) <= 0)
275
+ low = middle + 1;
276
+ else
277
+ high = middle;
278
+ }
279
+ const anchor = (_a = primary[Math.max(0, low - 1)]) !== null && _a !== void 0 ? _a : primary[0];
280
+ if (!anchor)
281
+ continue;
282
+ primaryAnchorByFallbackId.set(fallback.id, anchor);
283
+ const paths = (_b = circPathsByPrimaryPath.get(anchor.chunkPath)) !== null && _b !== void 0 ? _b : new Set();
284
+ paths.add(fallback.chunkPath);
285
+ circPathsByPrimaryPath.set(anchor.chunkPath, paths);
286
+ }
287
+ return { fallbackSummaries, circPathsByPrimaryPath, primaryAnchorByFallbackId };
288
+ }
289
+ function navigationEntry(summary, source = summary.document, parentId = summary.hierarchy.parentId) {
290
+ const rawNumber = summary.numbering.official;
291
+ const displayNumber = source === "circ2019" && !/^C/iu.test(rawNumber) ? `C${rawNumber}` : rawNumber;
292
+ const baseNumber = baseNumbering(displayNumber);
293
+ const level = Math.max(0, baseNumber.split(".").length - 1);
294
+ return { summary, source, baseNumber, displayNumber, level, parentId, parentBaseNumber: parentNumbering(baseNumber) };
295
+ }
296
+ function sameRelatedRecords(left, right) {
297
+ return left.length === right.length && left.every((record, index) => { var _a, _b; return record.edge.relationId === ((_a = right[index]) === null || _a === void 0 ? void 0 : _a.edge.relationId) && record.unit === ((_b = right[index]) === null || _b === void 0 ? void 0 : _b.unit); });
298
+ }
299
+ function escapeRegExp(value) {
300
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
301
+ }
302
+ function unitTitleContent(unit) {
303
+ var _a;
304
+ const titleBlock = unit.blocks.find((block) => block.blockId === unit.titleBlockId);
305
+ const inline = (_a = titleBlock === null || titleBlock === void 0 ? void 0 : titleBlock.text) === null || _a === void 0 ? void 0 : _a.inline;
306
+ if (!inline)
307
+ return unit.title;
308
+ const officialNumber = unit.numbering.official;
309
+ const prefix = new RegExp(`^${escapeRegExp(officialNumber)}\\s*`, "u");
310
+ const titleInline = inline.map((segment, index) => index === 0 && typeof segment.value === "string" ? Object.assign(Object.assign({}, segment), { value: segment.value.replace(prefix, "") }) : segment);
311
+ return renderInlineSegments(titleInline);
312
+ }
313
+ const MemoizedUnit = memo(function MemoizedUnit({ record, mode, relatedRecords, assetsBaseUrl, notesByTarget, onOpenAnnotation }) {
314
+ var _a;
315
+ const { unit, chunk } = record;
316
+ const isChapter = depth(unit) === 0;
317
+ const isCircularFallback = mode === "combined" && unit.document === "circ2019";
318
+ const visibleRelated = mode === "combined" ? relatedRecords.filter(({ unit: relatedUnit }) => hasUnitContent(relatedUnit)) : emptyRelatedRecords;
319
+ return _jsxs("section", { tabIndex: 0, className: `scv-unit scv-unit-depth-${Math.min(depth(unit), 4)}${isCircularFallback ? " scv-circular-fallback" : ""}`, "data-provenance": isCircularFallback ? "Circolare 7/2019" : undefined, "data-scv-text-unit": unit.id, "data-scv-citation-target": "unit", "data-scv-source-unit-id": unit.id, "data-scv-chunk-path": record.summary.chunkPath, children: [isChapter ? _jsxs("h2", { className: "scv-chapter-heading", children: [_jsxs("span", { className: "scv-chapter-badge", children: [_jsx("span", { className: "scv-chapter-badge-label", children: "Capitolo" }), _jsxs("strong", { children: [unit.numbering.official, "."] })] }), _jsx("span", { className: "scv-chapter-rule", "aria-hidden": "true" }), _jsx("span", { className: "scv-chapter-title", children: _jsx("button", { type: "button", className: "scv-permalink-trigger", "data-scv-copy-link": true, "aria-label": `Copia link al capitolo ${unit.numbering.official}`, children: unit.title }) })] }) : _jsxs("h2", { children: [_jsx("span", { className: "scv-unit-number", children: unit.numbering.official }), _jsx("span", { className: "scv-unit-title", children: _jsx("button", { type: "button", className: "scv-permalink-trigger", "data-scv-copy-link": true, "aria-label": `Copia link al paragrafo ${unit.numbering.official}`, children: unitTitleContent(unit) }) })] }), _jsx(AnnotationMarginMarker, { notes: (_a = notesByTarget.get(annotationTargetKey({ kind: "unit", unitId: unit.id }))) !== null && _a !== void 0 ? _a : [], onOpen: onOpenAnnotation }), _jsx(ScvBlockFlow, { blocks: unit.blocks.filter((block) => !isRepeatedUnitTitle(unit, block)), assets: chunk.assets, assetsBaseUrl: assetsBaseUrl, sourceUnitId: unit.id, sourceDocument: unit.document, notesByTarget: notesByTarget, onOpenAnnotation: onOpenAnnotation }), visibleRelated.map(({ edge, unit: relatedUnit, chunk: relatedChunk }) => { var _a; return _jsxs("section", { tabIndex: 0, className: "scv-related-unit", "data-provenance": "Circolare 7/2019", "data-scv-related-unit": relatedUnit.id, "data-scv-citation-target": "unit", "data-scv-source-unit-id": relatedUnit.id, children: [_jsx("header", { children: _jsxs("h3", { children: [_jsx("span", { className: "scv-related-number", children: relatedUnit.numbering.official }), _jsx("span", { className: "scv-related-title", children: _jsx("button", { type: "button", className: "scv-permalink-trigger", "data-scv-copy-link": true, "aria-label": `Copia link al paragrafo ${relatedUnit.numbering.official}`, children: relatedUnit.title }) })] }) }), _jsx(AnnotationMarginMarker, { notes: (_a = notesByTarget.get(annotationTargetKey({ kind: "unit", unitId: relatedUnit.id }))) !== null && _a !== void 0 ? _a : [], onOpen: onOpenAnnotation }), _jsx(ScvBlockFlow, { blocks: relatedUnit.blocks.filter((block) => !isRepeatedUnitTitle(relatedUnit, block)), assets: relatedChunk.assets, assetsBaseUrl: assetsBaseUrl, sourceUnitId: relatedUnit.id, sourceDocument: relatedUnit.document, notesByTarget: notesByTarget, onOpenAnnotation: onOpenAnnotation })] }, edge.relationId); })] });
320
+ }, (previous, next) => previous.record.unit === next.record.unit && previous.record.chunk === next.record.chunk && previous.mode === next.mode && previous.assetsBaseUrl === next.assetsBaseUrl && previous.notesByTarget === next.notesByTarget && previous.onOpenAnnotation === next.onOpenAnnotation && sameRelatedRecords(previous.relatedRecords, next.relatedRecords));
321
+ const DocumentContent = memo(function DocumentContent({ records, relatedByTarget, mode, assetsBaseUrl, documentLabel, documentUnits, documentChunks, hasPrevious, hasNext, notesByTarget, onOpenAnnotation }) {
322
+ if (records.length === 0)
323
+ return _jsx(LoadingPanel, { label: "Caricamento dei contenuti\u2026" });
324
+ const version = records[0].chunk.structuralCodesVersion;
325
+ return _jsxs("div", { className: "scv-text-flow", children: [hasPrevious && _jsx("div", { className: "scv-progressive-edge", "aria-hidden": "true" }), _jsxs("p", { className: "scv-chunk-note", children: ["Documento continuo \u00B7 ", documentLabel, " \u00B7 ", documentUnits, " unit\u00E0 \u00B7 ", documentChunks, " chunk \u00B7 structural-codes ", version] }), records.map((record) => { var _a; return _jsx(MemoizedUnit, { record: record, mode: mode, relatedRecords: (_a = relatedByTarget.get(record.unit.id)) !== null && _a !== void 0 ? _a : emptyRelatedRecords, assetsBaseUrl: assetsBaseUrl, notesByTarget: notesByTarget, onOpenAnnotation: onOpenAnnotation }, record.unit.id); }), hasNext ? _jsx("div", { className: "scv-progressive-edge", "aria-hidden": "true" }) : _jsx("div", { className: "scv-end-note", children: "Fine del documento." })] });
326
+ });
327
+ const GlobalDocumentScrubber = memo(function GlobalDocumentScrubber({ rootRef, entries, activeId, onSelect, annotationMarkers, onAnnotationSelect }) {
328
+ var _a, _b, _c, _d;
329
+ const [interaction, setInteraction] = useState("idle");
330
+ const [previewEntry, setPreviewEntry] = useState(null);
331
+ const [openCluster, setOpenCluster] = useState(null);
332
+ const hideTimerRef = useRef(null);
333
+ const pointerFrameRef = useRef(null);
334
+ const pendingPointerYRef = useRef(null);
335
+ const dragRef = useRef(null);
336
+ const activeEntry = (_a = entries.find((entry) => entry.id === activeId)) !== null && _a !== void 0 ? _a : null;
337
+ const activeIndex = (_b = activeEntry === null || activeEntry === void 0 ? void 0 : activeEntry.index) !== null && _b !== void 0 ? _b : 0;
338
+ const ratio = interaction === "dragging" && previewEntry ? previewEntry.ratio : globalScrubberRatioForId(entries, activeId, (_c = activeEntry === null || activeEntry === void 0 ? void 0 : activeEntry.ratio) !== null && _c !== void 0 ? _c : 0);
339
+ const visible = interaction !== "idle";
340
+ const annotationClusters = useMemo(() => clusterAnnotationMarkers(annotationMarkers), [annotationMarkers]);
341
+ const clearHideTimer = useCallback(() => {
342
+ if (hideTimerRef.current !== null)
343
+ window.clearTimeout(hideTimerRef.current);
344
+ hideTimerRef.current = null;
345
+ }, []);
346
+ const hideLater = useCallback(() => {
347
+ clearHideTimer();
348
+ hideTimerRef.current = window.setTimeout(() => setInteraction((current) => current === "scrolling" ? "idle" : current), 1000);
349
+ }, [clearHideTimer]);
350
+ const showTransient = useCallback(() => {
351
+ setInteraction((current) => current === "dragging" || current === "hover" || current === "focus" ? current : "scrolling");
352
+ hideLater();
353
+ }, [hideLater]);
354
+ useEffect(() => {
355
+ const root = rootRef.current;
356
+ if (!root)
357
+ return;
358
+ const onScroll = () => showTransient();
359
+ root.addEventListener("scroll", onScroll, { passive: true });
360
+ return () => root.removeEventListener("scroll", onScroll);
361
+ }, [rootRef, showTransient]);
362
+ useEffect(() => () => {
363
+ clearHideTimer();
364
+ if (pointerFrameRef.current !== null)
365
+ window.cancelAnimationFrame(pointerFrameRef.current);
366
+ }, [clearHideTimer]);
367
+ if (entries.length < 2 || !activeEntry)
368
+ return null;
369
+ const previewPointer = (clientY) => {
370
+ const drag = dragRef.current;
371
+ if (!drag)
372
+ return null;
373
+ const entry = drag.session.preview((clientY - drag.trackTop) / Math.max(1, drag.trackHeight));
374
+ if (entry)
375
+ setPreviewEntry(entry);
376
+ return entry;
377
+ };
378
+ const startDrag = (event) => {
379
+ if (!event.isPrimary || event.button !== 0)
380
+ return;
381
+ const rect = event.currentTarget.getBoundingClientRect();
382
+ event.currentTarget.setPointerCapture(event.pointerId);
383
+ dragRef.current = { pointerId: event.pointerId, trackTop: rect.top, trackHeight: rect.height, session: createGlobalScrubberDragSession(entries, (entry) => onSelect(entry.id)) };
384
+ previewPointer(event.clientY);
385
+ setInteraction("dragging");
386
+ clearHideTimer();
387
+ event.preventDefault();
388
+ };
389
+ const moveDrag = (event) => {
390
+ const drag = dragRef.current;
391
+ if (!drag || drag.pointerId !== event.pointerId)
392
+ return;
393
+ pendingPointerYRef.current = event.clientY;
394
+ if (pointerFrameRef.current !== null)
395
+ return;
396
+ pointerFrameRef.current = window.requestAnimationFrame(() => {
397
+ pointerFrameRef.current = null;
398
+ if (pendingPointerYRef.current !== null)
399
+ previewPointer(pendingPointerYRef.current);
400
+ });
401
+ };
402
+ const stopDrag = (event, commit) => {
403
+ const drag = dragRef.current;
404
+ if (!drag || drag.pointerId !== event.pointerId)
405
+ return;
406
+ if (pointerFrameRef.current !== null) {
407
+ window.cancelAnimationFrame(pointerFrameRef.current);
408
+ pointerFrameRef.current = null;
409
+ }
410
+ previewPointer(event.clientY);
411
+ if (commit)
412
+ drag.session.commit();
413
+ else
414
+ drag.session.cancel();
415
+ dragRef.current = null;
416
+ pendingPointerYRef.current = null;
417
+ setInteraction("scrolling");
418
+ hideLater();
419
+ };
420
+ const onKeyDown = (event) => {
421
+ if (!["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End"].includes(event.key))
422
+ return;
423
+ event.preventDefault();
424
+ const target = entries[globalScrubberKeyboardIndex(activeIndex < 0 ? 0 : activeIndex, event.key, entries.length)];
425
+ if (target) {
426
+ setPreviewEntry(target);
427
+ onSelect(target.id);
428
+ }
429
+ };
430
+ const overlayEntry = previewEntry !== null && previewEntry !== void 0 ? previewEntry : activeEntry;
431
+ const displayedEntry = interaction === "dragging" && previewEntry ? previewEntry : activeEntry;
432
+ const valueText = displayedEntry ? `§ ${displayedEntry.label} — ${displayedEntry.title}` : "Posizione nel documento";
433
+ return _jsxs("div", { className: `scv-scroll-rail ${visible ? "is-visible" : "is-idle"} is-${interaction}`, "aria-label": "Navigazione globale del documento", style: { "--scv-scrubber-ratio": ratio }, onMouseEnter: () => { clearHideTimer(); setInteraction((current) => current === "dragging" ? current : "hover"); }, onMouseLeave: () => { if (interaction !== "dragging" && interaction !== "focus") {
434
+ setInteraction("scrolling");
435
+ hideLater();
436
+ } }, onFocusCapture: () => { clearHideTimer(); setInteraction("focus"); }, onBlurCapture: () => { setInteraction("scrolling"); hideLater(); }, children: [_jsxs("div", { className: "scv-scroll-track", role: "presentation", onPointerDown: startDrag, onPointerMove: moveDrag, onPointerUp: (event) => stopDrag(event, true), onPointerCancel: (event) => stopDrag(event, false), children: [annotationClusters.map((cluster) => {
437
+ var _a;
438
+ const id = cluster.markers.map(({ annotation }) => annotation.id).join(":");
439
+ const single = cluster.markers.length === 1 ? cluster.markers[0].annotation : null;
440
+ const type = (_a = single === null || single === void 0 ? void 0 : single.type) !== null && _a !== void 0 ? _a : "cluster";
441
+ const label = single ? `${single.type === "bookmark" ? "Segnalibro" : "Nota"}: § ${single.numbering} — ${single.title}` : `${cluster.markers.length} annotazioni vicine`;
442
+ return _jsxs("span", { className: "scv-annotation-scrubber-group", style: { "--scv-annotation-ratio": cluster.ratio }, children: [_jsx("button", { type: "button", className: `scv-annotation-scrubber-marker is-${type}`, "aria-label": label, title: label, onPointerDown: (event) => event.stopPropagation(), onClick: (event) => { event.stopPropagation(); if (single)
443
+ onAnnotationSelect(single);
444
+ else
445
+ setOpenCluster((current) => current === id ? null : id); }, children: _jsx("span", { "aria-hidden": "true", children: type === "bookmark" ? "‹" : type === "note" ? "›" : cluster.markers.length }) }), !single && openCluster === id && _jsx("div", { className: "scv-annotation-cluster-popover", role: "menu", "aria-label": "Annotazioni vicine", children: cluster.markers.map(({ annotation }) => _jsxs("button", { type: "button", role: "menuitem", onPointerDown: (event) => event.stopPropagation(), onClick: (event) => { event.stopPropagation(); setOpenCluster(null); onAnnotationSelect(annotation); }, children: [_jsx("b", { "aria-hidden": "true", children: annotation.type === "bookmark" ? "‹" : "›" }), _jsxs("span", { children: [annotation.type === "bookmark" ? "Segnalibro" : "Nota", " \u00B7 \u00A7 ", annotation.numbering] })] }, annotation.id)) })] }, id);
446
+ }), _jsx("button", { type: "button", className: "scv-scroll-thumb", role: "slider", "aria-label": "Posizione nel documento", "aria-valuemin": 1, "aria-valuemax": entries.length, "aria-valuenow": ((_d = displayedEntry === null || displayedEntry === void 0 ? void 0 : displayedEntry.index) !== null && _d !== void 0 ? _d : 0) + 1, "aria-valuetext": valueText, onKeyDown: onKeyDown })] }), interaction === "dragging" && overlayEntry && _jsxs("output", { className: "scv-scroll-overlay", "aria-live": "off", children: [_jsxs("strong", { children: ["\u00A7 ", overlayEntry.label] }), _jsx("span", { children: overlayEntry.title })] })] });
447
+ });
448
+ function useVisibleUnitObserver(rootRef, records, onVisible) {
449
+ useEffect(() => {
450
+ const root = rootRef.current;
451
+ if (!root || records.length === 0)
452
+ return;
453
+ const elements = [...root.querySelectorAll("[data-scv-text-unit]")];
454
+ if (elements.length === 0)
455
+ return;
456
+ const orderedIds = elements.flatMap((element) => element.dataset.scvTextUnit ? [element.dataset.scvTextUnit] : []);
457
+ const nearEnd = () => root.scrollTop + root.clientHeight >= root.scrollHeight - Math.max(24, root.clientHeight * 0.5);
458
+ if (typeof IntersectionObserver === "undefined") {
459
+ const positions = elements.map((element) => { var _a; return ({ id: (_a = element.dataset.scvTextUnit) !== null && _a !== void 0 ? _a : "", top: element.offsetTop }); });
460
+ let frame = null;
461
+ const update = () => {
462
+ frame = null;
463
+ const marker = root.scrollTop + Math.min(root.clientHeight * 0.28, 220);
464
+ let low = 0;
465
+ let high = positions.length;
466
+ while (low < high) {
467
+ const middle = (low + high) >>> 1;
468
+ if (positions[middle].top <= marker)
469
+ low = middle + 1;
470
+ else
471
+ high = middle;
472
+ }
473
+ const candidate = nearEnd() ? positions[positions.length - 1] : positions[Math.max(0, low - 1)];
474
+ if (candidate === null || candidate === void 0 ? void 0 : candidate.id)
475
+ onVisible(candidate.id);
476
+ };
477
+ const onScroll = () => { if (frame === null)
478
+ frame = window.requestAnimationFrame(update); };
479
+ root.addEventListener("scroll", onScroll, { passive: true });
480
+ update();
481
+ return () => {
482
+ root.removeEventListener("scroll", onScroll);
483
+ if (frame !== null)
484
+ window.cancelAnimationFrame(frame);
485
+ };
486
+ }
487
+ const visible = new Set();
488
+ let frame = null;
489
+ const commit = () => {
490
+ frame = null;
491
+ for (let index = orderedIds.length - 1; index >= 0; index -= 1) {
492
+ if (visible.has(orderedIds[index])) {
493
+ onVisible(orderedIds[index]);
494
+ return;
495
+ }
496
+ }
497
+ };
498
+ const observer = new IntersectionObserver((entries) => {
499
+ for (const entry of entries) {
500
+ const id = entry.target.dataset.scvTextUnit;
501
+ if (!id)
502
+ continue;
503
+ if (entry.isIntersecting)
504
+ visible.add(id);
505
+ else
506
+ visible.delete(id);
507
+ }
508
+ if (frame === null)
509
+ frame = window.requestAnimationFrame(commit);
510
+ }, { root, rootMargin: "0px 0px -72% 0px", threshold: 0.01 });
511
+ const onScroll = () => {
512
+ if (!nearEnd())
513
+ return;
514
+ const lastId = orderedIds[orderedIds.length - 1];
515
+ if (lastId)
516
+ onVisible(lastId);
517
+ };
518
+ root.addEventListener("scroll", onScroll, { passive: true });
519
+ for (const element of elements)
520
+ observer.observe(element);
521
+ return () => {
522
+ root.removeEventListener("scroll", onScroll);
523
+ observer.disconnect();
524
+ if (frame !== null)
525
+ window.cancelAnimationFrame(frame);
526
+ };
527
+ }, [onVisible, records, rootRef]);
528
+ }
529
+ function HighlightedSnippet({ result }) {
530
+ const parts = [];
531
+ let offset = 0;
532
+ for (const [start, end] of result.highlights) {
533
+ const from = Math.max(offset, Math.min(result.snippet.length, start));
534
+ const to = Math.max(from, Math.min(result.snippet.length, end));
535
+ if (from > offset)
536
+ parts.push(result.snippet.slice(offset, from));
537
+ if (to > from)
538
+ parts.push(_jsx("mark", { children: result.snippet.slice(from, to) }, `${from}:${to}`));
539
+ offset = to;
540
+ }
541
+ if (offset < result.snippet.length)
542
+ parts.push(result.snippet.slice(offset));
543
+ return _jsx(_Fragment, { children: parts });
544
+ }
545
+ const NavigationPane = memo(function NavigationPane({ id, mode, onModeChange, hierarchy, activeLevelIds, sidebarMode, sidebarSession, indexReady, query, onQueryChange, searchReady, searchStatus, searchSource, searchDurationMs, searchResults, onSearchSubmit, onSearchResult, onSelectUnit, onRequestClose, onHistoryBack, onHistoryForward, canHistoryBack, canHistoryForward, searchRef, darkMode, onToggleTheme }) {
546
+ var _a, _b, _c, _d, _e, _f;
547
+ const indexListRef = useRef(null);
548
+ const searchResultsRef = useRef(null);
549
+ const [searchEditing, setSearchEditing] = useState(false);
550
+ const [settingsOpen, setSettingsOpen] = useState(false);
551
+ const [sidebarExpanded, setSidebarExpanded] = useState({ session: -1, chapterId: null, paragraphId: null });
552
+ const lastSidebarEntryRef = useRef(null);
553
+ const searchResultsId = useId();
554
+ const settingsId = useId();
555
+ const themeTooltipId = useId();
556
+ const chapters = (_a = hierarchy[0]) !== null && _a !== void 0 ? _a : [];
557
+ const paragraphs = (_b = hierarchy[1]) !== null && _b !== void 0 ? _b : emptyNavigationEntries;
558
+ const subparagraphs = (_c = hierarchy[2]) !== null && _c !== void 0 ? _c : emptyNavigationEntries;
559
+ const activeChapterId = (_d = activeLevelIds[0]) !== null && _d !== void 0 ? _d : null;
560
+ const activeParagraphId = (_e = activeLevelIds[1]) !== null && _e !== void 0 ? _e : null;
561
+ const activeSubparagraphId = (_f = activeLevelIds[2]) !== null && _f !== void 0 ? _f : null;
562
+ const paragraphsByChapter = useMemo(() => {
563
+ var _a, _b, _c;
564
+ const grouped = new Map();
565
+ for (const entry of paragraphs) {
566
+ const group = (_b = grouped.get((_a = entry.parentId) !== null && _a !== void 0 ? _a : "")) !== null && _b !== void 0 ? _b : [];
567
+ group.push(entry);
568
+ grouped.set((_c = entry.parentId) !== null && _c !== void 0 ? _c : "", group);
569
+ }
570
+ return grouped;
571
+ }, [paragraphs]);
572
+ const subparagraphsByParagraph = useMemo(() => {
573
+ var _a, _b, _c;
574
+ const grouped = new Map();
575
+ for (const entry of subparagraphs) {
576
+ const group = (_b = grouped.get((_a = entry.parentId) !== null && _a !== void 0 ? _a : "")) !== null && _b !== void 0 ? _b : [];
577
+ group.push(entry);
578
+ grouped.set((_c = entry.parentId) !== null && _c !== void 0 ? _c : "", group);
579
+ }
580
+ return grouped;
581
+ }, [subparagraphs]);
582
+ useEffect(() => {
583
+ var _a;
584
+ const targetId = (_a = activeSubparagraphId !== null && activeSubparagraphId !== void 0 ? activeSubparagraphId : activeParagraphId) !== null && _a !== void 0 ? _a : activeChapterId;
585
+ if (!targetId)
586
+ return;
587
+ const timeout = window.setTimeout(() => {
588
+ var _a, _b;
589
+ const target = (_a = indexListRef.current) === null || _a === void 0 ? void 0 : _a.querySelector(`[data-index-unit="${CSS.escape(targetId)}"]`);
590
+ if (!target)
591
+ return;
592
+ const reducedMotion = (_b = window.matchMedia) === null || _b === void 0 ? void 0 : _b.call(window, "(prefers-reduced-motion: reduce)").matches;
593
+ target.scrollIntoView({ block: "nearest", behavior: reducedMotion ? "auto" : "smooth" });
594
+ }, 360);
595
+ return () => window.clearTimeout(timeout);
596
+ }, [activeChapterId, activeParagraphId, activeSubparagraphId]);
597
+ const focusSearchResult = (position) => {
598
+ var _a, _b, _c;
599
+ const results = [...((_b = (_a = searchResultsRef.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll(".scv-search-result")) !== null && _b !== void 0 ? _b : [])];
600
+ if (results.length === 0)
601
+ return;
602
+ (_c = results[(position + results.length) % results.length]) === null || _c === void 0 ? void 0 : _c.focus();
603
+ };
604
+ const searchResultKeyDown = (event, position) => {
605
+ var _a, _b;
606
+ if (event.key === "ArrowDown") {
607
+ event.preventDefault();
608
+ focusSearchResult(position + 1);
609
+ }
610
+ else if (event.key === "ArrowUp") {
611
+ event.preventDefault();
612
+ if (position === 0)
613
+ (_a = searchRef.current) === null || _a === void 0 ? void 0 : _a.focus();
614
+ else
615
+ focusSearchResult(position - 1);
616
+ }
617
+ else if (event.key === "Home") {
618
+ event.preventDefault();
619
+ focusSearchResult(0);
620
+ }
621
+ else if (event.key === "End") {
622
+ event.preventDefault();
623
+ focusSearchResult(searchResults.length - 1);
624
+ }
625
+ else if (event.key === "Escape") {
626
+ event.preventDefault();
627
+ onQueryChange("");
628
+ (_b = searchRef.current) === null || _b === void 0 ? void 0 : _b.focus();
629
+ }
630
+ };
631
+ const renderEntryButton = (entry, level, active, tabIndex, expanded) => _jsxs("button", { type: "button", "data-index-unit": entry.summary.id, className: `scv-index-entry scv-index-level-${level} ${active ? "active" : ""}`, onClick: () => {
632
+ var _a;
633
+ if (sidebarMode && expanded !== undefined) {
634
+ if (((_a = lastSidebarEntryRef.current) === null || _a === void 0 ? void 0 : _a.session) !== sidebarSession || lastSidebarEntryRef.current.id !== entry.summary.id) {
635
+ lastSidebarEntryRef.current = { session: sidebarSession, id: entry.summary.id };
636
+ setSidebarExpanded((current) => {
637
+ var _a;
638
+ return level === 0
639
+ ? { session: sidebarSession, chapterId: entry.summary.id, paragraphId: null }
640
+ : { session: sidebarSession, chapterId: (_a = entry.parentId) !== null && _a !== void 0 ? _a : current.chapterId, paragraphId: entry.summary.id };
641
+ });
642
+ return;
643
+ }
644
+ }
645
+ lastSidebarEntryRef.current = null;
646
+ onSelectUnit(entry.summary);
647
+ onRequestClose();
648
+ }, title: `${entry.displayNumber} ${entry.summary.title}`, "aria-current": active ? "page" : undefined, "aria-expanded": expanded, tabIndex: tabIndex, children: [_jsx("strong", { children: entry.displayNumber }), _jsx("span", { children: entry.summary.title })] });
649
+ return _jsxs("aside", { id: id, className: "scv-index-pane", "aria-label": "Indice gerarchico", onClickCapture: (event) => {
650
+ if (!event.target.closest("[data-index-unit]"))
651
+ lastSidebarEntryRef.current = null;
652
+ }, children: [_jsxs("div", { className: "scv-search-toolbar", children: [_jsxs("div", { className: "scv-history-controls", role: "group", "aria-label": "Cronologia di navigazione", children: [_jsx("button", { type: "button", "aria-label": "Indietro", disabled: !canHistoryBack, onClick: onHistoryBack, children: "\u2190" }), _jsx("button", { type: "button", "aria-label": "Avanti", disabled: !canHistoryForward, onClick: onHistoryForward, children: "\u2192" })] }), _jsxs("div", { className: `scv-search-box ${searchEditing ? "is-editing" : ""}`, onBlur: (event) => {
653
+ if (!event.currentTarget.contains(event.relatedTarget))
654
+ setSearchEditing(false);
655
+ }, children: [_jsxs("form", { className: "scv-search-form", role: "search", onMouseDown: (event) => {
656
+ var _a;
657
+ if (!event.target.closest("button"))
658
+ (_a = searchRef.current) === null || _a === void 0 ? void 0 : _a.focus();
659
+ }, onSubmit: (event) => {
660
+ onSearchSubmit(event);
661
+ if (searchResults.length > 0)
662
+ setSearchEditing(false);
663
+ }, children: [_jsx("span", { className: "scv-search-icon", "aria-hidden": "true", children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "10.75", cy: "10.75", r: "6.75" }), _jsx("path", { d: "m15.75 15.75 4.25 4.25" })] }) }), _jsx("input", { ref: searchRef, type: "search", role: "combobox", "aria-autocomplete": "list", value: query, onChange: (event) => onQueryChange(event.target.value), onKeyDown: (event) => {
664
+ if (event.key === "Escape" && query) {
665
+ event.preventDefault();
666
+ onQueryChange("");
667
+ }
668
+ else if (event.key === "ArrowDown" && searchResults.length > 0) {
669
+ event.preventDefault();
670
+ focusSearchResult(0);
671
+ }
672
+ }, onFocus: () => setSearchEditing(true), placeholder: searchEditing ? "" : "Cerca nella normativa…", "aria-label": "Cerca nella normativa", "aria-controls": searchResultsId, "aria-expanded": searchEditing && searchReady }), searchEditing && query && _jsx("button", { type: "button", className: "scv-clear-search", onClick: () => onQueryChange(""), "aria-label": "Cancella ricerca", children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "m6.5 6.5 11 11M17.5 6.5l-11 11" }) }) }), !searchEditing && !query && _jsx("kbd", { children: "/" })] }), searchEditing && searchReady && _jsx("div", { id: searchResultsId, ref: searchResultsRef, className: "scv-search-results", role: "listbox", "aria-label": "Risultati ricerca", "aria-busy": searchStatus === "loading", "data-scv-search-source": searchSource, "data-scv-search-duration-ms": searchDurationMs !== null && searchDurationMs !== void 0 ? searchDurationMs : undefined, children: searchStatus === "loading" ? _jsx("p", { className: "scv-search-status", children: "Ricerca in corso\u2026" }) : searchStatus === "error" ? _jsx("p", { className: "scv-search-status", children: "Ricerca non disponibile." }) : searchResults.length === 0 ? _jsx("p", { className: "scv-search-status", children: "Nessun risultato nella modalit\u00E0 corrente." }) : searchResults.map((result, index) => { var _a, _b; return _jsxs("button", { type: "button", role: "option", "aria-selected": false, className: "scv-search-result", "data-search-match": result.matchKind, onKeyDown: (event) => searchResultKeyDown(event, index), onClick: () => { setSearchEditing(false); onSearchResult(result); onRequestClose(); }, children: [_jsxs("span", { children: [result.document === "ntc2018" ? "NTC 2018" : "Circolare 7/2019", " \u00B7 ", result.numbering, result.assetKind ? ` · ${result.assetKind === "formula" ? "Formula" : result.assetKind === "table" ? "Tabella" : "Figura"}` : ""] }), _jsx("strong", { children: result.title }), _jsx("small", { children: _jsx(HighlightedSnippet, { result: result }) })] }, `${result.id}:${(_a = result.blockId) !== null && _a !== void 0 ? _a : ""}:${(_b = result.assetId) !== null && _b !== void 0 ? _b : ""}`); }) })] }), _jsx("button", { type: "button", className: "scv-index-settings-toggle", "aria-controls": settingsId, "aria-expanded": settingsOpen, onClick: () => setSettingsOpen((current) => !current), "aria-label": "Impostazioni indice", children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [_jsx("path", { d: "M4 7h16M4 12h16M4 17h16" }), _jsx("circle", { cx: "9", cy: "7", r: "2" }), _jsx("circle", { cx: "15", cy: "12", r: "2" }), _jsx("circle", { cx: "10", cy: "17", r: "2" })] }) }), _jsx("div", { id: settingsId, className: `scv-index-settings ${settingsOpen ? "is-open" : ""}`, children: _jsxs("div", { className: "scv-index-settings-inner", children: [_jsx(ModeSegmentedControl, { mode: mode, onChange: onModeChange }), _jsxs("button", { type: "button", className: "scv-theme-button", onClick: onToggleTheme, "aria-label": darkMode ? "Attiva modalità giorno" : "Attiva modalità notte", "aria-describedby": themeTooltipId, "aria-pressed": darkMode, children: [_jsx("span", { className: "scv-theme-icon", "aria-hidden": "true", children: darkMode
673
+ ? _jsxs("svg", { viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "12", r: "3.75" }), _jsx("path", { d: "M12 2.25v2.1M12 19.65v2.1M21.75 12h-2.1M4.35 12h-2.1M18.9 5.1l-1.48 1.48M6.58 17.42 5.1 18.9M18.9 18.9l-1.48-1.48M6.58 6.58 5.1 5.1" })] })
674
+ : _jsx("svg", { viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M20.2 15.15A8.35 8.35 0 0 1 8.85 3.8 8.36 8.36 0 1 0 20.2 15.15Z" }) }) }), _jsx("span", { id: themeTooltipId, className: "scv-control-tooltip", role: "tooltip", children: darkMode ? "Attiva modalità giorno" : "Attiva modalità notte" })] })] }) }), _jsx("button", { type: "button", className: "scv-mobile-index-close", onClick: onRequestClose, "aria-label": "Chiudi indice", children: "\u00D7" })] }), _jsx("div", { className: "scv-index-grid", children: _jsxs("section", { className: "scv-index-cell", children: [_jsx("header", { children: _jsx("span", { children: "Indice" }) }), _jsx("div", { className: "scv-index-list", ref: indexListRef, children: !indexReady ? _jsx(LoadingRows, {}) : chapters.length === 0 ? _jsx("p", { className: "scv-index-empty", children: "L\u2019indice non \u00E8 disponibile." }) : _jsx("ul", { className: "scv-index-tree", children: chapters.map((chapter) => {
675
+ var _a;
676
+ const chapterParagraphs = (_a = paragraphsByChapter.get(chapter.summary.id)) !== null && _a !== void 0 ? _a : [];
677
+ const chapterActive = activeChapterId === chapter.summary.id;
678
+ const chapterOpen = sidebarMode ? sidebarExpanded.session === sidebarSession && sidebarExpanded.chapterId === chapter.summary.id : chapterActive;
679
+ return _jsxs("li", { className: `scv-index-tree-item scv-index-tree-item-level-0 ${chapterOpen ? "is-open" : ""}`, children: [renderEntryButton(chapter, 0, chapterActive, 0, chapterParagraphs.length > 0 ? chapterOpen : undefined), chapterParagraphs.length > 0 && _jsx("div", { className: `scv-index-children ${chapterOpen ? "is-open" : ""}`, "aria-hidden": !chapterOpen, children: _jsx("div", { className: "scv-index-children-inner", children: _jsx("ul", { className: "scv-index-children-list scv-index-paragraph-list", children: chapterParagraphs.map((paragraph) => {
680
+ var _a;
681
+ const paragraphSubparagraphs = (_a = subparagraphsByParagraph.get(paragraph.summary.id)) !== null && _a !== void 0 ? _a : [];
682
+ const paragraphActive = activeParagraphId === paragraph.summary.id;
683
+ const paragraphOpen = chapterOpen && (sidebarMode ? sidebarExpanded.session === sidebarSession && sidebarExpanded.paragraphId === paragraph.summary.id : paragraphActive);
684
+ return _jsxs("li", { className: `scv-index-tree-item scv-index-tree-item-level-1 ${paragraphOpen ? "is-open" : ""}`, children: [renderEntryButton(paragraph, 1, paragraphActive, chapterOpen ? 0 : -1, paragraphSubparagraphs.length > 0 ? paragraphOpen : undefined), paragraphSubparagraphs.length > 0 && _jsx("div", { className: `scv-index-children ${paragraphOpen ? "is-open" : ""}`, "aria-hidden": !paragraphOpen, children: _jsx("div", { className: "scv-index-children-inner", children: _jsx("ul", { className: "scv-index-children-list scv-index-subparagraph-list", children: paragraphSubparagraphs.map((subparagraph) => _jsx("li", { className: "scv-index-tree-item scv-index-tree-item-level-2", children: renderEntryButton(subparagraph, 2, activeSubparagraphId === subparagraph.summary.id, paragraphOpen ? 0 : -1) }, subparagraph.summary.id)) }) }) })] }, paragraph.summary.id);
685
+ }) }) }) })] }, chapter.summary.id);
686
+ }) }) })] }) })] });
687
+ });
688
+ function scheduleIdle(callback) {
689
+ const idleWindow = window;
690
+ if (idleWindow.requestIdleCallback) {
691
+ const id = idleWindow.requestIdleCallback(callback, { timeout: 1800 });
692
+ return () => { var _a; return (_a = idleWindow.cancelIdleCallback) === null || _a === void 0 ? void 0 : _a.call(idleWindow, id); };
693
+ }
694
+ const id = window.setTimeout(callback, 250);
695
+ return () => window.clearTimeout(id);
696
+ }
697
+ export function NormativeViewer({ defaultMode = "combined", dataBaseUrl = "/data/codes", assetsBaseUrl = "/assets", auxiliaryPanel, auxiliaryPanelLabel = "PDF ufficiale", auxiliaryPanelButtonText = "Apri", auxiliaryPanelDefaultVisible = false, auxiliaryPanelDesktopDefaultVisible = false, auxiliaryPanelModes, auxiliaryPanelKeepMounted = false, searchMaxResults = 12, className, annotationStore }) {
698
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
59
699
  const [manifest, setManifest] = useState(null);
60
- const [index, setIndex] = useState(null);
61
- const [chunk, setChunk] = useState(null);
62
- const [scopeRecords, setScopeRecords] = useState([]);
63
- const [loadedScopeKey, setLoadedScopeKey] = useState("");
700
+ const [indexes, setIndexes] = useState(new Map());
701
+ const [loadedChunks, setLoadedChunks] = useState(new Map());
702
+ const [renderedChunkPaths, setRenderedChunkPaths] = useState(new Set());
703
+ const [requestedCircPaths, setRequestedCircPaths] = useState(new Set());
64
704
  const [mode, setMode] = useState(defaultMode);
705
+ const [modeRevision, setModeRevision] = useState(0);
65
706
  const [activeUnitId, setActiveUnitId] = useState(null);
707
+ const [historyPosition, setHistoryPosition] = useState({ index: 0, max: 0 });
66
708
  const [relations, setRelations] = useState([]);
67
- const [relatedByTarget, setRelatedByTarget] = useState(new Map());
68
- const [searchIndex, setSearchIndex] = useState(null);
709
+ const [relationsLoaded, setRelationsLoaded] = useState(false);
710
+ const [crossReferenceIndex, setCrossReferenceIndex] = useState(null);
711
+ const [referencePreview, setReferencePreview] = useState(null);
712
+ const [citationSelection, setCitationSelection] = useState(null);
713
+ const [permalinkNotice, setPermalinkNotice] = useState(null);
69
714
  const [query, setQuery] = useState("");
70
715
  const [loadError, setLoadError] = useState(false);
71
- const [settingsOpen, setSettingsOpen] = useState(false);
72
- const [auxiliaryVisible, setAuxiliaryVisible] = useState(Boolean(auxiliaryPanel && auxiliaryPanelDefaultVisible));
716
+ const [contentLoadNotice, setContentLoadNotice] = useState(null);
717
+ const [mobileIndexOpen, setMobileIndexOpen] = useState(false);
718
+ const [mobileIndexSession, setMobileIndexSession] = useState(0);
719
+ const [darkMode, setDarkMode] = useState(null);
720
+ const [auxiliaryVisible, setAuxiliaryVisible] = useState(false);
721
+ const [annotations, setAnnotations] = useState([]);
722
+ const [annotationsLoading, setAnnotationsLoading] = useState(Boolean(annotationStore));
723
+ const [annotationError, setAnnotationError] = useState(null);
724
+ const [annotationMenu, setAnnotationMenu] = useState(null);
725
+ const [annotationEditor, setAnnotationEditor] = useState(null);
726
+ const [annotationBusy, setAnnotationBusy] = useState(false);
73
727
  const searchRef = useRef(null);
74
- const settingsButtonRef = useRef(null);
75
- const dialogCloseRef = useRef(null);
728
+ const mobileIndexButtonRef = useRef(null);
729
+ const auxiliaryButtonRef = useRef(null);
730
+ const auxiliaryPaneRef = useRef(null);
731
+ const auxiliaryId = useId();
732
+ const navigationId = useId();
76
733
  const requestedIdRef = useRef(null);
77
- const deferredQuery = normalizedQuery(query);
734
+ const requestedTargetRef = useRef(null);
735
+ const scrollRequestRef = useRef(null);
736
+ const activeIdRef = useRef(null);
737
+ const manualTargetRef = useRef(null);
738
+ const navigationGenerationRef = useRef(0);
739
+ const renderedChunkPathsRef = useRef(new Set());
740
+ const pendingScrollAnchorRef = useRef(null);
741
+ const initializedContextRef = useRef("");
742
+ const initializedOnceRef = useRef(false);
743
+ const historyNavigationRef = useRef(false);
744
+ const historySessionRef = useRef(crypto.randomUUID());
745
+ const historyPositionRef = useRef({ index: 0, max: 0 });
746
+ const pendingHistoryScrollRef = useRef(null);
747
+ const crossReferencePromiseRef = useRef(null);
748
+ const relationsPromiseRef = useRef(null);
749
+ const permalinkNoticeTimerRef = useRef(null);
750
+ const searchHighlightTimerRef = useRef(null);
751
+ const pendingSearchHighlightRef = useRef(null);
752
+ const referencePreviewRequestRef = useRef(0);
753
+ const touchReferenceRef = useRef(null);
754
+ const lastPointerTypeRef = useRef("");
755
+ const suppressAnnotationClickRef = useRef(false);
756
+ const longPressRef = useRef(null);
757
+ const textPaneRef = useRef(null);
758
+ const documentId = documentForMode(mode);
759
+ const documentIdRef = useRef(documentId);
78
760
  const hasAuxiliary = Boolean(auxiliaryPanel);
761
+ const auxiliaryAvailable = hasAuxiliary && (auxiliaryPanelModes ? auxiliaryPanelModes.includes(mode) : mode !== "combined");
762
+ useEffect(() => {
763
+ var _a, _b;
764
+ window.history.replaceState(withViewerHistoryEntry(window.history.state, { session: historySessionRef.current, index: 0, scrollTop: (_b = (_a = textPaneRef.current) === null || _a === void 0 ? void 0 : _a.scrollTop) !== null && _b !== void 0 ? _b : 0 }), "", window.location.href);
765
+ }, []);
766
+ const saveHistoryScroll = useCallback(() => {
767
+ var _a, _b;
768
+ const entry = viewerHistoryEntry(window.history.state);
769
+ if ((entry === null || entry === void 0 ? void 0 : entry.session) !== historySessionRef.current)
770
+ return;
771
+ window.history.replaceState(withViewerHistoryEntry(window.history.state, Object.assign(Object.assign({}, entry), { scrollTop: (_b = (_a = textPaneRef.current) === null || _a === void 0 ? void 0 : _a.scrollTop) !== null && _b !== void 0 ? _b : 0 })), "", window.location.href);
772
+ }, []);
773
+ const pushViewerHistory = useCallback((nextMode, target) => {
774
+ const url = urlForViewerTarget(window.location.href, nextMode, defaultMode, target);
775
+ if (url.href === window.location.href)
776
+ return;
777
+ saveHistoryScroll();
778
+ const index = historyPositionRef.current.index + 1;
779
+ const position = { index, max: index };
780
+ historyPositionRef.current = position;
781
+ setHistoryPosition(position);
782
+ updateDeepLink(nextMode, target, defaultMode, "push", withViewerHistoryEntry(window.history.state, { session: historySessionRef.current, index, scrollTop: 0 }));
783
+ }, [defaultMode, saveHistoryScroll]);
79
784
  useEffect(() => {
785
+ const tablet = window.matchMedia("(min-width: 992px)");
786
+ const desktop = window.matchMedia("(min-width: 1400px)");
787
+ const syncLayout = () => {
788
+ setAuxiliaryVisible(Boolean(hasAuxiliary && (desktop.matches ? auxiliaryPanelDesktopDefaultVisible || auxiliaryPanelDefaultVisible : false)));
789
+ setMobileIndexOpen(false);
790
+ };
791
+ syncLayout();
792
+ tablet.addEventListener("change", syncLayout);
793
+ desktop.addEventListener("change", syncLayout);
794
+ return () => {
795
+ tablet.removeEventListener("change", syncLayout);
796
+ desktop.removeEventListener("change", syncLayout);
797
+ };
798
+ }, [hasAuxiliary, auxiliaryPanelDefaultVisible, auxiliaryPanelDesktopDefaultVisible]);
799
+ const flashSearchTarget = useCallback((target) => {
800
+ var _a;
801
+ const root = textPaneRef.current;
802
+ const element = findViewerTarget(root, target);
803
+ if (!element)
804
+ return false;
805
+ (_a = root === null || root === void 0 ? void 0 : root.querySelector(".scv-search-target")) === null || _a === void 0 ? void 0 : _a.classList.remove("scv-search-target");
806
+ element.classList.add("scv-search-target");
807
+ if (searchHighlightTimerRef.current !== null)
808
+ window.clearTimeout(searchHighlightTimerRef.current);
809
+ searchHighlightTimerRef.current = window.setTimeout(() => {
810
+ element.classList.remove("scv-search-target");
811
+ searchHighlightTimerRef.current = null;
812
+ }, 2200);
813
+ return true;
814
+ }, []);
815
+ useEffect(() => () => {
816
+ if (searchHighlightTimerRef.current !== null)
817
+ window.clearTimeout(searchHighlightTimerRef.current);
818
+ if (permalinkNoticeTimerRef.current !== null)
819
+ window.clearTimeout(permalinkNoticeTimerRef.current);
820
+ }, []);
821
+ const reportChunkLoadFailure = useCallback((message = "Una parte del documento non è disponibile. Puoi continuare a consultare i contenuti già caricati.") => {
822
+ if (renderedChunkPathsRef.current.size === 0)
823
+ setLoadError(true);
824
+ else
825
+ setContentLoadNotice(message);
826
+ }, []);
827
+ useEffect(() => {
828
+ documentIdRef.current = documentId;
829
+ }, [documentId]);
830
+ useEffect(() => {
831
+ if (!annotationStore)
832
+ return;
833
+ let cancelled = false;
834
+ annotationStore.listAnnotations().then((items) => {
835
+ if (!cancelled) {
836
+ setAnnotations(items);
837
+ setAnnotationError(null);
838
+ }
839
+ }).catch((error) => {
840
+ if (!cancelled)
841
+ setAnnotationError(error instanceof Error ? error.message : "Annotazioni locali non disponibili.");
842
+ }).finally(() => { if (!cancelled)
843
+ setAnnotationsLoading(false); });
844
+ return () => { cancelled = true; };
845
+ }, [annotationStore]);
846
+ const notesByTarget = useMemo(() => {
847
+ var _a;
848
+ const grouped = new Map();
849
+ for (const annotation of annotations) {
850
+ if (annotation.type !== "note")
851
+ continue;
852
+ const key = annotationTargetKey(annotation.target);
853
+ const group = (_a = grouped.get(key)) !== null && _a !== void 0 ? _a : [];
854
+ group.push(annotation);
855
+ grouped.set(key, group);
856
+ }
857
+ return grouped;
858
+ }, [annotations]);
859
+ const openAnnotation = useCallback((annotation) => {
860
+ var _a;
861
+ setAnnotationMenu(null);
862
+ setAnnotationError(null);
863
+ setAnnotationEditor({ type: annotation.type, target: annotation.target, annotation, metadata: {
864
+ documentId: annotation.documentId, numbering: annotation.numbering, title: annotation.title,
865
+ automaticLabel: (_a = annotation.label) !== null && _a !== void 0 ? _a : automaticAnnotationLabel(annotation.target, annotation.numbering, annotation.title),
866
+ } });
867
+ }, []);
868
+ useEffect(() => {
869
+ const timer = window.setTimeout(() => {
870
+ try {
871
+ setDarkMode(window.localStorage.getItem("scv-theme") === "dark");
872
+ }
873
+ catch (_a) {
874
+ setDarkMode(false);
875
+ }
876
+ }, 0);
877
+ return () => window.clearTimeout(timer);
878
+ }, []);
879
+ useEffect(() => {
880
+ if (darkMode === null)
881
+ return;
882
+ try {
883
+ window.localStorage.setItem("scv-theme", darkMode ? "dark" : "light");
884
+ }
885
+ catch ( /* Storage can be unavailable in private or embedded browsing contexts. */_a) { /* Storage can be unavailable in private or embedded browsing contexts. */ }
886
+ }, [darkMode]);
887
+ useEffect(() => {
888
+ let cancelled = false;
80
889
  loadManifest(dataBaseUrl).then((loaded) => {
890
+ var _a, _b;
891
+ if (cancelled)
892
+ return;
893
+ setIndexes(new Map());
894
+ setLoadedChunks(new Map());
895
+ setRenderedChunkPaths(new Set());
896
+ renderedChunkPathsRef.current = new Set();
897
+ setRequestedCircPaths(new Set());
898
+ setRelations([]);
899
+ setRelationsLoaded(false);
900
+ relationsPromiseRef.current = null;
901
+ setCrossReferenceIndex(null);
902
+ crossReferencePromiseRef.current = null;
903
+ setContentLoadNotice(null);
81
904
  const url = new URL(window.location.href);
82
905
  const requestedMode = url.searchParams.get("mode");
83
906
  if (modeOptions.some((option) => option.id === requestedMode))
84
907
  setMode(requestedMode);
85
- requestedIdRef.current = url.searchParams.get("unit");
908
+ requestedTargetRef.current = targetFromUrl(url);
909
+ pendingSearchHighlightRef.current = requestedTargetRef.current;
910
+ setCitationSelection(requestedTargetRef.current);
911
+ requestedIdRef.current = (_b = (_a = requestedTargetRef.current) === null || _a === void 0 ? void 0 : _a.unitId) !== null && _b !== void 0 ? _b : null;
86
912
  setActiveUnitId(requestedIdRef.current);
87
913
  setManifest(loaded);
88
- }).catch(() => setLoadError(true));
914
+ }).catch(() => { if (!cancelled)
915
+ setLoadError(true); });
916
+ return () => { cancelled = true; };
89
917
  }, [dataBaseUrl]);
90
- const documentId = documentForMode(mode);
91
918
  useEffect(() => {
92
- if (!manifest)
919
+ if (!manifest || indexes.has(documentId))
93
920
  return;
94
921
  let cancelled = false;
95
922
  loadDocumentIndex(manifest, documentId, dataBaseUrl).then((loaded) => {
96
- var _a;
97
- if (cancelled)
98
- return;
99
- setIndex(loaded);
100
- const requested = loaded.units.find((unit) => unit.id === requestedIdRef.current);
101
- const initial = (_a = requested !== null && requested !== void 0 ? requested : loaded.units.find((unit) => unit.numbering.official === (documentId === "ntc2018" ? "4.1" : "C4.1"))) !== null && _a !== void 0 ? _a : loaded.units[0];
102
- if (initial) {
103
- requestedIdRef.current = initial.id;
104
- setActiveUnitId(initial.id);
105
- updateDeepLink(mode, initial.id, defaultMode);
106
- }
107
- }).catch(() => setLoadError(true));
923
+ if (!cancelled)
924
+ setIndexes((current) => new Map(current).set(documentId, loaded));
925
+ }).catch(() => { if (!cancelled)
926
+ setLoadError(true); });
108
927
  return () => { cancelled = true; };
109
- }, [dataBaseUrl, defaultMode, documentId, manifest, mode]);
110
- const activeSummary = (_a = index === null || index === void 0 ? void 0 : index.units.find((unit) => unit.id === activeUnitId)) !== null && _a !== void 0 ? _a : null;
111
- const activeChapterId = activeSummary ? ancestorIdAtDepth(activeSummary, 0) : null;
112
- const activeParagraphId = activeSummary && depth(activeSummary) >= 1 ? ancestorIdAtDepth(activeSummary, 1) : null;
113
- const activeSubparagraphId = activeSummary && depth(activeSummary) >= 2 ? ancestorIdAtDepth(activeSummary, 2) : null;
114
- const scope = useMemo(() => {
115
- var _a;
116
- if (!index || !activeSummary || !activeChapterId) {
117
- return { mode: "paragraph", summaries: [], chapterId: null, paragraphId: null };
118
- }
119
- const chapterSummaries = index.units.filter((unit) => containsUnit(unit, activeChapterId));
120
- const chunkBytes = new Map(((_a = manifest === null || manifest === void 0 ? void 0 : manifest.chunks) !== null && _a !== void 0 ? _a : []).map((entry) => [entry.path, entry.bytes]));
121
- const chapterBytes = [...new Set(chapterSummaries.map((unit) => unit.chunkPath))].reduce((total, path) => { var _a; return total + ((_a = chunkBytes.get(path)) !== null && _a !== void 0 ? _a : 0); }, 0);
122
- const chapterCanBeContinuous = chapterSummaries.length <= maxContinuousChapterUnits && chapterBytes <= maxContinuousChapterBytes;
123
- if (depth(activeSummary) === 0 && chapterCanBeContinuous) {
124
- return { mode: "chapter", summaries: chapterSummaries, chapterId: activeChapterId, paragraphId: null };
125
- }
126
- if (depth(activeSummary) === 0) {
127
- const paragraphSummaries = index.units.filter((unit) => unit.hierarchy.parentId === activeChapterId && depth(unit) === 1);
128
- const chapter = index.units.find((unit) => unit.id === activeChapterId);
129
- return { mode: "paragraph", summaries: [chapter, ...paragraphSummaries].filter(Boolean), chapterId: activeChapterId, paragraphId: null };
130
- }
131
- const paragraphId = activeParagraphId !== null && activeParagraphId !== void 0 ? activeParagraphId : activeChapterId;
132
- return { mode: "paragraph", summaries: index.units.filter((unit) => containsUnit(unit, paragraphId)), chapterId: activeChapterId, paragraphId };
133
- }, [activeChapterId, activeParagraphId, activeSummary, index, manifest]);
134
- const scopePaths = useMemo(() => [...new Set(scope.summaries.map((unit) => unit.chunkPath))], [scope.summaries]);
135
- const scopeKey = useMemo(() => scope.summaries.map((unit) => unit.id).join("|"), [scope.summaries]);
136
- const scopeLoading = scope.summaries.length > 0 && loadedScopeKey !== scopeKey;
928
+ }, [dataBaseUrl, documentId, indexes, manifest]);
137
929
  useEffect(() => {
138
- if (!activeSummary)
930
+ if (!manifest || mode !== "combined" || indexes.has("circ2019"))
139
931
  return;
140
932
  let cancelled = false;
141
- loadChunk(activeSummary.chunkPath, dataBaseUrl).then((loaded) => {
933
+ loadDocumentIndex(manifest, "circ2019", dataBaseUrl).then((loaded) => {
142
934
  if (!cancelled)
143
- setChunk(loaded);
144
- }).catch(() => setLoadError(true));
935
+ setIndexes((current) => new Map(current).set("circ2019", loaded));
936
+ }).catch(() => { if (!cancelled)
937
+ setContentLoadNotice("L’indice della Circolare non è disponibile; la lettura NTC resta utilizzabile."); });
145
938
  return () => { cancelled = true; };
146
- }, [activeSummary, dataBaseUrl]);
939
+ }, [dataBaseUrl, indexes, manifest, mode]);
940
+ const ensureRelations = useCallback(async () => {
941
+ if (relationsLoaded)
942
+ return relations;
943
+ if (!manifest)
944
+ return [];
945
+ let pending = relationsPromiseRef.current;
946
+ if (!pending) {
947
+ pending = loadRelations(manifest, dataBaseUrl).then(({ relations: loadedRelations }) => {
948
+ setRelations(loadedRelations);
949
+ setRelationsLoaded(true);
950
+ return loadedRelations;
951
+ });
952
+ relationsPromiseRef.current = pending;
953
+ pending.catch(() => { if (relationsPromiseRef.current === pending)
954
+ relationsPromiseRef.current = null; });
955
+ }
956
+ return pending;
957
+ }, [dataBaseUrl, manifest, relations, relationsLoaded]);
147
958
  useEffect(() => {
148
- if (!scope.summaries.length)
959
+ if (mode !== "combined" || relationsLoaded)
149
960
  return;
150
- let cancelled = false;
151
- Promise.all(scopePaths.map(async (path) => [path, await loadChunk(path, dataBaseUrl)])).then((loadedChunks) => {
152
- if (cancelled)
153
- return;
154
- const chunks = new Map(loadedChunks);
155
- const records = scope.summaries.flatMap((summary) => {
156
- const loadedChunk = chunks.get(summary.chunkPath);
157
- const unit = loadedChunk === null || loadedChunk === void 0 ? void 0 : loadedChunk.units.find((candidate) => candidate.id === summary.id);
158
- return loadedChunk && unit ? [{ summary, unit, chunk: loadedChunk }] : [];
961
+ void ensureRelations().catch(() => setContentLoadNotice("Le relazioni NTC–Circolare non sono temporaneamente disponibili."));
962
+ }, [ensureRelations, mode, relationsLoaded]);
963
+ const ensureCrossReferenceIndex = useCallback(async () => {
964
+ if (crossReferenceIndex)
965
+ return crossReferenceIndex;
966
+ if (!manifest)
967
+ throw new Error("Manifest non ancora disponibile.");
968
+ let pending = crossReferencePromiseRef.current;
969
+ if (!pending) {
970
+ pending = loadCrossReferenceIndex(manifest, dataBaseUrl).then((loaded) => {
971
+ setCrossReferenceIndex(loaded);
972
+ return loaded;
159
973
  });
160
- setScopeRecords(records);
161
- setLoadedScopeKey(scopeKey);
162
- }).catch(() => {
163
- if (!cancelled) {
164
- setLoadError(true);
165
- }
166
- });
167
- return () => { cancelled = true; };
168
- }, [dataBaseUrl, scope.summaries, scopeKey, scopePaths]);
169
- useEffect(() => {
170
- if (!manifest || mode !== "combined")
974
+ crossReferencePromiseRef.current = pending;
975
+ pending.catch(() => { if (crossReferencePromiseRef.current === pending)
976
+ crossReferencePromiseRef.current = null; });
977
+ }
978
+ return pending;
979
+ }, [crossReferenceIndex, dataBaseUrl, manifest]);
980
+ const index = (_a = indexes.get(documentId)) !== null && _a !== void 0 ? _a : null;
981
+ const circIndex = (_b = indexes.get("circ2019")) !== null && _b !== void 0 ? _b : null;
982
+ const lookup = useMemo(() => index ? createDocumentLookup(index) : null, [index]);
983
+ const circLookup = useMemo(() => circIndex ? createDocumentLookup(circIndex) : null, [circIndex]);
984
+ const crossReferenceLookup = useMemo(() => crossReferenceIndex ? createCrossReferenceLookup(crossReferenceIndex) : null, [crossReferenceIndex]);
985
+ const search = useViewerSearch({ query, mode, manifest, dataBaseUrl, lookup, circLookup, maxResults: searchMaxResults });
986
+ const combinedPlan = useMemo(() => buildCombinedPlan(mode === "combined" ? index : null, mode === "combined" ? circIndex : null, relations), [circIndex, index, mode, relations]);
987
+ const primaryRenderedPaths = useMemo(() => { var _a; return new Set((_a = lookup === null || lookup === void 0 ? void 0 : lookup.chunkPaths.filter((path) => renderedChunkPaths.has(path))) !== null && _a !== void 0 ? _a : []); }, [lookup, renderedChunkPaths]);
988
+ const rememberChunk = useCallback(async (path) => {
989
+ const chunk = await loadChunk(path, dataBaseUrl);
990
+ setLoadedChunks((current) => current.get(path) === chunk ? current : new Map(current).set(path, chunk));
991
+ return chunk;
992
+ }, [dataBaseUrl]);
993
+ const preserveActiveScrollAnchor = useCallback((generation) => {
994
+ const root = textPaneRef.current;
995
+ const anchorId = activeIdRef.current;
996
+ const anchor = anchorId ? findTextUnit(root, anchorId) : null;
997
+ if (!root || !anchor || !anchorId)
171
998
  return;
172
- let cancelled = false;
173
- loadRelations(manifest, dataBaseUrl).then(({ relations: loadedRelations }) => {
174
- if (!cancelled)
175
- setRelations(loadedRelations);
176
- }).catch(() => setLoadError(true));
177
- return () => { cancelled = true; };
178
- }, [dataBaseUrl, manifest, mode]);
999
+ pendingScrollAnchorRef.current = {
1000
+ id: anchorId,
1001
+ top: anchor.getBoundingClientRect().top - root.getBoundingClientRect().top,
1002
+ generation,
1003
+ };
1004
+ }, []);
1005
+ const mountPrimaryChunk = useCallback(async (path, replace, preserveAnchor = false, generation = navigationGenerationRef.current) => {
1006
+ const chunk = await rememberChunk(path);
1007
+ if (generation !== navigationGenerationRef.current || chunk.document !== documentIdRef.current)
1008
+ return;
1009
+ const current = renderedChunkPathsRef.current;
1010
+ if (!replace && current.has(path))
1011
+ return;
1012
+ if (preserveAnchor)
1013
+ preserveActiveScrollAnchor(generation);
1014
+ const next = replace ? new Set([path]) : new Set(current).add(path);
1015
+ renderedChunkPathsRef.current = next;
1016
+ setRenderedChunkPaths(next);
1017
+ }, [preserveActiveScrollAnchor, rememberChunk]);
1018
+ const mountPrimaryWindow = useCallback(async (summary, replace, generation) => {
1019
+ await mountPrimaryChunk(summary.chunkPath, replace, false, generation);
1020
+ if (!replace || generation !== navigationGenerationRef.current)
1021
+ return;
1022
+ if (!lookup)
1023
+ return;
1024
+ const window = navigationChunkWindow(lookup, summary.chunkPath);
1025
+ if (window.previous)
1026
+ void mountPrimaryChunk(window.previous, false, true, generation).catch(() => reportChunkLoadFailure("Il chunk precedente non è disponibile; il target resta consultabile."));
1027
+ if (window.next)
1028
+ void mountPrimaryChunk(window.next, false, false, generation).catch(() => reportChunkLoadFailure("Il chunk successivo non è disponibile; il target resta consultabile."));
1029
+ }, [lookup, mountPrimaryChunk, reportChunkLoadFailure]);
1030
+ const requiredCombinedCircPaths = useMemo(() => {
1031
+ var _a;
1032
+ const paths = new Set(requestedCircPaths);
1033
+ if (mode !== "combined")
1034
+ return paths;
1035
+ for (const primaryPath of primaryRenderedPaths) {
1036
+ for (const path of (_a = combinedPlan.circPathsByPrimaryPath.get(primaryPath)) !== null && _a !== void 0 ? _a : [])
1037
+ paths.add(path);
1038
+ }
1039
+ for (const edge of relations) {
1040
+ const target = lookup === null || lookup === void 0 ? void 0 : lookup.unitById.get(edge.targetUnitId);
1041
+ if (target && primaryRenderedPaths.has(target.chunkPath))
1042
+ paths.add(edge.sourceChunkPath);
1043
+ }
1044
+ return paths;
1045
+ }, [combinedPlan.circPathsByPrimaryPath, lookup, mode, primaryRenderedPaths, relations, requestedCircPaths]);
179
1046
  useEffect(() => {
180
- if (mode !== "combined" || scopeRecords.length === 0 || relations.length === 0)
1047
+ if (mode !== "combined")
1048
+ return;
1049
+ const missing = [...requiredCombinedCircPaths].filter((path) => !loadedChunks.has(path));
1050
+ if (missing.length === 0)
181
1051
  return;
182
1052
  let cancelled = false;
183
- const targetIds = new Set(scopeRecords.map(({ unit }) => unit.id));
184
- const incoming = relations.filter((edge) => targetIds.has(edge.targetUnitId));
185
- Promise.all([...new Set(incoming.map((edge) => edge.sourceChunkPath))].map(async (path) => [path, await loadChunk(path, dataBaseUrl)])).then((loadedChunks) => {
186
- var _a;
1053
+ const generation = navigationGenerationRef.current;
1054
+ Promise.all(missing.map((path) => loadChunk(path, dataBaseUrl))).then((chunks) => {
187
1055
  if (cancelled)
188
1056
  return;
189
- const relatedChunks = new Map(loadedChunks);
190
- const grouped = new Map();
191
- for (const edge of incoming) {
192
- const sourceChunk = relatedChunks.get(edge.sourceChunkPath);
193
- const unit = sourceChunk === null || sourceChunk === void 0 ? void 0 : sourceChunk.units.find((candidate) => candidate.id === edge.sourceUnitId);
194
- if (!sourceChunk || !unit)
195
- continue;
196
- const group = (_a = grouped.get(edge.targetUnitId)) !== null && _a !== void 0 ? _a : [];
197
- group.push({ edge, unit, chunk: sourceChunk });
198
- grouped.set(edge.targetUnitId, group);
199
- }
200
- for (const group of grouped.values())
201
- group.sort((left, right) => left.unit.numbering.sortKey.localeCompare(right.unit.numbering.sortKey, "it", { numeric: true }));
202
- setRelatedByTarget(grouped);
1057
+ if (generation === navigationGenerationRef.current)
1058
+ preserveActiveScrollAnchor(generation);
1059
+ setLoadedChunks((current) => {
1060
+ const next = new Map(current);
1061
+ missing.forEach((path, position) => next.set(path, chunks[position]));
1062
+ return next;
1063
+ });
203
1064
  }).catch(() => { if (!cancelled)
204
- setLoadError(true); });
1065
+ setContentLoadNotice("Alcuni contenuti correlati della Circolare non sono disponibili."); });
205
1066
  return () => { cancelled = true; };
206
- }, [dataBaseUrl, mode, relations, scopeRecords]);
1067
+ }, [dataBaseUrl, loadedChunks, mode, preserveActiveScrollAnchor, requiredCombinedCircPaths]);
1068
+ const documentRecords = useMemo(() => recordsForPaths(index, lookup, primaryRenderedPaths, loadedChunks), [index, loadedChunks, lookup, primaryRenderedPaths]);
1069
+ const fallbackIds = useMemo(() => new Set(combinedPlan.fallbackSummaries.map((summary) => summary.id)), [combinedPlan.fallbackSummaries]);
1070
+ const circRecords = useMemo(() => {
1071
+ if (mode !== "combined" || !index || !circIndex || !circLookup)
1072
+ return [];
1073
+ const primaryBases = new Set(index.units.map((summary) => baseNumbering(summary.numbering.official)));
1074
+ return recordsForPaths(circIndex, circLookup, requiredCombinedCircPaths, loadedChunks).filter(({ unit }) => fallbackIds.has(unit.id) && (hasUnitContent(unit) || !primaryBases.has(baseNumbering(unit.numbering.official))));
1075
+ }, [circIndex, circLookup, fallbackIds, index, loadedChunks, mode, requiredCombinedCircPaths]);
1076
+ const relatedByTarget = useMemo(() => {
1077
+ var _a;
1078
+ const grouped = new Map();
1079
+ if (mode !== "combined")
1080
+ return grouped;
1081
+ for (const edge of relations) {
1082
+ const target = lookup === null || lookup === void 0 ? void 0 : lookup.unitById.get(edge.targetUnitId);
1083
+ if (!target || !primaryRenderedPaths.has(target.chunkPath))
1084
+ continue;
1085
+ const sourceChunk = loadedChunks.get(edge.sourceChunkPath);
1086
+ const unit = sourceChunk ? unitsForChunk(sourceChunk).get(edge.sourceUnitId) : null;
1087
+ if (!sourceChunk || !unit)
1088
+ continue;
1089
+ const group = (_a = grouped.get(edge.targetUnitId)) !== null && _a !== void 0 ? _a : [];
1090
+ group.push({ edge, unit, chunk: sourceChunk });
1091
+ grouped.set(edge.targetUnitId, group);
1092
+ }
1093
+ for (const group of grouped.values())
1094
+ group.sort((left, right) => compareNumbering(left.unit, right.unit));
1095
+ return grouped;
1096
+ }, [loadedChunks, lookup, mode, primaryRenderedPaths, relations]);
1097
+ const renderRecords = useMemo(() => [...documentRecords, ...circRecords].sort((left, right) => compareBaseNumbering(left.summary, right.summary)), [circRecords, documentRecords]);
1098
+ const navigationEntries = useMemo(() => {
1099
+ if (!index)
1100
+ return [];
1101
+ const primaryBases = new Set(index.units.map((summary) => baseNumbering(summary.numbering.official)));
1102
+ const summaries = mode === "combined"
1103
+ ? [...index.units, ...combinedPlan.fallbackSummaries].filter((summary) => summary.document !== "circ2019" || !primaryBases.has(baseNumbering(summary.numbering.official)))
1104
+ : index.units;
1105
+ const entries = summaries.map((summary) => navigationEntry(summary));
1106
+ if (mode !== "combined")
1107
+ return entries.sort((left, right) => compareBaseNumbering(left.summary, right.summary));
1108
+ const entryIdByBase = new Map(entries.map((entry) => [entry.baseNumber, entry.summary.id]));
1109
+ return entries.map((entry) => {
1110
+ var _a;
1111
+ const parentBase = parentNumbering(entry.baseNumber);
1112
+ const parentId = parentBase ? (_a = entryIdByBase.get(parentBase)) !== null && _a !== void 0 ? _a : null : null;
1113
+ return entry.parentId === parentId ? entry : Object.assign(Object.assign({}, entry), { parentId });
1114
+ }).sort((left, right) => compareBaseNumbering(left.summary, right.summary));
1115
+ }, [combinedPlan.fallbackSummaries, index, mode]);
1116
+ const entryById = useMemo(() => new Map(navigationEntries.map((entry) => [entry.summary.id, entry])), [navigationEntries]);
1117
+ const recordById = useMemo(() => new Map(renderRecords.map((record) => [record.unit.id, record])), [renderRecords]);
1118
+ const summaryById = useMemo(() => new Map([...indexes.values()].flatMap((documentIndex) => documentIndex.units).map((summary) => [summary.id, summary])), [indexes]);
1119
+ const annotationRanks = useMemo(() => annotationPositionRanks(indexes.values()), [indexes]);
1120
+ const revealSummary = useCallback(async (summary, replace = false, generation = navigationGenerationRef.current) => {
1121
+ var _a;
1122
+ if (generation !== navigationGenerationRef.current)
1123
+ return;
1124
+ scrollRequestRef.current = ((_a = requestedTargetRef.current) === null || _a === void 0 ? void 0 : _a.unitId) === summary.id ? requestedTargetRef.current : { kind: "unit", unitId: summary.id };
1125
+ if (summary.document === documentIdRef.current) {
1126
+ await mountPrimaryWindow(summary, replace, generation);
1127
+ return;
1128
+ }
1129
+ if (mode !== "combined" || !lookup)
1130
+ return;
1131
+ const explicitTarget = relationTargets(summary.id, relations)[0];
1132
+ if (explicitTarget) {
1133
+ const target = lookup.unitById.get(explicitTarget.targetUnitId);
1134
+ if (target) {
1135
+ setRequestedCircPaths((current) => replace ? new Set([summary.chunkPath]) : new Set(current).add(summary.chunkPath));
1136
+ await Promise.all([rememberChunk(summary.chunkPath), mountPrimaryWindow(target, replace, generation)]);
1137
+ }
1138
+ return;
1139
+ }
1140
+ const matchingPrimary = matchingPrimarySummary(summary, lookup);
1141
+ if (matchingPrimary) {
1142
+ scrollRequestRef.current = { kind: "unit", unitId: matchingPrimary.id };
1143
+ await mountPrimaryWindow(matchingPrimary, replace, generation);
1144
+ return;
1145
+ }
1146
+ const anchor = combinedPlan.primaryAnchorByFallbackId.get(summary.id);
1147
+ if (!anchor)
1148
+ return;
1149
+ setRequestedCircPaths((current) => replace ? new Set([summary.chunkPath]) : new Set(current).add(summary.chunkPath));
1150
+ await Promise.all([rememberChunk(summary.chunkPath), mountPrimaryWindow(anchor, replace, generation)]);
1151
+ }, [combinedPlan.primaryAnchorByFallbackId, lookup, mode, mountPrimaryWindow, relations, rememberChunk]);
207
1152
  useEffect(() => {
208
- if (!manifest || deferredQuery.length < 2 || searchIndex)
1153
+ var _a, _b, _c, _d, _e;
1154
+ if (!index || !lookup)
209
1155
  return;
210
- loadSearchIndex(manifest, dataBaseUrl).then(setSearchIndex).catch(() => setLoadError(true));
211
- }, [dataBaseUrl, deferredQuery, manifest, searchIndex]);
1156
+ const contextKey = `${mode}:${modeRevision}:${index.document}`;
1157
+ if (initializedContextRef.current === contextKey)
1158
+ return;
1159
+ const requestedId = requestedIdRef.current;
1160
+ let initial = requestedId ? (_a = lookup.unitById.get(requestedId)) !== null && _a !== void 0 ? _a : null : null;
1161
+ let revealInitial = initial;
1162
+ if (!initial && mode === "combined" && (requestedId === null || requestedId === void 0 ? void 0 : requestedId.includes(":circ2019:"))) {
1163
+ if (!circLookup || !relationsLoaded)
1164
+ return;
1165
+ const requestedCirc = (_b = circLookup.unitById.get(requestedId)) !== null && _b !== void 0 ? _b : null;
1166
+ const explicitTarget = requestedCirc ? relationTargets(requestedCirc.id, relations)[0] : null;
1167
+ const explicitPrimary = explicitTarget ? (_c = lookup.unitById.get(explicitTarget.targetUnitId)) !== null && _c !== void 0 ? _c : null : null;
1168
+ const matchingPrimary = requestedCirc ? matchingPrimarySummary(requestedCirc, lookup) : null;
1169
+ initial = (_d = explicitPrimary !== null && explicitPrimary !== void 0 ? explicitPrimary : matchingPrimary) !== null && _d !== void 0 ? _d : requestedCirc;
1170
+ revealInitial = explicitPrimary || !matchingPrimary ? requestedCirc : matchingPrimary;
1171
+ }
1172
+ initial !== null && initial !== void 0 ? initial : (initial = initialUnitForIndex(index, requestedId));
1173
+ revealInitial !== null && revealInitial !== void 0 ? revealInitial : (revealInitial = initial);
1174
+ if (!initial || !revealInitial)
1175
+ return;
1176
+ initializedContextRef.current = contextKey;
1177
+ requestedIdRef.current = initial.id;
1178
+ activeIdRef.current = initial.id;
1179
+ setActiveUnitId(initial.id);
1180
+ const requestedTarget = ((_e = requestedTargetRef.current) === null || _e === void 0 ? void 0 : _e.unitId) === revealInitial.id ? requestedTargetRef.current : { kind: "unit", unitId: initial.id };
1181
+ if (!historyNavigationRef.current) {
1182
+ if (initializedOnceRef.current)
1183
+ pushViewerHistory(mode, requestedTarget);
1184
+ else
1185
+ updateDeepLink(mode, requestedTarget, defaultMode);
1186
+ }
1187
+ initializedOnceRef.current = true;
1188
+ historyNavigationRef.current = false;
1189
+ const generation = ++navigationGenerationRef.current;
1190
+ pendingScrollAnchorRef.current = null;
1191
+ void revealSummary(revealInitial, primaryRenderedPaths.size === 0 || revealInitial.document !== documentId, generation).catch(() => reportChunkLoadFailure());
1192
+ }, [circLookup, defaultMode, documentId, index, lookup, mode, modeRevision, primaryRenderedPaths.size, pushViewerHistory, relations, relationsLoaded, reportChunkLoadFailure, revealSummary]);
212
1193
  useEffect(() => {
213
- const onKeyDown = (event) => {
214
- var _a, _b;
215
- if (event.key === "/" && ((_a = window.document.activeElement) === null || _a === void 0 ? void 0 : _a.tagName.toLowerCase()) !== "input") {
216
- event.preventDefault();
217
- (_b = searchRef.current) === null || _b === void 0 ? void 0 : _b.focus();
1194
+ if (!lookup || !activeUnitId)
1195
+ return;
1196
+ const active = lookup.unitById.get(activeUnitId);
1197
+ if (!active)
1198
+ return;
1199
+ const candidates = new Set();
1200
+ const { previous, next } = adjacentChunkPaths(lookup, active.chunkPath);
1201
+ if (previous)
1202
+ candidates.add(previous);
1203
+ if (next)
1204
+ candidates.add(next);
1205
+ if (candidates.size === 0)
1206
+ return;
1207
+ return scheduleIdle(() => { for (const path of candidates)
1208
+ void loadChunk(path, dataBaseUrl).catch(() => undefined); });
1209
+ }, [activeUnitId, dataBaseUrl, lookup]);
1210
+ useLayoutEffect(() => {
1211
+ const root = textPaneRef.current;
1212
+ const pendingAnchor = pendingScrollAnchorRef.current;
1213
+ if (root && pendingAnchor && pendingAnchor.generation === navigationGenerationRef.current) {
1214
+ const anchor = findTextUnit(root, pendingAnchor.id);
1215
+ if (anchor) {
1216
+ const nextTop = anchor.getBoundingClientRect().top - root.getBoundingClientRect().top;
1217
+ root.scrollTop += nextTop - pendingAnchor.top;
218
1218
  }
219
- if (event.key === "Escape" && settingsOpen)
220
- setSettingsOpen(false);
221
- };
222
- window.addEventListener("keydown", onKeyDown);
223
- return () => window.removeEventListener("keydown", onKeyDown);
224
- }, [settingsOpen]);
1219
+ pendingScrollAnchorRef.current = null;
1220
+ }
1221
+ const target = scrollRequestRef.current;
1222
+ if (!target || renderRecords.length === 0)
1223
+ return;
1224
+ if (!scrollViewerTarget(root, target)) {
1225
+ if (target.kind === "unit")
1226
+ return;
1227
+ const unitTarget = { kind: "unit", unitId: target.unitId };
1228
+ if (!scrollViewerTarget(root, unitTarget))
1229
+ return;
1230
+ scrollRequestRef.current = null;
1231
+ requestedTargetRef.current = unitTarget;
1232
+ updateDeepLink(mode, unitTarget, defaultMode);
1233
+ setContentLoadNotice("Il blocco o asset richiesto non esiste; è stata aperta l’unità normativa corrispondente.");
1234
+ return;
1235
+ }
1236
+ scrollRequestRef.current = null;
1237
+ if (pendingHistoryScrollRef.current !== null && root) {
1238
+ root.scrollTop = pendingHistoryScrollRef.current;
1239
+ pendingHistoryScrollRef.current = null;
1240
+ }
1241
+ if (sameViewerTarget(pendingSearchHighlightRef.current, target) && flashSearchTarget(target))
1242
+ pendingSearchHighlightRef.current = null;
1243
+ manualTargetRef.current = { id: target.unitId, expires: performance.now() + 500 };
1244
+ }, [auxiliaryAvailable, auxiliaryVisible, defaultMode, flashSearchTarget, mode, relatedByTarget, renderRecords]);
1245
+ const navigationRef = useRef({ entryById, lookup, mode, relations, primaryAnchorByFallbackId: combinedPlan.primaryAnchorByFallbackId });
225
1246
  useEffect(() => {
226
- var _a, _b;
227
- if (settingsOpen)
228
- (_a = dialogCloseRef.current) === null || _a === void 0 ? void 0 : _a.focus();
229
- else
230
- (_b = settingsButtonRef.current) === null || _b === void 0 ? void 0 : _b.focus();
231
- }, [settingsOpen]);
232
- const hierarchy = useMemo(() => {
233
- if (!index)
234
- return [[], [], [], []];
235
- const chapters = index.units.filter((unit) => depth(unit) === 0);
236
- const paragraphs = activeChapterId ? index.units.filter((unit) => depth(unit) === 1 && unit.hierarchy.parentId === activeChapterId) : [];
237
- const subparagraphs = activeParagraphId ? index.units.filter((unit) => depth(unit) === 2 && unit.hierarchy.parentId === activeParagraphId) : [];
238
- const detailsAnchor = activeSubparagraphId !== null && activeSubparagraphId !== void 0 ? activeSubparagraphId : activeParagraphId;
239
- const details = detailsAnchor ? index.units.filter((unit) => depth(unit) >= 3 && unit.hierarchy.ancestorIds.includes(detailsAnchor)) : [];
240
- return [chapters, paragraphs, subparagraphs, details];
241
- }, [activeChapterId, activeParagraphId, activeSubparagraphId, index]);
242
- const pageBounds = useMemo(() => {
243
- if (!scopeRecords.length)
244
- return { from: 1, to: 1 };
245
- const pages = scopeRecords.flatMap(({ unit, chunk: loadedChunk }) => evidencePages(unit, loadedChunk));
246
- return pages.length > 0 ? { from: Math.min(...pages), to: Math.max(...pages) } : { from: 1, to: 1 };
247
- }, [scopeRecords]);
248
- const searchResults = useMemo(() => {
249
- if (!searchIndex || deferredQuery.length < 2)
250
- return [];
251
- return searchIndex.units.filter((unit) => {
252
- if (mode === "ntc" && unit.document !== "ntc2018")
253
- return false;
254
- if (mode === "circ" && unit.document !== "circ2019")
255
- return false;
256
- return `${unit.numbering} ${unit.title} ${unit.text}`.normalize("NFKC").toLocaleLowerCase("it").includes(deferredQuery);
257
- }).slice(0, 12);
258
- }, [deferredQuery, mode, searchIndex]);
259
- function selectUnit(unit) {
1247
+ navigationRef.current = { entryById, lookup, mode, relations, primaryAnchorByFallbackId: combinedPlan.primaryAnchorByFallbackId };
1248
+ }, [combinedPlan.primaryAnchorByFallbackId, entryById, lookup, mode, relations]);
1249
+ const loadAdjacentAtUnit = useCallback((unitId) => {
1250
+ const current = navigationRef.current;
1251
+ const adjacent = progressiveChunkTargetsForVisibleUnit(current.lookup, unitId, current.primaryAnchorByFallbackId);
1252
+ if (adjacent.previous)
1253
+ void mountPrimaryChunk(adjacent.previous, false, true).catch(() => reportChunkLoadFailure("Il chunk precedente non è disponibile; il resto del documento rimane consultabile."));
1254
+ if (adjacent.next)
1255
+ void mountPrimaryChunk(adjacent.next, false).catch(() => reportChunkLoadFailure("Il chunk successivo non è disponibile; il resto del documento rimane consultabile."));
1256
+ }, [mountPrimaryChunk, reportChunkLoadFailure]);
1257
+ const onVisibleUnit = useCallback((nextId) => {
1258
+ const guard = manualTargetRef.current;
1259
+ if (guard && performance.now() < guard.expires && guard.id !== nextId)
1260
+ return;
1261
+ if ((guard === null || guard === void 0 ? void 0 : guard.id) === nextId || (guard && performance.now() >= guard.expires))
1262
+ manualTargetRef.current = null;
1263
+ if (nextId !== activeIdRef.current) {
1264
+ setCitationSelection(null);
1265
+ activeIdRef.current = nextId;
1266
+ requestedIdRef.current = nextId;
1267
+ requestedTargetRef.current = { kind: "unit", unitId: nextId };
1268
+ setActiveUnitId(nextId);
1269
+ updateDeepLink(navigationRef.current.mode, requestedTargetRef.current, defaultMode);
1270
+ }
1271
+ loadAdjacentAtUnit(nextId);
1272
+ }, [defaultMode, loadAdjacentAtUnit]);
1273
+ useVisibleUnitObserver(textPaneRef, renderRecords, onVisibleUnit);
1274
+ const selectUnit = useCallback((unit) => {
1275
+ const target = { kind: "unit", unitId: unit.id };
1276
+ setCitationSelection(null);
1277
+ setMobileIndexOpen(false);
1278
+ const generation = ++navigationGenerationRef.current;
1279
+ pendingScrollAnchorRef.current = null;
1280
+ setContentLoadNotice(null);
260
1281
  requestedIdRef.current = unit.id;
1282
+ requestedTargetRef.current = target;
1283
+ pendingSearchHighlightRef.current = target;
1284
+ manualTargetRef.current = { id: unit.id, expires: performance.now() + 500 };
1285
+ pushViewerHistory(navigationRef.current.mode, target);
1286
+ const present = scrollTextUnit(textPaneRef.current, unit.id);
1287
+ activeIdRef.current = unit.id;
261
1288
  setActiveUnitId(unit.id);
262
- updateDeepLink(mode, unit.id, defaultMode);
263
- }
1289
+ if (present) {
1290
+ if (flashSearchTarget(target))
1291
+ pendingSearchHighlightRef.current = null;
1292
+ return;
1293
+ }
1294
+ const currentLookup = navigationRef.current.lookup;
1295
+ const nearMountedWindow = currentLookup ? isChunkNearRenderedWindow(currentLookup, unit.chunkPath, primaryRenderedPaths) : false;
1296
+ void revealSummary(unit, !nearMountedWindow, generation).catch(() => reportChunkLoadFailure());
1297
+ }, [flashSearchTarget, primaryRenderedPaths, pushViewerHistory, reportChunkLoadFailure, revealSummary]);
1298
+ const navigateViewerTarget = useCallback(async (target, action = "push", forcedMode) => {
1299
+ var _a;
1300
+ setCitationSelection(target);
1301
+ setMobileIndexOpen(false);
1302
+ pendingSearchHighlightRef.current = target;
1303
+ const generation = ++navigationGenerationRef.current;
1304
+ pendingScrollAnchorRef.current = null;
1305
+ setContentLoadNotice(null);
1306
+ const targetDocument = documentFromUnitId(target.unitId);
1307
+ const nextMode = forcedMode !== null && forcedMode !== void 0 ? forcedMode : (mode === "combined" || documentForMode(mode) === targetDocument ? mode : targetDocument === "ntc2018" ? "ntc" : "circ");
1308
+ requestedIdRef.current = target.unitId;
1309
+ requestedTargetRef.current = target;
1310
+ scrollRequestRef.current = target;
1311
+ manualTargetRef.current = { id: target.unitId, expires: performance.now() + 700 };
1312
+ if (action === "push")
1313
+ pushViewerHistory(nextMode, target);
1314
+ else if (action === "replace")
1315
+ updateDeepLink(nextMode, target, defaultMode);
1316
+ if (nextMode !== mode) {
1317
+ historyNavigationRef.current = true;
1318
+ initializedContextRef.current = "";
1319
+ setRequestedCircPaths(new Set());
1320
+ setModeRevision((revision) => revision + 1);
1321
+ setMode(nextMode);
1322
+ return;
1323
+ }
1324
+ if (targetDocument === documentIdRef.current) {
1325
+ activeIdRef.current = target.unitId;
1326
+ setActiveUnitId(target.unitId);
1327
+ }
1328
+ if (scrollViewerTarget(textPaneRef.current, target)) {
1329
+ if (pendingHistoryScrollRef.current !== null && textPaneRef.current) {
1330
+ textPaneRef.current.scrollTop = pendingHistoryScrollRef.current;
1331
+ pendingHistoryScrollRef.current = null;
1332
+ }
1333
+ if (sameViewerTarget(pendingSearchHighlightRef.current, target) && flashSearchTarget(target))
1334
+ pendingSearchHighlightRef.current = null;
1335
+ return;
1336
+ }
1337
+ if (!manifest)
1338
+ return;
1339
+ let targetIndex = indexes.get(targetDocument);
1340
+ if (!targetIndex) {
1341
+ targetIndex = await loadDocumentIndex(manifest, targetDocument, dataBaseUrl);
1342
+ if (generation !== navigationGenerationRef.current)
1343
+ return;
1344
+ setIndexes((current) => current.has(targetDocument) ? current : new Map(current).set(targetDocument, targetIndex));
1345
+ }
1346
+ const summary = targetIndex.units.find((unit) => unit.id === target.unitId);
1347
+ if (!summary) {
1348
+ const fallbackId = activeIdRef.current;
1349
+ if (fallbackId)
1350
+ updateDeepLink(mode, { kind: "unit", unitId: fallbackId }, defaultMode);
1351
+ setContentLoadNotice("Il target richiesto non esiste nel documento corrente.");
1352
+ return;
1353
+ }
1354
+ let activeId = summary.id;
1355
+ if (mode === "combined" && summary.document === "circ2019") {
1356
+ const explicit = relationTargets(summary.id, relations)[0];
1357
+ activeId = (_a = explicit === null || explicit === void 0 ? void 0 : explicit.targetUnitId) !== null && _a !== void 0 ? _a : summary.id;
1358
+ }
1359
+ activeIdRef.current = activeId;
1360
+ setActiveUnitId(activeId);
1361
+ const currentLookup = navigationRef.current.lookup;
1362
+ const nearMountedWindow = summary.document === documentIdRef.current && currentLookup ? isChunkNearRenderedWindow(currentLookup, summary.chunkPath, primaryRenderedPaths) : false;
1363
+ if (generation !== navigationGenerationRef.current)
1364
+ return;
1365
+ await revealSummary(summary, summary.document === documentIdRef.current && !nearMountedWindow, generation);
1366
+ }, [dataBaseUrl, defaultMode, flashSearchTarget, indexes, manifest, mode, primaryRenderedPaths, pushViewerHistory, relations, revealSummary]);
264
1367
  useEffect(() => {
265
- if (scopeLoading || !activeUnitId || scopeRecords.length === 0)
1368
+ const onPopState = (event) => {
1369
+ const url = new URL(window.location.href);
1370
+ const target = targetFromUrl(url);
1371
+ if (!target)
1372
+ return;
1373
+ const entry = viewerHistoryEntry(event.state);
1374
+ if ((entry === null || entry === void 0 ? void 0 : entry.session) === historySessionRef.current) {
1375
+ const position = { index: entry.index, max: historyPositionRef.current.max };
1376
+ historyPositionRef.current = position;
1377
+ setHistoryPosition(position);
1378
+ pendingHistoryScrollRef.current = entry.scrollTop;
1379
+ }
1380
+ else {
1381
+ pendingHistoryScrollRef.current = null;
1382
+ historyPositionRef.current = { index: 0, max: 0 };
1383
+ setHistoryPosition({ index: 0, max: 0 });
1384
+ }
1385
+ const requestedMode = url.searchParams.get("mode");
1386
+ const nextMode = modeOptions.some((option) => option.id === requestedMode) ? requestedMode : defaultMode;
1387
+ void navigateViewerTarget(target, "none", nextMode).catch(() => reportChunkLoadFailure());
1388
+ };
1389
+ window.addEventListener("popstate", onPopState);
1390
+ return () => window.removeEventListener("popstate", onPopState);
1391
+ }, [defaultMode, navigateViewerTarget, reportChunkLoadFailure]);
1392
+ const resolveReferenceElement = useCallback(async (element) => {
1393
+ const descriptor = referenceDescriptor(element);
1394
+ if (!descriptor)
1395
+ return null;
1396
+ const loaded = await ensureCrossReferenceIndex();
1397
+ const referenceLookup = crossReferenceLookup !== null && crossReferenceLookup !== void 0 ? crossReferenceLookup : createCrossReferenceLookup(loaded);
1398
+ return resolveCrossReference(referenceLookup, descriptor, descriptor.sourceDocument);
1399
+ }, [crossReferenceLookup, ensureCrossReferenceIndex]);
1400
+ const clearReferencePreview = useCallback(() => {
1401
+ referencePreviewRequestRef.current += 1;
1402
+ setReferencePreview(null);
1403
+ touchReferenceRef.current = null;
1404
+ }, []);
1405
+ const showReferencePreview = useCallback((element, interactive = false) => {
1406
+ var _a;
1407
+ const requestId = ++referencePreviewRequestRef.current;
1408
+ const bounds = element.getBoundingClientRect();
1409
+ const left = Math.max(12, Math.min(window.innerWidth - 332, bounds.left));
1410
+ const previewHeight = 190;
1411
+ const below = bounds.bottom + 7;
1412
+ const top = Math.max(12, below + previewHeight <= window.innerHeight - 12 ? below : bounds.top - previewHeight - 7);
1413
+ setReferencePreview({ label: (_a = element.textContent) !== null && _a !== void 0 ? _a : "Riferimento", title: "", snippet: "", left, top, loading: true, interactive });
1414
+ void resolveReferenceElement(element).then((reference) => {
1415
+ var _a, _b, _c;
1416
+ if (requestId !== referencePreviewRequestRef.current)
1417
+ return;
1418
+ if (!reference) {
1419
+ element.dataset.scvReferenceResolved = "false";
1420
+ setReferencePreview({ label: (_a = element.textContent) !== null && _a !== void 0 ? _a : "Riferimento", title: "Target non disponibile", snippet: "Il riferimento non è stato reso cliccabile perché non è risolto dall’indice derivato.", left, top, interactive, available: false });
1421
+ return;
1422
+ }
1423
+ element.dataset.scvReferenceResolved = "true";
1424
+ setReferencePreview({ label: referenceLabel(reference), title: ((_b = reference.asset) === null || _b === void 0 ? void 0 : _b.title) || reference.unit.title, snippet: ((_c = reference.asset) === null || _c === void 0 ? void 0 : _c.snippet) || reference.unit.snippet, left, top, interactive, available: true });
1425
+ }).catch(() => {
1426
+ if (requestId === referencePreviewRequestRef.current)
1427
+ setReferencePreview(null);
1428
+ });
1429
+ }, [resolveReferenceElement]);
1430
+ const activateReference = useCallback((element) => {
1431
+ clearReferencePreview();
1432
+ void resolveReferenceElement(element).then((reference) => {
1433
+ if (!reference)
1434
+ return;
1435
+ return navigateViewerTarget(viewerTargetForReference(reference), "push");
1436
+ }).catch(() => { });
1437
+ }, [clearReferencePreview, navigateViewerTarget, resolveReferenceElement]);
1438
+ const showPermalinkNotice = useCallback((element, message, status) => {
1439
+ const bounds = element.getBoundingClientRect();
1440
+ const placement = bounds.top >= 54 ? "above" : "below";
1441
+ setPermalinkNotice({
1442
+ message,
1443
+ left: Math.max(116, Math.min(window.innerWidth - 116, bounds.left + bounds.width / 2)),
1444
+ top: placement === "above" ? bounds.top : bounds.bottom,
1445
+ placement,
1446
+ status,
1447
+ });
1448
+ if (permalinkNoticeTimerRef.current !== null)
1449
+ window.clearTimeout(permalinkNoticeTimerRef.current);
1450
+ permalinkNoticeTimerRef.current = window.setTimeout(() => {
1451
+ setPermalinkNotice(null);
1452
+ permalinkNoticeTimerRef.current = null;
1453
+ }, status === "success" ? 1800 : 2400);
1454
+ }, []);
1455
+ const copyElementPermalink = useCallback((element) => {
1456
+ const citationElement = element.closest("[data-scv-citation-target]");
1457
+ const target = citationElement ? citationSelectionFromElement(citationElement) : null;
1458
+ if (!target)
266
1459
  return;
267
- requestAnimationFrame(() => { var _a; return (_a = window.document.querySelector(`[data-scv-text-unit="${CSS.escape(activeUnitId)}"]`)) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ block: "start" }); });
268
- }, [activeUnitId, scopeLoading, scopeRecords]);
269
- function changeMode(nextMode, preferredUnitId = null) {
1460
+ const targetDocument = documentFromUnitId(target.unitId);
1461
+ const linkMode = mode === "combined" || documentForMode(mode) === targetDocument ? mode : targetDocument === "ntc2018" ? "ntc" : "circ";
1462
+ const permalink = urlForViewerTarget(window.location.href, linkMode, defaultMode, target).href;
1463
+ setCitationSelection(target);
1464
+ void writeTextClipboard(permalink)
1465
+ .then(() => showPermalinkNotice(element, "Link copiato negli appunti", "success"))
1466
+ .catch(() => showPermalinkNotice(element, "Copia del link non disponibile", "error"));
1467
+ }, [defaultMode, mode, showPermalinkNotice]);
1468
+ const metadataForAnnotationTarget = useCallback((target) => {
1469
+ var _a, _b;
1470
+ const summary = summaryById.get(target.unitId);
1471
+ if (!summary)
1472
+ return null;
1473
+ let officialAssetNumber;
1474
+ if (target.kind === "asset") {
1475
+ for (const loaded of loadedChunks.values()) {
1476
+ const asset = (_b = (_a = loaded.assets.formulas[target.assetId]) !== null && _a !== void 0 ? _a : loaded.assets.tables[target.assetId]) !== null && _b !== void 0 ? _b : loaded.assets.figures[target.assetId];
1477
+ if (asset) {
1478
+ officialAssetNumber = asset.officialNumber;
1479
+ break;
1480
+ }
1481
+ }
1482
+ }
1483
+ return { documentId: summary.document, numbering: summary.numbering.official, title: summary.title,
1484
+ automaticLabel: automaticAnnotationLabel(target, summary.numbering.official, summary.title, officialAssetNumber) };
1485
+ }, [loadedChunks, summaryById]);
1486
+ const showAnnotationMenu = useCallback((target, point) => {
1487
+ const metadata = metadataForAnnotationTarget(target);
1488
+ if (!metadata || !annotationStore)
1489
+ return false;
1490
+ setCitationSelection(target);
1491
+ setAnnotationEditor(null);
1492
+ setAnnotationMenu({ target, metadata, x: point.x, y: point.y, bookmark: annotationsForTarget(annotations, target, "bookmark")[0] });
1493
+ return true;
1494
+ }, [annotationStore, annotations, metadataForAnnotationTarget]);
1495
+ const handleDocumentContextMenu = useCallback((event) => {
1496
+ const target = annotationTargetFromElement(event.target instanceof Element ? event.target : null);
1497
+ if (target && showAnnotationMenu(target, { x: event.clientX, y: event.clientY }))
1498
+ event.preventDefault();
1499
+ }, [showAnnotationMenu]);
1500
+ const handleDocumentKeyDown = useCallback((event) => {
270
1501
  var _a;
271
- const keepCurrent = activeSummary && (nextMode === "combined" ? activeSummary.document === "ntc2018" : activeSummary.document === documentForMode(nextMode));
272
- requestedIdRef.current = preferredUnitId !== null && preferredUnitId !== void 0 ? preferredUnitId : (keepCurrent ? (_a = activeSummary === null || activeSummary === void 0 ? void 0 : activeSummary.id) !== null && _a !== void 0 ? _a : null : null);
273
- setMode(nextMode);
274
- setIndex(null);
275
- setChunk(null);
276
- setScopeRecords([]);
277
- setRelatedByTarget(new Map());
278
- setSettingsOpen(false);
279
- }
280
- function selectSearchResult(result) {
281
- setQuery("");
282
- if (result.document === "ntc2018" && index) {
283
- const summary = index.units.find((unit) => unit.id === result.id);
284
- if (summary)
285
- selectUnit(summary);
1502
+ if (event.key !== "ContextMenu" && !(event.shiftKey && event.key === "F10"))
1503
+ return;
1504
+ const element = event.target instanceof Element ? event.target : null;
1505
+ const target = annotationTargetFromElement(element);
1506
+ const anchor = (_a = element === null || element === void 0 ? void 0 : element.closest("[data-scv-citation-target]")) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
1507
+ if (target && anchor && showAnnotationMenu(target, { x: anchor.left + Math.min(32, anchor.width / 2), y: anchor.top + Math.min(32, anchor.height / 2) }))
1508
+ event.preventDefault();
1509
+ }, [showAnnotationMenu]);
1510
+ const handleDocumentClick = useCallback((event) => {
1511
+ if (suppressAnnotationClickRef.current) {
1512
+ suppressAnnotationClickRef.current = false;
1513
+ event.preventDefault();
1514
+ event.stopPropagation();
286
1515
  return;
287
1516
  }
288
- const explicit = mode === "combined" ? relationTargets(result.id, relations)[0] : null;
289
- if (explicit && index) {
290
- const summary = index.units.find((unit) => unit.id === explicit.targetUnitId);
291
- if (summary)
292
- selectUnit(summary);
1517
+ const permalinkElement = event.target instanceof Element ? event.target.closest("[data-scv-copy-link]") : null;
1518
+ if (permalinkElement) {
1519
+ event.preventDefault();
1520
+ copyElementPermalink(permalinkElement);
293
1521
  return;
294
1522
  }
295
- if (mode === "combined")
296
- changeMode("circ", result.id);
297
- }
298
- function submitSearch(event) {
1523
+ const element = event.target instanceof Element ? event.target.closest(".scv-cross-reference") : null;
1524
+ if (element) {
1525
+ if (lastPointerTypeRef.current === "touch" && touchReferenceRef.current !== element) {
1526
+ event.preventDefault();
1527
+ touchReferenceRef.current = element;
1528
+ showReferencePreview(element, true);
1529
+ return;
1530
+ }
1531
+ activateReference(element);
1532
+ return;
1533
+ }
1534
+ const citationElement = event.target instanceof Element ? event.target.closest("[data-scv-citation-target]") : null;
1535
+ if (citationElement)
1536
+ setCitationSelection(citationSelectionFromElement(citationElement));
1537
+ }, [activateReference, copyElementPermalink, showReferencePreview]);
1538
+ const handleDocumentPointerDown = useCallback((event) => {
1539
+ var _a;
1540
+ lastPointerTypeRef.current = event.pointerType;
1541
+ (_a = longPressRef.current) === null || _a === void 0 ? void 0 : _a.session.cancel();
1542
+ longPressRef.current = null;
1543
+ if (event.pointerType !== "touch" || !event.isPrimary || event.button !== 0 || !annotationStore)
1544
+ return;
1545
+ const element = event.target instanceof Element ? event.target : null;
1546
+ if (element === null || element === void 0 ? void 0 : element.closest(".scv-cross-reference, button, a, input, textarea, select"))
1547
+ return;
1548
+ const target = annotationTargetFromElement(element);
1549
+ if (!target)
1550
+ return;
1551
+ const session = createLongPressSession((point) => {
1552
+ if (showAnnotationMenu(target, point))
1553
+ suppressAnnotationClickRef.current = true;
1554
+ longPressRef.current = null;
1555
+ });
1556
+ longPressRef.current = { pointerId: event.pointerId, session };
1557
+ session.start({ x: event.clientX, y: event.clientY });
1558
+ }, [annotationStore, showAnnotationMenu]);
1559
+ const handleDocumentPointerMove = useCallback((event) => {
1560
+ const active = longPressRef.current;
1561
+ if ((active === null || active === void 0 ? void 0 : active.pointerId) === event.pointerId)
1562
+ active.session.move({ x: event.clientX, y: event.clientY });
1563
+ }, []);
1564
+ const cancelDocumentLongPress = useCallback((event) => {
1565
+ const active = longPressRef.current;
1566
+ if ((active === null || active === void 0 ? void 0 : active.pointerId) !== event.pointerId)
1567
+ return;
1568
+ active.session.cancel();
1569
+ longPressRef.current = null;
1570
+ }, []);
1571
+ const handleDocumentPointerOver = useCallback((event) => {
1572
+ if (event.pointerType === "touch")
1573
+ return;
1574
+ const element = event.target instanceof Element ? event.target.closest(".scv-cross-reference") : null;
1575
+ if (element && !element.contains(event.relatedTarget))
1576
+ showReferencePreview(element);
1577
+ }, [showReferencePreview]);
1578
+ const handleDocumentPointerOut = useCallback((event) => {
1579
+ if (event.pointerType === "touch" || (referencePreview === null || referencePreview === void 0 ? void 0 : referencePreview.interactive))
1580
+ return;
1581
+ const element = event.target instanceof Element ? event.target.closest(".scv-cross-reference") : null;
1582
+ if (element && !element.contains(event.relatedTarget) && document.activeElement !== element)
1583
+ clearReferencePreview();
1584
+ }, [clearReferencePreview, referencePreview === null || referencePreview === void 0 ? void 0 : referencePreview.interactive]);
1585
+ const handleDocumentFocus = useCallback((event) => {
1586
+ const element = event.target instanceof Element ? event.target.closest(".scv-cross-reference") : null;
1587
+ if (element)
1588
+ showReferencePreview(element);
1589
+ }, [showReferencePreview]);
1590
+ const handleDocumentBlur = useCallback((event) => {
1591
+ if (event.target instanceof Element && event.target.matches(".scv-cross-reference") && !(referencePreview === null || referencePreview === void 0 ? void 0 : referencePreview.interactive))
1592
+ clearReferencePreview();
1593
+ }, [clearReferencePreview, referencePreview === null || referencePreview === void 0 ? void 0 : referencePreview.interactive]);
1594
+ const openPreviewReference = useCallback(() => {
1595
+ const element = touchReferenceRef.current;
1596
+ if (element)
1597
+ activateReference(element);
1598
+ }, [activateReference]);
1599
+ const selectScrollMarker = useCallback((unitId) => {
1600
+ const entry = navigationRef.current.entryById.get(unitId);
1601
+ if (entry)
1602
+ selectUnit(entry.summary);
1603
+ }, [selectUnit]);
1604
+ const activeSummary = (_f = (_d = (_c = lookup === null || lookup === void 0 ? void 0 : lookup.unitById.get(activeUnitId !== null && activeUnitId !== void 0 ? activeUnitId : "")) !== null && _c !== void 0 ? _c : circLookup === null || circLookup === void 0 ? void 0 : circLookup.unitById.get(activeUnitId !== null && activeUnitId !== void 0 ? activeUnitId : "")) !== null && _d !== void 0 ? _d : (_e = entryById.get(activeUnitId !== null && activeUnitId !== void 0 ? activeUnitId : "")) === null || _e === void 0 ? void 0 : _e.summary) !== null && _f !== void 0 ? _f : null;
1605
+ const contextSummary = activeSummary && baseNumbering(activeSummary.numbering.official).split(".").length > 3
1606
+ ? (_g = activeSummary.hierarchy.ancestorIds.map((id) => summaryById.get(id)).find((summary) => summary && baseNumbering(summary.numbering.official).split(".").length === 3)) !== null && _g !== void 0 ? _g : null
1607
+ : activeSummary;
1608
+ const chapters = useMemo(() => navigationEntries.filter(({ level }) => level === 0), [navigationEntries]);
1609
+ const paragraphs = useMemo(() => navigationEntries.filter(({ level }) => level === 1), [navigationEntries]);
1610
+ const subparagraphs = useMemo(() => navigationEntries.filter(({ level }) => level === 2), [navigationEntries]);
1611
+ const hierarchy = useMemo(() => [chapters, paragraphs, subparagraphs], [chapters, paragraphs, subparagraphs]);
1612
+ const activeLevelIds = useMemo(() => {
1613
+ if (!activeSummary)
1614
+ return [null, null, null];
1615
+ const activeBaseParts = baseNumbering(activeSummary.numbering.official).split(".");
1616
+ return [chapters, paragraphs, subparagraphs].map((levelEntries, level) => {
1617
+ var _a, _b;
1618
+ const baseNumber = activeBaseParts.slice(0, level + 1).join(".");
1619
+ return (_b = (_a = levelEntries.find((entry) => entry.baseNumber === baseNumber)) === null || _a === void 0 ? void 0 : _a.summary.id) !== null && _b !== void 0 ? _b : null;
1620
+ });
1621
+ }, [activeSummary, chapters, paragraphs, subparagraphs]);
1622
+ const scrubberEntries = useMemo(() => buildGlobalScrubberEntries(navigationEntries.map(({ summary, displayNumber, baseNumber, level }) => ({ id: summary.id, label: displayNumber, title: summary.title, baseNumber, level })), 2), [navigationEntries]);
1623
+ const scrubberActiveId = resolveGlobalScrubberActiveId(scrubberEntries, activeUnitId, activeSummary ? baseNumbering(activeSummary.numbering.official) : null);
1624
+ const annotationMarkers = useMemo(() => {
1625
+ const scrubberById = new Map(scrubberEntries.map((entry) => [entry.id, entry]));
1626
+ return annotations.flatMap((annotation) => {
1627
+ var _a;
1628
+ if (mode !== "combined" && annotation.documentId !== documentId)
1629
+ return [];
1630
+ let entry = scrubberById.get(annotation.target.unitId);
1631
+ const summary = summaryById.get(annotation.target.unitId);
1632
+ if (!entry && mode === "combined" && (summary === null || summary === void 0 ? void 0 : summary.document) === "circ2019") {
1633
+ const relatedId = (_a = relationTargets(summary.id, relations)[0]) === null || _a === void 0 ? void 0 : _a.targetUnitId;
1634
+ entry = relatedId ? scrubberById.get(relatedId) : undefined;
1635
+ if (!entry)
1636
+ entry = scrubberEntries.find((candidate) => candidate.baseNumber === baseNumbering(summary.numbering.official));
1637
+ }
1638
+ return entry ? [{ annotation, ratio: entry.ratio }] : [];
1639
+ });
1640
+ }, [annotations, documentId, mode, relations, scrubberEntries, summaryById]);
1641
+ const selectAnnotationMarker = useCallback((annotation) => {
1642
+ void navigateViewerTarget(annotation.target, "push").then(() => { if (annotation.type === "note")
1643
+ openAnnotation(annotation); }).catch(() => reportChunkLoadFailure());
1644
+ }, [navigateViewerTarget, openAnnotation, reportChunkLoadFailure]);
1645
+ const activeRecord = (_h = recordById.get(activeUnitId !== null && activeUnitId !== void 0 ? activeUnitId : "")) !== null && _h !== void 0 ? _h : null;
1646
+ const chunk = (_j = activeRecord === null || activeRecord === void 0 ? void 0 : activeRecord.chunk) !== null && _j !== void 0 ? _j : null;
1647
+ const activePages = activeRecord ? evidencePages(activeRecord.unit, activeRecord.chunk) : [];
1648
+ const pageBounds = activePages.length > 0 ? { from: Math.min(...activePages), to: Math.max(...activePages) } : { from: 1, to: 1 };
1649
+ const changeMode = useCallback((nextMode, preferredUnitId = null) => {
1650
+ var _a;
1651
+ if (nextMode === mode)
1652
+ return;
1653
+ navigationGenerationRef.current += 1;
1654
+ pendingScrollAnchorRef.current = null;
1655
+ setContentLoadNotice(null);
1656
+ const keepCurrent = activeSummary && (nextMode === "combined" || activeSummary.document === documentForMode(nextMode));
1657
+ requestedIdRef.current = preferredUnitId !== null && preferredUnitId !== void 0 ? preferredUnitId : (keepCurrent ? (_a = activeSummary === null || activeSummary === void 0 ? void 0 : activeSummary.id) !== null && _a !== void 0 ? _a : null : null);
1658
+ requestedTargetRef.current = requestedIdRef.current ? { kind: "unit", unitId: requestedIdRef.current } : null;
1659
+ initializedContextRef.current = "";
1660
+ setRequestedCircPaths(new Set());
1661
+ setCitationSelection(null);
1662
+ clearReferencePreview();
1663
+ setModeRevision((revision) => revision + 1);
1664
+ setMode(nextMode);
1665
+ }, [activeSummary, clearReferencePreview, mode]);
1666
+ const closeMobileIndex = useCallback(() => {
1667
+ setMobileIndexOpen(false);
1668
+ if (window.matchMedia("(max-width: 991.98px)").matches)
1669
+ window.requestAnimationFrame(() => { var _a; return (_a = mobileIndexButtonRef.current) === null || _a === void 0 ? void 0 : _a.focus(); });
1670
+ }, []);
1671
+ useEffect(() => {
1672
+ const onKeyDown = (event) => {
1673
+ var _a;
1674
+ const editing = (event.target instanceof HTMLElement && event.target.closest("input, textarea, select, [contenteditable='true']"))
1675
+ || ((_a = window.document.activeElement) === null || _a === void 0 ? void 0 : _a.closest("input, textarea, select, [contenteditable='true']"));
1676
+ if (event.key === "/" && !editing) {
1677
+ event.preventDefault();
1678
+ if (window.matchMedia("(max-width: 1399.98px)").matches)
1679
+ setAuxiliaryVisible(false);
1680
+ if (window.matchMedia("(max-width: 991.98px)").matches) {
1681
+ setMobileIndexSession((session) => session + 1);
1682
+ setMobileIndexOpen(true);
1683
+ }
1684
+ window.requestAnimationFrame(() => { var _a; return (_a = searchRef.current) === null || _a === void 0 ? void 0 : _a.focus(); });
1685
+ }
1686
+ if (event.key === "Escape") {
1687
+ if (referencePreview === null || referencePreview === void 0 ? void 0 : referencePreview.interactive)
1688
+ clearReferencePreview();
1689
+ else if (mobileIndexOpen)
1690
+ closeMobileIndex();
1691
+ }
1692
+ };
1693
+ window.addEventListener("keydown", onKeyDown);
1694
+ return () => window.removeEventListener("keydown", onKeyDown);
1695
+ }, [clearReferencePreview, closeMobileIndex, mobileIndexOpen, referencePreview === null || referencePreview === void 0 ? void 0 : referencePreview.interactive]);
1696
+ const selectSearchResult = useCallback((result) => {
1697
+ const target = viewerTargetForSearchResult(result);
1698
+ void navigateViewerTarget(target, "push").catch(() => reportChunkLoadFailure());
1699
+ }, [navigateViewerTarget, reportChunkLoadFailure]);
1700
+ const submitSearch = useCallback((event) => {
299
1701
  event.preventDefault();
300
- if (searchResults[0])
301
- selectSearchResult(searchResults[0]);
302
- }
303
- const renderAuxiliary = !manifest
304
- ? null
305
- : typeof auxiliaryPanel === "function"
306
- ? auxiliaryPanel({ mode, documentId, manifest, chunk, pageBounds })
307
- : auxiliaryPanel;
308
- const analyticalUrl = analyticalHref ? (typeof analyticalHref === "function" ? analyticalHref(mode, activeUnitId) : analyticalHref) : null;
309
- const scopeNote = scope.mode === "chapter"
310
- ? `Continuità capitolo ${(_b = activeSummary === null || activeSummary === void 0 ? void 0 : activeSummary.numbering.official) !== null && _b !== void 0 ? _b : ""} · ${scope.summaries.length} unità · ${scopePaths.length} chunk`
311
- : scope.paragraphId
312
- ? `Continuità paragrafo ${(_d = (_c = index === null || index === void 0 ? void 0 : index.units.find((unit) => unit.id === scope.paragraphId)) === null || _c === void 0 ? void 0 : _c.numbering.official) !== null && _d !== void 0 ? _d : ""} · ${scope.summaries.length} unità · ${scopePaths.length} chunk`
313
- : `Capitolo ${(_e = activeSummary === null || activeSummary === void 0 ? void 0 : activeSummary.numbering.official) !== null && _e !== void 0 ? _e : ""} · continuità ridotta ai paragrafi per la dimensione del capitolo`;
314
- function renderRecord({ unit, chunk: unitChunk }) {
1702
+ if (search.results[0])
1703
+ selectSearchResult(search.results[0]);
1704
+ }, [search.results, selectSearchResult]);
1705
+ const primaryPathPositions = (_k = lookup === null || lookup === void 0 ? void 0 : lookup.chunkPaths.flatMap((path, position) => primaryRenderedPaths.has(path) ? [position] : [])) !== null && _k !== void 0 ? _k : [];
1706
+ const hasPrevious = primaryPathPositions.length > 0 && Math.min(...primaryPathPositions) > 0;
1707
+ const hasNext = Boolean(lookup && primaryPathPositions.length > 0 && Math.max(...primaryPathPositions) < lookup.chunkPaths.length - 1);
1708
+ const documentLoading = !index || documentRecords.length === 0;
1709
+ const auxiliaryUnit = (_l = (citationSelection ? summaryById.get(citationSelection.unitId) : null)) !== null && _l !== void 0 ? _l : activeSummary;
1710
+ const closeAuxiliary = useCallback(() => {
315
1711
  var _a;
316
- return _jsxs("section", { className: `scv-unit scv-unit-depth-${Math.min(depth(unit), 4)}`, "data-scv-text-unit": unit.id, children: [_jsxs("h2", { children: [_jsx("span", { children: unit.numbering.official }), unit.title] }), _jsx("div", { className: "scv-unit-blocks", children: unit.blocks.filter((block) => !isRepeatedUnitTitle(unit, block)).map((block) => _jsx("div", { className: `scv-block scv-block-${block.kind}`, children: _jsx(BlockContent, { block: block, assets: unitChunk.assets, assetsBaseUrl: assetsBaseUrl }) }, block.blockId)) }), mode === "combined" && ((_a = relatedByTarget.get(unit.id)) !== null && _a !== void 0 ? _a : []).map(({ edge, unit: relatedUnit, chunk: relatedChunk }) => _jsxs("section", { className: "scv-related-unit", "data-provenance": "Circolare 7/2019", children: [_jsx("header", { children: _jsxs("h3", { children: [_jsx("span", { children: relatedUnit.numbering.official }), relatedUnit.title] }) }), _jsx("div", { className: "scv-unit-blocks", children: relatedUnit.blocks.filter((block) => !isRepeatedUnitTitle(relatedUnit, block)).map((block) => _jsx("div", { className: `scv-block scv-block-${block.kind}`, children: _jsx(BlockContent, { block: block, assets: relatedChunk.assets, assetsBaseUrl: assetsBaseUrl }) }, block.blockId)) })] }, edge.relationId))] }, unit.id);
317
- }
1712
+ scrollRequestRef.current = requestedTargetRef.current;
1713
+ setAuxiliaryVisible(false);
1714
+ (_a = auxiliaryButtonRef.current) === null || _a === void 0 ? void 0 : _a.focus();
1715
+ }, []);
1716
+ const saveAnnotationEditor = useCallback(async (value) => {
1717
+ if (!annotationStore || !annotationEditor)
1718
+ return;
1719
+ setAnnotationBusy(true);
1720
+ setAnnotationError(null);
1721
+ try {
1722
+ const trimmed = value.trim();
1723
+ const saved = annotationEditor.annotation
1724
+ ? await annotationStore.updateAnnotation(annotationEditor.annotation.id, annotationEditor.type === "bookmark" ? { label: trimmed || annotationEditor.metadata.automaticLabel } : { text: trimmed })
1725
+ : await annotationStore.createAnnotation(Object.assign({ type: annotationEditor.type, target: annotationEditor.target, documentId: annotationEditor.metadata.documentId, numbering: annotationEditor.metadata.numbering, title: annotationEditor.metadata.title }, (annotationEditor.type === "bookmark" ? { label: trimmed || annotationEditor.metadata.automaticLabel } : { text: trimmed })));
1726
+ setAnnotations((current) => [...current.filter((annotation) => annotation.id !== saved.id && !(saved.type === "bookmark" && annotation.type === "bookmark" && annotationTargetKey(annotation.target) === annotationTargetKey(saved.target))), saved]);
1727
+ setAnnotationEditor(null);
1728
+ }
1729
+ catch (error) {
1730
+ setAnnotationError(error instanceof Error ? error.message : "Salvataggio non riuscito.");
1731
+ }
1732
+ finally {
1733
+ setAnnotationBusy(false);
1734
+ }
1735
+ }, [annotationEditor, annotationStore]);
1736
+ const removeAnnotation = useCallback(async (annotation) => {
1737
+ if (!annotationStore)
1738
+ return;
1739
+ setAnnotationBusy(true);
1740
+ setAnnotationError(null);
1741
+ try {
1742
+ await annotationStore.deleteAnnotation(annotation.id);
1743
+ setAnnotations((current) => current.filter((item) => item.id !== annotation.id));
1744
+ setAnnotationEditor((current) => { var _a; return ((_a = current === null || current === void 0 ? void 0 : current.annotation) === null || _a === void 0 ? void 0 : _a.id) === annotation.id ? null : current; });
1745
+ }
1746
+ catch (error) {
1747
+ setAnnotationError(error instanceof Error ? error.message : "Eliminazione non riuscita.");
1748
+ }
1749
+ finally {
1750
+ setAnnotationBusy(false);
1751
+ }
1752
+ }, [annotationStore]);
1753
+ const annotationController = useMemo(() => annotationStore ? {
1754
+ items: annotations, loading: annotationsLoading, error: annotationError, currentDocumentId: documentId, positionRanks: annotationRanks,
1755
+ navigate: (annotation) => { void navigateViewerTarget(annotation.target, "push").catch(() => reportChunkLoadFailure()); },
1756
+ edit: openAnnotation,
1757
+ remove: removeAnnotation,
1758
+ exportAnnotations: () => annotationStore.exportAnnotations(),
1759
+ importAnnotations: async (value) => { const result = await annotationStore.importAnnotations(value); setAnnotations(await annotationStore.listAnnotations()); setAnnotationError(null); return result; },
1760
+ } : undefined, [annotationError, annotationRanks, annotationStore, annotations, annotationsLoading, documentId, navigateViewerTarget, openAnnotation, removeAnnotation, reportChunkLoadFailure]);
1761
+ const renderAuxiliary = !manifest || !auxiliaryAvailable ? null : _jsx(AuxiliaryContent, { panel: auxiliaryPanel, context: { mode, documentId, manifest, chunk, pageBounds,
1762
+ currentUnit: auxiliaryUnit ? { documentId: auxiliaryUnit.document, unitId: auxiliaryUnit.id, numbering: auxiliaryUnit.numbering.official } : null,
1763
+ selectedTarget: (citationSelection === null || citationSelection === void 0 ? void 0 : citationSelection.unitId) === (auxiliaryUnit === null || auxiliaryUnit === void 0 ? void 0 : auxiliaryUnit.id) ? citationSelection : null,
1764
+ navigateTo: navigateViewerTarget,
1765
+ hrefForTarget: (target) => urlForViewerTarget(window.location.href, mode === "combined" || documentForMode(mode) === documentFromUnitId(target.unitId) ? mode : documentFromUnitId(target.unitId) === "ntc2018" ? "ntc" : "circ", defaultMode, target).href,
1766
+ close: closeAuxiliary,
1767
+ annotations: annotationController, } });
318
1768
  if (loadError)
319
1769
  return _jsxs("main", { className: "scv-fatal", children: [_jsx("strong", { children: "Il corpus non \u00E8 disponibile." }), _jsx("span", { children: "Rigenera gli artefatti del viewer e ricarica la pagina." })] });
320
- return _jsxs("div", { className: `scv-root ${auxiliaryVisible ? "scv-has-auxiliary" : ""} ${className !== null && className !== void 0 ? className : ""}`, children: [_jsxs("aside", { className: "scv-index-pane", "aria-label": "Indice gerarchico", children: [_jsxs("div", { className: "scv-search-toolbar", children: [_jsxs("form", { className: "scv-search-form", role: "search", onSubmit: submitSearch, children: [_jsx("span", { "aria-hidden": "true", children: "\u2315" }), _jsx("input", { ref: searchRef, type: "search", value: query, onChange: (event) => setQuery(event.target.value), placeholder: "Cerca nella normativa\u2026", "aria-label": "Cerca nella normativa" }), query && _jsx("button", { type: "button", className: "scv-clear-search", onClick: () => setQuery(""), "aria-label": "Cancella ricerca", children: "\u00D7" }), _jsx("kbd", { children: "/" })] }), _jsx("button", { ref: settingsButtonRef, type: "button", className: "scv-settings-button", onClick: () => setSettingsOpen(true), "aria-label": "Impostazioni consultazione", "aria-haspopup": "dialog", children: _jsx("span", { "aria-hidden": "true", children: "\u2699" }) }), deferredQuery.length >= 2 && _jsx("div", { className: "scv-search-results", role: "listbox", "aria-label": "Risultati ricerca", children: !searchIndex ? _jsx("p", { className: "scv-search-status", children: "Caricamento indice di ricerca\u2026" }) : searchResults.length === 0 ? _jsx("p", { className: "scv-search-status", children: "Nessun risultato nella modalit\u00E0 corrente." }) : searchResults.map((result) => _jsxs("button", { type: "button", role: "option", "aria-selected": false, className: "scv-search-result", onClick: () => selectSearchResult(result), children: [_jsxs("span", { children: [result.document === "ntc2018" ? "NTC 2018" : "Circolare 7/2019", " \u00B7 ", result.numbering] }), _jsx("strong", { children: result.title }), _jsx("small", { children: snippet(result.text || result.title) })] }, result.id)) })] }), _jsx("div", { className: "scv-index-grid", children: hierarchy.map((level, levelIndex) => _jsxs("section", { className: "scv-index-cell", children: [_jsxs("header", { children: [_jsx("span", { children: hierarchyLabels[levelIndex] }), _jsx("b", { children: level.length })] }), _jsx("div", { className: "scv-index-list", children: !index ? _jsx(LoadingRows, {}) : level.map((unit) => _jsxs("button", { type: "button", "data-index-unit": unit.id, className: activeUnitId === unit.id ? "active" : "", onClick: () => selectUnit(unit), title: `${unit.numbering.official} ${unit.title}`, "aria-current": activeUnitId === unit.id ? "page" : undefined, children: [_jsx("strong", { children: unit.numbering.official }), _jsx("span", { children: unit.title })] }, unit.id)) })] }, levelIndex)) })] }), _jsx("article", { className: "scv-text-pane", "aria-label": "Corpus JSON", children: scopeLoading || scope.summaries.length === 0 || scopeRecords.length === 0 ? _jsx(LoadingPanel, { label: "Caricamento della continuit\u00E0 normativa\u2026" }) : _jsxs("div", { className: "scv-text-flow", children: [_jsxs("p", { className: "scv-chunk-note", children: [scopeNote, " \u00B7 structural-codes ", scopeRecords[0].chunk.structuralCodesVersion] }), scopeRecords.map(renderRecord), _jsx("div", { className: "scv-end-note", children: "Fine della continuit\u00E0 caricata." })] }) }), auxiliaryVisible && renderAuxiliary && _jsx("aside", { className: "scv-auxiliary-pane", "aria-label": auxiliaryPanelLabel, children: renderAuxiliary }), settingsOpen && _jsx("div", { className: "scv-dialog-backdrop", role: "presentation", onMouseDown: (event) => { if (event.target === event.currentTarget)
321
- setSettingsOpen(false); }, children: _jsxs("section", { className: "scv-dialog", role: "dialog", "aria-modal": "true", "aria-labelledby": "scv-settings-title", children: [_jsxs("header", { children: [_jsx("h2", { id: "scv-settings-title", children: "Impostazioni consultazione" }), _jsx("button", { ref: dialogCloseRef, type: "button", onClick: () => setSettingsOpen(false), "aria-label": "Chiudi impostazioni", children: "\u00D7" })] }), _jsxs("fieldset", { children: [_jsx("legend", { children: "Visualizzazione" }), modeOptions.map((option) => _jsxs("label", { children: [_jsx("input", { type: "radio", name: "scv-mode", value: option.id, checked: mode === option.id, onChange: () => changeMode(option.id) }), option.label] }, option.id))] }), hasAuxiliary && _jsxs("label", { className: "scv-auxiliary-toggle", children: [_jsx("input", { type: "checkbox", checked: auxiliaryVisible, onChange: (event) => setAuxiliaryVisible(event.target.checked) }), "Mostra ", auxiliaryPanelLabel.toLowerCase()] }), analyticalUrl && _jsx("a", { className: "scv-analytical-link", href: analyticalUrl, children: "Apri viewer analitico" })] }) })] });
1770
+ return _jsxs("div", { className: `scv-root ${darkMode ? "scv-dark" : ""} ${auxiliaryAvailable ? "scv-auxiliary-available" : ""} ${auxiliaryVisible && auxiliaryAvailable ? "scv-has-auxiliary" : ""} ${mobileIndexOpen ? "scv-mobile-index-open" : ""} ${className !== null && className !== void 0 ? className : ""}`, "data-scv-mounted-chunks": primaryRenderedPaths.size, "data-scv-loaded-related-chunks": mode === "combined" ? requiredCombinedCircPaths.size : 0, "data-scv-search-index-requested": search.indexRequested, "data-scv-cross-reference-index-requested": Boolean(crossReferenceIndex), "data-scv-search-error": search.errorMessage, children: [_jsx(NavigationPane, { id: navigationId, mode: mode, onModeChange: changeMode, hierarchy: hierarchy, activeLevelIds: activeLevelIds, sidebarMode: mobileIndexOpen, sidebarSession: mobileIndexSession, indexReady: Boolean(index), query: query, onQueryChange: setQuery, searchReady: search.queryReady, searchStatus: search.status, searchSource: search.source, searchDurationMs: search.durationMs, searchResults: search.results, onSearchSubmit: submitSearch, onSearchResult: selectSearchResult, onSelectUnit: selectUnit, onRequestClose: closeMobileIndex, onHistoryBack: () => { saveHistoryScroll(); window.history.back(); }, onHistoryForward: () => { saveHistoryScroll(); window.history.forward(); }, canHistoryBack: historyPosition.index > 0, canHistoryForward: historyPosition.index < historyPosition.max, searchRef: searchRef, darkMode: Boolean(darkMode), onToggleTheme: () => setDarkMode((current) => !current) }), _jsxs("div", { className: "scv-text-pane-shell", children: [_jsxs("button", { ref: mobileIndexButtonRef, type: "button", className: "scv-mobile-index-toggle", "aria-controls": navigationId, "aria-expanded": mobileIndexOpen, onClick: () => {
1771
+ setMobileIndexSession((session) => session + 1);
1772
+ setMobileIndexOpen(true);
1773
+ window.requestAnimationFrame(() => { var _a; return (_a = searchRef.current) === null || _a === void 0 ? void 0 : _a.focus(); });
1774
+ }, children: [_jsx("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M4 6h16M4 12h16M4 18h16" }) }), _jsx("span", { className: "scv-visually-hidden", children: "Apri indice" })] }), contextSummary && _jsxs("div", { className: "scv-mobile-context", title: `${contextSummary.numbering.official} ${contextSummary.title}`, "aria-label": `Unità attiva: ${contextSummary.numbering.official} ${contextSummary.title}`, children: [_jsx("strong", { children: contextSummary.numbering.official }), _jsx("span", { children: contextSummary.title })] }), auxiliaryAvailable && _jsx("button", { type: "button", className: "scv-tools-toggle", ref: auxiliaryButtonRef, disabled: !manifest, "aria-controls": auxiliaryId, "aria-expanded": auxiliaryVisible, onClick: () => {
1775
+ if (auxiliaryVisible)
1776
+ closeAuxiliary();
1777
+ else {
1778
+ scrollRequestRef.current = requestedTargetRef.current;
1779
+ setMobileIndexOpen(false);
1780
+ setAuxiliaryVisible(true);
1781
+ window.requestAnimationFrame(() => {
1782
+ var _a, _b;
1783
+ (_b = (_a = auxiliaryPaneRef.current) === null || _a === void 0 ? void 0 : _a.querySelector("button")) === null || _b === void 0 ? void 0 : _b.focus({ preventScroll: true });
1784
+ });
1785
+ }
1786
+ }, "aria-label": `Apri ${auxiliaryPanelLabel}`, children: auxiliaryPanelButtonText }), contentLoadNotice && _jsx("p", { className: "scv-content-notice", role: "status", children: contentLoadNotice }), _jsx("article", { ref: textPaneRef, className: "scv-text-pane", "aria-label": "Corpus JSON", onClick: handleDocumentClick, onContextMenu: handleDocumentContextMenu, onKeyDown: handleDocumentKeyDown, onPointerDown: handleDocumentPointerDown, onPointerMove: handleDocumentPointerMove, onPointerUp: cancelDocumentLongPress, onPointerCancel: cancelDocumentLongPress, onPointerOver: handleDocumentPointerOver, onPointerOut: handleDocumentPointerOut, onFocus: handleDocumentFocus, onBlur: handleDocumentBlur, children: documentLoading || !index || !lookup ? _jsx(LoadingPanel, { label: "Caricamento del documento\u2026" }) : _jsx(DocumentContent, { records: renderRecords, relatedByTarget: relatedByTarget, mode: mode, assetsBaseUrl: assetsBaseUrl, documentLabel: documentId === "ntc2018" ? "NTC 2018" : "Circolare 7/2019", documentUnits: index.units.length, documentChunks: lookup.chunkPaths.length, hasPrevious: hasPrevious, hasNext: hasNext, notesByTarget: notesByTarget, onOpenAnnotation: openAnnotation }) }), _jsx(GlobalDocumentScrubber, { rootRef: textPaneRef, entries: scrubberEntries, activeId: scrubberActiveId, onSelect: selectScrollMarker, annotationMarkers: annotationMarkers, onAnnotationSelect: selectAnnotationMarker })] }), _jsx(ReferencePreview, { preview: referencePreview, onOpen: openPreviewReference, onClose: clearReferencePreview }), permalinkNotice && _jsx("span", { className: `scv-permalink-notice is-${permalinkNotice.placement} is-${permalinkNotice.status}`, style: { left: permalinkNotice.left, top: permalinkNotice.top }, role: "status", children: permalinkNotice.message }), annotationMenu && _jsx(AnnotationContextMenu, { state: annotationMenu, onClose: () => setAnnotationMenu(null), onBookmark: () => { setAnnotationEditor({ type: "bookmark", target: annotationMenu.target, metadata: annotationMenu.metadata, annotation: annotationMenu.bookmark, x: annotationMenu.x, y: annotationMenu.y }); setAnnotationMenu(null); }, onNote: () => { setAnnotationEditor({ type: "note", target: annotationMenu.target, metadata: annotationMenu.metadata, x: annotationMenu.x, y: annotationMenu.y }); setAnnotationMenu(null); } }), annotationEditor && _jsx(AnnotationEditor, { state: annotationEditor, busy: annotationBusy, error: annotationError, onSave: (value) => void saveAnnotationEditor(value), onDelete: annotationEditor.annotation ? () => void removeAnnotation(annotationEditor.annotation) : null, onClose: () => { setAnnotationEditor(null); setAnnotationError(null); } }), (auxiliaryVisible || auxiliaryPanelKeepMounted) && auxiliaryAvailable && renderAuxiliary && _jsx("aside", { ref: auxiliaryPaneRef, id: auxiliaryId, inert: !auxiliaryVisible ? true : undefined, "aria-hidden": !auxiliaryVisible, className: "scv-auxiliary-pane", "aria-label": auxiliaryPanelLabel, children: renderAuxiliary })] });
322
1787
  }
323
1788
  function LoadingRows() {
324
1789
  return _jsx("div", { className: "scv-loading-rows", "aria-label": "Caricamento", children: Array.from({ length: 7 }, (_, index) => _jsx("span", {}, index)) });