@hywax/cms 0.0.6 → 0.0.7

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 (46) hide show
  1. package/.nuxt/cms/editor/uplora-image.ts +1 -1
  2. package/.nuxt/cms/editor-content-full.ts +6 -0
  3. package/.nuxt/cms/editor-content-light.ts +6 -0
  4. package/.nuxt/cms/index.ts +3 -0
  5. package/.nuxt/cms/input-uplora-image.ts +15 -0
  6. package/cli/templates.mjs +6 -6
  7. package/dist/module.d.mts +1 -0
  8. package/dist/module.json +1 -1
  9. package/dist/module.mjs +139 -54
  10. package/dist/runtime/components/ButtonDelete.vue +3 -2
  11. package/dist/runtime/components/ButtonDelete.vue.d.ts +1 -0
  12. package/dist/runtime/components/EditorContentFull.vue +53 -0
  13. package/dist/runtime/components/EditorContentFull.vue.d.ts +20 -0
  14. package/dist/runtime/components/EditorContentLight.vue +46 -0
  15. package/dist/runtime/components/EditorContentLight.vue.d.ts +20 -0
  16. package/dist/runtime/components/InputUploraImage.vue +131 -0
  17. package/dist/runtime/components/InputUploraImage.vue.d.ts +35 -0
  18. package/dist/runtime/components/ModalConfirm.vue +2 -1
  19. package/dist/runtime/components/TablePanel.vue +1 -1
  20. package/dist/runtime/components/TablePanelColumnSorting.vue +1 -1
  21. package/dist/runtime/components/UploraImage.vue +2 -2
  22. package/dist/runtime/components/UploraImage.vue.d.ts +1 -1
  23. package/dist/runtime/components/prose/UploraImage.vue +1 -1
  24. package/dist/runtime/composables/useAsyncHandler.d.ts +12 -0
  25. package/dist/runtime/composables/useAsyncHandler.js +30 -0
  26. package/dist/runtime/composables/useUplora.d.ts +20 -0
  27. package/dist/runtime/composables/useUplora.js +52 -0
  28. package/dist/runtime/editor/components/BlockMenu.vue +43 -0
  29. package/dist/runtime/editor/components/BlockMenu.vue.d.ts +2 -0
  30. package/dist/runtime/editor/components/SlashCommand.vue +92 -0
  31. package/dist/runtime/editor/components/SlashCommand.vue.d.ts +7 -0
  32. package/dist/runtime/editor/components/TooltipLink.vue +81 -0
  33. package/dist/runtime/editor/components/TooltipLink.vue.d.ts +9 -0
  34. package/dist/runtime/editor/components/TooltipMenu.vue +75 -0
  35. package/dist/runtime/editor/components/TooltipMenu.vue.d.ts +9 -0
  36. package/dist/runtime/editor/extensions/callout/CalloutView.vue +7 -1
  37. package/dist/runtime/editor/extensions/uplora-image/UploraImageView.vue +7 -4
  38. package/dist/runtime/index.css +1 -1
  39. package/dist/runtime/server/api/uplora/[id].delete.d.ts +1 -1
  40. package/dist/runtime/server/api/uplora/[id].delete.js +1 -1
  41. package/dist/runtime/server/api/uplora/index.post.d.ts +4 -1
  42. package/dist/runtime/server/api/uplora/index.post.js +6 -2
  43. package/dist/runtime/types/index.d.ts +4 -0
  44. package/dist/runtime/types/index.js +3 -0
  45. package/dist/runtime/types/tv.d.ts +2 -2
  46. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  export default {
2
2
  "slots": {
3
- "root": "rounded-md overflow-hidden"
3
+ "root": ""
4
4
  }
5
5
  }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ "slots": {
3
+ "root": "h-full w-full relative",
4
+ "editor": "min-h-full w-full focus:outline-none editor-content-full relative"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ "slots": {
3
+ "root": "relative",
4
+ "editor": "w-full focus:outline-none editor-content-light relative"
5
+ }
6
+ }
@@ -2,11 +2,14 @@ export { default as autocompleteSelect } from './autocomplete-select'
2
2
  export { default as buttonClear } from './button-clear'
3
3
  export { default as buttonCopy } from './button-copy'
4
4
  export { default as buttonDelete } from './button-delete'
5
+ export { default as editorContentFull } from './editor-content-full'
6
+ export { default as editorContentLight } from './editor-content-light'
5
7
  export { default as formPanel } from './form-panel'
6
8
  export { default as formPanelAsideSection } from './form-panel-aside-section'
7
9
  export { default as formPanelSection } from './form-panel-section'
8
10
  export { default as inputSeo } from './input-seo'
9
11
  export { default as inputSlug } from './input-slug'
12
+ export { default as inputUploraImage } from './input-uplora-image'
10
13
  export { default as modalConfirm } from './modal-confirm'
11
14
  export { default as tableCellPreview } from './table-cell-preview'
12
15
  export { default as tableCellSeo } from './table-cell-seo'
@@ -0,0 +1,15 @@
1
+ export default {
2
+ "slots": {
3
+ "root": "relative w-full rounded-md overflow-hidden border border-default aspect-3/2 text-sm",
4
+ "image": "",
5
+ "imageActions": "absolute top-4 right-4 flex gap-2",
6
+ "uploader": "flex flex-col items-center justify-center w-full h-full p-4",
7
+ "uploaderPendingIcon": "size-6 animate-spin",
8
+ "uploaderIdleButton": "cursor-pointer bg-muted/50 border border-default border-dashed rounded-md h-full w-full",
9
+ "uploaderIdleIcon": "size-10 text-primary",
10
+ "uploaderIdleText": "mt-2 font-medium",
11
+ "uploaderIdleExtensions": "mt-2 text-xs text-muted uppercase",
12
+ "uploaderErrorText": "text-center font-medium",
13
+ "uploaderErrorActions": "flex gap-2 mt-4"
14
+ }
15
+ }
package/cli/templates.mjs CHANGED
@@ -69,13 +69,13 @@ import ComponentRender from '../../component-render'
69
69
  describe('${upperName}.vue', () => {
70
70
  it.each([
71
71
  // Props
72
- ['базовый компонент', { props: {} }],
73
- ['с алиасом', { props: { as: 'section' } }],
74
- ['с class', { props: { class: '' } }],
75
- ['с ui', { props: { ui: {} } }],
72
+ ['base component', { props: {} }],
73
+ ['with as', { props: { as: 'section' } }],
74
+ ['with class', { props: { class: '' } }],
75
+ ['with ui', { props: { ui: {} } }],
76
76
  // Slots
77
- ['с дефолтным слотом', { slots: { default: () => 'Default slot' } }]
78
- ])('рендерит %s корректно', async (nameOrHtml: string, options: { props?: ${upperName}Props, slots?: Partial<${upperName}Slots> }) => {
77
+ ['with default slot', { slots: { default: () => 'Default slot' } }],
78
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: ${upperName}Props, slots?: Partial<${upperName}Slots> }) => {
79
79
  const html = await ComponentRender(nameOrHtml, options, ${upperName})
80
80
  expect(html).toMatchSnapshot()
81
81
  })
package/dist/module.d.mts CHANGED
@@ -35,6 +35,7 @@ declare module '@nuxt/schema' {
35
35
  fluxorUrl: string;
36
36
  version: string;
37
37
  };
38
+ uploraApiKey: string;
38
39
  }
39
40
  }
40
41
 
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hywax/cms",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "configKey": "cms",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.1",
package/dist/module.mjs CHANGED
@@ -1,11 +1,12 @@
1
- import { createResolver, addComponentsDir, addImportsDir, addPlugin, addImports, addServerImports, addServerImportsDir, addServerHandler, addTypeTemplate, addTemplate, addServerTemplate, defineNuxtModule, hasNuxtModule, installModule } from '@nuxt/kit';
2
- import { defu } from 'defu';
1
+ import { createResolver, addComponentsDir, addImportsDir, addPlugin, addImports, addServerImports, addServerImportsDir, hasNuxtModule, installModule, addServerHandler, addTypeTemplate, addTemplate, addServerTemplate, defineNuxtModule } from '@nuxt/kit';
3
2
  import { fileURLToPath } from 'node:url';
4
- import { dirname } from 'pathe';
3
+ import { dirname, join } from 'pathe';
4
+ import { defu } from 'defu';
5
5
  import { snakeCase, kebabCase } from 'scule';
6
+ import { readFile, writeFile } from 'node:fs/promises';
6
7
 
7
8
  const name = "@hywax/cms";
8
- const version = "0.0.6";
9
+ const version = "0.0.7";
9
10
 
10
11
  function createContext(options, nuxt) {
11
12
  const { resolve } = createResolver(import.meta.url);
@@ -52,7 +53,7 @@ function prepareAutoImports({ resolve, options, nuxt }) {
52
53
  path: resolve("./runtime/components"),
53
54
  pathPrefix: false,
54
55
  prefix: options?.prefix,
55
- ignore: ["prose/**", "editor/**"]
56
+ ignore: ["prose/**"]
56
57
  });
57
58
  addComponentsDir({
58
59
  path: resolve("./runtime/components/prose"),
@@ -75,6 +76,102 @@ function prepareAutoImports({ resolve, options, nuxt }) {
75
76
  addServerImportsDir(resolve("./runtime/server/utils"));
76
77
  nuxt.options.nitro.alias ||= {};
77
78
  nuxt.options.nitro.alias["#cms/http-codes"] = httpCodesPath;
79
+ nuxt.options.alias["#cms"] = resolve("./runtime");
80
+ nuxt.options.appConfig.cms = defu(nuxt.options.appConfig.cms || {}, defaultCMSConfig);
81
+ }
82
+
83
+ async function prepareGenerateEnv({ nuxt }) {
84
+ const runtimeConfig = nuxt.options.runtimeConfig;
85
+ if (nuxt.options.dev && !runtimeConfig.uploraApiKey) {
86
+ const envPath = join(nuxt.options.rootDir, ".env");
87
+ const envContent = await readFile(envPath, "utf-8").catch(() => "");
88
+ const envKey = `${runtimeConfig.nitro?.envPrefix || "NUXT_"}UPLORA_API_KEY`;
89
+ if (!envContent.includes(envKey)) {
90
+ await writeFile(
91
+ envPath,
92
+ `${envContent ? `${envContent}
93
+ ` : envContent}${envKey}=put-your-api-key-here`,
94
+ "utf-8"
95
+ );
96
+ }
97
+ }
98
+ }
99
+
100
+ async function prepareInstallModules(_context) {
101
+ if (!hasNuxtModule("@nuxt/ui-pro")) {
102
+ await installModule("@nuxt/ui-pro");
103
+ }
104
+ if (!hasNuxtModule("@nuxtjs/mdc")) {
105
+ await installModule("@nuxtjs/mdc");
106
+ }
107
+ if (!hasNuxtModule("@vueuse/nuxt")) {
108
+ await installModule("@vueuse/nuxt");
109
+ }
110
+ if (!hasNuxtModule("nuxt-auth-utils")) {
111
+ await installModule("nuxt-auth-utils");
112
+ }
113
+ }
114
+
115
+ const icons = {
116
+ edit: "lucide:pencil",
117
+ trash: "lucide:trash-2",
118
+ imageUp: "lucide:image-up",
119
+ arrowDownUp: "lucide:arrow-down-up",
120
+ sortAsc: "lucide:arrow-up-wide-narrow",
121
+ sortDesc: "lucide:arrow-down-wide-narrow",
122
+ columns: "lucide:columns-3-cog",
123
+ filter: "lucide:filter",
124
+ link: "lucide:link-2",
125
+ linkOff: "lucide:link-2-off",
126
+ ellipsisVertical: "lucide:ellipsis-vertical",
127
+ heading2: "lucide:heading-2",
128
+ heading3: "lucide:heading-3",
129
+ heading4: "lucide:heading-4",
130
+ list: "lucide:list",
131
+ listOrdered: "lucide:list-ordered",
132
+ checkSquare: "lucide:check-square",
133
+ image: "lucide:image",
134
+ quote: "lucide:quote",
135
+ code: "lucide:code",
136
+ paragraph: "lucide:paragraph",
137
+ pilcrow: "lucide:pilcrow",
138
+ info: "lucide:info",
139
+ bold: "lucide:bold",
140
+ italic: "lucide:italic",
141
+ underline: "lucide:underline",
142
+ strike: "lucide:strikethrough"
143
+ };
144
+
145
+ function prepareMergeConfigs({ nuxt, options }) {
146
+ nuxt.options.runtimeConfig.public = defu(nuxt.options.runtimeConfig.public || {}, {
147
+ fluxorUrl: options.fluxorUrl,
148
+ version: ""
149
+ });
150
+ nuxt.options.runtimeConfig.nitro ||= {};
151
+ nuxt.options.runtimeConfig.nitro.envPrefix = options.envPrefix;
152
+ nuxt.options.appConfig.ui = defu(nuxt.options.appConfig.ui || {}, {
153
+ icons
154
+ });
155
+ nuxt.options.ui = defu(nuxt.options.ui || {}, {
156
+ colorMode: true,
157
+ fonts: true
158
+ });
159
+ nuxt.options.colorMode = defu(nuxt.options.colorMode || {}, {
160
+ storageKey: `${options.name}-color-mode`
161
+ });
162
+ nuxt.options.mdc = defu(nuxt.options.mdc || {}, {
163
+ components: {
164
+ map: {
165
+ "uplora-image": "ProseUploraImage"
166
+ }
167
+ }
168
+ });
169
+ nuxt.options.auth = defu(nuxt.options.auth || {}, {
170
+ name: `${options.name}-session`
171
+ });
172
+ nuxt.options.runtimeConfig.uplora = defu(nuxt.options.runtimeConfig.uplora || {}, {
173
+ apiKey: ""
174
+ });
78
175
  }
79
176
 
80
177
  function prepareServerRoutes({ resolve }) {
@@ -114,6 +211,20 @@ const buttonDelete = {
114
211
  }
115
212
  };
116
213
 
214
+ const editorContentFull = {
215
+ slots: {
216
+ root: "h-full w-full relative",
217
+ editor: "min-h-full w-full focus:outline-none editor-content-full relative"
218
+ }
219
+ };
220
+
221
+ const editorContentLight = {
222
+ slots: {
223
+ root: "relative",
224
+ editor: "w-full focus:outline-none editor-content-light relative"
225
+ }
226
+ };
227
+
117
228
  const formPanel = {
118
229
  slots: {
119
230
  root: "",
@@ -161,6 +272,22 @@ const inputSlug = {
161
272
  }
162
273
  };
163
274
 
275
+ const inputUploraImage = {
276
+ slots: {
277
+ root: "relative w-full rounded-md overflow-hidden border border-default aspect-3/2 text-sm",
278
+ image: "",
279
+ imageActions: "absolute top-4 right-4 flex gap-2",
280
+ uploader: "flex flex-col items-center justify-center w-full h-full p-4",
281
+ uploaderPendingIcon: "size-6 animate-spin",
282
+ uploaderIdleButton: "cursor-pointer bg-muted/50 border border-default border-dashed rounded-md h-full w-full",
283
+ uploaderIdleIcon: "size-10 text-primary",
284
+ uploaderIdleText: "mt-2 font-medium",
285
+ uploaderIdleExtensions: "mt-2 text-xs text-muted uppercase",
286
+ uploaderErrorText: "text-center font-medium",
287
+ uploaderErrorActions: "flex gap-2 mt-4"
288
+ }
289
+ };
290
+
164
291
  const modalConfirm = {
165
292
  slots: {
166
293
  root: ""
@@ -231,11 +358,14 @@ const theme = {
231
358
  buttonClear: buttonClear,
232
359
  buttonCopy: buttonCopy,
233
360
  buttonDelete: buttonDelete,
361
+ editorContentFull: editorContentFull,
362
+ editorContentLight: editorContentLight,
234
363
  formPanel: formPanel,
235
364
  formPanelAsideSection: formPanelAsideSection,
236
365
  formPanelSection: formPanelSection,
237
366
  inputSeo: inputSeo,
238
367
  inputSlug: inputSlug,
368
+ inputUploraImage: inputUploraImage,
239
369
  modalConfirm: modalConfirm,
240
370
  tableCellPreview: tableCellPreview,
241
371
  tableCellSeo: tableCellSeo,
@@ -271,7 +401,7 @@ const callout = {
271
401
 
272
402
  const uploraImage$1 = {
273
403
  slots: {
274
- root: "rounded-md overflow-hidden"
404
+ root: ""
275
405
  }
276
406
  };
277
407
 
@@ -410,14 +540,6 @@ function prepareTemplates(context) {
410
540
  }
411
541
  }
412
542
 
413
- const icons = {
414
- sortAsc: "lucide:arrow-up-wide-narrow",
415
- sortDesc: "lucide:arrow-down-wide-narrow",
416
- columns: "lucide:columns-3-cog",
417
- filter: "lucide:filter",
418
- link: "lucide:link-2"
419
- };
420
-
421
543
  const module = defineNuxtModule({
422
544
  meta: {
423
545
  name,
@@ -427,49 +549,12 @@ const module = defineNuxtModule({
427
549
  defaults: defaultModuleOptions,
428
550
  async setup(options, nuxt) {
429
551
  const context = createContext(options, nuxt);
430
- nuxt.options.runtimeConfig.public = defu(nuxt.options.runtimeConfig.public || {}, {
431
- fluxorUrl: options.fluxorUrl,
432
- version: ""
433
- });
434
- nuxt.options.runtimeConfig.nitro ||= {};
435
- nuxt.options.runtimeConfig.nitro.envPrefix = options.envPrefix;
436
- nuxt.options.appConfig.ui = defu(nuxt.options.appConfig.ui || {}, {
437
- icons
438
- });
439
- nuxt.options.ui = defu(nuxt.options.ui || {}, {
440
- colorMode: true,
441
- fonts: true
442
- });
443
- nuxt.options.colorMode = defu(nuxt.options.colorMode || {}, {
444
- storageKey: `${options.name}-color-mode`
445
- });
446
- nuxt.options.mdc = defu(nuxt.options.mdc || {}, {
447
- components: {
448
- map: {
449
- "uplora-image": "ProseUploraImage"
450
- }
451
- }
452
- });
453
- nuxt.options.auth = defu(nuxt.options.auth || {}, {
454
- name: `${options.name}-session`
455
- });
456
- if (!hasNuxtModule("@nuxt/ui-pro")) {
457
- await installModule("@nuxt/ui-pro");
458
- }
459
- if (!hasNuxtModule("@nuxtjs/mdc")) {
460
- await installModule("@nuxtjs/mdc");
461
- }
462
- if (!hasNuxtModule("@vueuse/nuxt")) {
463
- await installModule("@vueuse/nuxt");
464
- }
465
- if (!hasNuxtModule("nuxt-auth-utils")) {
466
- await installModule("nuxt-auth-utils");
467
- }
468
- nuxt.options.alias["#cms"] = context.resolve("./runtime");
469
- nuxt.options.appConfig.cms = defu(nuxt.options.appConfig.cms || {}, defaultCMSConfig);
552
+ prepareMergeConfigs(context);
553
+ await prepareInstallModules();
470
554
  prepareAutoImports(context);
471
555
  prepareTemplates(context);
472
556
  prepareServerRoutes(context);
557
+ await prepareGenerateEnv(context);
473
558
  }
474
559
  });
475
560
 
@@ -10,6 +10,7 @@
10
10
  :color="color"
11
11
  :variant="variant"
12
12
  :size="size"
13
+ :icon="icon"
13
14
  :class="ui.root({ class: [props.ui?.root, props.class] })"
14
15
  auto-loading
15
16
  @click="handleDeleteClick()"
@@ -34,6 +35,7 @@ const props = defineProps({
34
35
  tooltipText: { type: String, required: false },
35
36
  color: { type: null, required: false, default: "error" },
36
37
  variant: { type: null, required: false },
38
+ icon: { type: null, required: false },
37
39
  size: { type: null, required: false },
38
40
  class: { type: null, required: false },
39
41
  ui: { type: null, required: false },
@@ -47,9 +49,8 @@ function handleDeleteClick() {
47
49
  ...props.modalProps,
48
50
  title: props.title ?? "\u0423\u0434\u0430\u043B\u0435\u043D\u0438\u0435",
49
51
  message: props.message ?? "\u0412\u044B \u0434\u0435\u0438\u0306\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u0445\u043E\u0442\u0438\u0442\u0435 \u0443\u0434\u0430\u043B\u0438\u0442\u044C?",
50
- color: props.color,
52
+ color: "error",
51
53
  variant: props.variant,
52
- size: props.size,
53
54
  confirmLabel: props.confirmLabel,
54
55
  confirmText: props.confirmText,
55
56
  onConfirm: props.onConfirm
@@ -15,6 +15,7 @@ export interface ButtonDeleteProps extends Pick<ModalConfirmProps, 'onConfirm'>
15
15
  tooltipText?: string;
16
16
  color?: ButtonProps['color'];
17
17
  variant?: ButtonProps['variant'];
18
+ icon?: ButtonProps['icon'];
18
19
  size?: ButtonProps['size'];
19
20
  class?: any;
20
21
  ui?: ButtonDelete['slots'];
@@ -0,0 +1,53 @@
1
+ <template>
2
+ <Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
3
+ <ProseKit :editor="editor">
4
+ <div ref="editorRef" :class="ui.editor({ class: [props.ui?.editor, 'pl-14'] })" />
5
+
6
+ <BlockMenu />
7
+ <SlashCommand />
8
+ <TooltipMenu />
9
+ </ProseKit>
10
+ </Primitive>
11
+ </template>
12
+
13
+ <script>
14
+ import theme from "#build/cms/editor-content-full";
15
+ import { computed, useAppConfig, useTemplateRef, watchPostEffect } from "#imports";
16
+ import { createEditor } from "prosekit/core";
17
+ import { ProseKit, useDocChange } from "prosekit/vue";
18
+ import { Primitive } from "reka-ui";
19
+ import BlockMenu from "../editor/components/BlockMenu.vue";
20
+ import SlashCommand from "../editor/components/SlashCommand.vue";
21
+ import TooltipMenu from "../editor/components/TooltipMenu.vue";
22
+ import { defineFullExtension } from "../editor/extensions";
23
+ import { docToMarkdown, markdownToDoc } from "../editor/markdown";
24
+ import { tv } from "../utils/tv";
25
+ import "prosekit/basic/style.css";
26
+ </script>
27
+
28
+ <script setup>
29
+ const props = defineProps({
30
+ as: { type: null, required: false },
31
+ class: { type: null, required: false },
32
+ ui: { type: null, required: false }
33
+ });
34
+ const modelValue = defineModel({ type: String, ...{ default: "" } });
35
+ const appConfig = useAppConfig();
36
+ const editor = createEditor({
37
+ extension: defineFullExtension(),
38
+ defaultContent: await markdownToDoc(modelValue.value)
39
+ });
40
+ const editorRef = useTemplateRef("editorRef");
41
+ useDocChange(async () => {
42
+ modelValue.value = await docToMarkdown(editor.getDocJSON());
43
+ }, { editor });
44
+ watchPostEffect((onCleanup) => {
45
+ editor.mount(editorRef.value);
46
+ onCleanup(() => editor.unmount());
47
+ });
48
+ const ui = computed(() => tv({ extend: tv(theme), ...appConfig.cms?.editorContentFull || {} })());
49
+ </script>
50
+
51
+ <style scoped>
52
+ ::v-deep(.editor-content-full){line-height:calc(var(--spacing)*7)}::v-deep(.editor-content-full) [data-placeholder]:before{color:var(--ui-text-dimmed);content:attr(data-placeholder);height:0;pointer-events:none;position:absolute}::v-deep(.editor-content-full) h2,::v-deep(.editor-content-full) h3,::v-deep(.editor-content-full) h4{font-weight:600;margin-block:calc(var(--spacing)*5)}::v-deep(.editor-content-full) h2{font-size:var(--text-2xl)}::v-deep(.editor-content-full) h3{font-size:var(--text-xl)}::v-deep(.editor-content-full) h4{font-size:var(--text-lg)}::v-deep(.editor-content-full) blockquote{border-left:4px solid var(--ui-border-accented);margin-block:calc(var(--spacing)*4);padding-inline-start:calc(var(--spacing)*4)}::v-deep(.editor-content-full) [data-node-view-root=true],::v-deep(.editor-content-full)>p{margin-block:calc(var(--spacing)*5)}::v-deep(.editor-content-full) hr{border-block-start:1px solid var(--ui-border);margin-block:calc(var(--spacing)*5)}::v-deep(.editor-content-full) pre{background-color:var(--ui-bg-muted);border:1px solid var(--ui-border-muted);border-radius:calc(var(--ui-radius)*1.5);font-family:var(--font-mono);font-size:var(--text-sm);line-height:calc(var(--spacing)*6);overflow-x:auto;padding-block:calc(var(--spacing)*3);padding-inline:calc(var(--spacing)*4)}::v-deep(.editor-content-full) p>code{background-color:var(--ui-bg-muted);border:1px solid var(--ui-border-muted);border-radius:calc(var(--ui-radius)*1.5);font-family:var(--font-mono);font-size:var(--text-sm);padding-inline:var(--spacing)}::v-deep(.editor-content-full) a{color:var(--ui-primary);cursor:pointer;text-decoration:underline;-webkit-user-select:all;-moz-user-select:all;user-select:all}
53
+ </style>
@@ -0,0 +1,20 @@
1
+ import type { AppConfig } from '@nuxt/schema';
2
+ import type { ComponentConfig } from '../types';
3
+ import theme from '#build/cms/editor-content-full';
4
+ import 'prosekit/basic/style.css';
5
+ type EditorContentFull = ComponentConfig<typeof theme, AppConfig, 'editorContentFull'>;
6
+ export interface EditorContentFullProps {
7
+ as?: any;
8
+ class?: any;
9
+ ui?: EditorContentFull['slots'];
10
+ }
11
+ declare const _default: import("vue").DefineComponent<EditorContentFullProps & {
12
+ modelValue?: string;
13
+ }, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
14
+ "update:modelValue": (value: string) => any;
15
+ }, string, import("vue").PublicProps, Readonly<EditorContentFullProps & {
16
+ modelValue?: string;
17
+ }> & Readonly<{
18
+ "onUpdate:modelValue"?: ((value: string) => any) | undefined;
19
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
20
+ export default _default;
@@ -0,0 +1,46 @@
1
+ <template>
2
+ <Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
3
+ <ProseKit :editor="editor">
4
+ <div ref="editorRef" :class="ui.editor({ class: props.ui?.editor })" />
5
+ </ProseKit>
6
+ </Primitive>
7
+ </template>
8
+
9
+ <script>
10
+ import theme from "#build/cms/editor-content-light";
11
+ import { computed, useAppConfig, useTemplateRef, watchPostEffect } from "#imports";
12
+ import { createEditor } from "prosekit/core";
13
+ import { ProseKit, useDocChange } from "prosekit/vue";
14
+ import { Primitive } from "reka-ui";
15
+ import { defineLightExtension } from "../editor/extensions";
16
+ import { docToMarkdown, markdownToDoc } from "../editor/markdown";
17
+ import { tv } from "../utils/tv";
18
+ import "prosekit/basic/style.css";
19
+ </script>
20
+
21
+ <script setup>
22
+ const props = defineProps({
23
+ as: { type: null, required: false },
24
+ class: { type: null, required: false },
25
+ ui: { type: null, required: false }
26
+ });
27
+ const modelValue = defineModel({ type: String, ...{ default: "" } });
28
+ const appConfig = useAppConfig();
29
+ const editor = createEditor({
30
+ extension: defineLightExtension(),
31
+ defaultContent: await markdownToDoc(modelValue.value)
32
+ });
33
+ const editorRef = useTemplateRef("editorRef");
34
+ useDocChange(async () => {
35
+ modelValue.value = await docToMarkdown(editor.getDocJSON());
36
+ }, { editor });
37
+ watchPostEffect((onCleanup) => {
38
+ editor.mount(editorRef.value);
39
+ onCleanup(() => editor.unmount());
40
+ });
41
+ const ui = computed(() => tv({ extend: tv(theme), ...appConfig.cms?.editorContentLight || {} })());
42
+ </script>
43
+
44
+ <style scoped>
45
+ ::v-deep(.editor-content-light){line-height:calc(var(--spacing)*7)}::v-deep(.editor-content-light) [data-placeholder]:before{color:var(--ui-text-dimmed);content:attr(data-placeholder);height:0;pointer-events:none;position:absolute}::v-deep(.editor-content-light) blockquote{border-left:4px solid var(--ui-border-accented);margin-block:calc(var(--spacing)*4);padding-inline-start:calc(var(--spacing)*4)}::v-deep(.editor-content-light) [data-node-view-root=true],::v-deep(.editor-content-light)>p{margin-block:calc(var(--spacing)*5)}::v-deep(.editor-content-light) pre{background-color:var(--ui-bg-muted);border:1px solid var(--ui-border-muted);border-radius:calc(var(--ui-radius)*1.5);font-family:var(--font-mono);font-size:var(--text-sm);line-height:calc(var(--spacing)*6);overflow-x:auto;padding-block:calc(var(--spacing)*3);padding-inline:calc(var(--spacing)*4)}::v-deep(.editor-content-light) p>code{background-color:var(--ui-bg-muted);border:1px solid var(--ui-border-muted);border-radius:calc(var(--ui-radius)*1.5);font-family:var(--font-mono);font-size:var(--text-sm);padding-inline:var(--spacing)}::v-deep(.editor-content-light) a{color:var(--ui-primary);cursor:pointer;text-decoration:underline;-webkit-user-select:all;-moz-user-select:all;user-select:all}
46
+ </style>
@@ -0,0 +1,20 @@
1
+ import type { AppConfig } from '@nuxt/schema';
2
+ import type { ComponentConfig } from '../types';
3
+ import theme from '#build/cms/editor-content-light';
4
+ import 'prosekit/basic/style.css';
5
+ type EditorContentLight = ComponentConfig<typeof theme, AppConfig, 'editorContentLight'>;
6
+ export interface EditorContentLightProps {
7
+ as?: any;
8
+ class?: any;
9
+ ui?: EditorContentLight['slots'];
10
+ }
11
+ declare const _default: import("vue").DefineComponent<EditorContentLightProps & {
12
+ modelValue?: string;
13
+ }, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
14
+ "update:modelValue": (value: string) => any;
15
+ }, string, import("vue").PublicProps, Readonly<EditorContentLightProps & {
16
+ modelValue?: string;
17
+ }> & Readonly<{
18
+ "onUpdate:modelValue"?: ((value: string) => any) | undefined;
19
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
20
+ export default _default;
@@ -0,0 +1,131 @@
1
+ <template>
2
+ <Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
3
+ <template v-if="modelValue.image">
4
+ <UploraImage
5
+ :image="modelValue.image"
6
+ :alt="modelValue.alt"
7
+ :lqip="modelValue.lqip"
8
+ :class="ui.image({ class: [props.ui?.image] })"
9
+ />
10
+
11
+ <div :class="ui.imageActions({ class: [props.ui?.imageActions] })">
12
+ <ButtonDelete
13
+ :icon="appConfig.ui.icons.trash"
14
+ color="neutral"
15
+ size="sm"
16
+ @confirm="deleteExecute(modelValue.image)"
17
+ />
18
+
19
+ <UPopover
20
+ :ui="{ content: 'p-2' }"
21
+ :content="{ align: 'end', side: 'bottom', sideOffset: 8 }"
22
+ >
23
+ <UButton :icon="appConfig.ui.icons.ellipsisVertical" color="neutral" size="sm" />
24
+
25
+ <template #content>
26
+ <UFormField label="Описание изображения">
27
+ <UTextarea
28
+ :model-value="modelValue.alt"
29
+ placeholder="Введите описание..."
30
+ autoresize
31
+ @update:model-value="updateAlt"
32
+ />
33
+ </UFormField>
34
+ </template>
35
+ </UPopover>
36
+ </div>
37
+ </template>
38
+ <div v-else :class="ui.uploader({ class: [props.ui?.uploader] })">
39
+ <template v-if="uploadStatus === 'pending'">
40
+ <UIcon :name="appConfig.ui.icons.loading" :class="ui.uploaderPendingIcon({ class: [props.ui?.uploaderPendingIcon] })" />
41
+ </template>
42
+ <template v-else-if="uploadStatus === 'idle'">
43
+ <button
44
+ :class="ui.uploaderIdleButton({ class: [props.ui?.uploaderIdleButton] })"
45
+ type="button"
46
+ @click="open"
47
+ >
48
+ <UIcon :name="appConfig.ui.icons.imageUp" :class="ui.uploaderIdleIcon({ class: [props.ui?.uploaderIdleIcon] })" />
49
+ <p :class="ui.uploaderIdleText({ class: [props.ui?.uploaderIdleText] })">
50
+ Нажмите, чтобы загрузить изображение
51
+ </p>
52
+ <p v-if="props.showExtensions" :class="ui.uploaderIdleExtensions({ class: [props.ui?.uploaderIdleExtensions] })">
53
+ {{ imagesExtensions.join(", ") }}
54
+ </p>
55
+ </button>
56
+ </template>
57
+ <template v-else-if="uploadStatus === 'error'">
58
+ <p :class="ui.uploaderErrorText({ class: [props.ui?.uploaderErrorText] })">
59
+ Произошла ошибка при загрузке изображения
60
+ </p>
61
+ <div :class="ui.uploaderErrorActions({ class: [props.ui?.uploaderErrorActions] })">
62
+ <UButton
63
+ label="Отменить"
64
+ variant="ghost"
65
+ color="neutral"
66
+ @click="resetUpload"
67
+ />
68
+ <UButton
69
+ label="Повторить"
70
+ variant="soft"
71
+ color="neutral"
72
+ @click="uploadExecute"
73
+ />
74
+ </div>
75
+ </template>
76
+ </div>
77
+ </Primitive>
78
+ </template>
79
+
80
+ <script>
81
+ import theme from "#build/cms/input-uplora-image";
82
+ import { computed, useAppConfig } from "#imports";
83
+ import { imagesExtensions } from "@uplora/formats";
84
+ import { Primitive } from "reka-ui";
85
+ import { useUploraDelete, useUploraUpload } from "../composables/useUplora";
86
+ import { tv } from "../utils/tv";
87
+ import ButtonDelete from "./ButtonDelete.vue";
88
+ import UploraImage from "./UploraImage.vue";
89
+ </script>
90
+
91
+ <script setup>
92
+ const props = defineProps({
93
+ showExtensions: { type: Boolean, required: false, default: true },
94
+ as: { type: null, required: false },
95
+ class: { type: null, required: false },
96
+ ui: { type: null, required: false }
97
+ });
98
+ const emit = defineEmits(["upload", "delete"]);
99
+ const modelValue = defineModel({ type: Object, ...{
100
+ default: () => ({
101
+ image: "",
102
+ alt: ""
103
+ })
104
+ } });
105
+ const appConfig = useAppConfig();
106
+ const { open, execute: uploadExecute, status: uploadStatus, reset: resetUpload, onUploaded } = useUploraUpload();
107
+ const { execute: deleteExecute, onDeleted } = useUploraDelete();
108
+ onUploaded((file) => {
109
+ modelValue.value = {
110
+ ...modelValue.value,
111
+ image: file.id,
112
+ lqip: file.lqip
113
+ };
114
+ emit("upload", modelValue.value);
115
+ });
116
+ onDeleted(() => {
117
+ resetUpload();
118
+ modelValue.value = {
119
+ image: "",
120
+ alt: ""
121
+ };
122
+ emit("delete");
123
+ });
124
+ function updateAlt(alt) {
125
+ modelValue.value = {
126
+ ...modelValue.value,
127
+ alt
128
+ };
129
+ }
130
+ const ui = computed(() => tv({ extend: tv(theme), ...appConfig.cms?.inputUploraImage || {} })());
131
+ </script>