@rynt/sdk 0.9.53

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 (332) hide show
  1. package/README.md +122 -0
  2. package/REGISTRIES.md +189 -0
  3. package/env.d.ts +11 -0
  4. package/host-shims.d.ts +30 -0
  5. package/package.json +88 -0
  6. package/src/extension-marketplace/api-types.ts +141 -0
  7. package/src/extension-marketplace/client.ts +296 -0
  8. package/src/extension-marketplace/index.ts +22 -0
  9. package/src/extension-marketplace/schemas.ts +178 -0
  10. package/src/extensions/ExtensionRoutePage.vue +17 -0
  11. package/src/extensions/context.ts +37 -0
  12. package/src/extensions/disabled-folder.ts +21 -0
  13. package/src/extensions/extension-expose-map.ts +5 -0
  14. package/src/extensions/extension-expose.ts +48 -0
  15. package/src/extensions/graph.ts +67 -0
  16. package/src/extensions/index.ts +251 -0
  17. package/src/extensions/invite-handler/types.ts +20 -0
  18. package/src/extensions/launcher-entities/create-launcher-entity.ts +25 -0
  19. package/src/extensions/launcher-entities/keys.ts +46 -0
  20. package/src/extensions/launcher-entities/launcher-entity-components.ts +177 -0
  21. package/src/extensions/launcher-entities/props-map.ts +69 -0
  22. package/src/extensions/launcher-entities/registry.ts +32 -0
  23. package/src/extensions/launcher-models/apis/accounts-contracts.ts +102 -0
  24. package/src/extensions/launcher-models/apis/launcher-model-apis.ts +553 -0
  25. package/src/extensions/launcher-models/keys.ts +23 -0
  26. package/src/extensions/launcher-models/public.ts +9 -0
  27. package/src/extensions/launcher-models/registry-core.ts +34 -0
  28. package/src/extensions/manifest-types.ts +22 -0
  29. package/src/extensions/manifest.ts +46 -0
  30. package/src/extensions/marketplace-open-key.ts +26 -0
  31. package/src/extensions/plugin-types.ts +44 -0
  32. package/src/extensions/plugin.ts +62 -0
  33. package/src/extensions/registries/bootstrap.ts +11 -0
  34. package/src/extensions/registries/builtins/account-provider.ts +6 -0
  35. package/src/extensions/registries/builtins/app-topbar-left-widgets.ts +6 -0
  36. package/src/extensions/registries/builtins/app-topbar-right-widgets.ts +6 -0
  37. package/src/extensions/registries/builtins/app-topbar-status-widgets.ts +6 -0
  38. package/src/extensions/registries/builtins/build-card-actions.ts +6 -0
  39. package/src/extensions/registries/builtins/build-card-after-meta.ts +6 -0
  40. package/src/extensions/registries/builtins/build-card-before-media.ts +6 -0
  41. package/src/extensions/registries/builtins/build-card-before-meta.ts +6 -0
  42. package/src/extensions/registries/builtins/build-card-footer-actions.ts +6 -0
  43. package/src/extensions/registries/builtins/build-detail-after-content.ts +6 -0
  44. package/src/extensions/registries/builtins/build-detail-before-content.ts +6 -0
  45. package/src/extensions/registries/builtins/build-detail-before-hero.ts +6 -0
  46. package/src/extensions/registries/builtins/build-detail-header-actions.ts +6 -0
  47. package/src/extensions/registries/builtins/build-detail-mod-row-actions.ts +6 -0
  48. package/src/extensions/registries/builtins/build-detail-resourcepack-row-actions.ts +6 -0
  49. package/src/extensions/registries/builtins/build-detail-right-column-bottom.ts +6 -0
  50. package/src/extensions/registries/builtins/build-detail-right-column-top.ts +6 -0
  51. package/src/extensions/registries/builtins/dialog-footer-actions.ts +6 -0
  52. package/src/extensions/registries/builtins/feed-after-content.ts +6 -0
  53. package/src/extensions/registries/builtins/feed-before-content.ts +6 -0
  54. package/src/extensions/registries/builtins/file-editor.ts +19 -0
  55. package/src/extensions/registries/builtins/friends-after-list.ts +6 -0
  56. package/src/extensions/registries/builtins/friends-before-list.ts +6 -0
  57. package/src/extensions/registries/builtins/index.ts +141 -0
  58. package/src/extensions/registries/builtins/invite-handler.ts +7 -0
  59. package/src/extensions/registries/builtins/library-after-content.ts +6 -0
  60. package/src/extensions/registries/builtins/library-before-content.ts +6 -0
  61. package/src/extensions/registries/builtins/loader.ts +8 -0
  62. package/src/extensions/registries/builtins/map-card-actions.ts +6 -0
  63. package/src/extensions/registries/builtins/map-card-after-meta.ts +6 -0
  64. package/src/extensions/registries/builtins/map-card-before-meta.ts +6 -0
  65. package/src/extensions/registries/builtins/map-card-footer-actions.ts +6 -0
  66. package/src/extensions/registries/builtins/map-detail-after-content.ts +6 -0
  67. package/src/extensions/registries/builtins/map-detail-before-content.ts +6 -0
  68. package/src/extensions/registries/builtins/map-detail-header-actions.ts +6 -0
  69. package/src/extensions/registries/builtins/markdown-editor-tiptap-extensions.ts +7 -0
  70. package/src/extensions/registries/builtins/markdown-editor-toolbar-actions.ts +6 -0
  71. package/src/extensions/registries/builtins/markdown-renderer-after-content.ts +6 -0
  72. package/src/extensions/registries/builtins/markdown-renderer-before-content.ts +6 -0
  73. package/src/extensions/registries/builtins/mod-details-footer-actions.ts +6 -0
  74. package/src/extensions/registries/builtins/mod-manage-actions.ts +6 -0
  75. package/src/extensions/registries/builtins/mod-provider.ts +5 -0
  76. package/src/extensions/registries/builtins/nav.ts +7 -0
  77. package/src/extensions/registries/builtins/page.ts +13 -0
  78. package/src/extensions/registries/builtins/projects-after-content.ts +6 -0
  79. package/src/extensions/registries/builtins/projects-before-content.ts +6 -0
  80. package/src/extensions/registries/builtins/resourcepack-manage-actions.ts +7 -0
  81. package/src/extensions/registries/builtins/server-card-actions.ts +6 -0
  82. package/src/extensions/registries/builtins/server-card-after-meta.ts +6 -0
  83. package/src/extensions/registries/builtins/server-card-before-meta.ts +6 -0
  84. package/src/extensions/registries/builtins/server-card-footer-actions.ts +6 -0
  85. package/src/extensions/registries/builtins/server-detail-after-content.ts +6 -0
  86. package/src/extensions/registries/builtins/server-detail-before-content.ts +6 -0
  87. package/src/extensions/registries/builtins/server-detail-header-actions.ts +6 -0
  88. package/src/extensions/registries/builtins/settings-after-sections.ts +6 -0
  89. package/src/extensions/registries/builtins/settings-before-sections.ts +6 -0
  90. package/src/extensions/registries/builtins/settings-section-widgets.ts +6 -0
  91. package/src/extensions/registries/builtins/shaderpack-manage-actions.ts +7 -0
  92. package/src/extensions/registries/builtins/shell.ts +5 -0
  93. package/src/extensions/registries/builtins/sidebar-after-content.ts +6 -0
  94. package/src/extensions/registries/builtins/sidebar-before-content.ts +6 -0
  95. package/src/extensions/registries/builtins/sidebar-footer-widgets.ts +6 -0
  96. package/src/extensions/registries/builtins/sidebar-header-widgets.ts +6 -0
  97. package/src/extensions/registries/builtins/sidebar.ts +11 -0
  98. package/src/extensions/registries/builtins/theme.ts +5 -0
  99. package/src/extensions/registries/builtins/user-card-after-meta.ts +6 -0
  100. package/src/extensions/registries/builtins/user-card-before-meta.ts +6 -0
  101. package/src/extensions/registries/builtins/user-menu-actions.ts +6 -0
  102. package/src/extensions/registries/builtins/user-menu-after-actions.ts +6 -0
  103. package/src/extensions/registries/builtins/user-menu-before-actions.ts +6 -0
  104. package/src/extensions/registries/builtins/user-strip.ts +5 -0
  105. package/src/extensions/registries/clear-extension-ui-registries.ts +15 -0
  106. package/src/extensions/registries/define-extension-registry.ts +58 -0
  107. package/src/extensions/registries/extension-host-api.ts +41 -0
  108. package/src/extensions/registries/extension-registry-api.ts +103 -0
  109. package/src/extensions/registries/extension-registry-payload-map.ts +9 -0
  110. package/src/extensions/registries/extension-scope.ts +41 -0
  111. package/src/extensions/registries/get-registry.ts +23 -0
  112. package/src/extensions/registries/index.ts +58 -0
  113. package/src/extensions/registries/manifest-rynt.ts +193 -0
  114. package/src/extensions/registries/registry-slot.ts +40 -0
  115. package/src/extensions/registries/registry-value-map.ts +89 -0
  116. package/src/extensions/registries/store.ts +206 -0
  117. package/src/extensions/resolve-extensions.ts +245 -0
  118. package/src/extensions/router-bridge.ts +103 -0
  119. package/src/extensions/session.ts +6 -0
  120. package/src/extensions/slug.ts +23 -0
  121. package/src/extensions/version.ts +147 -0
  122. package/src/host/extensions-composables.ts +33 -0
  123. package/src/host/extensions-init.ts +194 -0
  124. package/src/host/index.ts +11 -0
  125. package/src/host/launcher-models/index.ts +4 -0
  126. package/src/index.ts +229 -0
  127. package/src/minecraft-loader/base-loader.ts +102 -0
  128. package/src/minecraft-loader/index.ts +11 -0
  129. package/src/minecraft-loader/loader-registry.ts +72 -0
  130. package/src/shared/api/assets.ts +112 -0
  131. package/src/shared/api/auth.ts +283 -0
  132. package/src/shared/api/builds.ts +647 -0
  133. package/src/shared/api/config.ts +19 -0
  134. package/src/shared/api/download-stats.ts +103 -0
  135. package/src/shared/api/downloads.ts +36 -0
  136. package/src/shared/api/entity-authorship.ts +60 -0
  137. package/src/shared/api/events.ts +393 -0
  138. package/src/shared/api/friends.ts +140 -0
  139. package/src/shared/api/graphql.ts +87 -0
  140. package/src/shared/api/index.ts +23 -0
  141. package/src/shared/api/invites.ts +262 -0
  142. package/src/shared/api/library.ts +44 -0
  143. package/src/shared/api/maps.ts +385 -0
  144. package/src/shared/api/notify-websocket.ts +140 -0
  145. package/src/shared/api/posts.ts +357 -0
  146. package/src/shared/api/projectServers.ts +379 -0
  147. package/src/shared/api/serverMembers.ts +173 -0
  148. package/src/shared/api/users.ts +294 -0
  149. package/src/shared/composables/buildEditor/useBuildEditor.ts +66 -0
  150. package/src/shared/composables/buildManifest/buildManifest.ts +447 -0
  151. package/src/shared/composables/filesEditor/filesEditor.ts +346 -0
  152. package/src/shared/composables/index.ts +10 -0
  153. package/src/shared/composables/modsEditor/modsEditor.ts +1678 -0
  154. package/src/shared/composables/registrySlot/registry-slot-utils.ts +25 -0
  155. package/src/shared/composables/registrySlot/useRegistrySlotMissing.ts +35 -0
  156. package/src/shared/composables/resourcePacksEditor/resourcePacksEditor.ts +448 -0
  157. package/src/shared/composables/shaderPacksEditor/shaderPacksEditor.ts +395 -0
  158. package/src/shared/composables/useSkinRender.ts +70 -0
  159. package/src/shared/composables/useZlDeepLink.ts +178 -0
  160. package/src/shared/definitions/defineGraphCache.ts +216 -0
  161. package/src/shared/definitions/defineStore.ts +32 -0
  162. package/src/shared/definitions/index.ts +2 -0
  163. package/src/shared/minecraft-types/build-manifest.ts +611 -0
  164. package/src/shared/minecraft-types/index.ts +3 -0
  165. package/src/shared/minecraft-types/launcher-versions.ts +32 -0
  166. package/src/shared/minecraft-types/minecraft-launcher-types.ts +276 -0
  167. package/src/shared/mocks/index.ts +1 -0
  168. package/src/shared/mocks/navigation.ts +17 -0
  169. package/src/shared/mods/http.ts +45 -0
  170. package/src/shared/mods/index.ts +5 -0
  171. package/src/shared/mods/marketplace-editor-search.ts +266 -0
  172. package/src/shared/mods/marketplace-search-utils.ts +42 -0
  173. package/src/shared/mods/mod-marketplace-registry.ts +66 -0
  174. package/src/shared/mods/mod-marketplace-types.ts +28 -0
  175. package/src/shared/mods/providers/curseforge.ts +464 -0
  176. package/src/shared/mods/providers/index.ts +8 -0
  177. package/src/shared/mods/providers/modrinth.ts +402 -0
  178. package/src/shared/mods/resolve-mods-provider-loader-ids.ts +77 -0
  179. package/src/shared/mods/types.ts +76 -0
  180. package/src/shared/styles/index.css +713 -0
  181. package/src/shared/themes/index.ts +23 -0
  182. package/src/shared/themes/theme-tokens-black.json +126 -0
  183. package/src/shared/themes/theme-tokens-classic.json +126 -0
  184. package/src/shared/themes/theme-tokens-pink.json +126 -0
  185. package/src/shared/themes/theme-tokens.json +126 -0
  186. package/src/shared/themes/types.ts +85 -0
  187. package/src/shared/types/API_DOCUMENTATION.md +422 -0
  188. package/src/shared/types/account.ts +40 -0
  189. package/src/shared/types/build.ts +8 -0
  190. package/src/shared/types/entities.ts +181 -0
  191. package/src/shared/types/index.ts +6 -0
  192. package/src/shared/types/invite-payloads.ts +60 -0
  193. package/src/shared/types/navigation.ts +16 -0
  194. package/src/shared/types/running-build.ts +51 -0
  195. package/src/shared/types/serverMember.ts +17 -0
  196. package/src/shared/types/user.ts +55 -0
  197. package/src/shared/ui/base/Avatar.vue +262 -0
  198. package/src/shared/ui/base/Badge.vue +47 -0
  199. package/src/shared/ui/base/Button.vue +78 -0
  200. package/src/shared/ui/base/Divider.vue +42 -0
  201. package/src/shared/ui/base/Icon.vue +597 -0
  202. package/src/shared/ui/base/StatusIndicator.vue +44 -0
  203. package/src/shared/ui/base/index.ts +7 -0
  204. package/src/shared/ui/cards/InviteCard.vue +47 -0
  205. package/src/shared/ui/cards/index.ts +2 -0
  206. package/src/shared/ui/dialog/Dialog.vue +71 -0
  207. package/src/shared/ui/dialog/DialogContent.vue +31 -0
  208. package/src/shared/ui/dialog/DialogFooter.vue +14 -0
  209. package/src/shared/ui/dialog/DialogHeader.vue +41 -0
  210. package/src/shared/ui/dialog/index.ts +5 -0
  211. package/src/shared/ui/editors/AttachmentImagesEditor.vue +133 -0
  212. package/src/shared/ui/editors/ContentAttachmentsDisplay.vue +76 -0
  213. package/src/shared/ui/editors/MarkdownEditor.vue +956 -0
  214. package/src/shared/ui/editors/MarkdownRenderer.vue +299 -0
  215. package/src/shared/ui/editors/RichContentImageViewer.vue +85 -0
  216. package/src/shared/ui/editors/SocialPostMediaZone.vue +320 -0
  217. package/src/shared/ui/editors/index.ts +6 -0
  218. package/src/shared/ui/editors/markdown-editor-gallery.ts +234 -0
  219. package/src/shared/ui/editors/markdown-editor-image.ts +178 -0
  220. package/src/shared/ui/form/Checkbox.vue +38 -0
  221. package/src/shared/ui/form/FormField.vue +30 -0
  222. package/src/shared/ui/form/FormGrid.vue +38 -0
  223. package/src/shared/ui/form/ImageEditor.vue +598 -0
  224. package/src/shared/ui/form/Input.vue +72 -0
  225. package/src/shared/ui/form/Range.vue +65 -0
  226. package/src/shared/ui/form/Select.vue +76 -0
  227. package/src/shared/ui/form/Switch.vue +38 -0
  228. package/src/shared/ui/form/Textarea.vue +144 -0
  229. package/src/shared/ui/form/index.ts +9 -0
  230. package/src/shared/ui/index.ts +9 -0
  231. package/src/shared/ui/layout/BusyOverlay.vue +31 -0
  232. package/src/shared/ui/layout/Callout.vue +44 -0
  233. package/src/shared/ui/layout/Card.vue +38 -0
  234. package/src/shared/ui/layout/Container.vue +36 -0
  235. package/src/shared/ui/layout/EmptyState.vue +99 -0
  236. package/src/shared/ui/layout/EntityMediaRow.vue +54 -0
  237. package/src/shared/ui/layout/FilterResultsLayout.vue +22 -0
  238. package/src/shared/ui/layout/FloatingPanel.vue +37 -0
  239. package/src/shared/ui/layout/FullscreenDimmer.vue +11 -0
  240. package/src/shared/ui/layout/Grid.vue +40 -0
  241. package/src/shared/ui/layout/Inline.vue +59 -0
  242. package/src/shared/ui/layout/LoadingState.vue +39 -0
  243. package/src/shared/ui/layout/MediaBox.vue +47 -0
  244. package/src/shared/ui/layout/OverlayPanel.vue +28 -0
  245. package/src/shared/ui/layout/OverlayWaitPanel.vue +22 -0
  246. package/src/shared/ui/layout/PageSection.vue +43 -0
  247. package/src/shared/ui/layout/PageToolbar.vue +29 -0
  248. package/src/shared/ui/layout/Panel.vue +39 -0
  249. package/src/shared/ui/layout/ProgressBar.vue +49 -0
  250. package/src/shared/ui/layout/Section.vue +30 -0
  251. package/src/shared/ui/layout/SegmentedControl.vue +43 -0
  252. package/src/shared/ui/layout/SelectableCard.vue +46 -0
  253. package/src/shared/ui/layout/SelectableRow.vue +41 -0
  254. package/src/shared/ui/layout/Skeleton.vue +25 -0
  255. package/src/shared/ui/layout/SkeletonAvatar.vue +30 -0
  256. package/src/shared/ui/layout/SkeletonEntityCard.vue +20 -0
  257. package/src/shared/ui/layout/SkeletonFeedPost.vue +22 -0
  258. package/src/shared/ui/layout/SkeletonGrid.vue +18 -0
  259. package/src/shared/ui/layout/SkeletonListRow.vue +31 -0
  260. package/src/shared/ui/layout/SkeletonText.vue +25 -0
  261. package/src/shared/ui/layout/Stack.vue +42 -0
  262. package/src/shared/ui/layout/StateBlock.vue +44 -0
  263. package/src/shared/ui/layout/TwoPaneLayout.vue +35 -0
  264. package/src/shared/ui/layout/VirtualList.vue +160 -0
  265. package/src/shared/ui/layout/index.ts +35 -0
  266. package/src/shared/ui/layout/skeletonSurfaceStyles.ts +24 -0
  267. package/src/shared/ui/navigation/NavItem.vue +139 -0
  268. package/src/shared/ui/navigation/Tab.vue +61 -0
  269. package/src/shared/ui/navigation/Tabs.vue +37 -0
  270. package/src/shared/ui/navigation/index.ts +4 -0
  271. package/src/shared/ui/primitives/Action.vue +19 -0
  272. package/src/shared/ui/primitives/Block.vue +28 -0
  273. package/src/shared/ui/primitives/CanvasView.vue +19 -0
  274. package/src/shared/ui/primitives/Control.vue +24 -0
  275. package/src/shared/ui/primitives/ControlSelect.vue +19 -0
  276. package/src/shared/ui/primitives/ControlTextarea.vue +17 -0
  277. package/src/shared/ui/primitives/FieldLabel.vue +19 -0
  278. package/src/shared/ui/primitives/Form.vue +19 -0
  279. package/src/shared/ui/primitives/Heading.vue +29 -0
  280. package/src/shared/ui/primitives/Image.vue +17 -0
  281. package/src/shared/ui/primitives/LineBreak.vue +3 -0
  282. package/src/shared/ui/primitives/Link.vue +19 -0
  283. package/src/shared/ui/primitives/List.vue +28 -0
  284. package/src/shared/ui/primitives/ListItem.vue +19 -0
  285. package/src/shared/ui/primitives/OptionItem.vue +19 -0
  286. package/src/shared/ui/primitives/Text.vue +28 -0
  287. package/src/shared/ui/primitives/VideoView.vue +19 -0
  288. package/src/shared/ui/primitives/index.ts +19 -0
  289. package/src/shared/ui/primitives/resolveElement.ts +25 -0
  290. package/src/shared/ui/special/AngularAccent.vue +106 -0
  291. package/src/shared/ui/special/ExtensionRegistrySlotButton.vue +143 -0
  292. package/src/shared/ui/special/InfoRow.vue +39 -0
  293. package/src/shared/ui/special/LogViewer.vue +53 -0
  294. package/src/shared/ui/special/PageHeader.vue +23 -0
  295. package/src/shared/ui/special/RegistrySlotMissingCallout.vue +48 -0
  296. package/src/shared/ui/special/WelcomeCard.vue +32 -0
  297. package/src/shared/ui/special/index.ts +9 -0
  298. package/src/shared/utils/app-paths.ts +50 -0
  299. package/src/shared/utils/attachments.ts +16 -0
  300. package/src/shared/utils/autostart.ts +213 -0
  301. package/src/shared/utils/build-files.ts +439 -0
  302. package/src/shared/utils/build-manifest-init.ts +176 -0
  303. package/src/shared/utils/cloudinary.ts +67 -0
  304. package/src/shared/utils/cn.ts +7 -0
  305. package/src/shared/utils/download-stats-week.ts +165 -0
  306. package/src/shared/utils/entity-api-to-cache.ts +84 -0
  307. package/src/shared/utils/entity-build-from-api.ts +1 -0
  308. package/src/shared/utils/entity-display.ts +27 -0
  309. package/src/shared/utils/entity-map-from-api.ts +1 -0
  310. package/src/shared/utils/file-hash.ts +65 -0
  311. package/src/shared/utils/formatSize.ts +5 -0
  312. package/src/shared/utils/formatTime.ts +157 -0
  313. package/src/shared/utils/getAccountSkinRender.ts +32 -0
  314. package/src/shared/utils/index.ts +34 -0
  315. package/src/shared/utils/local-mods.ts +678 -0
  316. package/src/shared/utils/local-settings.ts +217 -0
  317. package/src/shared/utils/member-join-stats.ts +35 -0
  318. package/src/shared/utils/platform.ts +86 -0
  319. package/src/shared/utils/play-host-slug.ts +92 -0
  320. package/src/shared/utils/rich-content.ts +294 -0
  321. package/src/shared/utils/safeRequest.ts +23 -0
  322. package/src/shared/utils/semver.ts +81 -0
  323. package/src/shared/utils/serverPermissions.ts +155 -0
  324. package/src/shared/utils/skin-render-cache.ts +372 -0
  325. package/src/shared/utils/stripMarkdown.ts +45 -0
  326. package/src/shared/utils/transliterate.ts +74 -0
  327. package/src/shared/utils/updateAccountSkinRender.ts +64 -0
  328. package/src/shared/utils/updater.ts +218 -0
  329. package/src/shared/utils/uploadImage.ts +195 -0
  330. package/src/shared/utils/user-status.ts +9 -0
  331. package/src/tiptap/index.ts +7 -0
  332. package/tsconfig.json +13 -0
@@ -0,0 +1,956 @@
1
+ <template>
2
+ <div ref="editorContainerRef" class="markdown-editor relative">
3
+ <Transition name="toolbar">
4
+ <div
5
+ v-if="mode === 'inline' && hasSelection && editor"
6
+ class="absolute flex bg-surface-overlay border border-border shadow-lg z-40"
7
+ :style="toolbarStyle"
8
+ >
9
+ <button
10
+ type="button"
11
+ @mousedown.prevent="editor.chain().focus().toggleBold().run()"
12
+ :class="toolbarBtnClass(editor.isActive('bold'))"
13
+ title="Жирный (Ctrl+B)"
14
+ >
15
+ B
16
+ </button>
17
+ <button
18
+ type="button"
19
+ @mousedown.prevent="editor.chain().focus().toggleItalic().run()"
20
+ :class="toolbarBtnClass(editor.isActive('italic'))"
21
+ title="Курсив (Ctrl+I)"
22
+ >
23
+ I
24
+ </button>
25
+ <button
26
+ type="button"
27
+ @mousedown.prevent="editor.chain().focus().toggleStrike().run()"
28
+ :class="toolbarBtnClass(editor.isActive('strike'))"
29
+ title="Зачеркнутый (Ctrl+Shift+S)"
30
+ >
31
+ S
32
+ </button>
33
+ <button
34
+ v-if="isRichMode"
35
+ type="button"
36
+ @mousedown.prevent="openImagePickerFromMenu"
37
+ class="px-2.5 py-1.5 hover:text-foreground hover:bg-card transition-colors text-muted-foreground"
38
+ title="Вставить изображение"
39
+ >
40
+ <Icon name="image" size="sm" />
41
+ </button>
42
+ <button
43
+ v-if="isRichMode"
44
+ type="button"
45
+ @mousedown.prevent="openGalleryPickerFromMenu"
46
+ class="px-2.5 py-1.5 hover:text-foreground hover:bg-card transition-colors text-muted-foreground"
47
+ title="Вставить галерею"
48
+ >
49
+ <Icon name="images" size="sm" />
50
+ </button>
51
+ <template
52
+ v-for="entry in markdownEditorToolbarRegistryEntries"
53
+ :key="`${entry.extensionId}:${entry.registryKey}`"
54
+ >
55
+ <component
56
+ :is="entry.value"
57
+ :editor="editor"
58
+ :is-rich-mode="isRichMode"
59
+ :disabled="props.disabled"
60
+ :mode="mode"
61
+ />
62
+ </template>
63
+ </div>
64
+ </Transition>
65
+
66
+ <div
67
+ v-if="mode === 'toolbar'"
68
+ class="flex items-center gap-2 mb-3 p-2 bg-card border border-border"
69
+ >
70
+ <Button
71
+ variant="ghost"
72
+ size="sm"
73
+ @mousedown.prevent="editor?.chain().focus().toggleBold().run()"
74
+ title="Жирный текст"
75
+ >
76
+ <Icon name="bold" size="sm" />
77
+ </Button>
78
+ <Button
79
+ variant="ghost"
80
+ size="sm"
81
+ @mousedown.prevent="editor?.chain().focus().toggleItalic().run()"
82
+ title="Курсив"
83
+ >
84
+ <Icon name="italic" size="sm" />
85
+ </Button>
86
+ <Button
87
+ variant="ghost"
88
+ size="sm"
89
+ @mousedown.prevent="editor?.chain().focus().toggleStrike().run()"
90
+ title="Зачеркнутый"
91
+ >
92
+ S
93
+ </Button>
94
+ <div class="w-px h-5 bg-border"></div>
95
+ <Button
96
+ variant="ghost"
97
+ size="sm"
98
+ @mousedown.prevent="
99
+ editor?.chain().focus().toggleHeading({ level: 2 }).run()
100
+ "
101
+ title="Заголовок"
102
+ >
103
+ H2
104
+ </Button>
105
+ <Button
106
+ variant="ghost"
107
+ size="sm"
108
+ @mousedown.prevent="editor?.chain().focus().toggleBulletList().run()"
109
+ title="Маркированный список"
110
+ >
111
+ <Icon name="list" size="sm" />
112
+ </Button>
113
+ <Button
114
+ variant="ghost"
115
+ size="sm"
116
+ @mousedown.prevent="editor?.chain().focus().toggleOrderedList().run()"
117
+ title="Нумерованный список"
118
+ >
119
+ <Icon name="list-ordered" size="sm" />
120
+ </Button>
121
+ <Button
122
+ v-if="isRichMode"
123
+ variant="ghost"
124
+ size="sm"
125
+ @mousedown.prevent="openImagePickerFromMenu"
126
+ title="Вставить изображение"
127
+ >
128
+ <Icon name="image" size="sm" />
129
+ </Button>
130
+ <Button
131
+ v-if="isRichMode"
132
+ variant="ghost"
133
+ size="sm"
134
+ @mousedown.prevent="openGalleryPickerFromMenu"
135
+ title="Вставить галерею"
136
+ >
137
+ <Icon name="images" size="sm" />
138
+ </Button>
139
+ <template
140
+ v-for="entry in markdownEditorToolbarRegistryEntries"
141
+ :key="`${entry.extensionId}:${entry.registryKey}`"
142
+ >
143
+ <component
144
+ :is="entry.value"
145
+ :editor="editor"
146
+ :is-rich-mode="isRichMode"
147
+ :disabled="props.disabled"
148
+ :mode="mode"
149
+ />
150
+ </template>
151
+ </div>
152
+
153
+ <input
154
+ v-if="isRichMode"
155
+ ref="imageInputRef"
156
+ type="file"
157
+ accept="image/*"
158
+ class="hidden"
159
+ @change="handleImageInputChange"
160
+ />
161
+
162
+ <input
163
+ v-if="isRichMode"
164
+ ref="galleryInputRef"
165
+ type="file"
166
+ accept="image/*"
167
+ multiple
168
+ class="hidden"
169
+ @change="handleGalleryInputChange"
170
+ />
171
+
172
+ <div class="editor-shell relative overflow-hidden border border-border bg-surface-overlay" :class="{ 'editor-shell--rich': isRichMode }">
173
+ <EditorContent :editor="editor" class="tiptap-editor" />
174
+ <div
175
+ v-if="isEmpty"
176
+ class="absolute top-0 left-0 p-4 text-sm text-subtle-fg pointer-events-none select-none"
177
+ >
178
+ {{ placeholder }}
179
+ </div>
180
+
181
+ </div>
182
+
183
+ <Teleport to="body">
184
+ <div
185
+ v-if="contextMenu.visible"
186
+ ref="contextMenuRef"
187
+ class="fixed z-[9999] min-w-[11rem] overflow-hidden rounded border border-border bg-card py-1 shadow-xl"
188
+ :style="contextMenuStyle"
189
+ @contextmenu.prevent
190
+ >
191
+ <template v-for="item in contextMenuItems" :key="item.id">
192
+ <div
193
+ v-if="item.type === 'separator'"
194
+ class="my-1 border-t border-border"
195
+ />
196
+ <button
197
+ v-else
198
+ type="button"
199
+ class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors hover:bg-surface-overlay"
200
+ :class="
201
+ item.destructive
202
+ ? 'text-destructive-soft hover:text-destructive'
203
+ : item.active
204
+ ? 'text-foreground'
205
+ : 'text-muted-foreground'
206
+ "
207
+ @mousedown.prevent="item.action && runContextMenuAction(item.action)"
208
+ >
209
+ <Icon v-if="item.icon" :name="item.icon" size="sm" class="shrink-0" />
210
+ <span>{{ item.label }}</span>
211
+ </button>
212
+ </template>
213
+ </div>
214
+ </Teleport>
215
+ </div>
216
+ </template>
217
+
218
+ <script setup lang="ts">
219
+ import {
220
+ computed,
221
+ nextTick,
222
+ onBeforeUnmount,
223
+ onMounted,
224
+ ref,
225
+ watch,
226
+ } from 'vue';
227
+ import { Button, Icon } from '..';
228
+ import { EditorContent, useEditor } from '@tiptap/vue-3';
229
+ import type { AnyExtension, Editor as TiptapEditor } from '@tiptap/core';
230
+ import StarterKit from '@tiptap/starter-kit';
231
+ import Link from '@tiptap/extension-link';
232
+ import { toEditorHtmlContent } from '../../utils/rich-content';
233
+ import { uploadImage } from '../../utils/uploadImage';
234
+ import {
235
+ MarkdownEditorImage,
236
+ insertImageAtCursor,
237
+ insertImageGallery,
238
+ removeImageByUploadId,
239
+ updateImageByUploadId,
240
+ } from './markdown-editor-image';
241
+ import {
242
+ MarkdownEditorImageGallery,
243
+ removeGalleryImageByUploadId,
244
+ updateGalleryImageByUploadId,
245
+ } from './markdown-editor-gallery';
246
+ import { extensionRegistriesRevision } from '../../../extensions/registries/store';
247
+ import {
248
+ useMarkdownEditorToolbarActionsRegistry,
249
+ useMarkdownEditorTiptapExtensionsRegistry,
250
+ } from '../../../extensions/registries/builtins';
251
+
252
+ interface Props {
253
+ modelValue: string;
254
+ placeholder?: string;
255
+ mode?: 'toolbar' | 'inline';
256
+ minHeight?: string | number;
257
+ enableImages?: boolean;
258
+ contentMode?: 'plain' | 'rich';
259
+ uploadFolder?: string;
260
+ disabled?: boolean;
261
+ }
262
+
263
+ const props = withDefaults(defineProps<Props>(), {
264
+ placeholder: 'Введите описание...',
265
+ mode: 'inline',
266
+ enableImages: true,
267
+ contentMode: 'rich',
268
+ uploadFolder: 'content',
269
+ disabled: false,
270
+ });
271
+
272
+ const emit = defineEmits<{
273
+ 'update:modelValue': [value: string];
274
+ }>();
275
+
276
+ type ContextMenuAction =
277
+ | 'bold'
278
+ | 'italic'
279
+ | 'strike'
280
+ | 'heading'
281
+ | 'bulletList'
282
+ | 'orderedList'
283
+ | 'insertImage'
284
+ | 'insertGallery';
285
+
286
+ interface ContextMenuItem {
287
+ id: string;
288
+ label: string;
289
+ icon?: string;
290
+ action?: ContextMenuAction;
291
+ active?: boolean;
292
+ type?: 'separator';
293
+ destructive?: boolean;
294
+ }
295
+
296
+ const hasSelection = ref(false);
297
+ const imageInputRef = ref<HTMLInputElement | null>(null);
298
+ const galleryInputRef = ref<HTMLInputElement | null>(null);
299
+ const mode = computed(() => props.mode);
300
+ const placeholder = computed(() => props.placeholder);
301
+ const isRichMode = computed(
302
+ () => props.enableImages && props.contentMode === 'rich',
303
+ );
304
+ const editorContainerRef = ref<HTMLElement | null>(null);
305
+ const contextMenuRef = ref<HTMLElement | null>(null);
306
+ const toolbarStyle = ref<Record<string, string>>({});
307
+ const contextMenu = ref({ visible: false, x: 0, y: 0 });
308
+ const isApplyingExternalContent = ref(false);
309
+ let lastEmittedHtml = '';
310
+
311
+ const contextMenuStyle = computed(() => ({
312
+ left: `${contextMenu.value.x}px`,
313
+ top: `${contextMenu.value.y}px`,
314
+ }));
315
+ const markdownEditorToolbarRegistryEntries = computed(() =>
316
+ useMarkdownEditorToolbarActionsRegistry().entries(),
317
+ );
318
+
319
+ const toolbarBtnClass = (active: boolean) =>
320
+ active
321
+ ? 'px-2.5 py-1.5 text-foreground bg-card transition-colors text-xs font-bold'
322
+ : 'px-2.5 py-1.5 text-muted-foreground hover:text-foreground hover:bg-card transition-colors text-xs font-bold';
323
+
324
+ const contextMenuItems = computed<ContextMenuItem[]>(() => {
325
+ const ed = editor.value;
326
+ if (!ed) return [];
327
+
328
+ const items: ContextMenuItem[] = [
329
+ {
330
+ id: 'bold',
331
+ label: 'Жирный',
332
+ icon: 'bold',
333
+ action: 'bold',
334
+ active: ed.isActive('bold'),
335
+ },
336
+ {
337
+ id: 'italic',
338
+ label: 'Курсив',
339
+ icon: 'italic',
340
+ action: 'italic',
341
+ active: ed.isActive('italic'),
342
+ },
343
+ {
344
+ id: 'strike',
345
+ label: 'Зачеркнутый',
346
+ action: 'strike',
347
+ active: ed.isActive('strike'),
348
+ },
349
+ { id: 'sep-heading', label: 'Заголовок H2', action: 'heading', active: ed.isActive('heading', { level: 2 }) },
350
+ {
351
+ id: 'bulletList',
352
+ label: 'Маркированный список',
353
+ icon: 'list',
354
+ action: 'bulletList',
355
+ active: ed.isActive('bulletList'),
356
+ },
357
+ {
358
+ id: 'orderedList',
359
+ label: 'Нумерованный список',
360
+ icon: 'list-ordered',
361
+ action: 'orderedList',
362
+ active: ed.isActive('orderedList'),
363
+ },
364
+ ];
365
+
366
+ if (isRichMode.value) {
367
+ items.push({ id: 'sep-image', type: 'separator', label: '' });
368
+ items.push({
369
+ id: 'insertImage',
370
+ label: 'Вставить изображение',
371
+ icon: 'image',
372
+ action: 'insertImage',
373
+ });
374
+ items.push({
375
+ id: 'insertGallery',
376
+ label: 'Вставить галерею',
377
+ icon: 'images',
378
+ action: 'insertGallery',
379
+ });
380
+ }
381
+
382
+ return items;
383
+ });
384
+
385
+ const updateInlineToolbarPosition = (ed: {
386
+ state: { selection: { from: number; to: number } };
387
+ view: {
388
+ coordsAtPos: (pos: number) => {
389
+ left: number;
390
+ right: number;
391
+ top: number;
392
+ bottom: number;
393
+ };
394
+ };
395
+ }) => {
396
+ if (!editorContainerRef.value) return;
397
+
398
+ const { from, to } = ed.state.selection;
399
+ if (from === to) return;
400
+
401
+ const start = ed.view.coordsAtPos(from);
402
+ const end = ed.view.coordsAtPos(to);
403
+ const containerRect = editorContainerRef.value.getBoundingClientRect();
404
+
405
+ const rawX =
406
+ (Math.min(start.left, end.left) + Math.max(start.right, end.right)) / 2;
407
+ const x = Math.max(
408
+ 16,
409
+ Math.min(rawX - containerRect.left, containerRect.width - 16),
410
+ );
411
+
412
+ const top = Math.min(start.top, end.top) - containerRect.top;
413
+ const bottom = Math.max(start.bottom, end.bottom) - containerRect.top;
414
+ const preferAbove = top > 44;
415
+
416
+ toolbarStyle.value = {
417
+ left: `${x}px`,
418
+ top: `${preferAbove ? top - 8 : bottom + 8}px`,
419
+ transform: preferAbove ? 'translate(-50%, -100%)' : 'translate(-50%, 0)',
420
+ };
421
+ };
422
+
423
+ const getRegistryTiptapExtensions = (): AnyExtension[] => {
424
+ void extensionRegistriesRevision.value;
425
+ return useMarkdownEditorTiptapExtensionsRegistry()
426
+ .entries()
427
+ .map((entry) => entry.value);
428
+ };
429
+
430
+ const buildExtensions = (): AnyExtension[] => {
431
+ const extensions: AnyExtension[] = [
432
+ StarterKit.configure({
433
+ heading: { levels: [1, 2, 3] },
434
+ }),
435
+ Link.configure({
436
+ openOnClick: false,
437
+ enableClickSelection: true,
438
+ autolink: true,
439
+ linkOnPaste: true,
440
+ defaultProtocol: 'https',
441
+ HTMLAttributes: {
442
+ target: '_blank',
443
+ rel: 'noopener noreferrer',
444
+ },
445
+ }),
446
+ ...getRegistryTiptapExtensions(),
447
+ ];
448
+
449
+ if (isRichMode.value) {
450
+ extensions.push(
451
+ MarkdownEditorImage.configure({
452
+ inline: true,
453
+ allowBase64: false,
454
+ }),
455
+ MarkdownEditorImageGallery,
456
+ );
457
+ }
458
+
459
+ return extensions;
460
+ };
461
+
462
+ const closeContextMenu = () => {
463
+ contextMenu.value.visible = false;
464
+ };
465
+
466
+ const openContextMenu = (clientX: number, clientY: number) => {
467
+ const menuWidth = 200;
468
+ const menuHeight = 320;
469
+ const padding = 8;
470
+
471
+ contextMenu.value = {
472
+ visible: true,
473
+ x: Math.min(clientX, window.innerWidth - menuWidth - padding),
474
+ y: Math.min(clientY, window.innerHeight - menuHeight - padding),
475
+ };
476
+ };
477
+
478
+ const runContextMenuAction = (action: ContextMenuAction) => {
479
+ const ed = editor.value;
480
+ if (!ed) return;
481
+
482
+ closeContextMenu();
483
+
484
+ switch (action) {
485
+ case 'bold':
486
+ ed.chain().focus().toggleBold().run();
487
+ break;
488
+ case 'italic':
489
+ ed.chain().focus().toggleItalic().run();
490
+ break;
491
+ case 'strike':
492
+ ed.chain().focus().toggleStrike().run();
493
+ break;
494
+ case 'heading':
495
+ ed.chain().focus().toggleHeading({ level: 2 }).run();
496
+ break;
497
+ case 'bulletList':
498
+ ed.chain().focus().toggleBulletList().run();
499
+ break;
500
+ case 'orderedList':
501
+ ed.chain().focus().toggleOrderedList().run();
502
+ break;
503
+ case 'insertImage':
504
+ openImagePickerFromMenu();
505
+ break;
506
+ case 'insertGallery':
507
+ openGalleryPickerFromMenu();
508
+ break;
509
+ }
510
+ };
511
+
512
+ const getEditorHtml = (ed: TiptapEditor) =>
513
+ ed.isEmpty ? '' : ed.getHTML();
514
+
515
+ const createUploadId = () =>
516
+ typeof crypto !== 'undefined' && crypto.randomUUID
517
+ ? crypto.randomUUID()
518
+ : `upload-${Date.now()}-${Math.random().toString(36).slice(2)}`;
519
+
520
+ const insertImageFromFile = (file: File) => {
521
+ const ed = editor.value;
522
+ if (!isRichMode.value || !ed) return;
523
+
524
+ const uploadId = createUploadId();
525
+ const previewUrl = URL.createObjectURL(file);
526
+
527
+ insertImageAtCursor(ed, {
528
+ src: previewUrl,
529
+ uploading: true,
530
+ uploadId,
531
+ });
532
+
533
+ void (async () => {
534
+ try {
535
+ const result = await uploadImage(file, {
536
+ folder: props.uploadFolder,
537
+ maxSizeBytes: 10 * 1024 * 1024,
538
+ });
539
+
540
+ if (!editor.value) return;
541
+
542
+ updateImageByUploadId(editor.value, uploadId, {
543
+ src: result.url,
544
+ uploading: false,
545
+ uploadId: null,
546
+ });
547
+ } catch (error) {
548
+ console.error('[MarkdownEditor] Не удалось загрузить изображение:', error);
549
+ if (editor.value) {
550
+ removeImageByUploadId(editor.value, uploadId);
551
+ }
552
+ } finally {
553
+ URL.revokeObjectURL(previewUrl);
554
+ }
555
+ })();
556
+ };
557
+
558
+ const openGalleryPickerFromMenu = () => {
559
+ closeContextMenu();
560
+ nextTick(() => {
561
+ galleryInputRef.value?.click();
562
+ });
563
+ };
564
+
565
+ const handleGalleryInputChange = async (event: Event) => {
566
+ const input = event.target as HTMLInputElement;
567
+ const files = input.files ? Array.from(input.files) : [];
568
+ input.value = '';
569
+ if (!files.length) return;
570
+
571
+ const ed = editor.value;
572
+ if (!isRichMode.value || !ed) return;
573
+
574
+ const entries = files.slice(0, 12).map((file) => ({
575
+ file,
576
+ uploadId: createUploadId(),
577
+ previewUrl: URL.createObjectURL(file),
578
+ }));
579
+
580
+ insertImageGallery(
581
+ ed,
582
+ entries.map((entry) => ({
583
+ src: entry.previewUrl,
584
+ alt: '',
585
+ uploading: true,
586
+ uploadId: entry.uploadId,
587
+ })),
588
+ );
589
+
590
+ await Promise.all(
591
+ entries.map(async (entry) => {
592
+ try {
593
+ const result = await uploadImage(entry.file, {
594
+ folder: props.uploadFolder,
595
+ maxSizeBytes: 10 * 1024 * 1024,
596
+ });
597
+
598
+ if (editor.value) {
599
+ updateGalleryImageByUploadId(editor.value, entry.uploadId, {
600
+ src: result.url,
601
+ });
602
+ }
603
+ } catch (error) {
604
+ console.error('[MarkdownEditor] Не удалось загрузить изображение галереи:', error);
605
+ if (editor.value) {
606
+ removeGalleryImageByUploadId(editor.value, entry.uploadId);
607
+ }
608
+ } finally {
609
+ URL.revokeObjectURL(entry.previewUrl);
610
+ }
611
+ }),
612
+ );
613
+ };
614
+
615
+ const openImagePickerFromMenu = () => {
616
+ closeContextMenu();
617
+ nextTick(() => {
618
+ imageInputRef.value?.click();
619
+ });
620
+ };
621
+
622
+ const handleImageInputChange = (event: Event) => {
623
+ const input = event.target as HTMLInputElement;
624
+ const file = input.files?.[0];
625
+ input.value = '';
626
+ if (file) {
627
+ insertImageFromFile(file);
628
+ }
629
+ };
630
+
631
+ const handleDocumentPointerDown = (event: MouseEvent) => {
632
+ if (!contextMenu.value.visible) return;
633
+ const target = event.target as Node | null;
634
+ if (contextMenuRef.value?.contains(target)) return;
635
+ closeContextMenu();
636
+ };
637
+
638
+ const handleDocumentKeyDown = (event: KeyboardEvent) => {
639
+ if (event.key === 'Escape') {
640
+ closeContextMenu();
641
+ }
642
+ };
643
+
644
+ onMounted(() => {
645
+ document.addEventListener('mousedown', handleDocumentPointerDown);
646
+ document.addEventListener('keydown', handleDocumentKeyDown);
647
+ });
648
+
649
+ onBeforeUnmount(() => {
650
+ document.removeEventListener('mousedown', handleDocumentPointerDown);
651
+ document.removeEventListener('keydown', handleDocumentKeyDown);
652
+ });
653
+
654
+ const editor = useEditor({
655
+ extensions: buildExtensions(),
656
+ content: toEditorHtmlContent(props.modelValue || ''),
657
+ editable: !props.disabled,
658
+ editorProps: {
659
+ attributes: {
660
+ class:
661
+ 'p-4 text-foreground focus:outline-none focus:border-accent-secondary prose prose-invert prose-headings:text-foreground prose-p:text-muted-foreground prose-a:text-accent-secondary prose-strong:text-foreground prose-code:text-accent-secondary max-w-none overflow-y-auto',
662
+ style: props.minHeight
663
+ ? `min-height: ${props.minHeight}`
664
+ : 'min-height: 300px',
665
+ },
666
+ handleDOMEvents: {
667
+ contextmenu: (_view, event) => {
668
+ event.preventDefault();
669
+ const ed = editor.value;
670
+ if (!ed) return true;
671
+
672
+ ed.chain().focus().run();
673
+ openContextMenu(event.clientX, event.clientY);
674
+ return true;
675
+ },
676
+ },
677
+ handlePaste: (_view, event) => {
678
+ if (!isRichMode.value) return false;
679
+ const items = event.clipboardData?.items;
680
+ if (!items) return false;
681
+
682
+ for (const item of items) {
683
+ if (item.type.startsWith('image/')) {
684
+ event.preventDefault();
685
+ const file = item.getAsFile();
686
+ if (file) {
687
+ insertImageFromFile(file);
688
+ }
689
+ return true;
690
+ }
691
+ }
692
+
693
+ return false;
694
+ },
695
+ handleDrop: (_view, event, _slice, moved) => {
696
+ if (!isRichMode.value || moved) return false;
697
+ const files = event.dataTransfer?.files;
698
+ if (!files?.length) return false;
699
+
700
+ const imageFile = Array.from(files).find((file) =>
701
+ file.type.startsWith('image/'),
702
+ );
703
+ if (!imageFile) return false;
704
+
705
+ event.preventDefault();
706
+ insertImageFromFile(imageFile);
707
+ return true;
708
+ },
709
+ },
710
+ onUpdate: ({ editor: ed }) => {
711
+ if (isApplyingExternalContent.value) return;
712
+
713
+ const html = getEditorHtml(ed);
714
+ if (html === lastEmittedHtml) return;
715
+
716
+ lastEmittedHtml = html;
717
+ emit('update:modelValue', html);
718
+ },
719
+ onCreate: ({ editor: ed }) => {
720
+ lastEmittedHtml = getEditorHtml(ed);
721
+ },
722
+ onSelectionUpdate: ({ editor: ed }) => {
723
+ const { from, to } = ed.state.selection;
724
+ hasSelection.value = from !== to;
725
+ if (hasSelection.value && mode.value === 'inline') {
726
+ nextTick(() => updateInlineToolbarPosition(ed));
727
+ }
728
+ },
729
+ onBlur: () => {
730
+ hasSelection.value = false;
731
+ },
732
+ });
733
+
734
+ const isEmpty = computed(() => !props.modelValue);
735
+
736
+ watch(
737
+ () => props.disabled,
738
+ (disabled) => {
739
+ editor.value?.setEditable(!disabled);
740
+ },
741
+ );
742
+
743
+ watch(
744
+ () => props.modelValue,
745
+ (value) => {
746
+ if (!editor.value || isApplyingExternalContent.value) return;
747
+
748
+ const nextHtml = value || '';
749
+ if (nextHtml === lastEmittedHtml) return;
750
+
751
+ const currentHtml = getEditorHtml(editor.value);
752
+ if (nextHtml === currentHtml) {
753
+ lastEmittedHtml = nextHtml;
754
+ return;
755
+ }
756
+
757
+ isApplyingExternalContent.value = true;
758
+ editor.value.commands.setContent(toEditorHtmlContent(nextHtml), { emitUpdate: false });
759
+ lastEmittedHtml = getEditorHtml(editor.value);
760
+ nextTick(() => {
761
+ isApplyingExternalContent.value = false;
762
+ });
763
+ },
764
+ );
765
+ </script>
766
+
767
+ <style scoped>
768
+ .tiptap-editor :deep(.ProseMirror) {
769
+ min-height: 300px;
770
+ }
771
+
772
+ .tiptap-editor :deep(.ProseMirror p) {
773
+ margin-bottom: 0.75rem;
774
+ }
775
+
776
+ .tiptap-editor :deep(.ProseMirror:focus) {
777
+ outline: none;
778
+ }
779
+
780
+ .tiptap-editor :deep(.ProseMirror ul),
781
+ .tiptap-editor :deep(.ProseMirror ol) {
782
+ padding-left: 1.25rem;
783
+ margin-bottom: 0.75rem;
784
+ }
785
+
786
+ .tiptap-editor :deep(.ProseMirror li > p) {
787
+ margin-bottom: 0;
788
+ }
789
+
790
+ .tiptap-editor :deep(.ProseMirror ul) {
791
+ list-style-type: disc;
792
+ }
793
+
794
+ .tiptap-editor :deep(.ProseMirror ol) {
795
+ list-style-type: decimal;
796
+ }
797
+
798
+ .tiptap-editor :deep(.ProseMirror li::marker) {
799
+ color: #9ca3af;
800
+ }
801
+
802
+ .tiptap-editor :deep(.ProseMirror a) {
803
+ color: #818cf8;
804
+ text-decoration: underline;
805
+ text-underline-offset: 2px;
806
+ }
807
+
808
+ .tiptap-editor :deep(.ProseMirror a:hover) {
809
+ color: #a5b4fc;
810
+ }
811
+
812
+ .editor-shell:not(.editor-shell--rich) .tiptap-editor :deep(.ProseMirror img),
813
+ .editor-shell:not(.editor-shell--rich) .tiptap-editor :deep(.md-editor-image-wrap) {
814
+ display: none;
815
+ }
816
+
817
+ .editor-shell--rich .tiptap-editor :deep(.ProseMirror img) {
818
+ display: inline-block;
819
+ max-width: 100%;
820
+ height: auto;
821
+ border-radius: var(--radius-ui-lg);
822
+ margin: 0.5rem 0;
823
+ vertical-align: middle;
824
+ }
825
+
826
+ .editor-shell--rich .tiptap-editor :deep(.md-gallery) {
827
+ display: grid;
828
+ grid-template-columns: repeat(auto-fill, minmax(5.5rem, 1fr));
829
+ gap: 0.5rem;
830
+ margin: 0.75rem 0;
831
+ padding: 0.5rem;
832
+ border: 1px solid var(--border);
833
+ border-radius: var(--radius-ui-lg);
834
+ background: var(--card);
835
+ }
836
+
837
+ .editor-shell--rich .tiptap-editor :deep(.md-gallery img),
838
+ .editor-shell--rich .tiptap-editor :deep(.md-gallery__image) {
839
+ width: 100%;
840
+ aspect-ratio: 1;
841
+ object-fit: cover;
842
+ margin: 0;
843
+ }
844
+
845
+ .editor-shell--rich .tiptap-editor :deep(.md-gallery__cell) {
846
+ position: relative;
847
+ display: block;
848
+ overflow: hidden;
849
+ border-radius: var(--radius-ui-md);
850
+ aspect-ratio: 1;
851
+ }
852
+
853
+ .editor-shell--rich .tiptap-editor :deep(.md-gallery__cell--loading) {
854
+ background: color-mix(in srgb, var(--card) 70%, var(--muted-foreground));
855
+ animation: md-gallery-shimmer 1.2s ease-in-out infinite;
856
+ }
857
+
858
+ .editor-shell--rich .tiptap-editor :deep(.md-gallery__cell--loading .md-gallery__image) {
859
+ opacity: 0.45;
860
+ filter: blur(1px);
861
+ }
862
+
863
+ .editor-shell--rich .tiptap-editor :deep(.md-gallery__cell--loading::after) {
864
+ content: '';
865
+ position: absolute;
866
+ inset: 0;
867
+ display: flex;
868
+ align-items: center;
869
+ justify-content: center;
870
+ background: color-mix(in srgb, var(--background) 35%, transparent);
871
+ pointer-events: none;
872
+ }
873
+
874
+ .editor-shell--rich .tiptap-editor :deep(.md-gallery__cell--loading::before) {
875
+ content: '';
876
+ position: absolute;
877
+ left: 50%;
878
+ top: 50%;
879
+ z-index: 1;
880
+ width: 1.25rem;
881
+ height: 1.25rem;
882
+ margin: -0.625rem 0 0 -0.625rem;
883
+ border-radius: 9999px;
884
+ border: 2px solid color-mix(in srgb, var(--foreground) 25%, transparent);
885
+ border-top-color: var(--accent-secondary);
886
+ animation: md-gallery-spin 0.7s linear infinite;
887
+ pointer-events: none;
888
+ }
889
+
890
+ .editor-shell--rich .tiptap-editor :deep(.md-editor-image-wrap) {
891
+ position: relative;
892
+ display: inline-block;
893
+ max-width: 100%;
894
+ vertical-align: middle;
895
+ }
896
+
897
+ .editor-shell--rich .tiptap-editor :deep(.md-editor-image-wrap--loading) {
898
+ background: color-mix(in srgb, var(--card) 70%, var(--muted-foreground));
899
+ border-radius: var(--radius-ui-lg);
900
+ animation: md-gallery-shimmer 1.2s ease-in-out infinite;
901
+ }
902
+
903
+ .editor-shell--rich .tiptap-editor :deep(.md-editor-image-wrap--loading .md-editor-image) {
904
+ opacity: 0.45;
905
+ filter: blur(1px);
906
+ }
907
+
908
+ .editor-shell--rich .tiptap-editor :deep(.md-editor-image-wrap--loading::before) {
909
+ content: '';
910
+ position: absolute;
911
+ left: 50%;
912
+ top: 50%;
913
+ z-index: 1;
914
+ width: 1.25rem;
915
+ height: 1.25rem;
916
+ margin: -0.625rem 0 0 -0.625rem;
917
+ border-radius: 9999px;
918
+ border: 2px solid color-mix(in srgb, var(--foreground) 25%, transparent);
919
+ border-top-color: var(--accent-secondary);
920
+ animation: md-gallery-spin 0.7s linear infinite;
921
+ pointer-events: none;
922
+ }
923
+
924
+ @keyframes md-gallery-shimmer {
925
+ 0%,
926
+ 100% {
927
+ opacity: 1;
928
+ }
929
+ 50% {
930
+ opacity: 0.72;
931
+ }
932
+ }
933
+
934
+ @keyframes md-gallery-spin {
935
+ to {
936
+ transform: rotate(360deg);
937
+ }
938
+ }
939
+
940
+ .editor-shell:focus-within {
941
+ border-color: var(--accent-secondary);
942
+ }
943
+
944
+ .toolbar-enter-active,
945
+ .toolbar-leave-active {
946
+ transition:
947
+ opacity 0.12s ease,
948
+ transform 0.12s ease;
949
+ }
950
+
951
+ .toolbar-enter-from,
952
+ .toolbar-leave-to {
953
+ opacity: 0;
954
+ transform: translateY(4px);
955
+ }
956
+ </style>