@getdom/studio 0.1.0

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 (213) hide show
  1. package/LICENSE.md +109 -0
  2. package/README.md +44 -0
  3. package/SKILL.md +83 -0
  4. package/package.json +73 -0
  5. package/public/llms.txt +57 -0
  6. package/public/skills.sh +11 -0
  7. package/src/pages/_layout/componentCatalog.generated.js +4514 -0
  8. package/src/pages/_layout/componentInspector.js +60 -0
  9. package/src/pages/_layout/componentManager.js +192 -0
  10. package/src/pages/_layout/componentRuntimeRecords.js +49 -0
  11. package/src/pages/_layout/inspector/componentRegistry.js +639 -0
  12. package/src/pages/_layout/inspector/serialize.js +134 -0
  13. package/src/pages/_layout/inspector/useInspector.js +293 -0
  14. package/src/pages/components/accordion/DomAccordion.vue +39 -0
  15. package/src/pages/components/breadcrumbs/DomBreadcrumbs.vue +347 -0
  16. package/src/pages/components/button/DomButton.vue +132 -0
  17. package/src/pages/components/command-palette/DomCommandPalette.vue +482 -0
  18. package/src/pages/components/context-menu/DomContextMenu.vue +178 -0
  19. package/src/pages/components/dialog/DomDialog.vue +271 -0
  20. package/src/pages/components/dialog/DomDialogSchemaForm.vue +72 -0
  21. package/src/pages/components/dialog/DomDialogStack.vue +289 -0
  22. package/src/pages/components/dialog/useDialogs.js +253 -0
  23. package/src/pages/components/drawer/DomDrawer.vue +159 -0
  24. package/src/pages/components/dropdown/DomDropdown.vue +261 -0
  25. package/src/pages/components/icon-button/DomIconButton.vue +155 -0
  26. package/src/pages/components/lightbox/DomLightbox.vue +1561 -0
  27. package/src/pages/components/menu/DomMenu.vue +359 -0
  28. package/src/pages/components/menu/DomMenuItem.vue +233 -0
  29. package/src/pages/components/menu/DomMenuLabel.vue +19 -0
  30. package/src/pages/components/menu/DomMenuSeparator.vue +14 -0
  31. package/src/pages/components/menu/menuContext.js +11 -0
  32. package/src/pages/components/menu/menuPlacement.js +26 -0
  33. package/src/pages/components/menu/submenu.js +58 -0
  34. package/src/pages/components/pagination/DomPagination.vue +265 -0
  35. package/src/pages/components/playground/DomRenderer.vue +48 -0
  36. package/src/pages/components/popover/DomPopover.vue +244 -0
  37. package/src/pages/components/profile-menu/DomProfileMenu.vue +408 -0
  38. package/src/pages/components/profile-menu/DomProfileMenuItemContent.vue +45 -0
  39. package/src/pages/components/scroll-menu/DomScrollMenu.vue +430 -0
  40. package/src/pages/components/scroll-menu/DomScrollMenuItem.vue +212 -0
  41. package/src/pages/components/scroll-menu/scrollMenuContext.js +22 -0
  42. package/src/pages/components/splitter-panel/DomSplitterPanel.vue +283 -0
  43. package/src/pages/components/stepper/DomStepper.vue +386 -0
  44. package/src/pages/components/tabs/DomTabs.vue +149 -0
  45. package/src/pages/components/theme-switcher/DomThemeSwitcher.vue +226 -0
  46. package/src/pages/components/toast/DomToastItem.vue +52 -0
  47. package/src/pages/components/toast/DomToastStack.vue +768 -0
  48. package/src/pages/components/toast/useToasts.js +237 -0
  49. package/src/pages/components/toolbar/DomToolbar.vue +158 -0
  50. package/src/pages/components/tooltip/DomTooltip.vue +69 -0
  51. package/src/pages/components/tooltip/vDomTooltip.js +106 -0
  52. package/src/pages/components/tree-view/DomTreeView.vue +961 -0
  53. package/src/pages/components/tree-view/treeSchemas.js +129 -0
  54. package/src/pages/components/tree-view/treeUtils.js +228 -0
  55. package/src/pages/data/_shared/calculateVirtualWindow.js +101 -0
  56. package/src/pages/data/_shared/resourceRecord.js +29 -0
  57. package/src/pages/data/activity-timeline/DomActivityTimeline.vue +417 -0
  58. package/src/pages/data/agent-chat/DomAgentChatShell.vue +565 -0
  59. package/src/pages/data/agent-chat/DomAgentConversation.vue +737 -0
  60. package/src/pages/data/agent-chat/DomAgentConversationList.vue +319 -0
  61. package/src/pages/data/agent-chat/examples/agentChatMockApi.js +620 -0
  62. package/src/pages/data/agent-chat/examples/useAgentChatDemoStore.js +730 -0
  63. package/src/pages/data/data-grid/DataGridVirtualRows.vue +220 -0
  64. package/src/pages/data/data-grid/DomDataGrid.vue +1643 -0
  65. package/src/pages/data/document-editor/DomDocumentEditor.vue +3030 -0
  66. package/src/pages/data/document-editor/documentEditorCodeBlock.js +282 -0
  67. package/src/pages/data/document-editor/examples/documentEditorMockApi.js +272 -0
  68. package/src/pages/data/document-editor/examples/useDocumentEditorDemoStore.js +284 -0
  69. package/src/pages/data/filter-bar/DomFilterBar.vue +533 -0
  70. package/src/pages/data/kanban/DomKanban.vue +1703 -0
  71. package/src/pages/data/kanban/kanbanApi.js +80 -0
  72. package/src/pages/data/media-browser/DomMediaBrowser.vue +2344 -0
  73. package/src/pages/data/media-browser/examples/mediaBrowserMockApi.js +653 -0
  74. package/src/pages/data/media-browser/examples/useMediaBrowserDemoStore.js +1157 -0
  75. package/src/pages/data/month-calendar/DomMonthCalendar.vue +1718 -0
  76. package/src/pages/data/week-planner/DomWeekPlanner.vue +2308 -0
  77. package/src/pages/dev/code-block/DomCodeBlock.vue +263 -0
  78. package/src/pages/dev/json-viewer/DomJsonViewer.vue +962 -0
  79. package/src/pages/forms/_shared/DomFieldLoadingSpinner.vue +12 -0
  80. package/src/pages/forms/_shared/options.js +61 -0
  81. package/src/pages/forms/_shared/useOptionMenu.js +59 -0
  82. package/src/pages/forms/autocomplete/DomAutocomplete.vue +180 -0
  83. package/src/pages/forms/calendar/DomCalendar.vue +442 -0
  84. package/src/pages/forms/checkbox/DomCheckbox.vue +69 -0
  85. package/src/pages/forms/class-toggle-input/DomClassToggleInput.vue +316 -0
  86. package/src/pages/forms/code-input/DomCodeInput.vue +202 -0
  87. package/src/pages/forms/color-input/DomColorInput.vue +118 -0
  88. package/src/pages/forms/combobox/DomCombobox.vue +237 -0
  89. package/src/pages/forms/confirmation-code-input/DomConfirmationCodeInput.vue +345 -0
  90. package/src/pages/forms/date-picker/DomDatePicker.vue +299 -0
  91. package/src/pages/forms/date-time-picker/DomDateTimePicker.vue +630 -0
  92. package/src/pages/forms/email-input/DomEmailInput.vue +55 -0
  93. package/src/pages/forms/field/DomField.vue +267 -0
  94. package/src/pages/forms/field/fieldLayout.js +1 -0
  95. package/src/pages/forms/field/fieldProps.js +114 -0
  96. package/src/pages/forms/field/layouts/DomGridFieldLayout.vue +38 -0
  97. package/src/pages/forms/field/useField.js +343 -0
  98. package/src/pages/forms/field/validators.js +364 -0
  99. package/src/pages/forms/file-upload/DomFileUpload.vue +361 -0
  100. package/src/pages/forms/form/DomForm.vue +985 -0
  101. package/src/pages/forms/form/adapters/fieldBuilder.js +45 -0
  102. package/src/pages/forms/form/adapters/jsonSchema.js +264 -0
  103. package/src/pages/forms/form/adapters/shared.js +53 -0
  104. package/src/pages/forms/form/adapters/zod.js +235 -0
  105. package/src/pages/forms/form/formApi.js +38 -0
  106. package/src/pages/forms/form/formDefinition.js +392 -0
  107. package/src/pages/forms/form/schemaAdapter.js +54 -0
  108. package/src/pages/forms/icon-selector/DomIconSelector.vue +460 -0
  109. package/src/pages/forms/icon-selector/iconSelectorItems.js +490 -0
  110. package/src/pages/forms/input-group/DomInputGroup.vue +232 -0
  111. package/src/pages/forms/json-input/DomJsonInput.vue +227 -0
  112. package/src/pages/forms/json-list-input/DomJsonListInput.vue +206 -0
  113. package/src/pages/forms/listbox/DomListbox.vue +105 -0
  114. package/src/pages/forms/money-input/DomMoneyInput.vue +147 -0
  115. package/src/pages/forms/native-select/DomNativeSelect.vue +67 -0
  116. package/src/pages/forms/number-input/DomNumberInput.vue +47 -0
  117. package/src/pages/forms/password-input/DomPasswordInput.vue +218 -0
  118. package/src/pages/forms/password-input/pwnedPasswords.js +47 -0
  119. package/src/pages/forms/password-strength/DomPasswordStrength.vue +128 -0
  120. package/src/pages/forms/password-strength/passwordStrength.js +92 -0
  121. package/src/pages/forms/position-input/DomPositionInput.vue +83 -0
  122. package/src/pages/forms/radio-group/DomRadioGroup.vue +102 -0
  123. package/src/pages/forms/range-input/DomRangeInput.vue +296 -0
  124. package/src/pages/forms/select/DomSelect.vue +479 -0
  125. package/src/pages/forms/select-input/DomSelectInput.vue +155 -0
  126. package/src/pages/forms/tag-combobox/DomTagCombobox.vue +358 -0
  127. package/src/pages/forms/text-input/DomTextInput.vue +48 -0
  128. package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +234 -0
  129. package/src/pages/forms/textarea-input/DomTextareaInput.vue +86 -0
  130. package/src/pages/forms/textarea-input/useTextareaAutosize.js +147 -0
  131. package/src/pages/forms/time-selector/DomTimeSelector.vue +389 -0
  132. package/src/pages/forms/time-selector/timeUtils.js +194 -0
  133. package/src/pages/forms/toggle/DomToggle.vue +158 -0
  134. package/src/pages/forms/toggle-button/DomToggleButton.vue +88 -0
  135. package/src/pages/forms/toggle-button-group/DomToggleButtonGroup.vue +340 -0
  136. package/src/pages/forms/url-input/DomUrlInput.vue +55 -0
  137. package/src/pages/forms/wysiwyg/DomWysiwyg.vue +204 -0
  138. package/src/pages/lib/css/color.js +245 -0
  139. package/src/pages/lib/headless/accordion.d.ts +7 -0
  140. package/src/pages/lib/headless/accordion.js +246 -0
  141. package/src/pages/lib/headless/autocomplete.d.ts +4 -0
  142. package/src/pages/lib/headless/autocomplete.js +33 -0
  143. package/src/pages/lib/headless/base.d.ts +25 -0
  144. package/src/pages/lib/headless/base.js +127 -0
  145. package/src/pages/lib/headless/checkbox.d.ts +19 -0
  146. package/src/pages/lib/headless/checkbox.js +99 -0
  147. package/src/pages/lib/headless/combobox.d.ts +7 -0
  148. package/src/pages/lib/headless/combobox.js +488 -0
  149. package/src/pages/lib/headless/dialog.d.ts +23 -0
  150. package/src/pages/lib/headless/dialog.js +384 -0
  151. package/src/pages/lib/headless/drawer.d.ts +21 -0
  152. package/src/pages/lib/headless/drawer.js +682 -0
  153. package/src/pages/lib/headless/dropdown.d.ts +19 -0
  154. package/src/pages/lib/headless/dropdown.js +412 -0
  155. package/src/pages/lib/headless/floating-effect.js +118 -0
  156. package/src/pages/lib/headless/floating.d.ts +34 -0
  157. package/src/pages/lib/headless/floating.js +382 -0
  158. package/src/pages/lib/headless/index.d.ts +19 -0
  159. package/src/pages/lib/headless/index.js +19 -0
  160. package/src/pages/lib/headless/listbox.d.ts +19 -0
  161. package/src/pages/lib/headless/listbox.js +126 -0
  162. package/src/pages/lib/headless/menu.d.ts +18 -0
  163. package/src/pages/lib/headless/menu.js +117 -0
  164. package/src/pages/lib/headless/popover-panel.d.ts +18 -0
  165. package/src/pages/lib/headless/popover-panel.js +309 -0
  166. package/src/pages/lib/headless/popover.d.ts +19 -0
  167. package/src/pages/lib/headless/popover.js +391 -0
  168. package/src/pages/lib/headless/radio-group.d.ts +19 -0
  169. package/src/pages/lib/headless/radio-group.js +100 -0
  170. package/src/pages/lib/headless/tabs.d.ts +18 -0
  171. package/src/pages/lib/headless/tabs.js +119 -0
  172. package/src/pages/lib/headless/toast.d.ts +13 -0
  173. package/src/pages/lib/headless/toast.js +65 -0
  174. package/src/pages/lib/headless/toggle.d.ts +19 -0
  175. package/src/pages/lib/headless/toggle.js +162 -0
  176. package/src/pages/lib/headless/tooltip-controller.d.ts +22 -0
  177. package/src/pages/lib/headless/tooltip-controller.js +549 -0
  178. package/src/pages/lib/headless/tooltip.d.ts +19 -0
  179. package/src/pages/lib/headless/tooltip.js +153 -0
  180. package/src/pages/lib/headless/transitions.d.ts +5 -0
  181. package/src/pages/lib/headless/transitions.js +89 -0
  182. package/src/pages/lib/vue/_internal/DomLiquidGlassSurface.vue +310 -0
  183. package/src/pages/lib/vue/_internal/liquidGlass.js +112 -0
  184. package/src/pages/lib/vue/_internal/liquidGlassConfig.js +10 -0
  185. package/src/pages/lib/vue/codeHighlighter.js +132 -0
  186. package/src/pages/lib/vue/floatingTeleportTarget.js +23 -0
  187. package/src/pages/lib/vue/index.d.ts +6806 -0
  188. package/src/pages/lib/vue/index.js +218 -0
  189. package/src/pages/lib/vue/useTheme.js +116 -0
  190. package/src/pages/mobile/action-sheet/DomActionSheet.vue +186 -0
  191. package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +94 -0
  192. package/src/pages/mobile/list-item/DomAppListItem.vue +90 -0
  193. package/src/pages/mobile/safe-area/DomSafeArea.vue +39 -0
  194. package/src/pages/mobile/shell/DomAppShell.vue +51 -0
  195. package/src/pages/mobile/stack/AppStackScreen.vue +63 -0
  196. package/src/pages/mobile/stack/DomAppStack.vue +711 -0
  197. package/src/pages/mobile/top-bar/DomAppTopBar.vue +67 -0
  198. package/src/pages/visual/alert/DomAlert.vue +363 -0
  199. package/src/pages/visual/avatar/DomAvatar.vue +110 -0
  200. package/src/pages/visual/avatar-stack/DomAvatarStack.vue +89 -0
  201. package/src/pages/visual/badge/DomBadge.vue +105 -0
  202. package/src/pages/visual/card/DomCard.vue +65 -0
  203. package/src/pages/visual/card-rail/DomCardRail.vue +99 -0
  204. package/src/pages/visual/empty-state/DomEmptyState.vue +88 -0
  205. package/src/pages/visual/progress/DomProgress.vue +212 -0
  206. package/src/pages/visual/progress/DomRadialProgress.vue +186 -0
  207. package/src/pages/visual/progress/DomSegmentedProgress.vue +350 -0
  208. package/src/pages/visual/skeleton/DomSkeleton.vue +174 -0
  209. package/src/pages/visual/spinner/DomSpinner.vue +122 -0
  210. package/src/pages/visual/status-pill/DomStatusPill.vue +75 -0
  211. package/src/pages/visual/text-diff/DomTextDiff.vue +577 -0
  212. package/src/pages/visual/text-diff/textDiff.js +300 -0
  213. package/src/style.css +446 -0
@@ -0,0 +1,282 @@
1
+ import {
2
+ CODE_LANGUAGE_OPTIONS,
3
+ codeLanguageOptionValue,
4
+ highlight,
5
+ } from '../../lib/vue/codeHighlighter.js';
6
+
7
+ const DEFAULT_CODE_LANGUAGE = 'txt';
8
+ const HIGHLIGHT_DELAY = 80;
9
+
10
+ export const documentEditorCodeLanguages = CODE_LANGUAGE_OPTIONS;
11
+
12
+ /**
13
+ * Creates the editable code-block node with a persistent DOM Studio language value.
14
+ *
15
+ * @param {unknown} CodeBlock Tiptap code-block extension.
16
+ * @param {Function} mergeAttributes Tiptap HTML attribute merger.
17
+ * @returns {unknown} Configured code-block extension.
18
+ */
19
+ export function createDocumentEditorCodeBlockExtension(CodeBlock, mergeAttributes) {
20
+ return CodeBlock.extend({
21
+ /**
22
+ * Renders a language-aware pre/code structure for saved editor HTML.
23
+ *
24
+ * @param {Record<string, unknown>} context Tiptap render context.
25
+ * @returns {Array<unknown>} ProseMirror DOM output specification.
26
+ */
27
+ renderHTML({ node, HTMLAttributes }) {
28
+ const language = codeLanguageOptionValue(node.attrs.language || DEFAULT_CODE_LANGUAGE);
29
+ const classPrefix = this.options.languageClassPrefix || 'language-';
30
+ return [
31
+ 'pre',
32
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
33
+ class: 'dom-code-block',
34
+ 'data-language': language,
35
+ }),
36
+ [
37
+ 'code',
38
+ { class: `${classPrefix}${language}` },
39
+ 0,
40
+ ],
41
+ ];
42
+ },
43
+ }).configure({
44
+ defaultLanguage: DEFAULT_CODE_LANGUAGE,
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Creates a ProseMirror extension that highlights editable code with shared Shiki output.
50
+ *
51
+ * @param {Record<string, unknown>} modules Tiptap and ProseMirror runtime modules.
52
+ * @returns {unknown} Tiptap highlighting extension.
53
+ */
54
+ export function createDocumentEditorCodeHighlightExtension(modules) {
55
+ const pluginKey = new modules.PluginKey('domDocumentEditorCodeHighlight');
56
+ return modules.Extension.create({
57
+ name: 'domDocumentEditorCodeHighlight',
58
+
59
+ /**
60
+ * Installs the asynchronous syntax-decoration plugin.
61
+ *
62
+ * @returns {Array<unknown>} ProseMirror plugins.
63
+ */
64
+ addProseMirrorPlugins() {
65
+ return [createCodeHighlightPlugin(modules, pluginKey)];
66
+ },
67
+ });
68
+ }
69
+
70
+ /**
71
+ * Creates the stateful ProseMirror plugin that schedules syntax decoration refreshes.
72
+ *
73
+ * @param {Record<string, unknown>} modules Tiptap and ProseMirror runtime modules.
74
+ * @param {unknown} pluginKey ProseMirror plugin key.
75
+ * @returns {unknown} ProseMirror plugin.
76
+ */
77
+ function createCodeHighlightPlugin(modules, pluginKey) {
78
+ return new modules.Plugin({
79
+ key: pluginKey,
80
+ state: {
81
+ /**
82
+ * Initializes the editor with no syntax decorations.
83
+ *
84
+ * @returns {unknown} Empty ProseMirror decoration set.
85
+ */
86
+ init() {
87
+ return modules.DecorationSet.empty;
88
+ },
89
+
90
+ /**
91
+ * Maps existing decorations or applies a completed asynchronous refresh.
92
+ *
93
+ * @param {unknown} transaction ProseMirror transaction.
94
+ * @param {unknown} decorations Current decoration set.
95
+ * @returns {unknown} Next ProseMirror decoration set.
96
+ */
97
+ apply(transaction, decorations) {
98
+ const highlighted = transaction.getMeta(pluginKey);
99
+ if (highlighted !== undefined) return highlighted;
100
+ return decorations.map(transaction.mapping, transaction.doc);
101
+ },
102
+ },
103
+ props: {
104
+ /**
105
+ * Returns the current syntax decorations for the editor view.
106
+ *
107
+ * @param {unknown} state ProseMirror editor state.
108
+ * @returns {unknown} Current ProseMirror decoration set.
109
+ */
110
+ decorations(state) {
111
+ return pluginKey.getState(state);
112
+ },
113
+ },
114
+ /**
115
+ * Creates the browser view lifecycle that debounces asynchronous highlighting.
116
+ *
117
+ * @param {unknown} editorView ProseMirror editor view.
118
+ * @returns {Record<string, Function>} ProseMirror plugin view handlers.
119
+ */
120
+ view(editorView) {
121
+ let destroyed = false;
122
+ let lastSignature = '';
123
+ let requestId = 0;
124
+ let timer = null;
125
+
126
+ /**
127
+ * Schedules a highlight refresh when code content or languages change.
128
+ *
129
+ * @param {unknown} view Current ProseMirror editor view.
130
+ * @param {boolean} immediate Skip the typing debounce for initial rendering.
131
+ * @returns {void}
132
+ */
133
+ function scheduleHighlight(view, immediate = false) {
134
+ const signature = codeBlockSignature(view.state.doc);
135
+ if (signature === lastSignature) return;
136
+ lastSignature = signature;
137
+ requestId += 1;
138
+ const scheduledRequest = requestId;
139
+ if (timer) window.clearTimeout(timer);
140
+ timer = window.setTimeout(async () => {
141
+ const sourceDocument = view.state.doc;
142
+ const decorations = await buildCodeDecorations(sourceDocument, modules);
143
+ if (destroyed || scheduledRequest !== requestId) return;
144
+ if (codeBlockSignature(view.state.doc) !== signature) return;
145
+ view.dispatch(view.state.tr.setMeta(pluginKey, decorations));
146
+ }, immediate ? 0 : HIGHLIGHT_DELAY);
147
+ }
148
+
149
+ scheduleHighlight(editorView, true);
150
+ return {
151
+ /**
152
+ * Refreshes highlighting after document transactions that affect code blocks.
153
+ *
154
+ * @param {unknown} view Updated ProseMirror editor view.
155
+ * @param {unknown} previousState Previous ProseMirror editor state.
156
+ * @returns {void}
157
+ */
158
+ update(view, previousState) {
159
+ if (view.state.doc !== previousState.doc) scheduleHighlight(view);
160
+ },
161
+
162
+ /**
163
+ * Cancels pending syntax work when the editor view is destroyed.
164
+ *
165
+ * @returns {void}
166
+ */
167
+ destroy() {
168
+ destroyed = true;
169
+ requestId += 1;
170
+ if (timer) window.clearTimeout(timer);
171
+ },
172
+ };
173
+ },
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Builds a stable signature for the code blocks that affect syntax decorations.
179
+ *
180
+ * @param {unknown} documentNode ProseMirror document node.
181
+ * @returns {string} Serialized code-block signature.
182
+ */
183
+ function codeBlockSignature(documentNode) {
184
+ return JSON.stringify(collectCodeBlocks(documentNode).map((block) => [block.position, block.language, block.code]));
185
+ }
186
+
187
+ /**
188
+ * Collects language-aware code blocks and their ProseMirror positions.
189
+ *
190
+ * @param {unknown} documentNode ProseMirror document node.
191
+ * @returns {Array<{ position: number, code: string, language: string }>} Code-block records.
192
+ */
193
+ function collectCodeBlocks(documentNode) {
194
+ const blocks = [];
195
+ documentNode.descendants((node, position) => {
196
+ if (node.type.name !== 'codeBlock') return;
197
+ blocks.push({
198
+ position,
199
+ code: node.textContent,
200
+ language: codeLanguageOptionValue(node.attrs.language || DEFAULT_CODE_LANGUAGE),
201
+ });
202
+ });
203
+ return blocks;
204
+ }
205
+
206
+ /**
207
+ * Highlights every code block and combines the results into one decoration set.
208
+ *
209
+ * @param {unknown} documentNode ProseMirror document node.
210
+ * @param {Record<string, unknown>} modules Tiptap and ProseMirror runtime modules.
211
+ * @returns {Promise<unknown>} ProseMirror decoration set.
212
+ */
213
+ async function buildCodeDecorations(documentNode, modules) {
214
+ const blocks = collectCodeBlocks(documentNode);
215
+ const results = await Promise.all(blocks.map((block) => buildBlockDecorations(block, modules.Decoration)));
216
+ return modules.DecorationSet.create(documentNode, results.flat());
217
+ }
218
+
219
+ /**
220
+ * Converts one Shiki-rendered code block into editable inline decorations.
221
+ *
222
+ * @param {{ position: number, code: string, language: string }} block Code-block record.
223
+ * @param {unknown} Decoration ProseMirror decoration factory.
224
+ * @returns {Promise<Array<unknown>>} Inline syntax decorations.
225
+ */
226
+ async function buildBlockDecorations(block, Decoration) {
227
+ const highlighted = await highlight(block.code, block.language);
228
+ if (!highlighted) return [];
229
+ return highlightedStyleRanges(highlighted)
230
+ .filter((range) => range.from < range.to && range.from < block.code.length)
231
+ .map((range) => Decoration.inline(
232
+ block.position + 1 + range.from,
233
+ block.position + 1 + Math.min(range.to, block.code.length),
234
+ {
235
+ class: 'dom-code-token',
236
+ style: range.style,
237
+ },
238
+ ));
239
+ }
240
+
241
+ /**
242
+ * Extracts Shiki token styles and UTF-16 text offsets from highlighted HTML.
243
+ *
244
+ * @param {string} highlightedHtml Shiki HTML output.
245
+ * @returns {Array<{ from: number, to: number, style: string }>} Styled text ranges.
246
+ */
247
+ function highlightedStyleRanges(highlightedHtml) {
248
+ if (typeof DOMParser === 'undefined') return [];
249
+ const parsed = new DOMParser().parseFromString(highlightedHtml, 'text/html');
250
+ const code = parsed.querySelector('pre.shiki code') || parsed.querySelector('code');
251
+ if (!code) return [];
252
+ const ranges = [];
253
+ let offset = 0;
254
+
255
+ /**
256
+ * Walks highlighted nodes while carrying inherited inline token styles.
257
+ *
258
+ * @param {Node} node Current highlighted DOM node.
259
+ * @param {string} inheritedStyle Nearest ancestor token style.
260
+ * @returns {void}
261
+ */
262
+ function visit(node, inheritedStyle = '') {
263
+ if (node.nodeType === 3) {
264
+ const length = node.nodeValue?.length || 0;
265
+ if (length && inheritedStyle) {
266
+ ranges.push({
267
+ from: offset,
268
+ to: offset + length,
269
+ style: inheritedStyle,
270
+ });
271
+ }
272
+ offset += length;
273
+ return;
274
+ }
275
+ if (node.nodeType !== 1) return;
276
+ const style = node.getAttribute('style') || inheritedStyle;
277
+ node.childNodes.forEach((child) => visit(child, style));
278
+ }
279
+
280
+ visit(code);
281
+ return ranges;
282
+ }
@@ -0,0 +1,272 @@
1
+ let revisionCounter = 3;
2
+
3
+ /**
4
+ * Creates an in-memory document API for editor demos.
5
+ *
6
+ * @returns {Record<string, Function>} API methods for document loading, saving, and rewrite suggestions.
7
+ */
8
+ export function createDocumentEditorMockApi() {
9
+ const state = createInitialState();
10
+
11
+ /**
12
+ * Loads a saved document.
13
+ *
14
+ * @returns {Promise<Record<string, unknown>>} Document response.
15
+ */
16
+ async function loadDocument() {
17
+ await wait(220);
18
+ return {
19
+ document: clone(state.document),
20
+ meta: {
21
+ requestedAt: new Date().toISOString(),
22
+ action: 'load-document',
23
+ },
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Saves a document snapshot.
29
+ *
30
+ * @param {{ content: Record<string, unknown>|string, html?: string, text?: string }} request Save request.
31
+ * @returns {Promise<Record<string, unknown>>} Save response.
32
+ */
33
+ async function saveDocument(request = {}) {
34
+ await wait(260);
35
+ revisionCounter += 1;
36
+ state.document = {
37
+ ...state.document,
38
+ content: clone(request.content || state.document.content),
39
+ html: String(request.html || ''),
40
+ text: String(request.text || ''),
41
+ revision: revisionCounter,
42
+ updatedAt: new Date().toISOString(),
43
+ };
44
+ return {
45
+ document: clone(state.document),
46
+ meta: {
47
+ requestedAt: new Date().toISOString(),
48
+ action: 'save-document',
49
+ revision: state.document.revision,
50
+ },
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Generates a rewrite suggestion for the selected editor text.
56
+ *
57
+ * @param {{ selectedText?: string, instruction?: string }} request Rewrite request.
58
+ * @returns {Promise<Record<string, unknown>>} Rewrite response.
59
+ */
60
+ async function rewriteSelection(request = {}) {
61
+ await wait(680);
62
+ const selectedText = String(request.selectedText || '').trim();
63
+ const instruction = String(request.instruction || '').trim();
64
+ const proposed = proposeRewrite(selectedText, instruction);
65
+ return {
66
+ suggestion: {
67
+ id: `sug_ai_${Date.now()}`,
68
+ type: 'replace',
69
+ original: selectedText,
70
+ proposed,
71
+ instruction,
72
+ author: {
73
+ id: 'ai-editor',
74
+ name: 'AI editor',
75
+ color: '#2563eb',
76
+ type: 'ai',
77
+ },
78
+ createdAt: new Date().toISOString(),
79
+ },
80
+ meta: {
81
+ requestedAt: new Date().toISOString(),
82
+ action: 'rewrite-selection',
83
+ },
84
+ };
85
+ }
86
+
87
+ return {
88
+ loadDocument,
89
+ saveDocument,
90
+ rewriteSelection,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Creates initial demo API state.
96
+ *
97
+ * @returns {{ document: Record<string, unknown> }} Initial mock database state.
98
+ */
99
+ function createInitialState() {
100
+ return {
101
+ document: {
102
+ id: 'doc_launch_brief',
103
+ title: 'July launch note',
104
+ revision: revisionCounter,
105
+ updatedAt: '2026-07-07T08:30:00.000Z',
106
+ content: createInitialDocumentContent(),
107
+ html: '',
108
+ text: '',
109
+ },
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Creates the initial editor document in Tiptap JSON format.
115
+ *
116
+ * @returns {Record<string, unknown>} Tiptap document JSON.
117
+ */
118
+ function createInitialDocumentContent() {
119
+ return {
120
+ type: 'doc',
121
+ content: [
122
+ {
123
+ type: 'heading',
124
+ attrs: { level: 1 },
125
+ content: [{ type: 'text', text: 'July product launch note' }],
126
+ },
127
+ {
128
+ type: 'paragraph',
129
+ content: [
130
+ {
131
+ type: 'text',
132
+ text: 'The July release introduces a faster way for teams to draft pages, review AI suggestions, and keep every edit connected to structured document data.',
133
+ },
134
+ ],
135
+ },
136
+ {
137
+ type: 'heading',
138
+ attrs: { level: 2 },
139
+ content: [{ type: 'text', text: 'Audience' }],
140
+ },
141
+ {
142
+ type: 'paragraph',
143
+ content: [
144
+ {
145
+ type: 'text',
146
+ text: 'This note is for product leads, content teams, and developers who need an editor that feels familiar while still giving the application a reliable content model.',
147
+ },
148
+ ],
149
+ },
150
+ {
151
+ type: 'heading',
152
+ attrs: { level: 2 },
153
+ content: [{ type: 'text', text: 'Draft message' }],
154
+ },
155
+ {
156
+ type: 'paragraph',
157
+ content: [
158
+ {
159
+ type: 'text',
160
+ text: 'The new editing workspace should make everyday writing calm and direct. Authors can shape the document by hand, bring in the AI for a specific passage, and review the proposed change before it becomes part of the saved draft.',
161
+ },
162
+ ],
163
+ },
164
+ {
165
+ type: 'bulletList',
166
+ content: [
167
+ createListItem('Position the editor as the place where human writing, AI suggestions, and media choices meet.'),
168
+ createListItem('Keep the saved draft as structured JSON so public rendering can be handled by a separate output pipeline.'),
169
+ createListItem('Use the existing media browser for product images, screenshots, and campaign assets.'),
170
+ ],
171
+ },
172
+ {
173
+ type: 'blockquote',
174
+ content: [
175
+ {
176
+ type: 'paragraph',
177
+ content: [
178
+ {
179
+ type: 'text',
180
+ text: 'Open question: should accepted AI suggestions become normal text immediately, or should the saved revision keep an audit trail for later review?',
181
+ },
182
+ ],
183
+ },
184
+ ],
185
+ },
186
+ {
187
+ type: 'heading',
188
+ attrs: { level: 2 },
189
+ content: [{ type: 'text', text: 'Next edit' }],
190
+ },
191
+ {
192
+ type: 'paragraph',
193
+ content: [
194
+ {
195
+ type: 'text',
196
+ text: 'Tighten the draft message, add a product screenshot, and make the audience section less formal before the next review.',
197
+ },
198
+ ],
199
+ },
200
+ ],
201
+ };
202
+ }
203
+
204
+ /**
205
+ * Creates a Tiptap list item.
206
+ *
207
+ * @param {string} text List item text.
208
+ * @returns {Record<string, unknown>} Tiptap list item node.
209
+ */
210
+ function createListItem(text) {
211
+ return {
212
+ type: 'listItem',
213
+ content: [
214
+ {
215
+ type: 'paragraph',
216
+ content: [{ type: 'text', text }],
217
+ },
218
+ ],
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Builds a deterministic rewrite from a selected phrase and instruction.
224
+ *
225
+ * @param {string} selectedText Selected editor text.
226
+ * @param {string} instruction Rewrite instruction.
227
+ * @returns {string} Proposed rewrite.
228
+ */
229
+ function proposeRewrite(selectedText, instruction) {
230
+ if (!selectedText) return 'Select copy in the editor before asking the AI for a rewrite.';
231
+ const cleaned = selectedText.replace(/\s+/g, ' ').trim();
232
+ if (/short|concise|tight/i.test(instruction)) {
233
+ return tightenCopy(cleaned);
234
+ }
235
+ if (/formal|polish|professional/i.test(instruction)) {
236
+ return `This editor gives teams a controlled writing surface where human edits, AI suggestions, and publishing requirements can move through one reviewable document workflow.`;
237
+ }
238
+ return `The editor should let people write naturally, invite AI into a selected passage, and review the proposed change before it alters the saved document.`;
239
+ }
240
+
241
+ /**
242
+ * Produces a shorter version of selected copy.
243
+ *
244
+ * @param {string} value Selected text.
245
+ * @returns {string} Concise rewrite.
246
+ */
247
+ function tightenCopy(value) {
248
+ if (value.length < 120) return value;
249
+ return `${value.slice(0, 112).replace(/[,\s]+$/, '')}.`;
250
+ }
251
+
252
+ /**
253
+ * Waits for a small delay to make loading and pending states visible.
254
+ *
255
+ * @param {number} ms Delay in milliseconds.
256
+ * @returns {Promise<void>} Resolves after the delay.
257
+ */
258
+ function wait(ms) {
259
+ return new Promise((resolve) => {
260
+ setTimeout(resolve, ms);
261
+ });
262
+ }
263
+
264
+ /**
265
+ * Deep-clones JSON-compatible data.
266
+ *
267
+ * @param {unknown} value Value to clone.
268
+ * @returns {unknown} Cloned value.
269
+ */
270
+ function clone(value) {
271
+ return JSON.parse(JSON.stringify(value));
272
+ }