@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,598 @@
1
+ <template>
2
+ <div>
3
+ <label
4
+ v-if="label"
5
+ class="block text-muted-foreground mb-1.5 text-sm font-medium"
6
+ >{{ label }}</label
7
+ >
8
+
9
+ <!-- Превью изображения -->
10
+ <div
11
+ class="relative bg-surface-overlay border border-border overflow-hidden transition-colors"
12
+ :class="[
13
+ previewClasses,
14
+ isDragging && 'border-primary',
15
+ (isUploading || disabled) && 'opacity-50',
16
+ ]"
17
+ @click="handleClick"
18
+ @dragover.prevent="isDragging = true"
19
+ @dragleave.prevent="isDragging = false"
20
+ @drop.prevent="handleDrop"
21
+ >
22
+ <input
23
+ ref="fileInput"
24
+ type="file"
25
+ accept="image/png,image/jpeg,image/jpg,image/gif,image/webp"
26
+ class="hidden"
27
+ @change="handleFileSelect"
28
+ :disabled="isUploading || disabled"
29
+ />
30
+
31
+ <!-- Изображение или пустое состояние -->
32
+ <div v-if="modelValue && !isUploading" class="relative w-full h-full">
33
+ <img
34
+ :src="getPreviewUrl(modelValue)"
35
+ alt="Preview"
36
+ class="w-full h-full object-cover"
37
+ />
38
+
39
+ <!-- Overlay с кнопками (в square — только иконки) -->
40
+ <div
41
+ class="absolute inset-0 bg-black/45 opacity-0 hover:opacity-100 transition-opacity flex items-center justify-center gap-2"
42
+ >
43
+ <button
44
+ @click.stop="handleClick"
45
+ :class="[
46
+ 'bg-primary hover:bg-[var(--primary-hover)] text-primary-foreground transition-colors flex items-center justify-center',
47
+ mode === 'square' ? 'p-2' : 'px-4 py-2 gap-2',
48
+ ]"
49
+ :title="mode === 'square' ? 'Изменить' : undefined"
50
+ :disabled="disabled"
51
+ >
52
+ <Icon name="edit" size="sm" />
53
+ <span v-if="mode !== 'square'">Изменить</span>
54
+ </button>
55
+ <button
56
+ @click.stop="handleRemove"
57
+ :class="[
58
+ 'bg-destructive hover:bg-destructive/90 text-destructive-foreground transition-colors flex items-center justify-center',
59
+ mode === 'square' ? 'p-2' : 'px-4 py-2 gap-2',
60
+ ]"
61
+ :title="mode === 'square' ? 'Удалить' : undefined"
62
+ :disabled="disabled"
63
+ >
64
+ <Icon name="trash" size="sm" />
65
+ <span v-if="mode !== 'square'">Удалить</span>
66
+ </button>
67
+ </div>
68
+ </div>
69
+
70
+ <!-- Состояние загрузки -->
71
+ <div
72
+ v-else-if="isUploading"
73
+ class="w-full h-full flex flex-col items-center justify-center py-8"
74
+ >
75
+ <div class="animate-spin mb-2">
76
+ <Icon name="refresh-cw" size="lg" class="text-accent-secondary" />
77
+ </div>
78
+ <p class="text-muted-foreground text-sm">Загрузка изображения...</p>
79
+ </div>
80
+
81
+ <!-- Пустое состояние -->
82
+ <div
83
+ v-else
84
+ class="w-full h-full flex flex-col items-center justify-center py-8 cursor-pointer"
85
+ >
86
+ <Icon name="upload" size="xl" class="text-subtle-fg mb-2" />
87
+ <p class="text-muted-foreground text-sm mb-1 text-center">
88
+ Нажмите или перетащите изображение
89
+ </p>
90
+ <p class="text-subtle-fg text-xs">PNG, JPG, GIF, WEBP до 10MB</p>
91
+ </div>
92
+ </div>
93
+
94
+ <p v-if="error" class="text-red-400 text-xs mt-1">{{ error }}</p>
95
+
96
+ <!-- Диалог обрезки -->
97
+ <Dialog
98
+ :open="showCropDialog"
99
+ size="xl"
100
+ @close="showCropDialog = false"
101
+ :closeDisabled="true"
102
+ >
103
+ <DialogHeader
104
+ title="Обрезка изображения"
105
+ description="Выберите область для обрезки"
106
+ @close="showCropDialog = false"
107
+ />
108
+ <DialogContent class="p-4">
109
+ <div class="flex gap-4">
110
+ <!-- Cropper -->
111
+ <div
112
+ class="flex-1 relative overflow-hidden"
113
+ :style="{ height: '70vh', maxWidth: '100%' }"
114
+ >
115
+ <Cropper
116
+ ref="cropper"
117
+ :src="cropImageSrc"
118
+ :stencil-props="stencilProps"
119
+ class="cropper"
120
+ @change="updatePreviews"
121
+ />
122
+ </div>
123
+
124
+ <!-- Превью в двух размерах для banner режима -->
125
+ <div v-if="mode === 'banner'" class="w-80 space-y-4">
126
+ <div>
127
+ <p class="text-muted-foreground text-sm mb-2">
128
+ Превью баннера (1152x344)
129
+ </p>
130
+ <div
131
+ class="bg-surface-overlay border border-border aspect-[1152/344] overflow-hidden"
132
+ >
133
+ <img
134
+ v-if="cropPreviewBanner"
135
+ :src="cropPreviewBanner"
136
+ alt="Banner preview"
137
+ class="w-full h-full object-cover"
138
+ />
139
+ <div
140
+ v-else
141
+ class="w-full h-full flex items-center justify-center text-subtle-fg text-sm"
142
+ >
143
+ Превью появится после выбора области
144
+ </div>
145
+ </div>
146
+ </div>
147
+ <div>
148
+ <p class="text-muted-foreground text-sm mb-2">
149
+ Превью 16:9 (центр)
150
+ </p>
151
+ <div
152
+ class="bg-surface-overlay border border-border aspect-video overflow-hidden"
153
+ >
154
+ <img
155
+ v-if="cropPreview16x9"
156
+ :src="cropPreview16x9"
157
+ alt="16:9 preview"
158
+ class="w-full h-full object-cover"
159
+ />
160
+ <div
161
+ v-else
162
+ class="w-full h-full flex items-center justify-center text-subtle-fg text-sm"
163
+ >
164
+ Превью появится после выбора области
165
+ </div>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ </DialogContent>
171
+ <DialogFooter>
172
+ <Button
173
+ variant="secondary"
174
+ size="md"
175
+ @click="showCropDialog = false"
176
+ :disabled="isUploading"
177
+ >
178
+ Отмена
179
+ </Button>
180
+ <Button
181
+ variant="primary"
182
+ size="md"
183
+ @click="handleCrop"
184
+ :disabled="isUploading"
185
+ >
186
+ <Icon name="check" size="sm" />
187
+ Применить
188
+ </Button>
189
+ </DialogFooter>
190
+ </Dialog>
191
+ </div>
192
+ </template>
193
+
194
+ <script setup lang="ts">
195
+ import { ref, computed, watch } from 'vue';
196
+ import { Cropper } from 'vue-advanced-cropper';
197
+ import 'vue-advanced-cropper/dist/style.css';
198
+ import {
199
+ Icon,
200
+ Dialog,
201
+ DialogHeader,
202
+ DialogContent,
203
+ DialogFooter,
204
+ Button,
205
+ } from '..';
206
+ import { transformCloudinaryUrl } from '../../utils/cloudinary';
207
+
208
+ type ImageMode = 'square' | 'banner' | 'base';
209
+
210
+ const props = withDefaults(
211
+ defineProps<{
212
+ label?: string;
213
+ disabled?: boolean;
214
+ folder?: string;
215
+ mode?: ImageMode;
216
+ }>(),
217
+ {
218
+ label: '',
219
+ disabled: false,
220
+ folder: undefined,
221
+ mode: 'banner',
222
+ },
223
+ );
224
+
225
+ const modelValue = defineModel<string>('modelValue', { default: '' });
226
+
227
+ const fileInput = ref<HTMLInputElement | null>(null);
228
+ const cropper = ref<InstanceType<typeof Cropper> | null>(null);
229
+ const isDragging = ref(false);
230
+ const isUploading = ref(false);
231
+ const error = ref<string>('');
232
+ const showCropDialog = ref(false);
233
+ const cropImageSrc = ref<string>('');
234
+ const cropPreviewBanner = ref<string>('');
235
+ const cropPreview16x9 = ref<string>('');
236
+
237
+ // Поддержка двух вариантов конфигурации:
238
+ // 1. Отдельные переменные: VITE_CLOUDINARY_CLOUD_NAME и VITE_CLOUDINARY_UPLOAD_PRESET
239
+ // 2. CLOUDINARY_URL: cloudinary://api_key:api_secret@cloud_name (парсим для cloud_name)
240
+ const CLOUDINARY_URL = (import.meta as any).env?.VITE_CLOUDINARY_URL as
241
+ | string
242
+ | undefined;
243
+ const CLOUDINARY_CLOUD_NAME = (import.meta as any).env
244
+ ?.VITE_CLOUDINARY_CLOUD_NAME as string | undefined;
245
+ const CLOUDINARY_UPLOAD_PRESET = (import.meta as any).env
246
+ ?.VITE_CLOUDINARY_UPLOAD_PRESET as string | undefined;
247
+
248
+ // Парсим CLOUDINARY_URL если он указан: cloudinary://api_key:api_secret@cloud_name
249
+ const parseCloudinaryUrl = (url: string): string | null => {
250
+ try {
251
+ // Формат: cloudinary://api_key:api_secret@cloud_name
252
+ const match = url.match(/cloudinary:\/\/[^:]+:[^@]+@(.+)/);
253
+ return match ? match[1] : null;
254
+ } catch {
255
+ return null;
256
+ }
257
+ };
258
+
259
+ // Получаем cloud_name из переменных окружения или из CLOUDINARY_URL
260
+ const cloudName =
261
+ CLOUDINARY_CLOUD_NAME ||
262
+ (CLOUDINARY_URL ? parseCloudinaryUrl(CLOUDINARY_URL) : null);
263
+
264
+ // Вычисляем aspect ratio и размеры для превью
265
+ const previewClasses = computed(() => {
266
+ if (props.mode === 'square') {
267
+ return 'aspect-square';
268
+ } else if (props.mode === 'banner') {
269
+ return 'aspect-[1152/344]';
270
+ } else {
271
+ // base режим - 16:9
272
+ return 'aspect-video';
273
+ }
274
+ });
275
+
276
+ // Настройки cropper
277
+ const stencilProps = computed(() => {
278
+ if (props.mode === 'square') {
279
+ return {
280
+ aspectRatio: 1,
281
+ };
282
+ } else if (props.mode === 'banner') {
283
+ // Для режима banner используем aspect ratio 1152/344
284
+ return {
285
+ aspectRatio: 1152 / 344,
286
+ };
287
+ } else {
288
+ // base режим - 16:9
289
+ return {
290
+ aspectRatio: 16 / 9,
291
+ };
292
+ }
293
+ });
294
+
295
+ // Обновляем превью при изменении области обрезки
296
+ const updatePreviews = async () => {
297
+ if (!cropper.value || props.mode !== 'banner' || !cropImageSrc.value)
298
+ return;
299
+
300
+ try {
301
+ const result = cropper.value.getResult();
302
+ if (!result.canvas || !result.coordinates) return;
303
+
304
+ // Загружаем исходное изображение
305
+ const img = new Image();
306
+ img.crossOrigin = 'anonymous';
307
+
308
+ await new Promise((resolve, reject) => {
309
+ img.onload = resolve;
310
+ img.onerror = reject;
311
+ img.src = cropImageSrc.value;
312
+ });
313
+
314
+ const { coordinates } = result;
315
+ const sourceWidth = coordinates.width;
316
+ const sourceHeight = coordinates.height;
317
+ const sourceLeft = coordinates.left;
318
+ const sourceTop = coordinates.top;
319
+
320
+ // Превью баннера (1152x344)
321
+ const bannerCanvas = document.createElement('canvas');
322
+ bannerCanvas.width = 1152;
323
+ bannerCanvas.height = 344;
324
+ const bannerCtx = bannerCanvas.getContext('2d');
325
+ if (bannerCtx) {
326
+ // Рисуем из исходного изображения с учетом координат
327
+ bannerCtx.drawImage(
328
+ img,
329
+ sourceLeft,
330
+ sourceTop,
331
+ sourceWidth,
332
+ sourceHeight,
333
+ 0,
334
+ 0,
335
+ 1152,
336
+ 344,
337
+ );
338
+ cropPreviewBanner.value = bannerCanvas.toDataURL('image/png', 0.9);
339
+ }
340
+
341
+ // Превью 16:9 (центр изображения)
342
+ // Вычисляем размеры для 16:9 из области 1152x344
343
+ // Высота 344, для 16:9 ширина = 344 * 16 / 9 ≈ 611.56
344
+ const width16x9 = Math.round((344 * 16) / 9);
345
+ const left16x9 = Math.round((1152 - width16x9) / 2); // Центрируем
346
+
347
+ // Вычисляем координаты для 16:9 в исходном изображении
348
+ const sourceWidth16x9 = (width16x9 / 1152) * sourceWidth;
349
+ const sourceLeft16x9 = sourceLeft + (left16x9 / 1152) * sourceWidth;
350
+
351
+ const canvas16x9 = document.createElement('canvas');
352
+ canvas16x9.width = width16x9;
353
+ canvas16x9.height = 344;
354
+ const ctx16x9 = canvas16x9.getContext('2d');
355
+ if (ctx16x9) {
356
+ // Рисуем центральную часть из исходного изображения
357
+ ctx16x9.drawImage(
358
+ img,
359
+ sourceLeft16x9,
360
+ sourceTop,
361
+ sourceWidth16x9,
362
+ sourceHeight,
363
+ 0,
364
+ 0,
365
+ width16x9,
366
+ 344,
367
+ );
368
+ cropPreview16x9.value = canvas16x9.toDataURL('image/png', 0.9);
369
+ }
370
+ } catch (error) {
371
+ console.error('Ошибка при обновлении превью:', error);
372
+ }
373
+ };
374
+
375
+ const handleClick = () => {
376
+ if (props.disabled || isUploading.value) return;
377
+ fileInput.value?.click();
378
+ };
379
+
380
+ const handleFileSelect = async (event: Event) => {
381
+ const target = event.target as HTMLInputElement;
382
+ const file = target.files?.[0];
383
+ if (file) {
384
+ await processFile(file);
385
+ }
386
+ target.value = '';
387
+ };
388
+
389
+ const handleDrop = async (event: DragEvent) => {
390
+ isDragging.value = false;
391
+ if (props.disabled || isUploading.value) return;
392
+
393
+ const file = event.dataTransfer?.files[0];
394
+ if (file && file.type.startsWith('image/')) {
395
+ await processFile(file);
396
+ } else {
397
+ error.value = 'Пожалуйста, выберите изображение';
398
+ }
399
+ };
400
+
401
+ const processFile = async (file: File) => {
402
+ // Проверка размера файла (10MB)
403
+ if (file.size > 10 * 1024 * 1024) {
404
+ error.value = 'Размер файла не должен превышать 10MB';
405
+ return;
406
+ }
407
+
408
+ error.value = '';
409
+
410
+ // Создаем URL для превью
411
+ const reader = new FileReader();
412
+ reader.onload = (e) => {
413
+ cropImageSrc.value = e.target?.result as string;
414
+ cropPreviewBanner.value = '';
415
+ cropPreview16x9.value = '';
416
+ showCropDialog.value = true;
417
+ // Обновляем превью после открытия диалога
418
+ setTimeout(() => {
419
+ updatePreviews();
420
+ }, 100);
421
+ };
422
+ reader.readAsDataURL(file);
423
+ };
424
+
425
+ const handleCrop = async () => {
426
+ if (!cropper.value) return;
427
+
428
+ const { canvas } = cropper.value.getResult();
429
+ if (!canvas) {
430
+ error.value = 'Не удалось обрезать изображение';
431
+ return;
432
+ }
433
+
434
+ // Конвертируем canvas в blob
435
+ canvas.toBlob(
436
+ async (blob: Blob | null) => {
437
+ if (!blob) {
438
+ error.value = 'Ошибка при обработке изображения';
439
+ return;
440
+ }
441
+
442
+ // Создаем File из blob
443
+ const file = new File([blob], 'cropped-image.png', {
444
+ type: 'image/png',
445
+ });
446
+ await uploadImage(file);
447
+
448
+ showCropDialog.value = false;
449
+ cropImageSrc.value = '';
450
+ cropPreviewBanner.value = '';
451
+ cropPreview16x9.value = '';
452
+ },
453
+ 'image/png',
454
+ 0.95,
455
+ );
456
+ };
457
+
458
+ const uploadImage = async (file: File) => {
459
+ if (!cloudName) {
460
+ error.value = 'Cloudinary Cloud Name не настроен';
461
+ console.error(
462
+ 'Установите VITE_CLOUDINARY_CLOUD_NAME или VITE_CLOUDINARY_URL в переменных окружения',
463
+ );
464
+ return;
465
+ }
466
+
467
+ if (!CLOUDINARY_UPLOAD_PRESET) {
468
+ error.value = 'Cloudinary Upload Preset не настроен';
469
+ console.error(
470
+ 'VITE_CLOUDINARY_UPLOAD_PRESET не установлен в переменных окружения',
471
+ );
472
+ return;
473
+ }
474
+
475
+ isUploading.value = true;
476
+ error.value = '';
477
+
478
+ try {
479
+ const formData = new FormData();
480
+ formData.append('file', file);
481
+ formData.append('upload_preset', CLOUDINARY_UPLOAD_PRESET);
482
+ // Добавляем folder если указан
483
+ if (props.folder) {
484
+ formData.append('folder', props.folder);
485
+ }
486
+
487
+ const response = await fetch(
488
+ `https://api.cloudinary.com/v1_1/${cloudName}/image/upload`,
489
+ {
490
+ method: 'POST',
491
+ body: formData,
492
+ },
493
+ );
494
+
495
+ if (!response.ok) {
496
+ const errorData = await response.json().catch(() => ({}));
497
+ throw new Error(
498
+ errorData?.error?.message ||
499
+ `Ошибка загрузки: ${response.statusText}`,
500
+ );
501
+ }
502
+
503
+ const data = await response.json();
504
+
505
+ if (data.secure_url) {
506
+ // Cloudinary возвращает secure_url (HTTPS) для загруженного изображения
507
+ modelValue.value = data.secure_url;
508
+ } else if (data.url) {
509
+ // Fallback на обычный url, если secure_url недоступен
510
+ modelValue.value = data.url;
511
+ } else {
512
+ throw new Error('Не удалось получить ссылку на изображение');
513
+ }
514
+ } catch (err: any) {
515
+ error.value = err.message || 'Ошибка при загрузке изображения';
516
+ console.error('Ошибка загрузки изображения на Cloudinary:', err);
517
+ } finally {
518
+ isUploading.value = false;
519
+ // Сброс input для возможности повторной загрузки того же файла
520
+ if (fileInput.value) {
521
+ fileInput.value.value = '';
522
+ }
523
+ }
524
+ };
525
+
526
+ const handleRemove = () => {
527
+ if (props.disabled || isUploading.value) return;
528
+ modelValue.value = '';
529
+ error.value = '';
530
+ };
531
+
532
+ // Получаем URL изображения с трансформациями для превью
533
+ const getPreviewUrl = (url: string | undefined) => {
534
+ if (!url) return '';
535
+
536
+ if (props.mode === 'square') {
537
+ return transformCloudinaryUrl(url, {
538
+ width: 400,
539
+ height: 400,
540
+ crop: 'fill',
541
+ quality: 'auto',
542
+ format: 'auto',
543
+ });
544
+ } else if (props.mode === 'banner') {
545
+ // Для banner режима используем размеры 1152x344
546
+ return transformCloudinaryUrl(url, {
547
+ width: 1152,
548
+ height: 344,
549
+ crop: 'fill',
550
+ quality: 'auto',
551
+ format: 'auto',
552
+ });
553
+ } else {
554
+ // base режим - 16:9
555
+ return transformCloudinaryUrl(url, {
556
+ width: 1920,
557
+ height: 1080,
558
+ crop: 'fill',
559
+ quality: 'auto',
560
+ format: 'auto',
561
+ });
562
+ }
563
+ };
564
+
565
+ // Закрываем диалог при изменении modelValue извне
566
+ watch(
567
+ () => modelValue.value,
568
+ (newValue) => {
569
+ if (!newValue && showCropDialog.value) {
570
+ showCropDialog.value = false;
571
+ }
572
+ },
573
+ );
574
+ </script>
575
+
576
+ <style scoped>
577
+ .cropper {
578
+ height: 100%;
579
+ width: 100%;
580
+ max-width: 100%;
581
+ background: var(--surface-overlay);
582
+ overflow: hidden;
583
+ }
584
+
585
+ .cropper :deep(.vue-advanced-cropper) {
586
+ max-width: 100%;
587
+ overflow: hidden;
588
+ }
589
+
590
+ .cropper :deep(.vue-advanced-cropper__area) {
591
+ max-width: 100%;
592
+ }
593
+
594
+ .cropper :deep(.vue-advanced-cropper__background) {
595
+ max-width: 100%;
596
+ overflow: hidden;
597
+ }
598
+ </style>
@@ -0,0 +1,72 @@
1
+ <template>
2
+ <input
3
+ :type="type"
4
+ :value="displayValue"
5
+ :placeholder="placeholder"
6
+ :disabled="disabled"
7
+ :class="inputClasses"
8
+ @input="
9
+ $emit('update:modelValue', ($event.target as HTMLInputElement).value)
10
+ "
11
+ @blur="$emit('blur', $event)"
12
+ @focus="$emit('focus', $event)"
13
+ />
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ import { computed } from 'vue';
18
+ import { cn } from '../../utils/cn';
19
+
20
+ interface Props {
21
+ modelValue?: string | number;
22
+ type?:
23
+ | 'text'
24
+ | 'email'
25
+ | 'password'
26
+ | 'number'
27
+ | 'search'
28
+ | 'datetime-local'
29
+ | 'date'
30
+ | 'time';
31
+ placeholder?: string;
32
+ disabled?: boolean;
33
+ size?: 'sm' | 'md' | 'lg';
34
+ error?: boolean;
35
+ }
36
+
37
+ const props = withDefaults(defineProps<Props>(), {
38
+ type: 'text',
39
+ size: 'md',
40
+ disabled: false,
41
+ error: false,
42
+ });
43
+
44
+ const emit = defineEmits<{
45
+ 'update:modelValue': [value: string];
46
+ blur: [event: FocusEvent];
47
+ focus: [event: FocusEvent];
48
+ }>();
49
+
50
+ // Преобразуем значение в строку для отображения
51
+ const displayValue = computed(() => {
52
+ if (props.modelValue === null || props.modelValue === undefined) return '';
53
+ return String(props.modelValue);
54
+ });
55
+
56
+ const sizeClasses = {
57
+ sm: 'px-2 py-1 text-xs',
58
+ md: 'px-3 py-2 text-sm',
59
+ lg: 'px-4 py-2.5 text-base',
60
+ };
61
+
62
+ const inputClasses = computed(() => {
63
+ return cn(
64
+ 'w-full bg-surface-overlay text-foreground border border-border focus:outline-none transition-colors rounded',
65
+ sizeClasses[props.size],
66
+ props.error
67
+ ? 'border-destructive focus:ring-2 focus:ring-destructive'
68
+ : 'focus:border-accent-secondary focus:ring-2 focus:ring-ring/60',
69
+ props.disabled && 'opacity-50 cursor-not-allowed',
70
+ );
71
+ });
72
+ </script>