@terra-symposium/docs 0.0.0-stage → 0.1.17

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 (241) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +72 -2
  3. package/lib-dist/Docs.d.ts +22 -0
  4. package/lib-dist/Docs.d.ts.map +1 -0
  5. package/lib-dist/Docs.js +60 -0
  6. package/lib-dist/Docs.js.map +1 -0
  7. package/lib-dist/LocalDocsWorkspace.d.ts +30 -0
  8. package/lib-dist/LocalDocsWorkspace.d.ts.map +1 -0
  9. package/lib-dist/LocalDocsWorkspace.js +148 -0
  10. package/lib-dist/LocalDocsWorkspace.js.map +1 -0
  11. package/lib-dist/SiteLayout.d.ts +30 -0
  12. package/lib-dist/SiteLayout.d.ts.map +1 -0
  13. package/lib-dist/SiteLayout.js +34 -0
  14. package/lib-dist/SiteLayout.js.map +1 -0
  15. package/lib-dist/StaticWorkspace.d.ts +21 -0
  16. package/lib-dist/StaticWorkspace.d.ts.map +1 -0
  17. package/lib-dist/StaticWorkspace.js +179 -0
  18. package/lib-dist/StaticWorkspace.js.map +1 -0
  19. package/lib-dist/_Toolbar.d.ts +7 -0
  20. package/lib-dist/_Toolbar.d.ts.map +1 -0
  21. package/lib-dist/_Toolbar.js +85 -0
  22. package/lib-dist/_Toolbar.js.map +1 -0
  23. package/lib-dist/blocks.d.ts +23 -0
  24. package/lib-dist/blocks.d.ts.map +1 -0
  25. package/lib-dist/blocks.js +321 -0
  26. package/lib-dist/blocks.js.map +1 -0
  27. package/lib-dist/docs-catalog.d.ts +5 -0
  28. package/lib-dist/docs-catalog.d.ts.map +1 -0
  29. package/lib-dist/docs-catalog.js +38 -0
  30. package/lib-dist/docs-catalog.js.map +1 -0
  31. package/lib-dist/docs-style.css +31 -0
  32. package/lib-dist/fonts/Avara-Bold.woff2 +0 -0
  33. package/lib-dist/fonts/Avara-BoldItalic.woff2 +0 -0
  34. package/lib-dist/fonts/OFL-1.1.txt +93 -0
  35. package/lib-dist/fonts/README.md +5 -0
  36. package/lib-dist/host-runtime.d.ts +5 -0
  37. package/lib-dist/host-runtime.d.ts.map +1 -0
  38. package/lib-dist/host-runtime.js +21 -0
  39. package/lib-dist/host-runtime.js.map +1 -0
  40. package/lib-dist/host.d.ts +170 -0
  41. package/lib-dist/host.d.ts.map +1 -0
  42. package/lib-dist/host.js +2 -0
  43. package/lib-dist/host.js.map +1 -0
  44. package/lib-dist/index.d.ts +35 -0
  45. package/lib-dist/index.d.ts.map +1 -0
  46. package/lib-dist/index.js +28 -0
  47. package/lib-dist/index.js.map +1 -0
  48. package/lib-dist/markdown-model.d.ts +7 -0
  49. package/lib-dist/markdown-model.d.ts.map +1 -0
  50. package/lib-dist/markdown-model.js +181 -0
  51. package/lib-dist/markdown-model.js.map +1 -0
  52. package/lib-dist/markdown.d.ts +3 -0
  53. package/lib-dist/markdown.d.ts.map +1 -0
  54. package/lib-dist/markdown.js +83 -0
  55. package/lib-dist/markdown.js.map +1 -0
  56. package/lib-dist/navigation.d.ts +17 -0
  57. package/lib-dist/navigation.d.ts.map +1 -0
  58. package/lib-dist/navigation.js +9 -0
  59. package/lib-dist/navigation.js.map +1 -0
  60. package/lib-dist/schema.d.ts +135 -0
  61. package/lib-dist/schema.d.ts.map +1 -0
  62. package/lib-dist/schema.js +55 -0
  63. package/lib-dist/schema.js.map +1 -0
  64. package/lib-dist/style.css +1 -0
  65. package/lib-dist/terra/Workspace.d.ts +5 -0
  66. package/lib-dist/terra/Workspace.d.ts.map +1 -0
  67. package/lib-dist/terra/Workspace.js +9 -0
  68. package/lib-dist/terra/Workspace.js.map +1 -0
  69. package/lib-dist/terra/editor/TerraContentEditor.d.ts +10 -0
  70. package/lib-dist/terra/editor/TerraContentEditor.d.ts.map +1 -0
  71. package/lib-dist/terra/editor/TerraContentEditor.js +47 -0
  72. package/lib-dist/terra/editor/TerraContentEditor.js.map +1 -0
  73. package/lib-dist/terra/editor/_blocks.d.ts +71 -0
  74. package/lib-dist/terra/editor/_blocks.d.ts.map +1 -0
  75. package/lib-dist/terra/editor/_blocks.js +110 -0
  76. package/lib-dist/terra/editor/_blocks.js.map +1 -0
  77. package/lib-dist/terra/editor/docs/_Callout.d.ts +14 -0
  78. package/lib-dist/terra/editor/docs/_Callout.d.ts.map +1 -0
  79. package/lib-dist/terra/editor/docs/_Callout.js +41 -0
  80. package/lib-dist/terra/editor/docs/_Callout.js.map +1 -0
  81. package/lib-dist/terra/editor/docs/_CodeBlock.d.ts +39 -0
  82. package/lib-dist/terra/editor/docs/_CodeBlock.d.ts.map +1 -0
  83. package/lib-dist/terra/editor/docs/_CodeBlock.js +494 -0
  84. package/lib-dist/terra/editor/docs/_CodeBlock.js.map +1 -0
  85. package/lib-dist/terra/editor/docs/_DocsCode.d.ts +14 -0
  86. package/lib-dist/terra/editor/docs/_DocsCode.d.ts.map +1 -0
  87. package/lib-dist/terra/editor/docs/_DocsCode.js +95 -0
  88. package/lib-dist/terra/editor/docs/_DocsCode.js.map +1 -0
  89. package/lib-dist/terra/editor/docs/_DocsImage.d.ts +18 -0
  90. package/lib-dist/terra/editor/docs/_DocsImage.d.ts.map +1 -0
  91. package/lib-dist/terra/editor/docs/_DocsImage.js +62 -0
  92. package/lib-dist/terra/editor/docs/_DocsImage.js.map +1 -0
  93. package/lib-dist/terra/editor/docs/_Embed.d.ts +15 -0
  94. package/lib-dist/terra/editor/docs/_Embed.d.ts.map +1 -0
  95. package/lib-dist/terra/editor/docs/_Embed.js +27 -0
  96. package/lib-dist/terra/editor/docs/_Embed.js.map +1 -0
  97. package/lib-dist/terra/editor/docs/_Math.d.ts +31 -0
  98. package/lib-dist/terra/editor/docs/_Math.d.ts.map +1 -0
  99. package/lib-dist/terra/editor/docs/_Math.js +126 -0
  100. package/lib-dist/terra/editor/docs/_Math.js.map +1 -0
  101. package/lib-dist/terra/editor/docs/_Nodes.d.ts +15 -0
  102. package/lib-dist/terra/editor/docs/_Nodes.d.ts.map +1 -0
  103. package/lib-dist/terra/editor/docs/_Nodes.js +52 -0
  104. package/lib-dist/terra/editor/docs/_Nodes.js.map +1 -0
  105. package/lib-dist/terra/editor/docs/_Raw.d.ts +15 -0
  106. package/lib-dist/terra/editor/docs/_Raw.d.ts.map +1 -0
  107. package/lib-dist/terra/editor/docs/_Raw.js +26 -0
  108. package/lib-dist/terra/editor/docs/_Raw.js.map +1 -0
  109. package/lib-dist/terra/editor/docs/_Table.d.ts +12 -0
  110. package/lib-dist/terra/editor/docs/_Table.d.ts.map +1 -0
  111. package/lib-dist/terra/editor/docs/_Table.js +71 -0
  112. package/lib-dist/terra/editor/docs/_Table.js.map +1 -0
  113. package/lib-dist/terra/editor/docs/_Tabs.d.ts +14 -0
  114. package/lib-dist/terra/editor/docs/_Tabs.d.ts.map +1 -0
  115. package/lib-dist/terra/editor/docs/_Tabs.js +104 -0
  116. package/lib-dist/terra/editor/docs/_Tabs.js.map +1 -0
  117. package/lib-dist/terra/editor/docs/_TsDocs.d.ts +15 -0
  118. package/lib-dist/terra/editor/docs/_TsDocs.d.ts.map +1 -0
  119. package/lib-dist/terra/editor/docs/_TsDocs.js +81 -0
  120. package/lib-dist/terra/editor/docs/_TsDocs.js.map +1 -0
  121. package/lib-dist/terra/editor/docs/_math_inline.d.ts +14 -0
  122. package/lib-dist/terra/editor/docs/_math_inline.d.ts.map +1 -0
  123. package/lib-dist/terra/editor/docs/_math_inline.js +30 -0
  124. package/lib-dist/terra/editor/docs/_math_inline.js.map +1 -0
  125. package/lib-dist/terra/editor/docs/heading.d.ts +23 -0
  126. package/lib-dist/terra/editor/docs/heading.d.ts.map +1 -0
  127. package/lib-dist/terra/editor/docs/heading.js +60 -0
  128. package/lib-dist/terra/editor/docs/heading.js.map +1 -0
  129. package/lib-dist/terra/editor/docs/index.d.ts +13 -0
  130. package/lib-dist/terra/editor/docs/index.d.ts.map +1 -0
  131. package/lib-dist/terra/editor/docs/index.js +14 -0
  132. package/lib-dist/terra/editor/docs/index.js.map +1 -0
  133. package/lib-dist/terra/editor/types.d.ts +40 -0
  134. package/lib-dist/terra/editor/types.d.ts.map +1 -0
  135. package/lib-dist/terra/editor/types.js +11 -0
  136. package/lib-dist/terra/editor/types.js.map +1 -0
  137. package/lib-dist/terra/flow/Nodes.d.ts +78 -0
  138. package/lib-dist/terra/flow/Nodes.d.ts.map +1 -0
  139. package/lib-dist/terra/flow/Nodes.js +912 -0
  140. package/lib-dist/terra/flow/Nodes.js.map +1 -0
  141. package/lib-dist/terra/flow/_chrome.d.ts +267 -0
  142. package/lib-dist/terra/flow/_chrome.d.ts.map +1 -0
  143. package/lib-dist/terra/flow/_chrome.js +798 -0
  144. package/lib-dist/terra/flow/_chrome.js.map +1 -0
  145. package/lib-dist/terra/flow/_layout.d.ts +30 -0
  146. package/lib-dist/terra/flow/_layout.d.ts.map +1 -0
  147. package/lib-dist/terra/flow/_layout.js +53 -0
  148. package/lib-dist/terra/flow/_layout.js.map +1 -0
  149. package/lib-dist/terra/flow/menuContext.d.ts +13 -0
  150. package/lib-dist/terra/flow/menuContext.d.ts.map +1 -0
  151. package/lib-dist/terra/flow/menuContext.js +10 -0
  152. package/lib-dist/terra/flow/menuContext.js.map +1 -0
  153. package/lib-dist/terra/flow/powerSource.d.ts +16 -0
  154. package/lib-dist/terra/flow/powerSource.d.ts.map +1 -0
  155. package/lib-dist/terra/flow/powerSource.js +59 -0
  156. package/lib-dist/terra/flow/powerSource.js.map +1 -0
  157. package/lib-dist/terra/pages/docs/_attachments.d.ts +20 -0
  158. package/lib-dist/terra/pages/docs/_attachments.d.ts.map +1 -0
  159. package/lib-dist/terra/pages/docs/_attachments.js +904 -0
  160. package/lib-dist/terra/pages/docs/_attachments.js.map +1 -0
  161. package/lib-dist/terra/pages/docs/_blocks.d.ts +48 -0
  162. package/lib-dist/terra/pages/docs/_blocks.d.ts.map +1 -0
  163. package/lib-dist/terra/pages/docs/_blocks.js +192 -0
  164. package/lib-dist/terra/pages/docs/_blocks.js.map +1 -0
  165. package/lib-dist/terra/pages/docs/_custom.d.ts +5 -0
  166. package/lib-dist/terra/pages/docs/_custom.d.ts.map +1 -0
  167. package/lib-dist/terra/pages/docs/_custom.js +6 -0
  168. package/lib-dist/terra/pages/docs/_custom.js.map +1 -0
  169. package/lib-dist/terra/pages/docs/_editable.d.ts +61 -0
  170. package/lib-dist/terra/pages/docs/_editable.d.ts.map +1 -0
  171. package/lib-dist/terra/pages/docs/_editable.js +404 -0
  172. package/lib-dist/terra/pages/docs/_editable.js.map +1 -0
  173. package/lib-dist/terra/pages/docs/_export-dialog.d.ts +19 -0
  174. package/lib-dist/terra/pages/docs/_export-dialog.d.ts.map +1 -0
  175. package/lib-dist/terra/pages/docs/_export-dialog.js +78 -0
  176. package/lib-dist/terra/pages/docs/_export-dialog.js.map +1 -0
  177. package/lib-dist/terra/pages/docs/_helpers.d.ts +395 -0
  178. package/lib-dist/terra/pages/docs/_helpers.d.ts.map +1 -0
  179. package/lib-dist/terra/pages/docs/_helpers.js +911 -0
  180. package/lib-dist/terra/pages/docs/_helpers.js.map +1 -0
  181. package/lib-dist/terra/pages/docs/_icons.d.ts +20 -0
  182. package/lib-dist/terra/pages/docs/_icons.d.ts.map +1 -0
  183. package/lib-dist/terra/pages/docs/_icons.js +29 -0
  184. package/lib-dist/terra/pages/docs/_icons.js.map +1 -0
  185. package/lib-dist/terra/pages/docs/_main.d.ts +51 -0
  186. package/lib-dist/terra/pages/docs/_main.d.ts.map +1 -0
  187. package/lib-dist/terra/pages/docs/_main.js +1959 -0
  188. package/lib-dist/terra/pages/docs/_main.js.map +1 -0
  189. package/lib-dist/terra/pages/docs/_sidebar.d.ts +42 -0
  190. package/lib-dist/terra/pages/docs/_sidebar.d.ts.map +1 -0
  191. package/lib-dist/terra/pages/docs/_sidebar.js +2450 -0
  192. package/lib-dist/terra/pages/docs/_sidebar.js.map +1 -0
  193. package/lib-dist/terra/pages/docs/_toolbar.d.ts +23 -0
  194. package/lib-dist/terra/pages/docs/_toolbar.d.ts.map +1 -0
  195. package/lib-dist/terra/pages/docs/_toolbar.js +1539 -0
  196. package/lib-dist/terra/pages/docs/_toolbar.js.map +1 -0
  197. package/lib-dist/terra/pages/docs/_tree.d.ts +178 -0
  198. package/lib-dist/terra/pages/docs/_tree.d.ts.map +1 -0
  199. package/lib-dist/terra/pages/docs/_tree.js +1168 -0
  200. package/lib-dist/terra/pages/docs/_tree.js.map +1 -0
  201. package/lib-dist/terra/regex.mjs +398 -0
  202. package/lib-dist/terra/runtime/action-proxy.d.ts +5 -0
  203. package/lib-dist/terra/runtime/action-proxy.d.ts.map +1 -0
  204. package/lib-dist/terra/runtime/action-proxy.js +19 -0
  205. package/lib-dist/terra/runtime/action-proxy.js.map +1 -0
  206. package/lib-dist/terra/runtime/content.d.ts +150 -0
  207. package/lib-dist/terra/runtime/content.d.ts.map +1 -0
  208. package/lib-dist/terra/runtime/content.js +1171 -0
  209. package/lib-dist/terra/runtime/content.js.map +1 -0
  210. package/lib-dist/terra/runtime/leaf.d.ts +5 -0
  211. package/lib-dist/terra/runtime/leaf.d.ts.map +1 -0
  212. package/lib-dist/terra/runtime/leaf.js +13 -0
  213. package/lib-dist/terra/runtime/leaf.js.map +1 -0
  214. package/lib-dist/terra/runtime/nodes.d.ts +112 -0
  215. package/lib-dist/terra/runtime/nodes.d.ts.map +1 -0
  216. package/lib-dist/terra/runtime/nodes.js +534 -0
  217. package/lib-dist/terra/runtime/nodes.js.map +1 -0
  218. package/lib-dist/terra/runtime/runtime.d.ts +25 -0
  219. package/lib-dist/terra/runtime/runtime.d.ts.map +1 -0
  220. package/lib-dist/terra/runtime/runtime.js +50 -0
  221. package/lib-dist/terra/runtime/runtime.js.map +1 -0
  222. package/lib-dist/terra/runtime/shared.d.ts +35 -0
  223. package/lib-dist/terra/runtime/shared.d.ts.map +1 -0
  224. package/lib-dist/terra/runtime/shared.js +79 -0
  225. package/lib-dist/terra/runtime/shared.js.map +1 -0
  226. package/lib-dist/terra/runtime/types.d.ts +266 -0
  227. package/lib-dist/terra/runtime/types.d.ts.map +1 -0
  228. package/lib-dist/terra/runtime/types.js +18 -0
  229. package/lib-dist/terra/runtime/types.js.map +1 -0
  230. package/lib-dist/terra/runtime/ui.d.ts +31 -0
  231. package/lib-dist/terra/runtime/ui.d.ts.map +1 -0
  232. package/lib-dist/terra/runtime/ui.js +16 -0
  233. package/lib-dist/terra/runtime/ui.js.map +1 -0
  234. package/lib-dist/workspace.d.ts +78 -0
  235. package/lib-dist/workspace.d.ts.map +1 -0
  236. package/lib-dist/workspace.js +151 -0
  237. package/lib-dist/workspace.js.map +1 -0
  238. package/package.json +90 -3
  239. package/src/SiteLayout.astro +38 -0
  240. package/src/navigation.tsx +47 -0
  241. package/src/style.css +733 -0
@@ -0,0 +1,1539 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // @ts-nocheck -- source-preserving extraction; public host contracts are typed in src/host.ts.
3
+ /**
4
+ * astro/src/pages/docs/_toolbar.tsx
5
+ * Shared UI components, toolbars, and context menus for the visual MDX Editor.
6
+ * Folder boundary: editor chrome and commands; document state, block rendering, and navigation remain outside this module.
7
+ */
8
+ /*//////////////////////////////////////////////////
9
+ IMPORTS
10
+ //////////////////////////////////////////////////*/
11
+ // API
12
+ import { $attachmentFilter, clearTransition, codeViewOf, isDocBody, setTransition, toolbarFocusValue, useAtom } from './_helpers.js';
13
+ import { $createHeadingNode } from '@lexical/rich-text';
14
+ import { $createParagraphNode, $getNearestNodeFromDOMNode, $isElementNode } from 'lexical';
15
+ import { actions } from '../../runtime/action-proxy.js';
16
+ import { docsRootPath } from '../../runtime/shared.js';
17
+ import { cn, notify } from '../../runtime/runtime.js';
18
+ import { components as repoComponents } from './_custom.js';
19
+ import { dirname, toPascal } from '../../runtime/shared.js';
20
+ import { redo, redoDepth, undo, undoDepth } from '@codemirror/commands';
21
+ import { stringifyDocBody } from '../../runtime/content.js';
22
+ import { toast } from 'sonner';
23
+ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
24
+ import { getDocsHostAdapter } from '../../../host-runtime.js';
25
+ // COMPONENTS
26
+ import * as TerraQualityRegex from '../../regex.mjs';
27
+ import React from 'react';
28
+ import { TextTooltip, TooltipProvider } from '@terra-symposium/styling/ui/terra-original';
29
+ import { ArrowLeft as LeftArrowIcon, ArrowRight as RightArrowIcon, ArrowUpRight as PopOutIcon, ButtonCentered as FullWordIcon, CaretDown as CaretDownIcon, CharacterPatterns as ReplaceIcon, CharacterSentenceCase as CaseIcon, DataShare as ShareIcon, Image as ImageIcon, Link as LinkIcon, ListBulleted as BulletListIcon, ListNumbered as NumberListIcon, Paste as PasteIcon, Redo as RedoIcon, RepoSourceCode as ModuleIcon, Search as SearchIcon, Task as CheckListIcon, TextBold as BoldIcon, TextFont as TextFontIcon, TextItalic as ItalicIcon, TextStrikethrough as StrikethroughIcon, TextUnderline as UnderlineIcon, Undo as UndoIcon, } from '@carbon/icons-react';
30
+ import { Button as UiButton } from '@terra-symposium/styling/ui/terra-original';
31
+ import { ButtonGroup } from '@terra-symposium/styling/ui/terra-original';
32
+ import { CodeBlock } from '../../editor/docs/_CodeBlock.js';
33
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@terra-symposium/styling/ui/terra-original';
34
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '@terra-symposium/styling/ui/terra-original';
35
+ import { EditStateCtx } from './_helpers.js';
36
+ import { ErrorBoundary } from '../../runtime/ui.js';
37
+ import { Input } from '@terra-symposium/styling/ui/terra-original';
38
+ import { Label } from '@terra-symposium/styling/ui/terra-original';
39
+ import { Popover, PopoverContent, PopoverTrigger } from '@terra-symposium/styling/ui/terra-original';
40
+ import { SearchEmpty } from '@terra-symposium/styling/ui/terra-original';
41
+ import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue } from '@terra-symposium/styling/ui/terra-original';
42
+ import { Textarea } from '@terra-symposium/styling/ui/terra-original';
43
+ import { Toggle } from '@terra-symposium/styling/ui/terra-original';
44
+ import { ToggleGroup, ToggleGroupItem } from '@terra-symposium/styling/ui/terra-original';
45
+ // DATA
46
+ import { ATTCH_TYPES } from './_helpers.js';
47
+ import { DOCS_BLOCK_ICONS } from './_icons.js';
48
+ import { EMPTY_NODES } from '../../runtime/nodes.js';
49
+ /*//////////////////////////////////////////////////
50
+ CONFIG
51
+ //////////////////////////////////////////////////*/
52
+ const FIND_WORD = String.raw `[\p{L}\p{N}_]`;
53
+ const BLIND_REGEX = [/<[^>]+>/gu, /^[\t ]*(?:#{1,6}|>+|[-+*]|\d+\.)[\t ]*/gmu, /:::[^\n]*|===|\|\|\|/gu, /[`*_~[\]()]/gu, /\s+/gu];
54
+ const ICON_CLASS = 'size-4 text-primary-11';
55
+ const SECONDARY_TEXT_CLASS = 'text-xs text-primary-11';
56
+ const MENU_GAP_CLASS = 'gap-2';
57
+ const FIELD_GAP_CLASS = 'space-y-1.5';
58
+ const MENU_WIDTH_CLASS = 'w-56';
59
+ /*//////////////////////////////////////////////////
60
+ HELPERS
61
+ //////////////////////////////////////////////////*/
62
+ /**
63
+ * Handles code offset.
64
+ *
65
+ * @param content - Docs content containing the target block.
66
+ * @param node - Block node being transformed by the toolbar action.
67
+ * @param value - Replacement value supplied by the toolbar control.
68
+ */
69
+ const codeOffset = (content, node, val) => {
70
+ if (!content.contains(node))
71
+ return content.innerText.length;
72
+ const range = document.createRange();
73
+ range.selectNodeContents(content);
74
+ range.setEnd(node, val);
75
+ return range.toString().length;
76
+ };
77
+ /**
78
+ * Derives a clean, human-readable label from a raw filename or URL.
79
+ *
80
+ * @param {string} v - The raw filename or path string.
81
+ * @param {string} fallback - The default label if extraction fails.
82
+ *
83
+ * @return {string} The formatted, capitalized label.
84
+ */
85
+ /**
86
+ * Prevents the active selection range from being lost when clicking toolbar buttons.
87
+ *
88
+ * @param {MouseEvent<HTMLElement>} e - The triggered mouse event.
89
+ *
90
+ * @return Nothing; the browser's default mouse action is cancelled so toolbar commands retain the editor selection.
91
+ */
92
+ function preserveSel(e) {
93
+ e.preventDefault();
94
+ }
95
+ /**
96
+ * Constructs a Regular Expression used to power the Find & Replace feature.
97
+ *
98
+ * @param cfg - Search options controlling markup removal, case sensitivity, the query text, and whole-word matching.
99
+ * @param {boolean} cfg.blind - Whether to ignore markdown syntax.
100
+ * @param {boolean} cfg.case - Whether the search is case-sensitive.
101
+ * @param {string} cfg.find - The string sequence to search for.
102
+ * @param {boolean} cfg.full - Whether to match full words only.
103
+ *
104
+ * @return {RegExp | null} The compiled RegExp object or null if no search term exists.
105
+ */
106
+ /**
107
+ * Normalizes a saved document version into its structured body and preview representations.
108
+ *
109
+ * @param ver - Ver input supplied to parse ver body's operation.
110
+ *
111
+ * @return The parsed body, original JSON text, and Markdown preview; empty values are returned when no version is available or parsing fails.
112
+ */
113
+ function parseVerBody(ver) {
114
+ if (!ver)
115
+ return { body: [], json: '', markdown: '' };
116
+ const json = typeof ver.contentJson === 'string' ? ver.contentJson : '';
117
+ if (!json.trim())
118
+ return { body: [], json, markdown: '' };
119
+ try {
120
+ const parsed = JSON.parse(json);
121
+ const body = isDocBody(parsed) ? parsed : [];
122
+ return { body, json, markdown: stringifyDocBody(body) };
123
+ }
124
+ catch (_err) {
125
+ console.error('[Toolbar:versions:preview.parseVersionBody.fail]', {
126
+ savedAt: ver.savedAt,
127
+ title: ver.title,
128
+ message: _err instanceof Error ? _err.message : String(_err),
129
+ jsonHead: json.slice(0, 400),
130
+ jsonTail: json.slice(-400),
131
+ });
132
+ return { body: [], json, markdown: '' };
133
+ }
134
+ }
135
+ /**
136
+ * Detects long JSON-like text that should be treated as structured source rather than ordinary prose.
137
+ *
138
+ * @param val - Source text to check for JSON delimiters and a minimum payload length.
139
+ *
140
+ * @return `true` when the text has JSON-like boundaries and content; otherwise `false`.
141
+ */
142
+ function looksLikeJsonBlob(val) {
143
+ const v = val.trim();
144
+ // prettier-ignore
145
+ return ['{}', '[]'].some(p => v.startsWith(p[0]) && v.endsWith(p[1])) && v.length > 80;
146
+ }
147
+ /*//////////////////////////////////////////////////
148
+ COMPONENTS
149
+ //////////////////////////////////////////////////*/
150
+ const Button = React.forwardRef(function Button({ children: c, className: v, disabled: d, lbl: l, shortcut: s, onClick: o, onMouseDown: r, active: a, ...p }, x) {
151
+ void a;
152
+ return (_jsx(TextTooltip, { content: s ? `${l} (${s})` : l, side: 'top', align: 'center', delayDuration: 400, children: _jsx(UiButton, { ref: x, variant: 'ghost', size: 'xs', "aria-label": l, disabled: d, onClick: o, onMouseDown: r, className: cn('inline-flex h-6 !p-0.5 shadow-none', v), ...p, children: c }) }));
153
+ });
154
+ Button.displayName = 'Button';
155
+ /**
156
+ * Mode control for the document viewer, visual editor, and attachment browser.
157
+ *
158
+ * @param props - Current toolbar mode, mode-change callback, edit permission, optional disabled state, and control size.
159
+ * @param {ToolbarMode} props.mode - The active interface mode.
160
+ * @param {Function} props.onModeChange - Callback fired when a new mode is selected.
161
+ * @param {boolean} [props.disabled] - Locks the control.
162
+ * @param {boolean} props.editable - Validates if the user possesses edit warrants.
163
+ *
164
+ * @return {JSX.Element} The Select dropdown for mode switching.
165
+ */
166
+ function Mode({ mode, onModeChange, disabled, editable, size = 'xs', }) {
167
+ void editable;
168
+ return (_jsxs(Select, { size: size, disabled: disabled, value: mode, onValueChange: v => {
169
+ const n = (v || 'view');
170
+ if (mode === n)
171
+ return;
172
+ setTransition(`to-${n}`);
173
+ onModeChange(n);
174
+ }, children: [_jsx(SelectTrigger, { className: 'w-[150px]', "aria-label": 'Mode', children: _jsx(SelectValue, { placeholder: 'Mode' }) }), _jsx(SelectContent, { align: 'start', children: (editable ? ['view', 'edit', 'attachments'] : ['view', 'attachments']).map(o => _jsx(SelectItem, { value: o, children: o[0].toUpperCase() + o.slice(1) }, o)) })] }));
175
+ }
176
+ /**
177
+ * Renders Docs-specific controls beside a content editor toolbar.
178
+ *
179
+ * Formatting and content insertion remain editor-owned; these controls manage
180
+ * the document shell's mode and persistence state.
181
+ *
182
+ * @param props - Docs toolbar state and current mode.
183
+ *
184
+ * @return The mode, history, save, or version actions supported by the active Docs editor toolbar.
185
+ */
186
+ const TOOLBAR_COMPONENTS = {
187
+ DocsToolbarActions({ activeMode: aMode, editable, size = 'xs', toolbar }) {
188
+ const edit = useContext(EditStateCtx);
189
+ const [versions, setVersions] = useState([]);
190
+ const [historyOpen, setHistoryOpen] = useState(false);
191
+ const [selectedVersionId, setSelectedVersionId] = useState('');
192
+ const ready = !!toolbar;
193
+ const selectedVersion = versions.find(v => v.savedAt === selectedVersionId);
194
+ const preview = useMemo(() => parseVerBody(selectedVersion), [selectedVersion]);
195
+ useEffect(() => {
196
+ let live = true;
197
+ if (!editable || !toolbar?.path || !historyOpen) {
198
+ setVersions([]);
199
+ return;
200
+ }
201
+ const path = toolbar.path;
202
+ const id = path.split('/').pop()?.replace(TerraQualityRegex.REGEX_MDX_I_REGEX(), '') || path;
203
+ void (async () => {
204
+ try {
205
+ const result = await actions.write.doc.versions.get({ path });
206
+ if (result.error)
207
+ throw result.error;
208
+ let data = result.data;
209
+ if (Array.isArray(data) && data.length === 0 && path !== id) {
210
+ const fallback = await actions.write.doc.versions.get({ path: id });
211
+ if (fallback.error)
212
+ throw fallback.error;
213
+ if (Array.isArray(fallback.data) && fallback.data.length > 0)
214
+ data = fallback.data;
215
+ }
216
+ if (live && Array.isArray(data))
217
+ setVersions(data);
218
+ }
219
+ catch (err) {
220
+ console.error('[Toolbar:versions] load-failed', { path, error: err });
221
+ toast.error(err instanceof Error ? err.message : 'Failed to load doc versions.');
222
+ }
223
+ })();
224
+ return () => {
225
+ live = false;
226
+ };
227
+ }, [editable, historyOpen, toolbar?.path]);
228
+ useEffect(() => {
229
+ if (historyOpen && !selectedVersionId && versions[0])
230
+ setSelectedVersionId(versions[0].savedAt);
231
+ }, [historyOpen, selectedVersionId, versions]);
232
+ const save = async (e) => {
233
+ e.preventDefault();
234
+ if (!toolbar?.save)
235
+ return;
236
+ try {
237
+ await toolbar.save();
238
+ }
239
+ catch (err) {
240
+ console.error('[Docs:save] failed', err);
241
+ toast.error(err instanceof Error ? err.message : 'Unable to save document.');
242
+ }
243
+ };
244
+ useEffect(() => {
245
+ if (aMode === 'view')
246
+ return;
247
+ const onKeyDown = (e) => {
248
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 's')
249
+ void save(e);
250
+ };
251
+ document.addEventListener('keydown', onKeyDown);
252
+ return () => {
253
+ document.removeEventListener('keydown', onKeyDown);
254
+ };
255
+ }, [aMode, toolbar]);
256
+ if (!editable)
257
+ return null;
258
+ return (_jsx(TooltipProvider, { children: _jsxs("div", { className: 'flex items-center gap-1', "data-docs-toolbar-actions": true, children: [_jsx(Mode, { editable: editable, mode: aMode, size: size, disabled: !ready, onModeChange: m => {
259
+ if (!toolbar)
260
+ return;
261
+ toolbar.setMode(m).catch(e => {
262
+ console.error('[Toolbar:mode] change-failed', { error: e, mode: m });
263
+ toast.error(e instanceof Error ? e.message : 'Failed to change mode.');
264
+ clearTransition();
265
+ });
266
+ } }), aMode !== 'view' && aMode !== 'attachments' && (_jsx(_Fragment, { children: _jsxs(ButtonGroup, { children: [_jsx(TextTooltip, { content: 'Save (\u2318S)', side: 'top', align: 'center', delayDuration: 400, children: _jsx(UiButton, { size: size, disabled: !ready || !(toolbar?.dirty ?? false), loading: (toolbar?.busy ?? null) === 'saving', onMouseDown: preserveSel, onClick: e => void save(e), children: "Save" }) }), _jsx(TextTooltip, { content: 'Version History', side: 'top', align: 'center', delayDuration: 400, children: _jsx(UiButton, { size: size, className: 'px-0.5', disabled: !ready, "aria-label": 'Version History', onMouseDown: preserveSel, onClick: () => {
267
+ if (versions.length > 0 && !selectedVersionId)
268
+ setSelectedVersionId(versions[0].savedAt);
269
+ setHistoryOpen(true);
270
+ }, children: _jsx(CaretDownIcon, {}) }) }), _jsx(Dialog, { open: historyOpen, onOpenChange: setHistoryOpen, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Version History" }), _jsx(DialogDescription, { className: 'sr-only', children: "Select a version to preview and restore." }), _jsxs(Select, { value: selectedVersionId, onValueChange: setSelectedVersionId, children: [_jsx(SelectTrigger, { className: 'h-14', "aria-label": 'Select a version', children: _jsx(SelectValue, { placeholder: 'Select a version' }) }), _jsx(SelectContent, { children: versions.map(v => (_jsx(SelectItem, { value: v.savedAt, children: _jsxs("span", { className: 'flex flex-col text-left', children: [_jsxs("span", { children: [v.title, " (", new Date(v.savedAt).toLocaleString(), ")"] }), _jsx("span", { className: SECONDARY_TEXT_CLASS, children: v.description })] }) }, v.savedAt))) })] })] }), _jsx("div", { className: 'min-h-0 flex-1 overflow-y-auto', children: _jsx(CodeBlock, { tone: 'view', language: 'mdx', docsSource: true, code: preview.markdown.trim() || ' ' }) }), _jsxs(DialogFooter, { children: [_jsx(UiButton, { variant: 'ghost', onClick: () => setHistoryOpen(false), children: "Cancel" }), _jsx(UiButton, { disabled: !selectedVersion, onClick: () => {
271
+ if (!edit || !selectedVersionId)
272
+ return;
273
+ edit.update?.({ content: preview.body, description: selectedVersion?.description || '', source: preview.markdown, title: selectedVersion?.title || '' });
274
+ (edit.setCode ?? edit.setSrc)?.(preview.markdown);
275
+ setHistoryOpen(false);
276
+ notify({ error: null, fallback: 'Restored version to local draft. Save to apply.' });
277
+ }, children: "Restore" })] })] }) })] }) }))] }) }));
278
+ },
279
+ /**
280
+ * Undo and Redo control group dynamically checking CodeMirror depth levels.
281
+ *
282
+ * @param props - Editor mouse handler, disabled state, and optional toolbar adapter used to inspect undo/redo depth.
283
+ * @param {Function} props.onMouseDown - Bound handler guarding focus.
284
+ * @param {boolean} [props.disabled] - Locks the controls.
285
+ * @param adapter - Adapter input supplied to undo's operation.
286
+ *
287
+ * @return {JSX.Element} The paired undo/redo button group.
288
+ */
289
+ Undo({ onMouseDown, disabled, adapter }) {
290
+ // STATES
291
+ const [state, setState] = useState({ redo: false, undo: false });
292
+ // INIT
293
+ useEffect(() => {
294
+ let frame = 0;
295
+ /**
296
+ * Handles sync.
297
+ */
298
+ const sync = () => {
299
+ const view = codeViewOf();
300
+ const n = view ? { redo: redoDepth(view.state) > 0, undo: undoDepth(view.state) > 0 } : { redo: false, undo: false };
301
+ setState(c => (c.undo === n.undo && c.redo === n.redo ? c : n));
302
+ };
303
+ sync();
304
+ /**
305
+ * Handles refresh.
306
+ */
307
+ const refresh = () => {
308
+ cancelAnimationFrame(frame);
309
+ frame = requestAnimationFrame(sync);
310
+ };
311
+ ['focusin', 'keyup', 'mouseup'].forEach(e => {
312
+ document.addEventListener(e, refresh, true);
313
+ });
314
+ return () => {
315
+ cancelAnimationFrame(frame);
316
+ ['focusin', 'keyup', 'mouseup'].forEach(e => {
317
+ document.removeEventListener(e, refresh, true);
318
+ });
319
+ };
320
+ }, []);
321
+ // RENDER
322
+ return (_jsx(ButtonGroup, { children: [
323
+ { lbl: 'Undo', canFire: adapter?.undoable ?? state.undo, action: adapter?.undo ?? undo, icon: UndoIcon, shortcut: '⌘Z' },
324
+ { lbl: 'Redo', canFire: adapter?.redoable ?? state.redo, action: adapter?.redo ?? redo, icon: RedoIcon, shortcut: '⌘Y' },
325
+ ].map(b => (_jsx(Button, { lbl: b.lbl, shortcut: b.shortcut, disabled: disabled || !b.canFire, onMouseDown: onMouseDown, onClick: () => {
326
+ if (adapter) {
327
+ if (b.lbl === 'Undo')
328
+ adapter.undo?.();
329
+ else
330
+ adapter.redo?.();
331
+ return;
332
+ }
333
+ const view = codeViewOf();
334
+ if (!view)
335
+ return;
336
+ b.action(view);
337
+ view.focus();
338
+ }, children: _jsx(b.icon, {}) }, b.lbl))) }));
339
+ },
340
+ /**
341
+ * Complex Popover enabling blind/case-sensitive searches and sequential replacement inside active viewports.
342
+ *
343
+ * @param props - Active editing mode and toolbar state used to find text and apply replacements in the current editor.
344
+ * @param {ToolbarMode} props.mode - The active interface mode determining search limits.
345
+ * @param {ToolbarState | null} props.toolbar - System toolbar state reference to execute replacements.
346
+ *
347
+ * @return {JSX.Element | null} The Find and Replace popover or null in view mode.
348
+ */
349
+ FindReplace({ mode, toolbar }) {
350
+ // STATES
351
+ const ref = useRef(null);
352
+ const [cfg, setCfg] = useState({ blind: false, case: false, find: '', full: false, open: false, replace: '' });
353
+ const [idx, setIdx] = useState(0);
354
+ const [content, setContent] = useState('');
355
+ // INIT
356
+ useEffect(() => {
357
+ if (!cfg.open || !toolbar)
358
+ return;
359
+ let a = true;
360
+ void toolbar.readSearchText().then(r => {
361
+ if (a)
362
+ setContent(r || '');
363
+ });
364
+ return () => {
365
+ a = false;
366
+ };
367
+ }, [cfg.open, cfg.find, toolbar]);
368
+ // PROCESS
369
+ const searchable = useMemo(() => {
370
+ return cfg.blind ? BLIND_REGEX.reduce((a, r) => a.replace(r, ''), content) : content;
371
+ }, [cfg.blind, content]);
372
+ const matcher = useMemo(() => {
373
+ if (!cfg.find)
374
+ return null;
375
+ const v = cfg.blind ? BLIND_REGEX.reduce((a, r) => a.replace(r, ''), cfg.find) : cfg.find;
376
+ if (!v)
377
+ return null;
378
+ const body = v.replace(TerraQualityRegex.REGEX_G_REGEX_9(), '\\$&');
379
+ return new RegExp(cfg.full ? `(?<!${FIND_WORD})${body}(?!${FIND_WORD})` : body, `${cfg.case ? 'g' : 'gi'}u`);
380
+ }, [cfg]);
381
+ const matches = useMemo(() => (matcher && searchable ? Array.from(searchable.matchAll(matcher), m => m.index ?? 0) : []), [matcher, searchable]);
382
+ const jsonBlocked = looksLikeJsonBlob(cfg.find) || looksLikeJsonBlob(cfg.replace);
383
+ // HELPERS
384
+ /**
385
+ * Handles focus find.
386
+ */
387
+ const focusFind = () => {
388
+ requestAnimationFrame(() => {
389
+ ref.current?.focus();
390
+ ref.current?.select();
391
+ });
392
+ };
393
+ /**
394
+ * Handles open sel.
395
+ */
396
+ const openSel = () => {
397
+ setCfg(((selectedText) => (prev) => ({ ...prev, find: selectedText || prev.find, open: true }))(window.getSelection()?.toString().replace(/\r\n?/gu, '\n') ?? ''));
398
+ focusFind();
399
+ };
400
+ /**
401
+ * Handles navigate.
402
+ *
403
+ * @param backward - Whether navigation should move toward the previous item.
404
+ */
405
+ const navigate = (backward = false) => {
406
+ const browser = window;
407
+ const finder = typeof browser.find === 'function' ? browser.find.bind(browser) : null;
408
+ if (cfg.find && !cfg.blind && matches.length)
409
+ setIdx(p => (p + (backward ? -1 : 1) + matches.length) % matches.length);
410
+ if (cfg.find && !cfg.blind && finder) {
411
+ finder(cfg.find, cfg.case, backward, true, false, true, cfg.full);
412
+ requestAnimationFrame(() => {
413
+ const sel = window.getSelection();
414
+ if (!sel || !sel.rangeCount)
415
+ return;
416
+ let rect = sel.getRangeAt(0).getBoundingClientRect();
417
+ if (rect.width === 0 && rect.height === 0) {
418
+ const codemirrorSel = document.querySelector('.docs-code-block .cm-selectionBackground, .docs-code-block .cm-search-match');
419
+ if (codemirrorSel)
420
+ rect = codemirrorSel.getBoundingClientRect();
421
+ }
422
+ if (rect.width === 0 && rect.height === 0)
423
+ return;
424
+ const toolbarEl = document.querySelector('[data-docs-toolbar-frame]');
425
+ const top = toolbarEl ? toolbarEl.getBoundingClientRect().bottom : 60;
426
+ if (rect.top < top + 40) {
427
+ window.scrollBy({ top: rect.top - top - 60, behavior: 'auto' });
428
+ }
429
+ else if (rect.bottom > window.innerHeight - 40) {
430
+ window.scrollBy({ top: rect.bottom - window.innerHeight + 60, behavior: 'auto' });
431
+ }
432
+ });
433
+ }
434
+ };
435
+ // INIT
436
+ useEffect(() => {
437
+ if (cfg.open)
438
+ focusFind();
439
+ }, [cfg.open]);
440
+ useEffect(() => {
441
+ setIdx(0);
442
+ }, [cfg.find]);
443
+ useEffect(() => {
444
+ if (!matches.length) {
445
+ setIdx(0);
446
+ return;
447
+ }
448
+ setIdx(p => Math.min(p, matches.length - 1));
449
+ }, [matches.length]);
450
+ useEffect(() => {
451
+ if (mode === 'view')
452
+ return;
453
+ /**
454
+ * Handles on key down.
455
+ *
456
+ * @param e - Pointer event received from the toolbar control.
457
+ */
458
+ const onKeyDown = (e) => {
459
+ if ((e.metaKey || e.ctrlKey) && e.key?.toLowerCase() === 'f') {
460
+ e.preventDefault();
461
+ openSel();
462
+ return;
463
+ }
464
+ if (cfg.open && e.key === 'Enter') {
465
+ e.preventDefault();
466
+ navigate(e.shiftKey);
467
+ }
468
+ };
469
+ document.addEventListener('keydown', onKeyDown, true);
470
+ return () => {
471
+ document.removeEventListener('keydown', onKeyDown, true);
472
+ };
473
+ }, [cfg, mode]);
474
+ // RENDER
475
+ // validation
476
+ if (mode === 'view')
477
+ return null;
478
+ // component
479
+ return (_jsxs(Popover, { open: cfg.open, onOpenChange: o => {
480
+ setCfg(p => ({ ...p, open: o }));
481
+ }, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(UiButton, { size: 'xs', variant: 'ghost', onMouseDown: e => {
482
+ e.preventDefault();
483
+ }, onClick: openSel, children: [_jsx(SearchIcon, {}), " Find"] }) }), _jsxs(PopoverContent, { align: 'end', className: 'w-80 space-y-2', children: [[
484
+ { title: 'Find', val: cfg.find, key: 'find', ref, placeholder: 'Search text' },
485
+ { title: 'Replace', val: cfg.replace, key: 'replace', placeholder: 'Replace' },
486
+ ].map(f => (_jsxs("div", { className: 'space-y-1', children: [_jsx(Label, { children: f.title }), _jsx(Textarea, { ref: 'ref' in f ? f.ref : undefined, value: f.val, onChange: e => {
487
+ const inputChange = { field: f.key, value: e.currentTarget?.value ?? '' };
488
+ setCfg(p => ({ ...p, [inputChange.field]: inputChange.value }));
489
+ }, placeholder: f.placeholder, rows: Math.max(2, f.val.split('\n').length), onKeyDown: e => {
490
+ if (e.key === 'Enter') {
491
+ e.preventDefault();
492
+ navigate(e.shiftKey);
493
+ }
494
+ } })] }, f.title))), _jsxs("div", { className: 'flex items-center justify-between text-xs text-primary-11', children: [_jsxs("span", { children: [" ", cfg.find && (matches.length ? `Result ${idx + 1}/${matches.length}` : 'No matches')] }), cfg.blind && _jsx("span", { children: "Text only" })] }), _jsxs("div", { className: 'flex flex-wrap gap-2', children: [[
495
+ { icon: _jsx(LeftArrowIcon, {}), backward: true, lbl: 'Previous match' },
496
+ { icon: _jsx(RightArrowIcon, {}), backward: false, lbl: 'Next match' },
497
+ ].map(b => (_jsx(UiButton, { size: 'xs', variant: 'tonal', disabled: !cfg.find || cfg.blind, onClick: () => {
498
+ navigate(b.backward);
499
+ }, "aria-label": b.lbl, title: b.lbl, children: b.icon }, String(b.backward)))), [
500
+ { key: 'case', title: 'Case sensitive', content: _jsx(CaseIcon, {}) },
501
+ { key: 'full', title: 'Full words', content: _jsx(FullWordIcon, {}) },
502
+ { key: 'blind', title: 'Markdown blind', content: 'Blind' },
503
+ ].map(t => (_jsx(Toggle, { size: 'xs', variant: 'tonal', "aria-label": t.title, title: t.title, pressed: cfg[t.key], onPressedChange: v => {
504
+ setCfg(p => ({ ...p, [t.key]: v }));
505
+ }, children: t.content }, t.key))), _jsx(UiButton, { size: 'xs', disabled: !matches.length || !toolbar || toolbar.busy === 'saving' || cfg.blind || jsonBlocked, onClick: () => {
506
+ if (jsonBlocked) {
507
+ toast.error('Paste markdown, not JSON, into find or replace.');
508
+ return;
509
+ }
510
+ if (matcher && toolbar)
511
+ toolbar.applySearchText(content.replace(matcher, cfg.replace));
512
+ }, "aria-label": 'Replace', title: 'Replace', children: _jsx(ReplaceIcon, {}) }), jsonBlocked && _jsx("span", { className: 'text-red-11 text-xs', children: "JSON input blocked" })] })] })] }));
513
+ },
514
+ /**
515
+ * Dropdown dialog facilitating attachment uploads or selecting document attachments natively bound to the leaf.
516
+ *
517
+ * @param props - Icon component, optional insertion callback, selection-preserving mouse handler, disabled state, and attachment context.
518
+ * @param {React.ElementType} props.icon - The base icon to display.
519
+ * @param {Function} [props.insert] - Callback mutating the AST text content natively.
520
+ * @param {Function} props.onMouseDown - Preserves focus bindings.
521
+ * @param {boolean} [props.disabled] - Sets interactability state.
522
+ * @param {string} [props.ctx] - Current physical parent path evaluating available local attachments.
523
+ *
524
+ * @return {JSX.Element} The rendered Uploader dialog button.
525
+ */
526
+ Uploader({ icon: Icon, insert, onMouseDown, disabled, ctx, }) {
527
+ // STATES
528
+ const [cfg, setCfg] = useState({ busy: false, open: false, attachment: '' });
529
+ const [atts, setAtts] = useState([]);
530
+ const [insertMode, setInsertMode] = useState('embed');
531
+ // EFFECTS
532
+ useEffect(() => {
533
+ let a = true;
534
+ if (cfg.open && ctx) {
535
+ actions.read.doc
536
+ .attachments({ path: ctx })
537
+ .then(({ data: d }) => {
538
+ // prettier-ignore
539
+ if (a && Array.isArray(d))
540
+ setAtts(d
541
+ .filter((a) => Boolean(a && typeof a === 'object' && typeof a.path === 'string'))
542
+ .map(a => ({
543
+ path: a.path,
544
+ name: typeof a.name === 'string' ? a.name : a.path.split('/').pop() || 'attachment',
545
+ })));
546
+ })
547
+ .catch(e => {
548
+ console.error('[Toolbar:attachments] load-failed', { path: ctx, error: e });
549
+ toast.error(e instanceof Error ? e.message : 'Failed to load attachments.');
550
+ });
551
+ }
552
+ return () => {
553
+ a = false;
554
+ };
555
+ }, [cfg.open, ctx]);
556
+ // HELPERS
557
+ /**
558
+ * Handles submit.
559
+ */
560
+ const uploaderActions = {
561
+ reset: useCallback(() => {
562
+ setCfg({ busy: false, open: false, attachment: '' });
563
+ setInsertMode('embed');
564
+ }, []),
565
+ submit: async () => {
566
+ if (!cfg.attachment || cfg.attachment === 'none' || !insert)
567
+ return;
568
+ setCfg(p => ({ ...p, busy: true }));
569
+ try {
570
+ const att = atts.find(a => a.path === cfg.attachment);
571
+ if (!att)
572
+ throw new Error('Attachment not found');
573
+ const lbl = att.name
574
+ .split('/')
575
+ .pop()
576
+ ?.split('.')[0]
577
+ ?.replace(TerraQualityRegex.REGEX_G_REGEX_7(), ' ')
578
+ .replace(TerraQualityRegex.REGEX_B_W_G_REGEX(), c => c.toUpperCase()) || 'attachment';
579
+ const c = (att.path.startsWith('http') ? att.path : att.name).replace(TerraQualityRegex.REGEX_G_REGEX(), '%20');
580
+ let fmt = '';
581
+ if (insertMode === 'link')
582
+ fmt = `[${lbl}](<${c}>)`;
583
+ else {
584
+ if (TerraQualityRegex.REGEX_PNG_JPE_G_GIF_WEBP_SVG_AVIF_I_REGEX_2().test(att.name))
585
+ fmt = `\n![${lbl}](<${c}>)\n`;
586
+ else
587
+ fmt = `\n<Embed src="${c}" title="${lbl}" />\n`;
588
+ }
589
+ insert(fmt);
590
+ uploaderActions.reset();
591
+ }
592
+ catch {
593
+ setCfg(p => ({ ...p, busy: false }));
594
+ }
595
+ },
596
+ };
597
+ // RENDER
598
+ return (_jsxs(Popover, { open: cfg.open, onOpenChange: o => {
599
+ if (!o)
600
+ uploaderActions.reset();
601
+ else
602
+ setCfg(p => ({ ...p, open: o }));
603
+ }, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(Button, { disabled: disabled, lbl: 'Insert attachment', onMouseDown: onMouseDown, children: _jsx(Icon, {}) }) }), _jsxs(PopoverContent, { align: 'start', className: 'w-80 space-y-4', children: [_jsxs("div", { className: FIELD_GAP_CLASS, children: [_jsx(Label, { className: SECONDARY_TEXT_CLASS, children: "Document Attachments" }), _jsxs(Select, { value: cfg.attachment, onValueChange: p => {
604
+ setCfg(v => ({ ...v, attachment: p }));
605
+ }, children: [_jsx(SelectTrigger, { "aria-label": 'Document Attachment', children: _jsx(SelectValue, { placeholder: 'Select an attachment' }) }), _jsx(SelectContent, { children: atts.length === 0 ? (_jsx(SelectItem, { value: 'none', disabled: true, children: "No attachments found" })) : (atts.map(a => (_jsx(SelectItem, { value: a.path, children: a.name }, a.path)))) })] })] }), _jsxs("div", { className: FIELD_GAP_CLASS, children: [_jsx(Label, { children: "Format" }), _jsx(ToggleGroup, { type: 'single', value: insertMode, onValueChange: v => v && setInsertMode(v), className: 'justify-start', children: [['embed', 'Visual Embed'], ['link', 'Text Link']].map(([v, l]) => (_jsx(ToggleGroupItem, { value: v, size: 'sm', className: 'h-7 px-3 text-xs', children: l }, v))) })] }), _jsx(UiButton, { className: 'w-full', disabled: !cfg.attachment || cfg.attachment === 'none' || cfg.busy, loading: cfg.busy, onClick: () => void uploaderActions.submit(), children: "Insert" })] })] }));
606
+ },
607
+ /**
608
+ * Recursive structural map converting component tree branches into native interactive dropdown triggers natively.
609
+ *
610
+ * @param props - Component name, its nested menu tree, and optional callback for copying a selected component into the editor.
611
+ * @param {string} props.name - Branch literal mapping component segment text explicitly.
612
+ * @param {MenuTree} props.tree - Context bounding branch constraints natively.
613
+ * @param {Function} [props.onInsert] - Passed insertion trigger executing AST injections cleanly safely.
614
+ * @param onCopy - Callback invoked for copy.
615
+ *
616
+ * @return {JSX.Element} React rendered menu node explicitly containing nested branches cleanly.
617
+ */
618
+ GlobalComponentMenuNode({ name, tree, onCopy }) {
619
+ return (_jsxs(DropdownMenuSub, { children: [_jsx(DropdownMenuSubTrigger, { className: 'capitalize', children: name.replace(TerraQualityRegex.REGEX_G_REGEX_7(), ' ') }), _jsxs(DropdownMenuSubContent, { children: [tree.components.map(c => (_jsx(DropdownMenuItem, { onMouseDown: preserveSel, onSelect: e => {
620
+ e.preventDefault();
621
+ onCopy?.(c);
622
+ }, children: c.displayLabel.replace(TerraQualityRegex.REGEX_G_REGEX_7(), ' ').replace(TerraQualityRegex.REGEX_B_W_G_REGEX(), c => c.toUpperCase()) }, `${c.path}:${c.name}`))), Object.entries(tree.folders).map(([f, s]) => (_jsx(TOOLBAR_COMPONENTS.GlobalComponentMenuNode, { name: f, tree: s, onCopy: onCopy }, f)))] })] }));
623
+ },
624
+ /**
625
+ * Complex unified insertion menu tracking local sister references alongside globally configured repo components.
626
+ *
627
+ * @param props - Mapping parameters defining the interaction boundaries.
628
+ * @param onInsert - Executes JS AST modifications natively by parsing safe import and JSX parameters.
629
+ * @param onInsertMd - Fallback routine injecting pure un-compiled Markdown strings directly into the editor state.
630
+ * @param onMouseDown - Guards boundary constraints reliably to protect CodeMirror/Lexical focus tracking.
631
+ * @param disabled - Sets the interaction scope natively tracking external loading locks cleanly.
632
+ * @param ctx - Physical source directory routing constraints properly evaluating sibling lists and local scope.
633
+ *
634
+ * @return The React JSX Element representing the fully interactive Dropdown and Dialog boundary.
635
+ *
636
+ * @remarks
637
+ * Acts as the primary interface for embedding both local media, external components, and other documents
638
+ * directly into the active MDX editor. It natively handles Sanity ID resolution and AST injection.
639
+ */
640
+ Custom({ onInsert, onInsertMd, onMouseDown, disabled, ctx, }) {
641
+ // STATES
642
+ const [sis, setSis] = useState([]);
643
+ const [search, setSearch] = useState({ open: false, query: '', tree: [], busy: false });
644
+ const savedSel = useRef(null);
645
+ const copyLock = useRef(null);
646
+ // 1. FETCH LOCAL FILES (Allows ALL leaves as valid attachments)
647
+ useEffect(() => {
648
+ if (!ctx)
649
+ return;
650
+ const dir = dirname(TerraQualityRegex.REGEX_INDEX_MD_MDX_I_REGEX_2().test(ctx.split('/').pop() || '') ? dirname(ctx) : ctx);
651
+ actions.read.doc
652
+ .list({ type: 'folderEntries', path: dir })
653
+ .then(({ data: d }) => {
654
+ // prettier-ignore
655
+ if (Array.isArray(d))
656
+ setSis(d.filter((p) => p !== ctx && dirname(p) === dir && /\.(md|mdx|astro|tsx)$/i.test(p.split('/').pop() || '')).map((p) => ({ path: p, name: p.split('/').pop() || '' })));
657
+ })
658
+ .catch(e => {
659
+ console.error('[Toolbar:insert.load-failed]', { path: dir, error: e });
660
+ toast.error(e instanceof Error ? e.message : 'Failed to load insert targets.');
661
+ });
662
+ }, [ctx]);
663
+ // 2. BUILD GLOBAL COMPONENT TREE
664
+ const componentTree = useMemo(() => {
665
+ const root = { components: [], folders: {} };
666
+ const counts = new Map();
667
+ repoComponents.forEach(c => counts.set(c.path, (counts.get(c.path) ?? 0) + 1));
668
+ repoComponents.forEach(c => {
669
+ const parts = c.path.split('/_custom/');
670
+ if (parts.length <= 1) {
671
+ root.components.push({ ...c, displayLabel: c.name });
672
+ return;
673
+ }
674
+ const segs = parts[1].split('/');
675
+ const file = segs.pop().replace(TerraQualityRegex.REGEX_PATTERN_REGEX(), '');
676
+ if (file !== 'index' && (counts.get(c.path) ?? 0) > 1)
677
+ segs.push(file);
678
+ let cur = root;
679
+ segs.forEach(s => {
680
+ cur = cur.folders[s] ??= { components: [], folders: {} };
681
+ });
682
+ // prettier-ignore
683
+ cur.components.push({ ...c, displayLabel: c.name });
684
+ });
685
+ return root;
686
+ }, []);
687
+ const copyComponent = useCallback(async (c) => {
688
+ if (onInsert) {
689
+ onInsert(`import ${c.name} from '${c.path}';`, c.clipboardValue ?? `<${c.name} />`);
690
+ return;
691
+ }
692
+ const text = c.clipboardValue ?? `<${c.name} />`;
693
+ const key = `${c.path}:${c.name}`;
694
+ if (copyLock.current === key)
695
+ return;
696
+ copyLock.current = key;
697
+ try {
698
+ try {
699
+ await navigator.clipboard.writeText(text);
700
+ }
701
+ catch {
702
+ const fallback = document.createElement('textarea');
703
+ fallback.value = text;
704
+ fallback.setAttribute('readonly', 'true');
705
+ fallback.style.position = 'fixed';
706
+ fallback.style.opacity = '0';
707
+ document.body.appendChild(fallback);
708
+ fallback.select();
709
+ if (!document.execCommand('copy')) {
710
+ fallback.remove();
711
+ throw new Error('Clipboard write failed');
712
+ }
713
+ fallback.remove();
714
+ }
715
+ window.setTimeout(() => toast.success('Copied to clipboard', { duration: 4000 }), 0);
716
+ }
717
+ catch (_err) {
718
+ toast.error('Failed to copy to clipboard');
719
+ }
720
+ finally {
721
+ window.setTimeout(() => {
722
+ if (copyLock.current === key)
723
+ copyLock.current = null;
724
+ }, 0);
725
+ }
726
+ }, [onInsert]);
727
+ // 3. FETCH FULL DOC TREE FOR SEARCH/BROWSE
728
+ useEffect(() => {
729
+ if (!search.open)
730
+ return;
731
+ let a = true;
732
+ if (search.tree.length === 0) {
733
+ setSearch(s => ({ ...s, busy: true }));
734
+ actions.read.doc
735
+ .list({ type: 'nav', pathname: docsRootPath() })
736
+ .then(({ data: d }) => {
737
+ if (a)
738
+ setSearch(s => ({ ...s, tree: Array.isArray(d) ? d.filter((n) => Boolean(n && typeof n === 'object' && 'type' in n)) : [], busy: false }));
739
+ })
740
+ .catch(_err => {
741
+ console.error(`[Toolbar:Embed] Failed to fetch document tree:`, _err);
742
+ if (a)
743
+ setSearch(s => ({ ...s, busy: false }));
744
+ });
745
+ }
746
+ return () => {
747
+ a = false;
748
+ };
749
+ }, [search.open]);
750
+ // 4. FILTER LEAVES ON QUERY (Returns all leaves when query is empty)
751
+ const filteredLeaves = useMemo(() => {
752
+ const leaves = [];
753
+ // Track seen paths to prevent duplicate React key warnings when the nav tree
754
+ // contains the same node referenced from multiple locations.
755
+ const seen = new Set();
756
+ const lowerQuery = search.query.trim().toLowerCase();
757
+ const searchFrames = [{ nodes: search.tree, index: 0, path: '' }];
758
+ while (searchFrames.length) {
759
+ const frame = searchFrames[searchFrames.length - 1];
760
+ if (!frame)
761
+ break;
762
+ if (frame.index >= frame.nodes.length) {
763
+ searchFrames.pop();
764
+ continue;
765
+ }
766
+ const n = frame.nodes[frame.index];
767
+ frame.index += 1;
768
+ const currTitle = n.title || n.origName || 'Untitled';
769
+ if (n.type === 'leaf') {
770
+ const nodePath = n.path;
771
+ if (typeof nodePath !== 'string' || seen.has(nodePath))
772
+ continue;
773
+ if (!lowerQuery ||
774
+ (typeof currTitle === 'string' ? currTitle.toLowerCase() : '').includes(lowerQuery) ||
775
+ (n.path && typeof n.path === 'string' ? n.path.toLowerCase() : '').includes(lowerQuery)) {
776
+ seen.add(nodePath);
777
+ leaves.push({ path: nodePath, title: currTitle, breadcrumb: frame.path });
778
+ }
779
+ }
780
+ else if (n.type === 'branch' && n.children) {
781
+ searchFrames.push({ nodes: n.children, index: 0, path: frame.path ? `${frame.path} / ${currTitle}` : currTitle });
782
+ }
783
+ }
784
+ return leaves;
785
+ }, [search.query, search.tree]);
786
+ // 5. EMBED OR CREATE LEAF
787
+ /**
788
+ * Handles embed.
789
+ *
790
+ * @param targetPath - Destination path for the moved or renamed document.
791
+ * @param isNew - Whether the destination represents a newly created document.
792
+ */
793
+ const embed = async (tgtPath, isNew = false) => {
794
+ let finalPath = tgtPath;
795
+ let leafId = tgtPath;
796
+ // 1. Close Modal immediately to prevent multiple submissions
797
+ setSearch(s => ({ ...s, open: false, busy: true }));
798
+ setTransition('p');
799
+ try {
800
+ if (isNew) {
801
+ // scope the new document natively UNDER the current document's path.
802
+ // this ensures string-based `verifyWrite` auth checks pass by matching the prefix of the inherited warrant.
803
+ const basePath = ctx ? ctx.replace(TerraQualityRegex.REGEX_PATTERN_REGEX_2(), '') : '';
804
+ const sName = search.query.trim() ? search.query.replace(TerraQualityRegex.REGEX_A_Z0_9_GI_REGEX_2(), '-').toLowerCase() : 'untitled';
805
+ const nId = basePath ? `${basePath}/${sName}-${Date.now()}.mdx` : `${sName}-${Date.now()}.mdx`;
806
+ finalPath = nId;
807
+ // create the page purely as a stand-alone document without anchoring to the group
808
+ const writeRes = await actions.write.doc.editor({ path: nId, title: search.query, source: 'This page is empty.' }).catch(_err => ({ data: null, error: _err }));
809
+ if (writeRes.error) {
810
+ console.error(`[Toolbar:Embed] write.doc.editor action failed:`, writeRes.error);
811
+ throw writeRes.error;
812
+ }
813
+ finalPath = writeRes.data?.path || finalPath;
814
+ }
815
+ // map leaf explicitly to the editor's attachment array unconditionally
816
+ if (ctx) {
817
+ const attRes = await actions.write.attachment.docRef({ path: ctx, refId: finalPath, title: search.query || 'Embedded Page' }).catch(_err => {
818
+ console.error(`[Toolbar:Embed] write.attachment.docRef action failed:`, _err);
819
+ return { data: null, error: _err };
820
+ });
821
+ if (attRes && attRes.data) {
822
+ // Resolves the exact Sanity UUID returned to map directly into the React AST parameter
823
+ const attachment = {
824
+ leafId: typeof attRes.data === 'object' && 'refId' in attRes.data && typeof attRes.data.refId === 'string' ? attRes.data.refId : leafId,
825
+ sister: { path: finalPath, name: finalPath.split('/').pop() || finalPath },
826
+ };
827
+ leafId = attachment.leafId;
828
+ setSis(p => {
829
+ if (!p.some(s => s.path === finalPath))
830
+ return [...p, attachment.sister];
831
+ return p;
832
+ });
833
+ }
834
+ }
835
+ // delay AST injection to allow Sanity CDN replica propagation to finish
836
+ if (isNew || ctx)
837
+ await new Promise(r => setTimeout(r, 1500));
838
+ clearTransition();
839
+ // 2. Yield to event loop to allow DOM reconciliation
840
+ await new Promise(r => setTimeout(r, 200));
841
+ // 3. Force focus back into the active editor securely
842
+ const aEditor = document.querySelector('[contenteditable="true"], .cm-content');
843
+ if (aEditor) {
844
+ aEditor.focus({ preventScroll: true });
845
+ const sel = window.getSelection();
846
+ if (sel && savedSel.current) {
847
+ sel.removeAllRanges();
848
+ sel.addRange(savedSel.current);
849
+ savedSel.current = null; // clear after use
850
+ }
851
+ else if (!sel || sel.rangeCount === 0) {
852
+ try {
853
+ const range = document.createRange();
854
+ range.selectNodeContents(aEditor);
855
+ range.collapse(false);
856
+ sel?.removeAllRanges();
857
+ sel?.addRange(range);
858
+ }
859
+ catch {
860
+ // Selection restoration is best effort.
861
+ }
862
+ }
863
+ }
864
+ // clean up busy state and search query now that injection is ready
865
+ setSearch(s => ({ ...s, query: '', busy: false }));
866
+ await new Promise(r => requestAnimationFrame(r));
867
+ // 4. Inject purely as AST text (bypassing the import statement injection)
868
+ // This mirrors how globally configured components like <Nodes /> and <DocsMath /> are inserted.
869
+ if (onInsertMd) {
870
+ try {
871
+ onInsertMd(`\n\n<Leaf id="${leafId}" />\n\n`);
872
+ }
873
+ catch (_err) {
874
+ console.error(`[Toolbar:Embed] Exception thrown during onInsertMd AST mutation:`, _err);
875
+ }
876
+ }
877
+ else
878
+ console.error(`[Toolbar:Embed] onInsertMd callback is MISSING. Aborting AST injection.`);
879
+ }
880
+ catch (_err) {
881
+ clearTransition();
882
+ console.error(`[Toolbar:Embed] Critical pipeline failure during leaf embed process:`, _err);
883
+ notify({ error: _err instanceof Error ? _err : new Error('Embed failed'), fallback: 'Failed to embed leaf.' });
884
+ setSearch(s => ({ ...s, query: '', busy: false }));
885
+ }
886
+ };
887
+ // 6. LOCAL MENUS
888
+ // prettier-ignore
889
+ const menuGroups = {
890
+ globalComponents: componentTree.components,
891
+ globalFolders: componentTree.folders,
892
+ local: [
893
+ {
894
+ label: 'Copy attachments',
895
+ icon: _jsx(PasteIcon, { className: ICON_CLASS }),
896
+ items: sis,
897
+ onSelect: (s) => actions.read.doc
898
+ .get({ path: s.path })
899
+ .then(({ data: d }) => onInsertMd?.(`\n${d?.source || ''}\n`))
900
+ .catch(e => {
901
+ console.error('[Toolbar:insert.partial-load-failed]', { path: s.path, error: e });
902
+ toast.error(e instanceof Error ? e.message : 'Failed to load partial.');
903
+ }),
904
+ },
905
+ {
906
+ label: 'Share attachment',
907
+ icon: _jsx(ShareIcon, { className: ICON_CLASS }),
908
+ items: sis.filter(s => TerraQualityRegex.REGEX_MD_MDX_I_REGEX().test(s.name)),
909
+ onSelect: (s) => {
910
+ const importName = toPascal(s.name.replace(TerraQualityRegex.REGEX_PATTERN_REGEX(), ''));
911
+ onInsert?.(`import ${importName} from '${TerraQualityRegex.REGEX_INDEX_MD_MDX_I_REGEX_2().test(ctx?.split('/').pop() || '') ? '../' : './'}${s.name}';`, `<${importName} />\n`);
912
+ },
913
+ },
914
+ ],
915
+ };
916
+ return (_jsxs(_Fragment, { children: [_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { disabled: disabled, lbl: 'Insert component', onMouseDown: onMouseDown, children: _jsx(DOCS_BLOCK_ICONS.code, {}) }) }), _jsxs(DropdownMenuContent, { align: 'start', className: MENU_WIDTH_CLASS, children: [_jsxs(DropdownMenuItem, { onMouseDown: preserveSel, onSelect: e => {
917
+ e.preventDefault();
918
+ const sel = window.getSelection();
919
+ if (sel && sel.rangeCount > 0)
920
+ savedSel.current = sel.getRangeAt(0).cloneRange();
921
+ setSearch(s => ({ ...s, open: true }));
922
+ }, className: MENU_GAP_CLASS, children: [_jsx(PopOutIcon, { className: ICON_CLASS }), " Embed Page"] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuSub, { children: [_jsxs(DropdownMenuSubTrigger, { className: MENU_GAP_CLASS, children: [_jsx(ModuleIcon, { className: ICON_CLASS }), " Modules"] }), _jsxs(DropdownMenuSubContent, { className: MENU_WIDTH_CLASS, children: [menuGroups.globalComponents.map(c => (_jsx(DropdownMenuItem, { onMouseDown: preserveSel, onSelect: e => {
923
+ e.preventDefault();
924
+ void copyComponent(c);
925
+ }, children: c.displayLabel.replace(TerraQualityRegex.REGEX_G_REGEX_7(), ' ').replace(TerraQualityRegex.REGEX_B_W_G_REGEX(), c => c.toUpperCase()) }, c.path))), Object.entries(menuGroups.globalFolders).map(([f, s]) => (_jsx(TOOLBAR_COMPONENTS.GlobalComponentMenuNode, { name: f, tree: s, onCopy: copyComponent }, f)))] })] }), menuGroups.local.map(m => (_jsx(React.Fragment, { children: _jsxs(DropdownMenuSub, { children: [_jsxs(DropdownMenuSubTrigger, { disabled: !m.items.length, className: MENU_GAP_CLASS, children: [m.icon, " ", m.label] }), _jsx(DropdownMenuSubContent, { children: m.items.map(s => (_jsx(DropdownMenuItem, { onMouseDown: preserveSel, onSelect: e => {
926
+ e.preventDefault();
927
+ m.onSelect(s);
928
+ }, children: s.name }, s.path))) })] }) }, m.label)))] })] }), _jsx(Dialog, { open: search.open, onOpenChange: o => {
929
+ setSearch(s => ({ ...s, open: o }));
930
+ }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Embed Document" }), _jsx(DialogDescription, { className: 'sr-only', children: "Browse the document tree or search to embed a page." })] }), _jsxs("div", { className: 'space-y-4 pt-2', children: [_jsx(Input, { icon: _jsx(SearchIcon, { className: ICON_CLASS }), autoFocus: true, placeholder: 'Search or type to create...', value: search.query, onChange: e => { setSearch(s => ({ ...s, query: e.target.value })); } }), _jsxs("div", { className: 'flex max-h-64 min-h-24 flex-col gap-1 overflow-y-auto pr-2', children: [search.busy && search.tree.length === 0 && _jsx("p", { className: 'py-4 text-center text-sm text-primary-11', children: "Loading..." }), search.query.trim() && (_jsxs(UiButton, { variant: 'outline', className: 'mb-2 flex-col items-start gap-y-0 border-dashed py-6', onClick: () => void embed('', true), disabled: search.busy, children: [_jsx("span", { className: 'text-sm font-medium', children: search.busy ? 'Creating...' : `Create "${search.query}"` }), _jsx("span", { className: SECONDARY_TEXT_CLASS, children: "Create page and embed as attachment" })] })), search.query.trim() && !search.busy && filteredLeaves.length === 0 && _jsx(SearchEmpty, { query: search.query }), filteredLeaves.map(l => (_jsxs(UiButton, { variant: 'ghost', className: 'flex-col items-start gap-y-0 py-6', onClick: () => embed(l.path), children: [_jsx("span", { className: 'text-sm font-medium capitalize', children: l.title }), _jsxs("span", { className: SECONDARY_TEXT_CLASS, children: [l.breadcrumb ? `${l.breadcrumb} / ` : '', l.title] })] }, l.path)))] })] })] }) })] }));
931
+ },
932
+ /**
933
+ * Dropdown control for quickly converting a block or inserting a supported block template.
934
+ *
935
+ * @param props - Disabled state, toolbar context, optional editor adapter, available block options, and JSON mode for the block selector.
936
+ * @param {object} - Value passed to the helper.
937
+ * @param {boolean} [props.disabled] - Locks the control.
938
+ * @param {ToolbarState | null} props.toolbar - System toolbar state reference to execute replacements.
939
+ * @param {BlockOpt[]} props.options - Select actions with icon prefixes.
940
+ * @param adapter - Adapter input supplied to block type's operation.
941
+ * @param json - Json input supplied to block type's operation.
942
+ *
943
+ * @return {JSX.Element} The Select dropdown block wrapper.
944
+ */
945
+ BlockType({ disabled, toolbar, adapter, options: opts, json = false, }) {
946
+ // STATES
947
+ const [val, setVal] = useState('p');
948
+ // refs to persist the last known location of the user's cursor
949
+ const savedBlock = useRef(null);
950
+ // INIT
951
+ useEffect(() => {
952
+ let frame = 0;
953
+ if (disabled || !toolbar)
954
+ return;
955
+ /**
956
+ * Synchronizes the current view state with the source data.
957
+ *
958
+ */
959
+ const sync = () => {
960
+ const view = codeViewOf();
961
+ // 1. Visual Mode (Lexical) - Read from DOM selection safely
962
+ if (!view || view.dom.offsetParent === null) {
963
+ const sel = window.getSelection();
964
+ const blockCursor = document.querySelector('[data-docs-block-cursor="true"]');
965
+ if (blockCursor && sel?.anchorNode && !blockCursor.contains(sel.anchorNode)) {
966
+ const blockVal = toolbarFocusValue(blockCursor, opts.map(o => o.value));
967
+ if (blockVal) {
968
+ setVal(c => (c === blockVal ? c : blockVal));
969
+ return;
970
+ }
971
+ }
972
+ // Only update the saved reference if the selection is actively inside the editor.
973
+ // This prevents the dropdown click from overwriting the last known cursor location.
974
+ if (sel && sel.rangeCount > 0 && sel.anchorNode) {
975
+ const node = sel.anchorNode;
976
+ const el = node.nodeType === 3 ? node.parentElement : node;
977
+ if (el?.closest('.mdxeditor-root, [contenteditable="true"]')) {
978
+ const textBlock = el?.closest('h1, h2, h3, h4, h5, h6, p');
979
+ if (textBlock)
980
+ savedBlock.current = textBlock;
981
+ else if (el?.closest('td, th')?.closest('[data-docs-editor-block="DocsTable"], [data-terra-block="table"]'))
982
+ savedBlock.current = null;
983
+ const blockVal = el
984
+ ? toolbarFocusValue(el, opts.map(o => o.value))
985
+ : null;
986
+ if (blockVal) {
987
+ setVal(c => (c === blockVal ? c : blockVal));
988
+ return;
989
+ }
990
+ }
991
+ }
992
+ return;
993
+ }
994
+ // A focused rich code block uses CodeMirror for its own content.
995
+ const { head } = view.state.selection.main;
996
+ const text = view.state.doc.lineAt(head).text;
997
+ let n = 'p';
998
+ if (TerraQualityRegex.REGEX_S_REGEX_5().test(text))
999
+ n = 'h3';
1000
+ else if (TerraQualityRegex.REGEX_S_REGEX_6().test(text))
1001
+ n = 'h4';
1002
+ else if (TerraQualityRegex.REGEX_S_REGEX_4().test(text))
1003
+ n = 'h2';
1004
+ setVal(c => (c === n ? c : n));
1005
+ };
1006
+ sync();
1007
+ /**
1008
+ * Handles refresh.
1009
+ */
1010
+ const refresh = () => {
1011
+ cancelAnimationFrame(frame);
1012
+ frame = requestAnimationFrame(sync);
1013
+ };
1014
+ /**
1015
+ * Handles on block select.
1016
+ *
1017
+ * @param e - Change event received from the toolbar input.
1018
+ */
1019
+ const onBlockSelect = (e) => {
1020
+ const { block: selBlock, decorator } = e.detail ?? {};
1021
+ const block = selBlock instanceof HTMLElement
1022
+ ? selBlock
1023
+ : decorator instanceof HTMLElement
1024
+ ? decorator.querySelector('[data-docs-editor-block], .docs-math-wrapper, [data-math-code], [data-docs-view-root="true"]')
1025
+ : null;
1026
+ const blockVal = block
1027
+ ? toolbarFocusValue(block, opts.map(o => o.value))
1028
+ : null;
1029
+ if (!blockVal)
1030
+ return;
1031
+ savedBlock.current = null;
1032
+ setVal(c => (c === blockVal ? c : blockVal));
1033
+ };
1034
+ ['focusin', 'keyup', 'mouseup', 'selectionchange'].forEach(e => {
1035
+ document.addEventListener(e, refresh, true);
1036
+ });
1037
+ document.addEventListener('docs:block-select', onBlockSelect);
1038
+ document.addEventListener('docs:block-selected', onBlockSelect);
1039
+ return () => {
1040
+ cancelAnimationFrame(frame);
1041
+ ['focusin', 'keyup', 'mouseup', 'selectionchange'].forEach(e => {
1042
+ document.removeEventListener(e, refresh, true);
1043
+ });
1044
+ document.removeEventListener('docs:block-select', onBlockSelect);
1045
+ document.removeEventListener('docs:block-selected', onBlockSelect);
1046
+ };
1047
+ }, [disabled, toolbar]);
1048
+ // RENDER
1049
+ return (_jsxs(Select, { disabled: disabled || !toolbar, value: val, onValueChange: v => {
1050
+ const item = opts.find(i => i.value === v);
1051
+ if (!item)
1052
+ return;
1053
+ if (item.group === 'type') {
1054
+ if (json) {
1055
+ window.dispatchEvent(new CustomEvent('docs:json-command', { detail: { type: 'style', style: v === 'p' ? 'normal' : v } }));
1056
+ setVal(v);
1057
+ return;
1058
+ }
1059
+ if (adapter?.setBlockType) {
1060
+ adapter.setBlockType(v);
1061
+ setVal(v);
1062
+ return;
1063
+ }
1064
+ const view = codeViewOf();
1065
+ if (view && view.dom.offsetParent !== null) {
1066
+ const { head } = view.state.selection.main;
1067
+ const line = view.state.doc.lineAt(head);
1068
+ const text = line.text;
1069
+ let n = 'Text';
1070
+ let prefix = '';
1071
+ if (v === 'h2') {
1072
+ prefix = '## ';
1073
+ n = text.replace(TerraQualityRegex.REGEX_S_REGEX_4(), '').trimStart() || 'Heading';
1074
+ }
1075
+ else if (v === 'h3') {
1076
+ prefix = '### ';
1077
+ n = text.replace(TerraQualityRegex.REGEX_S_REGEX_5(), '').trimStart() || 'Heading';
1078
+ }
1079
+ else if (v === 'h4') {
1080
+ prefix = '#### ';
1081
+ n = text.replace(TerraQualityRegex.REGEX_S_REGEX_6(), '').trimStart() || 'Heading';
1082
+ }
1083
+ else
1084
+ n = text.replace(/^#{1,6}\s+/, '').trimStart() || 'Text';
1085
+ const replacementStr = `${prefix}${n}`;
1086
+ view.dispatch({ changes: { from: line.from, to: line.to, insert: replacementStr }, selection: { anchor: line.from + replacementStr.length } });
1087
+ view.focus();
1088
+ setVal(v);
1089
+ return;
1090
+ }
1091
+ try {
1092
+ if (savedBlock.current) {
1093
+ const editor = savedBlock.current.closest('[contenteditable="true"]')?.__lexicalEditor;
1094
+ if (editor) {
1095
+ editor.update(() => {
1096
+ const lexicalNode = $getNearestNodeFromDOMNode(savedBlock.current);
1097
+ if (lexicalNode) {
1098
+ let blockNode = lexicalNode;
1099
+ while (blockNode && !$isElementNode(blockNode))
1100
+ blockNode = blockNode.getParent();
1101
+ if (blockNode && $isElementNode(blockNode)) {
1102
+ let replacementNode;
1103
+ if (v === 'p')
1104
+ replacementNode = $createParagraphNode();
1105
+ else if (v === 'h2' || v === 'h3' || v === 'h4')
1106
+ replacementNode = $createHeadingNode(v);
1107
+ if (replacementNode) {
1108
+ blockNode.getChildren().forEach(c => {
1109
+ replacementNode.append(c);
1110
+ });
1111
+ blockNode.replace(replacementNode);
1112
+ replacementNode.selectStart();
1113
+ }
1114
+ }
1115
+ }
1116
+ });
1117
+ }
1118
+ else
1119
+ console.error('[BlockType] [VisualMode] ABORTED: Could not locate __lexicalEditor on editorRoot.');
1120
+ }
1121
+ else
1122
+ console.error('[BlockType] [VisualMode] ABORTED: No saved cursor context. Ensure the cursor was placed inside the editor before selection.');
1123
+ }
1124
+ catch (_err) {
1125
+ console.error('[BlockType] [VisualMode] CRITICAL PIPELINE FAILURE:', _err);
1126
+ }
1127
+ setVal(v);
1128
+ return;
1129
+ }
1130
+ item.onSelect();
1131
+ }, size: 'xs', children: [_jsx(SelectTrigger, { "aria-label": 'Block type', className: 'w-40', onMouseDown: preserveSel, children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { align: 'start', children: [_jsxs(SelectGroup, { children: [_jsx(SelectLabel, { children: "Block Type" }), opts.filter(o => o.group === 'type').map(({ value: v, label: l, icon: i }) => (_jsx(SelectItem, { value: v, icon: React.createElement(i, { className: ICON_CLASS }), children: l }, v)))] }), _jsx(SelectSeparator, {}), _jsxs(SelectGroup, { children: [_jsx(SelectLabel, { children: "Insert Block" }), opts
1132
+ .filter(o => o.group === 'insert')
1133
+ .map(({ value: v, label: l, icon: i }) => (_jsx(SelectItem, { value: v, icon: React.createElement(i, { className: ICON_CLASS }), children: l }, v)))] })] })] }));
1134
+ },
1135
+ /**
1136
+ * Main floating action bar assembling all text and format mutation interactions into a single cohesive boundary.
1137
+ *
1138
+ * @param props - Active mode, editability, toolbar state, optional block list and adapter, and whether Docs-specific actions are enabled.
1139
+ * @param {object} - Value passed to the helper.
1140
+ * @param {ToolbarMode} [props.activeMode] - Inherited mode state isolating interactive parameters.
1141
+ * @param {ToolbarState | null} props.toolbar - System level hook connecting buttons smoothly back into abstract DOM memory states.
1142
+ * @param editable - Editability flag controlling whether toolbar accepts changes.
1143
+ * @param blocks - Blocks input supplied to toolbar's operation.
1144
+ * @param adapter - Adapter input supplied to toolbar's operation.
1145
+ * @param docs - Docs input supplied to toolbar's operation.
1146
+ *
1147
+ * @return {JSX.Element | null} Assembled floating bar mapping components.
1148
+ */
1149
+ Toolbar({ activeMode: aMode, editable, toolbar, blocks, adapter, docs = true, }) {
1150
+ // STATES
1151
+ const toolbarCtx = { edit: useContext(EditStateCtx), attachmentFilter: useAtom($attachmentFilter) };
1152
+ const mode = aMode ?? 'view';
1153
+ const [vers, setVers] = useState([]);
1154
+ const [verDialogOpen, setVerDialogOpen] = useState(false);
1155
+ const [selVerId, setSelVerId] = useState('');
1156
+ const codeAtEnd = useRef(false);
1157
+ const codeSel = useRef(null);
1158
+ const codeLength = useRef(0);
1159
+ useEffect(() => {
1160
+ /**
1161
+ * Handles sync.
1162
+ */
1163
+ const sync = () => {
1164
+ const view = codeViewOf();
1165
+ if (!view)
1166
+ return;
1167
+ const { from, to } = view.state.selection.main;
1168
+ codeLength.current = view.state.doc.length;
1169
+ codeSel.current = { code: { from: Math.min(from, to), to: Math.max(from, to) }, dom: null, visible: true };
1170
+ codeAtEnd.current = false;
1171
+ };
1172
+ document.addEventListener('selectionchange', sync);
1173
+ return () => {
1174
+ document.removeEventListener('selectionchange', sync);
1175
+ };
1176
+ }, []);
1177
+ const [endpoint, setEndpoint] = useState('');
1178
+ const [endpointOpen, setEndpointOpen] = useState(false);
1179
+ const ready = adapter?.ready ?? !!toolbar;
1180
+ const saving = toolbar?.busy ?? null;
1181
+ const dirty = toolbar?.dirty ?? false;
1182
+ const publishDraft = getDocsHostAdapter()?.publishDraft;
1183
+ const publish = async () => {
1184
+ if (!publishDraft || !toolbar?.path)
1185
+ return;
1186
+ try {
1187
+ if (dirty && toolbar.save)
1188
+ await toolbar.save();
1189
+ await publishDraft(toolbar.path);
1190
+ toast.success('Published.');
1191
+ }
1192
+ catch (error) {
1193
+ toast.error(error instanceof Error ? error.message : 'Unable to publish document.');
1194
+ }
1195
+ };
1196
+ useEffect(() => {
1197
+ let live = true;
1198
+ if (!docs || !editable || !toolbar?.path || !verDialogOpen) {
1199
+ setVers([]);
1200
+ return;
1201
+ }
1202
+ const tgtPath = toolbar.path;
1203
+ // prettier-ignore
1204
+ const tgtId = tgtPath.split('/').pop()?.replace(TerraQualityRegex.REGEX_MDX_I_REGEX(), '') || tgtPath;
1205
+ (async () => {
1206
+ try {
1207
+ const res = await actions.write.doc.versions.get({ path: tgtPath });
1208
+ if (res.error)
1209
+ throw res.error;
1210
+ let resultData = res.data;
1211
+ if (Array.isArray(resultData) && resultData.length === 0 && tgtPath !== tgtId) {
1212
+ const fallback = await actions.write.doc.versions.get({ path: tgtId });
1213
+ if (fallback.error)
1214
+ throw fallback.error;
1215
+ if (Array.isArray(fallback.data) && fallback.data.length > 0)
1216
+ resultData = fallback.data;
1217
+ }
1218
+ if (!live || !Array.isArray(resultData))
1219
+ return;
1220
+ setVers(resultData);
1221
+ }
1222
+ catch (err) {
1223
+ console.error('[Toolbar:versions] load-failed', { path: tgtPath, error: err });
1224
+ toast.error(err instanceof Error ? err.message : 'Failed to load doc versions.');
1225
+ }
1226
+ })();
1227
+ return () => {
1228
+ live = false;
1229
+ };
1230
+ }, [docs, editable, toolbar?.path, verDialogOpen]);
1231
+ // HANDLERS
1232
+ const onSave = toolbar
1233
+ ? async (e) => {
1234
+ e.preventDefault();
1235
+ if (!toolbar.save)
1236
+ return;
1237
+ try {
1238
+ const res = toolbar.save();
1239
+ if (res instanceof Promise)
1240
+ await res;
1241
+ }
1242
+ catch (err) {
1243
+ console.error('[Docs:save] failed', err);
1244
+ toast.error(err instanceof Error ? err.message : 'Unable to save document.');
1245
+ }
1246
+ }
1247
+ : undefined;
1248
+ const selVer = vers.find(v => v.savedAt === selVerId);
1249
+ useEffect(() => {
1250
+ if (verDialogOpen && !selVerId && vers[0])
1251
+ setSelVerId(vers[0].savedAt);
1252
+ }, [verDialogOpen, selVerId, vers]);
1253
+ const previewPack = useMemo(() => parseVerBody(selVer), [selVer]);
1254
+ useEffect(() => {
1255
+ previewPack.markdown.trim();
1256
+ }, [toolbar?.path, vers, selVer, previewPack.json, previewPack.markdown, previewPack.body]);
1257
+ /**
1258
+ * On restore preview.
1259
+ */
1260
+ const toolbarActions = {
1261
+ discard: toolbar
1262
+ ? async (e) => {
1263
+ e.preventDefault();
1264
+ if (!toolbar.discard)
1265
+ return;
1266
+ try {
1267
+ await Promise.resolve(toolbar.discard());
1268
+ }
1269
+ catch (_err) {
1270
+ console.error('[Thundra] Discard failed:', _err);
1271
+ }
1272
+ }
1273
+ : undefined,
1274
+ restorePreview: async () => {
1275
+ if (!toolbarCtx.edit || !selVerId)
1276
+ return;
1277
+ console.info('[Toolbar:history:restore]', JSON.stringify({
1278
+ path: toolbar?.path ?? null,
1279
+ savedAt: selVerId,
1280
+ sourceLength: previewPack.markdown.length,
1281
+ sourceHead: previewPack.markdown.slice(0, 120),
1282
+ sourceTail: previewPack.markdown.slice(-120),
1283
+ }));
1284
+ if (toolbarCtx.edit.update)
1285
+ toolbarCtx.edit.update({ content: previewPack.body, title: selVer?.title || '', description: selVer?.description || '', source: previewPack.markdown });
1286
+ const setCode = toolbarCtx.edit.setCode ?? toolbarCtx.edit.setSrc;
1287
+ if (setCode)
1288
+ setCode(previewPack.markdown);
1289
+ setVerDialogOpen(false);
1290
+ notify({ error: null, fallback: 'Restored version to local draft. Save to apply.' });
1291
+ },
1292
+ };
1293
+ const insert = toolbar?.insert ?? adapter?.insert;
1294
+ /**
1295
+ * Handles json command.
1296
+ *
1297
+ * @param detail - Toolbar detail describing the requested document update.
1298
+ */
1299
+ const jsonCmd = (detail) => {
1300
+ if (mode !== 'edit')
1301
+ return false;
1302
+ window.dispatchEvent(new CustomEvent('docs:json-command', { detail }));
1303
+ return true;
1304
+ };
1305
+ /**
1306
+ * Handles json block.
1307
+ *
1308
+ * @param _type - Sanity field type associated with the value.
1309
+ * @param value - Field value being normalized for the toolbar.
1310
+ */
1311
+ const jsonBlock = (_type, val) => ({ _key: crypto.randomUUID(), _type, ...val });
1312
+ // SELECTION HANDLER
1313
+ /**
1314
+ * Safe sel.
1315
+ *
1316
+ * @param fallback - Value passed to the helper.
1317
+ * @param isInline - Value passed to the helper.
1318
+ *
1319
+ * @returns safe Sel.
1320
+ */
1321
+ const sSel = (fallback, isInline = false) => {
1322
+ let text = '';
1323
+ try {
1324
+ const view = codeViewOf();
1325
+ if (view && view.dom.offsetParent !== null) {
1326
+ if (!view.state.selection.main.empty)
1327
+ text = view.state.sliceDoc(view.state.selection.main.from, view.state.selection.main.to);
1328
+ }
1329
+ else {
1330
+ const winSel = window.getSelection();
1331
+ // explicitly check that the selection is not collapsed to avoid capturing the whole block bounds
1332
+ if (winSel && winSel.rangeCount > 0 && !winSel.isCollapsed)
1333
+ text = winSel.toString();
1334
+ }
1335
+ }
1336
+ catch {
1337
+ // Fall back to the caller-provided text below.
1338
+ }
1339
+ if (!text.trim())
1340
+ return fallback;
1341
+ // strip structural markdown (tables, asides, newlines) that break inline link/bold syntax
1342
+ // prettier-ignore
1343
+ if (isInline)
1344
+ return text.replace(/[\r\n]+/g, ' ').replace(/(:::|\|\|\||===|\|)/g, '').replace(/\s{2,}/g, ' ').trim() || fallback;
1345
+ return text;
1346
+ };
1347
+ /**
1348
+ * Handles click toolbar button.
1349
+ *
1350
+ * @param label - Label displayed beside the toolbar control.
1351
+ */
1352
+ const clickToolbarButton = (label) => {
1353
+ const button = document.querySelector(`button[aria-label="${label}"]`);
1354
+ button?.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, button: 0 }));
1355
+ button?.click();
1356
+ };
1357
+ // prettier-ignore
1358
+ const toolbarOpts = {
1359
+ blocks: [
1360
+ { group: 'type', value: 'p', label: 'Text', icon: TextFontIcon, onSelect: () => undefined },
1361
+ { group: 'type', value: 'h2', label: 'Heading 2', icon: DOCS_BLOCK_ICONS.heading, onSelect: () => undefined },
1362
+ { group: 'type', value: 'h3', label: 'Heading 3', icon: DOCS_BLOCK_ICONS.heading, onSelect: () => undefined },
1363
+ { group: 'type', value: 'h4', label: 'Heading 4', icon: DOCS_BLOCK_ICONS.heading, onSelect: () => undefined },
1364
+ { group: 'insert', value: 'table', label: 'Insert table', icon: DOCS_BLOCK_ICONS.table, onSelect: () => jsonCmd({ type: 'insert', block: jsonBlock('table', { header: 'Column 1|Column 2', rows: [jsonBlock('row', { cells: [jsonBlock('cell', { content: [jsonBlock('block', { children: [jsonBlock('span', { marks: [], text: 'Text' })], markDefs: [], style: 'normal' })] }), jsonBlock('cell', { content: [jsonBlock('block', { children: [jsonBlock('span', { marks: [], text: 'Text' })], markDefs: [], style: 'normal' })] })] })] }) }) || insert?.(`\n\n<table>\n\t<thead>\n\t\t<tr>\n\t\t\t<th>Column 1</th>\n\t\t\t<th>Column 2</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t<tr>\n\t\t\t<td>Text</td>\n\t\t\t<td>Text</td>\n\t\t</tr>\n\t</tbody>\n</table>\n\n`) },
1365
+ { group: 'insert', value: 'nodes', label: 'Insert nodes', icon: DOCS_BLOCK_ICONS.nodes, onSelect: () => jsonCmd({ type: 'insert', block: jsonBlock('nodes', { edges: [], nodes: [{ id: 'start', type: 'origin', data: { title: 'Start' } }] }) }) || insert?.(EMPTY_NODES) },
1366
+ { group: 'insert', value: 'code-local', label: 'Insert code block', icon: DOCS_BLOCK_ICONS.code, onSelect: () => insertionActions.insertLocalCode() },
1367
+ { group: 'insert', value: 'code-received', label: 'Receive value', icon: ShareIcon, onSelect: () => { setEndpointOpen(true); } },
1368
+ { group: 'insert', value: 'math', label: 'Insert math', icon: DOCS_BLOCK_ICONS.math, onSelect: () => jsonCmd({ type: 'insert', block: jsonBlock('math', { code: "f'(x) = \\lim_{h \\to 0} \\frac{f(x+h) - f(x)}{h}" }) }) || insert?.(`\n\n<DocsMath code="f'(x) = \\lim_{h \\to 0} \\frac{f(x+h) - f(x)}{h}" />\n\n`) },
1369
+ { group: 'insert', value: 'tsdocs', label: 'Insert TypeDoc', icon: DOCS_BLOCK_ICONS.tsDocs, onSelect: () => jsonCmd({ type: 'insert', block: jsonBlock('tsDocs', { scope: '' }) }) || insert?.(`\n\n<TsDocs scope="" />\n\n`) },
1370
+ { group: 'insert', value: 'accordion', label: 'Insert accordion', icon: DOCS_BLOCK_ICONS.accordion, onSelect: () => jsonCmd({ type: 'insert', block: jsonBlock('accordion', { items: [jsonBlock('item', { content: [jsonBlock('block', { children: [jsonBlock('span', { marks: [], text: sSel('Content...') })], markDefs: [], style: 'normal' })], title: 'Accordion item' })] }) }) || insert?.(`\n\n=== [Accordion Item]\n\n${sSel('Content...')}\n\n===\n\n`) },
1371
+ { group: 'insert', value: 'tabs', label: 'Insert tabs', icon: DOCS_BLOCK_ICONS.tabs, onSelect: () => jsonCmd({ type: 'insert', block: jsonBlock('tabs', { items: [jsonBlock('item', { content: [jsonBlock('block', { children: [jsonBlock('span', { marks: [], text: sSel('Content...') })], markDefs: [], style: 'normal' })], title: 'Tab' })] }) }) || insert?.(`\n\n||| [Tabs Item]\n\n${sSel('Content...')}\n\n|||\n\n`) },
1372
+ { group: 'insert', value: 'aside', label: 'Insert aside', icon: DOCS_BLOCK_ICONS.callout, onSelect: () => jsonCmd({ type: 'insert', block: jsonBlock('callout', { content: [jsonBlock('block', { children: [jsonBlock('span', { marks: [], text: sSel('Content...') })], markDefs: [], style: 'normal' })], kind: 'note', title: 'Note' }) }) || insert?.(`\n\n:::note[Note]\n\n${sSel('Content...')}\n\n:::\n\n`) },
1373
+ { group: 'insert', value: 'attachment', label: 'Insert image', icon: DOCS_BLOCK_ICONS.docImage, onSelect: () => jsonCmd({ type: 'insert', block: jsonBlock('docImage', { alt: '', url: '' }) }) || clickToolbarButton('Insert attachment') },
1374
+ { group: 'insert', value: 'component', label: 'Insert component', icon: DOCS_BLOCK_ICONS.code, onSelect: () => { clickToolbarButton('Insert component'); } },
1375
+ ...(mode === 'edit' ? [] : (blocks ?? []).map(({ type: v, label: l, icon: i, insert: c }) => ({ group: 'insert', value: v, label: `Insert ${l?.toLowerCase()}`, icon: i, onSelect: () => insert?.(c()) }))),
1376
+ ],
1377
+ formats: [
1378
+ { key: 'bold', lbl: 'Bold', icon: BoldIcon, shortcut: '⌘B', format: (val) => `**${val}**` },
1379
+ { key: 'italic', lbl: 'Italic', icon: ItalicIcon, shortcut: '⌘I', format: (val) => `_${val}_` },
1380
+ { key: 'underline', lbl: 'Underline', icon: UnderlineIcon, shortcut: '⌘U', format: (val) => `<u>${val}</u>` },
1381
+ { key: 'strikethrough', lbl: 'Strikethrough', icon: StrikethroughIcon, shortcut: '⌘X', format: (val) => `~~${val}~~` },
1382
+ { key: 'link', lbl: 'Link', icon: LinkIcon, shortcut: '⌘K', format: (val) => `[${val}](url)` },
1383
+ ],
1384
+ };
1385
+ // DATA
1386
+ /**
1387
+ * Handles insert code.
1388
+ *
1389
+ * @param source - Docs source payload sent to the endpoint.
1390
+ * @param endpoint - API endpoint receiving the toolbar request.
1391
+ */
1392
+ const insertionActions = {
1393
+ insertLocalCode: () => {
1394
+ if (jsonCmd({ type: 'insert', block: jsonBlock('code', { language: 'text', value: 'Code...' }) }))
1395
+ return;
1396
+ insert?.(`\n\n<Code language="text">\n{\`\nCode...\n\`}\n</Code>\n\n`);
1397
+ },
1398
+ insertCode: (src, endpoint = '') => {
1399
+ const code = src === 'received' ? `\n\n<Code language="json" source=${JSON.stringify(endpoint)} />\n\n` : '\n\n<Code language="text">\n{`\nCode...\n`}\n</Code>\n\n';
1400
+ if (jsonCmd({ type: 'insert', block: jsonBlock('code', src === 'received' ? { language: 'json', source: endpoint, value: '' } : { language: 'text', value: 'Code...' }) }))
1401
+ return;
1402
+ if (insert) {
1403
+ const srcLength = codeLength.current;
1404
+ insert(code, codeAtEnd.current || (srcLength > 0 && (codeSel.current?.code?.to ?? -1) >= srcLength), codeSel.current);
1405
+ codeAtEnd.current = false;
1406
+ codeSel.current = null;
1407
+ return;
1408
+ }
1409
+ const curr = toolbarCtx.edit?.code ?? toolbarCtx.edit?.source ?? '';
1410
+ const sel = codeSel.current?.code;
1411
+ const range = codeAtEnd.current || !sel ? { from: curr.length, to: curr.length } : { from: Math.min(sel.from, curr.length), to: Math.min(sel.to, curr.length) };
1412
+ const n = `${curr.slice(0, range.from)}${code}${curr.slice(range.to)}`;
1413
+ toolbarCtx.edit?.update({ source: n });
1414
+ toolbarCtx.edit?.setCode(n);
1415
+ if (toolbarCtx.edit) {
1416
+ codeAtEnd.current = false;
1417
+ codeSel.current = null;
1418
+ return;
1419
+ }
1420
+ const view = codeViewOf();
1421
+ if (view) {
1422
+ const range = codeAtEnd.current ? { from: view.state.doc.length, to: view.state.doc.length } : (codeSel.current?.code ?? view.state.selection.main);
1423
+ view.dispatch({ changes: { from: range.from, insert: code, to: range.to }, scrollIntoView: true, selection: { anchor: range.from + code.length } });
1424
+ view.focus();
1425
+ codeAtEnd.current = false;
1426
+ codeSel.current = null;
1427
+ return;
1428
+ }
1429
+ codeAtEnd.current = false;
1430
+ codeSel.current = null;
1431
+ },
1432
+ /*
1433
+ * Handles receive code.
1434
+ */
1435
+ receiveCode: () => {
1436
+ const src = endpoint.trim();
1437
+ if (!src)
1438
+ return;
1439
+ insertionActions.insertCode('received', src);
1440
+ setEndpoint('');
1441
+ setEndpointOpen(false);
1442
+ },
1443
+ };
1444
+ // KEYBOARD SHORTCUTS
1445
+ useEffect(() => {
1446
+ if (mode === 'view' || !onSave)
1447
+ return;
1448
+ /**
1449
+ * Handles key down.
1450
+ *
1451
+ * @param e - Value passed to the helper.
1452
+ */
1453
+ const handleKeyDown = (e) => {
1454
+ if ((e.metaKey || e.ctrlKey) && e.key?.toLowerCase() === 's') {
1455
+ e.preventDefault();
1456
+ void onSave(e);
1457
+ }
1458
+ };
1459
+ document.addEventListener('keydown', handleKeyDown);
1460
+ return () => {
1461
+ document.removeEventListener('keydown', handleKeyDown);
1462
+ };
1463
+ }, [mode, onSave]);
1464
+ // RENDER
1465
+ return (_jsx(ErrorBoundary, { name: 'Toolbar', children: _jsxs("div", { className: 'docs-toolbar-scroll not-prose pointer-events-auto sticky top-10 isolate z-50 -mx-3 -mt-5 mb-5 w-[calc(100%+1.5rem)] max-w-[calc(100%+1.5rem)] overflow-visible text-primary-12 print:hidden', "data-docs-toolbar-frame": true, "data-docs-toolbar-scroll": true, children: [_jsx("div", { "aria-hidden": 'true', className: 'docs-toolbar-backdrop pointer-events-none absolute -inset-x-4 -bottom-12 -top-16 z-0' }), _jsx("div", { className: 'relative z-10 rounded-lg border border-primary-6 bg-primary-3', children: _jsx(TooltipProvider, { children: _jsx("div", { className: 'mdxeditor-toolbar not-prose relative z-10 box-border flex w-full min-w-0 flex-wrap items-center justify-between gap-1.5 gap-x-3 px-2 py-1 text-primary-12 print:hidden', "data-docs-main-toolbar": true, children: mode === 'view' ? (docs && (_jsx(Mode, { editable: editable, mode: mode, disabled: !ready, onModeChange: v => {
1466
+ if (!toolbar)
1467
+ return;
1468
+ toolbar.setMode(v).catch(e => {
1469
+ console.error('[Toolbar:mode] change-failed', { mode: v, error: e });
1470
+ toast.error(e instanceof Error ? e.message : 'Failed to change mode.');
1471
+ clearTransition();
1472
+ });
1473
+ } }))) : mode === 'attachments' ? (
1474
+ // ATTACHMENT FILTERS
1475
+ _jsxs(_Fragment, { children: [docs && (_jsx(Mode, { editable: editable, mode: mode, disabled: !ready, onModeChange: v => {
1476
+ if (!toolbar)
1477
+ return;
1478
+ toolbar.setMode(v).catch(e => {
1479
+ console.error('[Toolbar:mode] change-failed', { mode: v, error: e });
1480
+ toast.error(e instanceof Error ? e.message : 'Failed to change mode.');
1481
+ clearTransition();
1482
+ });
1483
+ } })), _jsx(ToggleGroup, { type: 'multiple', value: [...toolbarCtx.attachmentFilter], onValueChange: v => {
1484
+ if (v)
1485
+ $attachmentFilter.set(v);
1486
+ }, className: 'ml-auto max-w-full flex-wrap justify-end', children: ATTCH_TYPES.map(f => (_jsx(ToggleGroupItem, { value: f, size: 'sm', className: 'h-6 px-2 text-xs capitalize', onMouseDown: preserveSel, children: f }, f))) })] })) : (_jsxs(_Fragment, { children: [_jsxs(ButtonGroup, { children: [_jsx(TOOLBAR_COMPONENTS.BlockType, { disabled: !ready, toolbar: toolbar, adapter: adapter, options: toolbarOpts.blocks, json: mode === 'edit' }), toolbarOpts.formats
1487
+ .map(({ key: k, lbl: l, icon: i, shortcut: s, format: f }) => ({
1488
+ key: k,
1489
+ lbl: l,
1490
+ icon: i,
1491
+ shortcut: s,
1492
+ onClick: () => jsonCmd({ type: 'decorator', decorator: k === 'strikethrough' ? 'strike-through' : k }) ||
1493
+ (adapter?.format ? adapter.format(k === 'strikethrough' ? 'strike' : k) : insert?.(f(sSel('text', true)))),
1494
+ }))
1495
+ .map(({ key: k, lbl: l, icon: i, shortcut: s, onClick: o }) => (_jsx(Button, { lbl: l, shortcut: s, disabled: !ready, onMouseDown: preserveSel, onClick: o, children: React.createElement(i) }, k))), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { lbl: 'Insert list', disabled: !ready, onMouseDown: preserveSel, children: _jsx(BulletListIcon, {}) }) }), _jsx(DropdownMenuContent, { align: 'start', children: [
1496
+ { key: 'bullet', lbl: 'Bulleted list', icon: BulletListIcon, prefix: '\n- ', fallback: 'Item' },
1497
+ { key: 'number', lbl: 'Numbered list', icon: NumberListIcon, prefix: '\n1. ', fallback: 'Item' },
1498
+ { key: 'check', lbl: 'Checklist', icon: CheckListIcon, prefix: '\n- [ ] ', fallback: 'Task' },
1499
+ ]
1500
+ .map(({ key: k, lbl: l, icon: i, prefix: p, fallback: f }) => ({
1501
+ key: k,
1502
+ lbl: l,
1503
+ icon: i,
1504
+ onClick: () => jsonCmd({ type: 'list', list: k === 'check' ? 'bullet' : k }) ||
1505
+ (adapter?.toggleList ? adapter.toggleList(k) : toolbar?.toggleList ? toolbar.toggleList(k) : insert?.(`${p}${sSel(f)}\n`)),
1506
+ }))
1507
+ .map(({ key: k, lbl: l, icon: i, onClick: o }) => (_jsxs(DropdownMenuItem, { disabled: !ready, onSelect: () => {
1508
+ if (o)
1509
+ o();
1510
+ }, className: MENU_GAP_CLASS, children: [React.createElement(i, { className: ICON_CLASS }), _jsx("span", { children: l })] }, k))) })] })] }), _jsxs("div", { className: 'flex items-center gap-1', children: [docs && (_jsx(Mode, { editable: editable, mode: mode, disabled: !ready, onModeChange: v => {
1511
+ if (!editable || !toolbar)
1512
+ return;
1513
+ toolbar.setMode(v).catch(e => {
1514
+ console.error('[Toolbar:mode] change-failed', { mode: v, error: e });
1515
+ toast.error(e instanceof Error ? e.message : 'Failed to change mode.');
1516
+ clearTransition();
1517
+ });
1518
+ } })), _jsx(TextTooltip, { content: 'Find (\u2318F)', side: 'top', align: 'center', delayDuration: 400, children: docs && _jsx(TOOLBAR_COMPONENTS.FindReplace, { mode: mode, toolbar: toolbar }) }), _jsx(TOOLBAR_COMPONENTS.Undo, { onMouseDown: preserveSel, disabled: !ready, adapter: adapter }), docs && (_jsxs(_Fragment, { children: [_jsx(UiButton, { size: 'xs', variant: 'tonal', disabled: !ready || !dirty || saving !== null || (toolbar?.optimistic ?? false), onMouseDown: e => {
1519
+ e.preventDefault();
1520
+ }, onClick: e => void toolbarActions.discard?.(e), children: "Discard" }), _jsxs(ButtonGroup, { children: [_jsx(TextTooltip, { content: 'Save (\u2318S)', side: 'top', align: 'center', delayDuration: 400, children: _jsx(UiButton, { size: 'xs', disabled: !ready || !dirty, loading: saving === 'saving', onMouseDown: e => {
1521
+ e.preventDefault();
1522
+ }, onClick: e => void onSave?.(e), children: "Save" }) }), _jsx(TextTooltip, { content: 'Version History', side: 'top', align: 'center', delayDuration: 400, children: _jsx(UiButton, { size: 'xs', className: 'px-0.5', disabled: !ready, "aria-label": 'Version History', onMouseDown: e => {
1523
+ e.preventDefault();
1524
+ }, onClick: () => {
1525
+ if (vers.length > 0 && !selVerId)
1526
+ setSelVerId(vers[0].savedAt);
1527
+ setVerDialogOpen(true);
1528
+ }, children: _jsx(CaretDownIcon, {}) }) }), _jsx(Dialog, { open: verDialogOpen, onOpenChange: setVerDialogOpen, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Version History" }), _jsx(DialogDescription, { className: 'sr-only', children: "Select a version to preview and restore." }), _jsxs(Select, { value: selVerId, onValueChange: setSelVerId, children: [_jsx(SelectTrigger, { className: 'h-14', "aria-label": 'Select a version...', children: _jsx(SelectValue, { placeholder: 'Select a version...' }) }), _jsx(SelectContent, { children: vers.map(v => (_jsx(SelectItem, { value: v.savedAt, children: _jsxs("span", { className: 'flex flex-col text-left', children: [_jsxs("span", { children: [v.title, " (", new Date(v.savedAt).toLocaleString(), ")"] }), _jsx("span", { className: SECONDARY_TEXT_CLASS, children: v.description })] }) }, v.savedAt))) })] })] }), _jsx("div", { className: 'min-h-0 flex-1 overflow-y-auto', children: _jsx(CodeBlock, { tone: 'view', language: 'mdx', docsSource: true, code: previewPack.markdown.trim() || ' ' }) }), _jsxs(DialogFooter, { children: [_jsx(UiButton, { variant: 'ghost', onClick: () => { setVerDialogOpen(false); }, children: "Cancel" }), _jsx(UiButton, { disabled: !selVer, onClick: toolbarActions.restorePreview, children: "Restore" })] })] }) })] }), docs && publishDraft && (_jsx(UiButton, { size: 'xs', disabled: !ready || saving === 'saving', onMouseDown: e => e.preventDefault(), onClick: () => void publish(), children: "Publish" }))] }))] }), _jsxs("div", { className: 'flex items-center gap-0.5', children: [_jsx(TOOLBAR_COMPONENTS.Uploader, { icon: ImageIcon, insert: insert, onMouseDown: preserveSel, disabled: !ready, ctx: toolbar?.path }), _jsx(TOOLBAR_COMPONENTS.Custom, { onInsert: (_statement, v) => insert?.(v, true), onInsertMd: insert, onMouseDown: preserveSel, disabled: !ready, ctx: toolbar?.path })] }), _jsx(Dialog, { open: endpointOpen, onOpenChange: o => {
1529
+ setEndpointOpen(o);
1530
+ if (!o)
1531
+ setEndpoint('');
1532
+ }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Receive value" }), _jsx(DialogDescription, { className: 'sr-only', children: "Enter an API endpoint to receive code value into the editor." })] }), _jsx(Label, { htmlFor: 'docs-received-code-endpoint', children: "API endpoint" }), _jsx(Input, { id: 'docs-received-code-endpoint', value: endpoint, onChange: e => {
1533
+ setEndpoint(e.target.value);
1534
+ }, placeholder: '/api/docs?action=testimonials' }), _jsxs("div", { className: 'flex justify-end gap-2', children: [_jsx(UiButton, { variant: 'outline', onClick: () => { setEndpointOpen(false); }, children: "Cancel" }), _jsx(UiButton, { disabled: !endpoint.trim(), onClick: insertionActions.receiveCode, children: "Receive value" })] })] }) })] })) }) }) })] }) }));
1535
+ },
1536
+ };
1537
+ export const { DocsToolbarActions } = TOOLBAR_COMPONENTS;
1538
+ export const { Toolbar } = TOOLBAR_COMPONENTS;
1539
+ //# sourceMappingURL=_toolbar.js.map