smartrte-react 0.3.5 → 1.0.0-beta.1

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 (141) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +106 -14
  3. package/dist/adapters/domChecklistCommandBridge.js +1 -1
  4. package/dist/adapters/domCommandBridge.d.ts +2 -2
  5. package/dist/adapters/domCommandBridge.js +2 -2
  6. package/dist/adapters/domInlineAtomCommandBridge.d.ts +10 -9
  7. package/dist/adapters/domInlineAtomCommandBridge.js +154 -50
  8. package/dist/adapters/domInlineImageCommandBridge.d.ts +7 -7
  9. package/dist/adapters/domInlineImageCommandBridge.js +6 -72
  10. package/dist/adapters/domMoveCommandBridge.js +1 -1
  11. package/dist/adapters/domSelectionBridge.d.ts +3 -3
  12. package/dist/adapters/domSmartDocument.d.ts +4 -4
  13. package/dist/adapters/domSmartDocument.js +2 -2
  14. package/dist/adapters/domTableCommandBridge.d.ts +42 -16
  15. package/dist/adapters/domTableCommandBridge.js +203 -173
  16. package/dist/adapters/internalFlags.d.ts +0 -8
  17. package/dist/adapters/internalFlags.js +0 -24
  18. package/dist/adapters/legacyListShadowComparator.d.ts +1 -1
  19. package/dist/adapters/legacyListShadowComparator.js +1 -1
  20. package/dist/adapters/pdfPrint.d.ts +8 -0
  21. package/dist/adapters/pdfPrint.js +22 -0
  22. package/dist/adapters/shadowMode.d.ts +3 -3
  23. package/dist/adapters/shadowMode.js +1 -1
  24. package/dist/canonicalEditorRuntime.d.ts +53 -1
  25. package/dist/canonicalEditorRuntime.js +61 -4
  26. package/dist/capabilityPresets.d.ts +27 -0
  27. package/dist/capabilityPresets.js +21 -0
  28. package/dist/collabTransport.d.ts +66 -0
  29. package/dist/collabTransport.js +1 -0
  30. package/dist/commentProvider.d.ts +24 -0
  31. package/dist/commentProvider.js +1 -0
  32. package/dist/components/CanonicalAuthorityEditor.d.ts +35 -1
  33. package/dist/components/CanonicalAuthorityEditor.js +1086 -112
  34. package/dist/components/ClassicEditorAuthority.d.ts +93 -22
  35. package/dist/components/ClassicEditorAuthority.js +11 -43
  36. package/dist/components/ColorPickerPopover.d.ts +39 -0
  37. package/dist/components/ColorPickerPopover.js +285 -0
  38. package/dist/components/CommentMarkers.d.ts +25 -0
  39. package/dist/components/CommentMarkers.js +105 -0
  40. package/dist/components/CommentThreadPanel.d.ts +26 -0
  41. package/dist/components/CommentThreadPanel.js +47 -0
  42. package/dist/components/ContextMenu.d.ts +23 -0
  43. package/dist/components/ContextMenu.js +141 -0
  44. package/dist/components/FormulaLibraryPopover.d.ts +16 -0
  45. package/dist/components/FormulaLibraryPopover.js +107 -0
  46. package/dist/components/LinkEditorPopover.d.ts +3 -1
  47. package/dist/components/LinkEditorPopover.js +22 -6
  48. package/dist/components/MediaManager.d.ts +4 -20
  49. package/dist/components/MediaManager.js +35 -6
  50. package/dist/components/MediaOverlay.d.ts +36 -0
  51. package/dist/components/MediaOverlay.js +163 -0
  52. package/dist/components/SpecialCharacterPopover.d.ts +18 -0
  53. package/dist/components/SpecialCharacterPopover.js +99 -0
  54. package/dist/components/StructuralSuggestionMarkers.d.ts +20 -0
  55. package/dist/components/StructuralSuggestionMarkers.js +56 -0
  56. package/dist/components/SuggestionPanel.d.ts +26 -0
  57. package/dist/components/SuggestionPanel.js +33 -0
  58. package/dist/components/TableBorderPopover.d.ts +59 -0
  59. package/dist/components/TableBorderPopover.js +147 -0
  60. package/dist/components/TableResizeHandles.d.ts +25 -0
  61. package/dist/components/TableResizeHandles.js +301 -0
  62. package/dist/components/TableSizePickerPopover.d.ts +16 -0
  63. package/dist/components/TableSizePickerPopover.js +126 -0
  64. package/dist/components/ToolbarPrimitives.d.ts +129 -0
  65. package/dist/components/ToolbarPrimitives.js +146 -0
  66. package/dist/components/VersionHistoryPanel.d.ts +9 -0
  67. package/dist/components/VersionHistoryPanel.js +157 -0
  68. package/dist/formulaLibrary.d.ts +31 -0
  69. package/dist/formulaLibrary.js +90 -0
  70. package/dist/index.d.ts +22 -24
  71. package/dist/index.js +14 -11
  72. package/dist/mediaManagerAdapter.d.ts +12 -0
  73. package/dist/mediaManagerAdapter.js +34 -0
  74. package/dist/mediaProvider.d.ts +21 -1
  75. package/dist/specialCharacters.d.ts +22 -0
  76. package/dist/specialCharacters.js +86 -0
  77. package/dist/standalone/classic-editor-embed.js +19 -12
  78. package/dist/suggestionProvider.d.ts +17 -0
  79. package/dist/suggestionProvider.js +1 -0
  80. package/dist/theme.d.ts +1 -1
  81. package/dist/theme.js +133 -1
  82. package/dist/versionProvider.d.ts +29 -0
  83. package/dist/versionProvider.js +1 -0
  84. package/package.json +28 -14
  85. package/dist/adapters/canonicalInlineCommandBridge.d.ts +0 -15
  86. package/dist/adapters/canonicalInlineCommandBridge.js +0 -269
  87. package/dist/adapters/canonicalListCommandBridge.d.ts +0 -26
  88. package/dist/adapters/canonicalListCommandBridge.js +0 -430
  89. package/dist/adapters/coreBold.d.ts +0 -5
  90. package/dist/adapters/coreBold.js +0 -3
  91. package/dist/adapters/documentFormats.d.ts +0 -26
  92. package/dist/adapters/documentFormats.js +0 -49
  93. package/dist/adapters/docxFormat.d.ts +0 -5
  94. package/dist/adapters/docxFormat.js +0 -272
  95. package/dist/adapters/domBlockCommandBridge.d.ts +0 -19
  96. package/dist/adapters/domBlockCommandBridge.js +0 -79
  97. package/dist/adapters/inlineMarkCoreExecution.d.ts +0 -9
  98. package/dist/adapters/inlineMarkCoreExecution.js +0 -25
  99. package/dist/adapters/pdfFormat.d.ts +0 -23
  100. package/dist/adapters/pdfFormat.js +0 -204
  101. package/dist/adapters/portableDocxAtoms.d.ts +0 -4
  102. package/dist/adapters/portableDocxAtoms.js +0 -58
  103. package/dist/adapters/styledDocxFormat.d.ts +0 -8
  104. package/dist/adapters/styledDocxFormat.js +0 -190
  105. package/dist/builtInFormatDefinitions.d.ts +0 -5
  106. package/dist/builtInFormatDefinitions.js +0 -82
  107. package/dist/canonicalAuthorityFlag.d.ts +0 -32
  108. package/dist/canonicalAuthorityFlag.js +0 -49
  109. package/dist/canonicalClipboardRuntime.d.ts +0 -15
  110. package/dist/canonicalClipboardRuntime.js +0 -76
  111. package/dist/components/ClassicEditor.d.ts +0 -76
  112. package/dist/components/ClassicEditor.js +0 -6729
  113. package/dist/editorController.d.ts +0 -78
  114. package/dist/editorController.js +0 -298
  115. package/dist/formatFidelity.d.ts +0 -14
  116. package/dist/formatFidelity.js +0 -107
  117. package/dist/formatRuntime.d.ts +0 -50
  118. package/dist/formatRuntime.js +0 -20
  119. package/dist/pluginRuntime.d.ts +0 -68
  120. package/dist/pluginRuntime.js +0 -78
  121. package/dist/standalone/editor.js +0 -1068
  122. package/dist/test-harness/atomShadowComparator.d.ts +0 -14
  123. package/dist/test-harness/atomShadowComparator.js +0 -63
  124. package/dist/test-harness/blockShadowComparator.d.ts +0 -30
  125. package/dist/test-harness/blockShadowComparator.js +0 -140
  126. package/dist/test-harness/clipboardShadowComparator.d.ts +0 -12
  127. package/dist/test-harness/clipboardShadowComparator.js +0 -46
  128. package/dist/test-harness/inlineShadowComparator.d.ts +0 -32
  129. package/dist/test-harness/inlineShadowComparator.js +0 -152
  130. package/dist/test-harness/legacyAtomEngine.d.ts +0 -10
  131. package/dist/test-harness/legacyAtomEngine.js +0 -15
  132. package/dist/test-harness/legacyBlockEngine.d.ts +0 -31
  133. package/dist/test-harness/legacyBlockEngine.js +0 -31
  134. package/dist/test-harness/legacyClipboardEngine.d.ts +0 -14
  135. package/dist/test-harness/legacyClipboardEngine.js +0 -67
  136. package/dist/test-harness/legacyInlineEngine.d.ts +0 -19
  137. package/dist/test-harness/legacyInlineEngine.js +0 -49
  138. package/dist/test-harness/legacyTableEngine.d.ts +0 -12
  139. package/dist/test-harness/legacyTableEngine.js +0 -25
  140. package/dist/test-harness/tableShadowComparator.d.ts +0 -29
  141. package/dist/test-harness/tableShadowComparator.js +0 -101
@@ -1,38 +1,109 @@
1
1
  import React from "react";
2
- import { type PersistedEditorDocument } from "smartrte-core/foundation";
3
- import { type CanonicalAuthorityContext } from "../canonicalAuthorityFlag.js";
4
- import type { SmartEditorChange, SmartEditorHandle } from "../canonicalEditorRuntime.js";
2
+ import type { SmartEditorHandle } from "../canonicalEditorRuntime.js";
5
3
  import type { MediaProvider } from "../mediaProvider.js";
6
4
  import type { MediaPickerComponent } from "./MediaPicker.js";
7
5
  import { type CanonicalAuthorityEditorProps } from "./CanonicalAuthorityEditor.js";
8
- import { type ClassicEditorProps as LegacyClassicEditorProps } from "./ClassicEditor.js";
9
- export type ClassicEditorProps = Omit<LegacyClassicEditorProps, "value" | "onChange"> & {
10
- /** Initial-only under canonical authority. Use replaceValue for external changes. */
11
- defaultValue?: string | PersistedEditorDocument;
12
- /** Legacy HTML value. Ignored after canonical construction. */
6
+ export type ClassicEditorProps = Omit<CanonicalAuthorityEditorProps, "onChange" | "onHtmlChange"> & {
7
+ /** Legacy HTML value. Superseded by defaultValue; retained for source compatibility. */
13
8
  value?: string;
14
- onChange?: ((change: SmartEditorChange) => void) | ((html: string) => void);
15
- onHtmlChange?: (html: string) => void;
16
- canonicalAuthority?: boolean;
17
- authorityContext?: CanonicalAuthorityContext;
9
+ /**
10
+ * Legacy-compat: always fires with a plain HTML string, matching every
11
+ * real pre-canonical caller's actual usage (e.g. Sootr's
12
+ * RichTextEditor.tsx: `onChange: (html: string) => void`, used directly
13
+ * as form state and passed to autosave/upload-on-save pipelines expecting
14
+ * a string). This used to be typed as a union also accepting the new
15
+ * canonical `SmartEditorChange` object, but the implementation only ever
16
+ * invoked it with that object regardless of which shape the caller's
17
+ * function actually expected - a real, live bug (a legacy caller
18
+ * expecting a string got an object with no type error), not just an
19
+ * imprecise type. Fixed by wiring this to the existing debounced HTML
20
+ * serialization (`onHtmlChange`, ~250ms after the last edit - see
21
+ * canonicalEditorRuntime.ts's `scheduleHtmlChange`) instead of the
22
+ * per-transaction `onChange`, since that's the mechanism already built
23
+ * for exactly this purpose. Callers who need the structured
24
+ * per-transaction event should use `CanonicalAuthorityEditorProps`
25
+ * directly, not this legacy-compat surface.
26
+ */
27
+ onChange?: (html: string) => void;
18
28
  mediaProvider?: MediaProvider;
19
29
  mediaPicker?: MediaPickerComponent;
20
30
  onRuntime?: CanonicalAuthorityEditorProps["onRuntime"];
31
+ /**
32
+ * `table={false}` selects the "simple" capability preset (see
33
+ * capabilityPresets.ts) unless `preset` is also explicitly given, in
34
+ * which case `preset` wins. This is the one legacy toggle real callers
35
+ * actually vary - Sootr's MCQ/Anomaly/PYEQ editors all pass
36
+ * `enableTable={false}` (see docs/SOOTR_MIGRATION_READINESS.md gap #3);
37
+ * nothing in this project's history has ever needed `media`/`formula` to
38
+ * vary independently of each other (both are owned by the single "atom"
39
+ * plugin today), so those two remain accepted-but-ignored, same as
40
+ * before.
41
+ */
42
+ table?: boolean;
43
+ media?: unknown;
44
+ formula?: unknown;
45
+ fonts?: unknown;
46
+ defaultFont?: unknown;
47
+ preserveFontFamily?: unknown;
48
+ preserveColors?: unknown;
49
+ preserveDocxStyles?: unknown;
50
+ theme?: unknown;
51
+ showFontSize?: unknown;
21
52
  };
22
53
  /**
23
- * Uncontrolled by default in canonical mode. The runtime flag supplies a
24
- * reversible legacy path during rollout; direct prop override has precedence.
54
+ * Canonical authority is unconditionally the only implementation as of
55
+ * Phase 8b closeout (2026-08-12) the DOM-authoritative legacy rollback
56
+ * path (LegacyClassicEditor, its four rollback bridges, and the
57
+ * canonicalAuthorityFlag rollback switch itself) was retired once the
58
+ * Gate 13/14 replay and production-surface gates passed. This wrapper
59
+ * remains the stable public import path and continues to accept (and
60
+ * ignore) legacy-only configuration props below so existing call sites
61
+ * keep compiling.
25
62
  */
26
- export declare const ClassicEditor: React.ForwardRefExoticComponent<Omit<LegacyClassicEditorProps, "value" | "onChange"> & {
27
- /** Initial-only under canonical authority. Use replaceValue for external changes. */
28
- defaultValue?: string | PersistedEditorDocument;
29
- /** Legacy HTML value. Ignored after canonical construction. */
63
+ export declare const ClassicEditor: React.ForwardRefExoticComponent<Omit<CanonicalAuthorityEditorProps, "onChange" | "onHtmlChange"> & {
64
+ /** Legacy HTML value. Superseded by defaultValue; retained for source compatibility. */
30
65
  value?: string;
31
- onChange?: ((change: SmartEditorChange) => void) | ((html: string) => void);
32
- onHtmlChange?: (html: string) => void;
33
- canonicalAuthority?: boolean;
34
- authorityContext?: CanonicalAuthorityContext;
66
+ /**
67
+ * Legacy-compat: always fires with a plain HTML string, matching every
68
+ * real pre-canonical caller's actual usage (e.g. Sootr's
69
+ * RichTextEditor.tsx: `onChange: (html: string) => void`, used directly
70
+ * as form state and passed to autosave/upload-on-save pipelines expecting
71
+ * a string). This used to be typed as a union also accepting the new
72
+ * canonical `SmartEditorChange` object, but the implementation only ever
73
+ * invoked it with that object regardless of which shape the caller's
74
+ * function actually expected - a real, live bug (a legacy caller
75
+ * expecting a string got an object with no type error), not just an
76
+ * imprecise type. Fixed by wiring this to the existing debounced HTML
77
+ * serialization (`onHtmlChange`, ~250ms after the last edit - see
78
+ * canonicalEditorRuntime.ts's `scheduleHtmlChange`) instead of the
79
+ * per-transaction `onChange`, since that's the mechanism already built
80
+ * for exactly this purpose. Callers who need the structured
81
+ * per-transaction event should use `CanonicalAuthorityEditorProps`
82
+ * directly, not this legacy-compat surface.
83
+ */
84
+ onChange?: (html: string) => void;
35
85
  mediaProvider?: MediaProvider;
36
86
  mediaPicker?: MediaPickerComponent;
37
87
  onRuntime?: CanonicalAuthorityEditorProps["onRuntime"];
88
+ /**
89
+ * `table={false}` selects the "simple" capability preset (see
90
+ * capabilityPresets.ts) unless `preset` is also explicitly given, in
91
+ * which case `preset` wins. This is the one legacy toggle real callers
92
+ * actually vary - Sootr's MCQ/Anomaly/PYEQ editors all pass
93
+ * `enableTable={false}` (see docs/SOOTR_MIGRATION_READINESS.md gap #3);
94
+ * nothing in this project's history has ever needed `media`/`formula` to
95
+ * vary independently of each other (both are owned by the single "atom"
96
+ * plugin today), so those two remain accepted-but-ignored, same as
97
+ * before.
98
+ */
99
+ table?: boolean;
100
+ media?: unknown;
101
+ formula?: unknown;
102
+ fonts?: unknown;
103
+ defaultFont?: unknown;
104
+ preserveFontFamily?: unknown;
105
+ preserveColors?: unknown;
106
+ preserveDocxStyles?: unknown;
107
+ theme?: unknown;
108
+ showFontSize?: unknown;
38
109
  } & React.RefAttributes<SmartEditorHandle>>;
@@ -1,49 +1,17 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { forwardRef, useRef, useSyncExternalStore } from "react";
3
- import { foundationSchema, parseCanonicalListHtml, serializeCanonicalListHtml } from "smartrte-core/foundation";
4
- import { canonicalAuthorityFlag } from "../canonicalAuthorityFlag.js";
2
+ import { forwardRef } from "react";
5
3
  import { CanonicalAuthorityEditor } from "./CanonicalAuthorityEditor.js";
6
- import { LegacyClassicEditor } from "./ClassicEditor.js";
7
4
  /**
8
- * Uncontrolled by default in canonical mode. The runtime flag supplies a
9
- * reversible legacy path during rollout; direct prop override has precedence.
5
+ * Canonical authority is unconditionally the only implementation as of
6
+ * Phase 8b closeout (2026-08-12) the DOM-authoritative legacy rollback
7
+ * path (LegacyClassicEditor, its four rollback bridges, and the
8
+ * canonicalAuthorityFlag rollback switch itself) was retired once the
9
+ * Gate 13/14 replay and production-surface gates passed. This wrapper
10
+ * remains the stable public import path and continues to accept (and
11
+ * ignore) legacy-only configuration props below so existing call sites
12
+ * keep compiling.
10
13
  */
11
14
  export const ClassicEditor = forwardRef(function ClassicEditor(props, ref) {
12
- useSyncExternalStore((listener) => canonicalAuthorityFlag.subscribe(listener), () => canonicalAuthorityFlag.getVersion(), () => canonicalAuthorityFlag.getVersion());
13
- const enabled = canonicalAuthorityFlag.enabled(props.authorityContext, props.canonicalAuthority);
14
- const latestEnvelope = useRef(typeof props.defaultValue === "object" ? props.defaultValue : undefined);
15
- const latestHtml = useRef(typeof props.defaultValue === "string" ? props.defaultValue : props.value);
16
- const runtime = useRef(null);
17
- if (!enabled) {
18
- if (runtime.current) {
19
- // Capture once at the authority boundary, not once per keystroke. This is
20
- // the ID-preserving rollback envelope; clean HTML remains the legacy view.
21
- latestEnvelope.current = runtime.current.getValue();
22
- latestHtml.current = serializeCanonicalListHtml(latestEnvelope.current.document, { clean: true });
23
- runtime.current = null;
24
- }
25
- const { defaultValue, onHtmlChange, canonicalAuthority: _canonical, authorityContext: _context, onRuntime: _runtime, mediaProvider: _provider, mediaPicker: _picker, ...legacy } = props;
26
- const value = latestEnvelope.current
27
- // Internal rollback HTML retains stable IDs. External callbacks receive
28
- // the clean representation below.
29
- ? serializeCanonicalListHtml(latestEnvelope.current.document, { clean: false })
30
- : latestHtml.current ?? (typeof props.value === "string" ? props.value : typeof defaultValue === "string" ? defaultValue : undefined);
31
- return _jsx(LegacyClassicEditor, { ...legacy, value: value, onChange: (html) => {
32
- const document = parseCanonicalListHtml(html);
33
- latestEnvelope.current = {
34
- schemaVersion: foundationSchema.version,
35
- revision: (latestEnvelope.current?.revision || 0) + 1,
36
- document,
37
- };
38
- const cleanHtml = serializeCanonicalListHtml(document, { clean: true });
39
- latestHtml.current = cleanHtml;
40
- onHtmlChange?.(cleanHtml);
41
- const callback = props.onChange;
42
- callback?.(cleanHtml);
43
- } });
44
- }
45
- const { value, canonicalAuthority: _canonical, authorityContext: _context, table: _table, media: _media, formula: _formula, features: _features, plugins: _plugins, formats: _formats, formatDefinitions: _definitions, mediaManager: _manager, fonts: _fonts, defaultFont: _font, preserveFontFamily: _preserveFont, preserveColors: _preserveColors, preserveDocxStyles: _preserveDocx, theme: _theme, showFontSize: _showFontSize, ...canonical } = props;
46
- return _jsx(CanonicalAuthorityEditor, { ...canonical, ref: ref, defaultValue: latestEnvelope.current ?? latestHtml.current ?? props.defaultValue ?? value, onChange: (change) => {
47
- props.onChange?.(change);
48
- }, onHtmlChange: (html) => { latestHtml.current = html; props.onHtmlChange?.(html); }, onRuntime: (value) => { runtime.current = value; props.onRuntime?.(value); } });
15
+ const { value, onChange, table, preset, media: _media, formula: _formula, fonts: _fonts, defaultFont: _font, preserveFontFamily: _preserveFont, preserveColors: _preserveColors, preserveDocxStyles: _preserveDocx, theme: _theme, showFontSize: _showFontSize, ...canonical } = props;
16
+ return _jsx(CanonicalAuthorityEditor, { ...canonical, ref: ref, defaultValue: props.defaultValue ?? (typeof value === "string" ? value : undefined), preset: preset ?? (table === false ? "simple" : "full"), onHtmlChange: onChange });
49
17
  });
@@ -0,0 +1,39 @@
1
+ export interface ColorPickerPopoverProps {
2
+ x: number;
3
+ y: number;
4
+ label: string;
5
+ initialValue?: string;
6
+ /**
7
+ * Continuous live-preview callback, fired on every drag frame of the
8
+ * saturation/value square or hue slider (and on every valid typed hex) -
9
+ * distinct from `onApply`, which fires once, when the popover is actually
10
+ * dismissed with a real change staged.
11
+ */
12
+ onPreview?: (hex: string) => void;
13
+ /** Up to 4 previously-committed colors for this same picker context, most-recent first. */
14
+ recentColors?: readonly string[];
15
+ onApply: (hex: string) => void;
16
+ /** Discard: revert to the color the popover opened with. The only way to *not* commit - every other dismissal (outside click, Escape, the close button) commits whatever is currently staged. */
17
+ onCancel: () => void;
18
+ }
19
+ /**
20
+ * Phase 11.5 §2.4 originally shipped a native `<input type="color">` as the
21
+ * primary picker (see git history) - reverted here in favor of a fully
22
+ * in-page saturation/value square + hue strip, specifically so the drag
23
+ * surface is visible on the very first click (a native color input needs a
24
+ * *second* click to open its own OS-level dialog) and so every drag frame
25
+ * is a real, page-owned pointer event this component fully controls -
26
+ * sidestepping the whole "which native event means the user is actually
27
+ * done" problem a native input can never answer reliably (see
28
+ * docs/bugs/color-popover-closes-on-first-native-picker-interaction.md and
29
+ * docs/bugs/color-preview-render-steals-focus-from-popover.md, both about
30
+ * that exact native-event ambiguity).
31
+ *
32
+ * Commit model: there is no separate Apply button. Dragging (or typing a
33
+ * valid hex, or clicking a recent swatch) stages a live preview via
34
+ * `onPreview`; closing the popover *any* way other than the explicit
35
+ * Discard button - outside click, Escape, the × button - commits whatever
36
+ * is currently staged via `onApply`. Discard is the only way back to the
37
+ * color the popover opened with.
38
+ */
39
+ export declare function ColorPickerPopover({ x, y, label, initialValue, recentColors, onPreview, onApply, onCancel }: ColorPickerPopoverProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,285 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
3
+ const HEX_PATTERN = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
4
+ const normalizeHex = (value) => {
5
+ if (!HEX_PATTERN.test(value))
6
+ return null;
7
+ if (value.length === 4)
8
+ return `#${value[1]}${value[1]}${value[2]}${value[2]}${value[3]}${value[3]}`.toLowerCase();
9
+ return value.toLowerCase();
10
+ };
11
+ const hsvToRgb = (h, s, v) => {
12
+ const sf = s / 100, vf = v / 100;
13
+ const c = vf * sf;
14
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
15
+ const m = vf - c;
16
+ const [r, g, b] = h < 60 ? [c, x, 0] : h < 120 ? [x, c, 0] : h < 180 ? [0, c, x] : h < 240 ? [0, x, c] : h < 300 ? [x, 0, c] : [c, 0, x];
17
+ return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
18
+ };
19
+ const rgbToHex = (r, g, b) => `#${[r, g, b].map((channel) => Math.max(0, Math.min(255, channel)).toString(16).padStart(2, "0")).join("")}`;
20
+ const hexToRgb = (hex) => {
21
+ const normalized = normalizeHex(hex);
22
+ if (!normalized)
23
+ return null;
24
+ const value = Number.parseInt(normalized.slice(1), 16);
25
+ return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
26
+ };
27
+ const rgbToHsv = (r, g, b) => {
28
+ const rf = r / 255, gf = g / 255, bf = b / 255;
29
+ const max = Math.max(rf, gf, bf), min = Math.min(rf, gf, bf), d = max - min;
30
+ let h = 0;
31
+ if (d !== 0) {
32
+ if (max === rf)
33
+ h = 60 * (((gf - bf) / d) % 6);
34
+ else if (max === gf)
35
+ h = 60 * ((bf - rf) / d + 2);
36
+ else
37
+ h = 60 * ((rf - gf) / d + 4);
38
+ }
39
+ if (h < 0)
40
+ h += 360;
41
+ return [h, max === 0 ? 0 : (d / max) * 100, max * 100];
42
+ };
43
+ const hexToHsv = (hex) => {
44
+ const rgb = hexToRgb(hex);
45
+ return rgb ? rgbToHsv(...rgb) : [0, 0, 0];
46
+ };
47
+ const buttonStyle = {
48
+ minHeight: 32,
49
+ padding: "0 12px",
50
+ border: "1px solid var(--srte-input-border)",
51
+ borderRadius: 8,
52
+ background: "var(--srte-input-bg)",
53
+ color: "var(--srte-menu-text)",
54
+ cursor: "pointer",
55
+ fontWeight: 500,
56
+ fontSize: 13,
57
+ };
58
+ const inputStyle = {
59
+ width: "100%",
60
+ height: 32,
61
+ boxSizing: "border-box",
62
+ padding: "0 10px",
63
+ border: "1px solid var(--srte-input-border)",
64
+ borderRadius: 8,
65
+ outline: "none",
66
+ background: "var(--srte-input-bg)",
67
+ color: "var(--srte-input-text)",
68
+ font: "inherit",
69
+ };
70
+ /** Drives a square/slider from pointer drag - shared by the SV square and the hue strip below, differing only in how a client offset maps to a value. */
71
+ const useDrag = (onMove) => {
72
+ const ref = useRef(null);
73
+ const onPointerDown = (event) => {
74
+ const rect = event.currentTarget.getBoundingClientRect();
75
+ onMove(event.clientX, event.clientY, rect);
76
+ const move = (moveEvent) => onMove(moveEvent.clientX, moveEvent.clientY, rect);
77
+ const up = () => {
78
+ window.removeEventListener("pointermove", move);
79
+ window.removeEventListener("pointerup", up);
80
+ };
81
+ window.addEventListener("pointermove", move);
82
+ window.addEventListener("pointerup", up);
83
+ };
84
+ return { ref, onPointerDown };
85
+ };
86
+ /**
87
+ * Phase 11.5 §2.4 originally shipped a native `<input type="color">` as the
88
+ * primary picker (see git history) - reverted here in favor of a fully
89
+ * in-page saturation/value square + hue strip, specifically so the drag
90
+ * surface is visible on the very first click (a native color input needs a
91
+ * *second* click to open its own OS-level dialog) and so every drag frame
92
+ * is a real, page-owned pointer event this component fully controls -
93
+ * sidestepping the whole "which native event means the user is actually
94
+ * done" problem a native input can never answer reliably (see
95
+ * docs/bugs/color-popover-closes-on-first-native-picker-interaction.md and
96
+ * docs/bugs/color-preview-render-steals-focus-from-popover.md, both about
97
+ * that exact native-event ambiguity).
98
+ *
99
+ * Commit model: there is no separate Apply button. Dragging (or typing a
100
+ * valid hex, or clicking a recent swatch) stages a live preview via
101
+ * `onPreview`; closing the popover *any* way other than the explicit
102
+ * Discard button - outside click, Escape, the × button - commits whatever
103
+ * is currently staged via `onApply`. Discard is the only way back to the
104
+ * color the popover opened with.
105
+ */
106
+ export function ColorPickerPopover({ x, y, label, initialValue = "#000000", recentColors, onPreview, onApply, onCancel }) {
107
+ const [hsv, setHsv] = useState(() => hexToHsv(initialValue));
108
+ const hex = rgbToHex(...hsvToRgb(hsv[0], hsv[1], hsv[2]));
109
+ const [hexText, setHexText] = useState(hex);
110
+ const [error, setError] = useState("");
111
+ const rootRef = useRef(null);
112
+ const titleId = useId();
113
+ const errorId = useId();
114
+ const [placement, setPlacement] = useState(null);
115
+ // Nothing has actually changed until a drag frame, a valid typed hex, or a
116
+ // recent-swatch click stages one - closing without ever touching anything
117
+ // must be a true no-op, not commit an identical "new" color as a fresh
118
+ // history entry.
119
+ const stagedRef = useRef(false);
120
+ const hexRef = useRef(hex);
121
+ hexRef.current = hex;
122
+ const onApplyRef = useRef(onApply);
123
+ onApplyRef.current = onApply;
124
+ const onCancelRef = useRef(onCancel);
125
+ onCancelRef.current = onCancel;
126
+ useEffect(() => { setHexText(hex); }, [hex]);
127
+ useLayoutEffect(() => {
128
+ const el = rootRef.current;
129
+ if (!el)
130
+ return;
131
+ const margin = 8;
132
+ const { width, height } = el.getBoundingClientRect();
133
+ const viewportWidth = window.innerWidth;
134
+ const viewportHeight = window.innerHeight;
135
+ const overflowsRight = x + width > viewportWidth - margin;
136
+ const left = overflowsRight ? x - width : x;
137
+ const clampedLeft = Math.min(Math.max(margin, left), Math.max(margin, viewportWidth - width - margin));
138
+ const overflowsBottom = y + height > viewportHeight - margin;
139
+ const top = overflowsBottom ? y - height : y;
140
+ const clampedTop = Math.min(Math.max(margin, top), Math.max(margin, viewportHeight - margin));
141
+ setPlacement({ left: clampedLeft, top: clampedTop });
142
+ }, [x, y]);
143
+ const stage = (nextHsv) => {
144
+ stagedRef.current = true;
145
+ setHsv(nextHsv);
146
+ onPreview?.(rgbToHex(...hsvToRgb(nextHsv[0], nextHsv[1], nextHsv[2])));
147
+ };
148
+ const svDrag = useDrag((clientX, clientY, rect) => {
149
+ const s = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)) * 100;
150
+ const v = 100 - Math.max(0, Math.min(1, (clientY - rect.top) / rect.height)) * 100;
151
+ stage([hsv[0], s, v]);
152
+ });
153
+ const hueDrag = useDrag((clientX, _clientY, rect) => {
154
+ const h = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)) * 360;
155
+ stage([h, hsv[1], hsv[2]]);
156
+ });
157
+ const commitAndClose = () => {
158
+ if (stagedRef.current)
159
+ onApplyRef.current(hexRef.current);
160
+ else
161
+ onCancelRef.current();
162
+ };
163
+ useEffect(() => {
164
+ const dismissIfOutside = (event) => {
165
+ if (rootRef.current && !rootRef.current.contains(event.target))
166
+ commitAndClose();
167
+ };
168
+ window.addEventListener("pointerdown", dismissIfOutside, true);
169
+ window.addEventListener("mousedown", dismissIfOutside, true);
170
+ return () => {
171
+ window.removeEventListener("pointerdown", dismissIfOutside, true);
172
+ window.removeEventListener("mousedown", dismissIfOutside, true);
173
+ };
174
+ // eslint-disable-next-line react-hooks/exhaustive-deps
175
+ }, []);
176
+ useEffect(() => {
177
+ const onWindowKeyDown = (event) => {
178
+ if (event.key === "Escape")
179
+ commitAndClose();
180
+ };
181
+ window.addEventListener("keydown", onWindowKeyDown);
182
+ return () => window.removeEventListener("keydown", onWindowKeyDown);
183
+ // eslint-disable-next-line react-hooks/exhaustive-deps
184
+ }, []);
185
+ const applyRecentColor = (swatch) => {
186
+ const rgb = hexToRgb(swatch);
187
+ if (!rgb)
188
+ return;
189
+ stage(rgbToHsv(...rgb));
190
+ };
191
+ return (_jsxs("div", { ref: rootRef, "data-srte-color-popover": "true", role: "dialog", "aria-labelledby": titleId, style: {
192
+ position: "fixed",
193
+ left: placement?.left ?? x,
194
+ top: placement?.top ?? y,
195
+ visibility: placement ? "visible" : "hidden",
196
+ zIndex: 70,
197
+ width: 224,
198
+ boxSizing: "border-box",
199
+ background: "var(--srte-menu-bg)",
200
+ color: "var(--srte-menu-text)",
201
+ border: "1px solid var(--srte-border)",
202
+ borderRadius: 12,
203
+ boxShadow: "var(--srte-menu-shadow)",
204
+ padding: 14,
205
+ }, children: [_jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 10 }, children: [_jsx("div", { id: titleId, style: { fontWeight: 650 }, children: label }), _jsx("button", { type: "button", "aria-label": "Close color picker", title: "Close (keeps the current color)", onClick: commitAndClose, style: { ...buttonStyle, minWidth: 28, minHeight: 28, padding: 0, border: 0, background: "transparent", fontSize: 18 }, children: "\u00D7" })] }), _jsx("div", { ref: svDrag.ref, "data-srte-color-sv-square": "true", role: "slider", "aria-label": "Saturation and brightness", tabIndex: 0,
206
+ // Without this, clicking to drag focuses this div (it's a focusable
207
+ // slider for keyboard access), which collapses the editor's own
208
+ // native selection before the preview can apply a mark to it - the
209
+ // same focus-stealing class of bug fixed for LinkEditorPopover
210
+ // (docs/bugs/link-overlay-autofocus-steals-editor-focus-on-plain-click.md)
211
+ // and the toolbar dropdowns (docs/bugs/toolbar-dropdown-summary-steals-editor-focus.md).
212
+ onMouseDown: (event) => event.preventDefault(), onPointerDown: svDrag.onPointerDown, onKeyDown: (event) => {
213
+ const step = event.shiftKey ? 10 : 2;
214
+ if (event.key === "ArrowRight")
215
+ stage([hsv[0], Math.min(100, hsv[1] + step), hsv[2]]);
216
+ else if (event.key === "ArrowLeft")
217
+ stage([hsv[0], Math.max(0, hsv[1] - step), hsv[2]]);
218
+ else if (event.key === "ArrowUp")
219
+ stage([hsv[0], hsv[1], Math.min(100, hsv[2] + step)]);
220
+ else if (event.key === "ArrowDown")
221
+ stage([hsv[0], hsv[1], Math.max(0, hsv[2] - step)]);
222
+ else
223
+ return;
224
+ event.preventDefault();
225
+ }, style: {
226
+ position: "relative",
227
+ width: "100%",
228
+ height: 130,
229
+ borderRadius: 8,
230
+ marginBottom: 10,
231
+ cursor: "crosshair",
232
+ touchAction: "none",
233
+ background: `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, hsl(${hsv[0]}, 100%, 50%))`,
234
+ }, children: _jsx("div", { "aria-hidden": "true", style: {
235
+ position: "absolute",
236
+ left: `${hsv[1]}%`, top: `${100 - hsv[2]}%`,
237
+ width: 14, height: 14, borderRadius: "50%",
238
+ border: "2px solid #fff", boxShadow: "0 0 0 1px rgba(0,0,0,0.4)",
239
+ transform: "translate(-50%, -50%)",
240
+ background: hex,
241
+ pointerEvents: "none",
242
+ } }) }), _jsx("div", { ref: hueDrag.ref, "data-srte-color-hue-slider": "true", role: "slider", "aria-label": "Hue", "aria-valuemin": 0, "aria-valuemax": 360, "aria-valuenow": Math.round(hsv[0]), tabIndex: 0, onMouseDown: (event) => event.preventDefault(), onPointerDown: hueDrag.onPointerDown, onKeyDown: (event) => {
243
+ const step = event.shiftKey ? 15 : 3;
244
+ if (event.key === "ArrowRight")
245
+ stage([Math.min(360, hsv[0] + step), hsv[1], hsv[2]]);
246
+ else if (event.key === "ArrowLeft")
247
+ stage([Math.max(0, hsv[0] - step), hsv[1], hsv[2]]);
248
+ else
249
+ return;
250
+ event.preventDefault();
251
+ }, style: {
252
+ position: "relative",
253
+ width: "100%",
254
+ height: 16,
255
+ borderRadius: 8,
256
+ marginBottom: 12,
257
+ cursor: "pointer",
258
+ touchAction: "none",
259
+ background: "linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00)",
260
+ }, children: _jsx("div", { "aria-hidden": "true", style: {
261
+ position: "absolute",
262
+ left: `${(hsv[0] / 360) * 100}%`, top: "50%",
263
+ width: 16, height: 16, borderRadius: "50%",
264
+ border: "2px solid #fff", boxShadow: "0 0 0 1px rgba(0,0,0,0.4)",
265
+ transform: "translate(-50%, -50%)",
266
+ background: `hsl(${hsv[0]}, 100%, 50%)`,
267
+ pointerEvents: "none",
268
+ } }) }), _jsxs("label", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: error ? 6 : 12 }, children: [_jsx("div", { "aria-hidden": "true", style: { width: 32, height: 32, borderRadius: 8, border: "1px solid var(--srte-input-border)", background: hex, flexShrink: 0 } }), _jsx("input", { "data-srte-color-hex-input": "true", value: hexText, autoComplete: "off", autoCapitalize: "none", autoCorrect: "off", spellCheck: false, placeholder: "#336699", "aria-label": "Hex color", "aria-invalid": Boolean(error), "aria-describedby": error ? errorId : undefined, onChange: (event) => {
269
+ const value = event.target.value;
270
+ setHexText(value);
271
+ const normalized = normalizeHex(value);
272
+ if (normalized) {
273
+ setError("");
274
+ const rgb = hexToRgb(normalized);
275
+ stage(rgbToHsv(...rgb));
276
+ }
277
+ else if (value.length >= 4) {
278
+ setError("Enter a valid hex color, e.g. #336699.");
279
+ }
280
+ }, style: { ...inputStyle, borderColor: error ? "var(--srte-danger)" : "var(--srte-input-border)" } })] }), error && (_jsx("div", { id: errorId, "data-srte-color-error": "true", role: "alert", style: { color: "var(--srte-danger)", fontSize: 12, marginBottom: 10 }, children: error })), recentColors && recentColors.length > 0 && (_jsxs("div", { style: { marginBottom: 12 }, children: [_jsx("div", { style: { fontSize: 12, fontWeight: 600, marginBottom: 6 }, children: "Recently used" }), _jsx("div", { "data-srte-recent-colors": "true", style: { display: "flex", gap: 6 }, children: recentColors.slice(0, 4).map((swatch) => (_jsx("button", { type: "button", "data-srte-recent-color": swatch, "aria-label": `Recently used colour ${swatch}`, title: swatch, onClick: () => applyRecentColor(swatch), style: {
281
+ width: 24, height: 24, padding: 0, borderRadius: 6,
282
+ border: "1px solid var(--srte-input-border)",
283
+ background: swatch, cursor: "pointer",
284
+ } }, swatch))) })] })), _jsx("div", { style: { display: "flex", justifyContent: "flex-end" }, children: _jsx("button", { type: "button", "data-srte-color-discard": "true", onClick: onCancel, style: buttonStyle, children: "Discard" }) })] }));
285
+ }
@@ -0,0 +1,25 @@
1
+ import type { CommentThread, ModelDomMapping, PositionLookup } from "smartrte-core/foundation";
2
+ export interface CommentMarkersProps {
3
+ positions: PositionLookup;
4
+ mapping: ModelDomMapping;
5
+ containerElement: HTMLElement;
6
+ threads: readonly CommentThread[];
7
+ activeThreadId: string | null;
8
+ onSelectThread: (threadId: string) => void;
9
+ }
10
+ /**
11
+ * Live inline markers for comment threads, following `TableResizeHandles`'
12
+ * precedent (Phase 12a §1.2 finding): comments need many simultaneous
13
+ * persistent anchors, not the single-instance overlay pattern the other
14
+ * popovers use, so this measures real DOM rects from the live document and
15
+ * re-measures via ResizeObserver/MutationObserver/scroll, exactly like that
16
+ * component's column/row handles do for table boundaries.
17
+ *
18
+ * A thread's range is resolved fresh on every recompute via
19
+ * `resolveAnnotationRange` (not cached) - it is the authoritative "is this
20
+ * thread still anchored to real content" check, matching every other
21
+ * AnnotationRange consumer. A thread whose range no longer resolves (fully
22
+ * orphaned - the content it anchored to is gone) renders nothing here; it
23
+ * still exists in `threads` for a host's thread-list UI to surface.
24
+ */
25
+ export declare function CommentMarkers({ positions, mapping, containerElement, threads, activeThreadId, onSelectThread }: CommentMarkersProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,105 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useState } from "react";
3
+ import { resolveAnnotationRange } from "smartrte-core/foundation";
4
+ /**
5
+ * Live inline markers for comment threads, following `TableResizeHandles`'
6
+ * precedent (Phase 12a §1.2 finding): comments need many simultaneous
7
+ * persistent anchors, not the single-instance overlay pattern the other
8
+ * popovers use, so this measures real DOM rects from the live document and
9
+ * re-measures via ResizeObserver/MutationObserver/scroll, exactly like that
10
+ * component's column/row handles do for table boundaries.
11
+ *
12
+ * A thread's range is resolved fresh on every recompute via
13
+ * `resolveAnnotationRange` (not cached) - it is the authoritative "is this
14
+ * thread still anchored to real content" check, matching every other
15
+ * AnnotationRange consumer. A thread whose range no longer resolves (fully
16
+ * orphaned - the content it anchored to is gone) renders nothing here; it
17
+ * still exists in `threads` for a host's thread-list UI to surface.
18
+ */
19
+ export function CommentMarkers({ positions, mapping, containerElement, threads, activeThreadId, onSelectThread }) {
20
+ const [rects, setRects] = useState([]);
21
+ const [badges, setBadges] = useState([]);
22
+ const recompute = useCallback(() => {
23
+ if (!threads.length) {
24
+ setRects([]);
25
+ setBadges([]);
26
+ return;
27
+ }
28
+ const nextRects = [];
29
+ const nextBadges = [];
30
+ threads.forEach((thread) => {
31
+ const range = resolveAnnotationRange(thread.range, positions);
32
+ if (!range)
33
+ return;
34
+ const fromDom = mapping.posToDom(range.from);
35
+ const toDom = mapping.posToDom(range.to);
36
+ if (!fromDom || !toDom)
37
+ return;
38
+ const domRange = containerElement.ownerDocument.createRange();
39
+ try {
40
+ domRange.setStart(fromDom.node, fromDom.offset);
41
+ domRange.setEnd(toDom.node, toDom.offset);
42
+ }
43
+ catch {
44
+ return;
45
+ }
46
+ const clientRects = [...domRange.getClientRects()];
47
+ clientRects.forEach((rect) => {
48
+ nextRects.push({ threadId: thread.id, resolved: thread.resolved, left: rect.left, top: rect.top, width: rect.width, height: rect.height });
49
+ });
50
+ const anchor = clientRects[clientRects.length - 1] || domRange.getBoundingClientRect();
51
+ nextBadges.push({ threadId: thread.id, resolved: thread.resolved, replyCount: thread.replies.length, left: anchor.right, top: anchor.top });
52
+ });
53
+ setRects(nextRects);
54
+ setBadges(nextBadges);
55
+ }, [positions, mapping, containerElement, threads]);
56
+ useEffect(() => {
57
+ recompute();
58
+ const observer = new ResizeObserver(recompute);
59
+ observer.observe(containerElement);
60
+ const mutationObserver = new MutationObserver(recompute);
61
+ mutationObserver.observe(containerElement, { childList: true, subtree: true, characterData: true });
62
+ const ownerWindow = containerElement.ownerDocument.defaultView;
63
+ ownerWindow?.addEventListener("scroll", recompute, true);
64
+ ownerWindow?.addEventListener("resize", recompute);
65
+ return () => {
66
+ observer.disconnect();
67
+ mutationObserver.disconnect();
68
+ ownerWindow?.removeEventListener("scroll", recompute, true);
69
+ ownerWindow?.removeEventListener("resize", recompute);
70
+ };
71
+ }, [containerElement, recompute]);
72
+ return (_jsxs("div", { "data-srte-comment-markers": "true", style: { position: "fixed", inset: 0, pointerEvents: "none", zIndex: 55 }, children: [rects.map((rect, index) => (_jsx("div", { "data-srte-comment-highlight": rect.threadId, onPointerDown: () => onSelectThread(rect.threadId), style: {
73
+ position: "fixed",
74
+ left: rect.left,
75
+ top: rect.top,
76
+ width: rect.width,
77
+ height: rect.height,
78
+ background: rect.resolved
79
+ ? "var(--srte-comment-highlight-resolved, rgba(148, 148, 148, 0.18))"
80
+ : activeThreadId === rect.threadId
81
+ ? "var(--srte-comment-highlight-active, rgba(245, 180, 0, 0.55))"
82
+ : "var(--srte-comment-highlight, rgba(245, 180, 0, 0.28))",
83
+ cursor: "pointer",
84
+ pointerEvents: "auto",
85
+ } }, `${rect.threadId}-${index}`))), badges.map((badge) => (_jsx("button", { type: "button", "aria-label": badge.resolved ? "Open resolved comment thread" : "Open comment thread", "data-srte-comment-badge": badge.threadId, onClick: () => onSelectThread(badge.threadId), style: {
86
+ position: "fixed",
87
+ left: badge.left + 4,
88
+ top: badge.top - 4,
89
+ minWidth: 16,
90
+ height: 16,
91
+ padding: "0 4px",
92
+ borderRadius: 8,
93
+ border: "1px solid var(--srte-border, #d0d0d0)",
94
+ background: badge.resolved
95
+ ? "var(--srte-comment-badge-resolved, #e3e3e3)"
96
+ : activeThreadId === badge.threadId
97
+ ? "var(--srte-primary)"
98
+ : "var(--srte-comment-badge, #f5b400)",
99
+ color: badge.resolved ? "#555" : "#1a1300",
100
+ fontSize: 10,
101
+ lineHeight: "14px",
102
+ cursor: "pointer",
103
+ pointerEvents: "auto",
104
+ }, children: badge.replyCount }, badge.threadId)))] }));
105
+ }