@hywax/cms 0.0.2 → 0.0.3
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.
- package/.nuxt/cms/index.ts +2 -1
- package/.nuxt/cms/uplora-image.ts +8 -0
- package/cli/commands/make/component.mjs +75 -0
- package/cli/commands/make/index.mjs +12 -0
- package/cli/index.mjs +15 -0
- package/cli/package.json +13 -0
- package/cli/templates.mjs +101 -0
- package/cli/utils.mjs +31 -0
- package/dist/module.d.mts +15 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +19 -4
- package/dist/runtime/components/ButtonClear.vue +1 -2
- package/dist/runtime/components/UploraImage.vue +108 -0
- package/dist/runtime/components/UploraImage.vue.d.ts +34 -0
- package/dist/runtime/components/prose/UploraImage.vue +1 -2
- package/dist/runtime/composables/useApi.d.ts +14 -0
- package/dist/runtime/composables/useApi.js +16 -0
- package/dist/runtime/composables/useSeoStats.d.ts +12 -0
- package/dist/runtime/composables/useSeoStats.js +44 -0
- package/dist/runtime/plugins/api.d.ts +6 -0
- package/dist/runtime/plugins/api.js +26 -0
- package/dist/runtime/types/image.d.ts +48 -0
- package/dist/runtime/types/image.js +0 -0
- package/dist/runtime/types/index.d.ts +3 -0
- package/dist/runtime/types/index.js +3 -0
- package/dist/runtime/types/seo.d.ts +4 -0
- package/dist/runtime/types/seo.js +0 -0
- package/dist/runtime/utils/image.d.ts +30 -0
- package/dist/runtime/utils/image.js +81 -0
- package/dist/runtime/utils/index.d.ts +1 -1
- package/dist/runtime/utils/index.js +1 -0
- package/package.json +10 -1
package/.nuxt/cms/index.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { default as buttonClear } from './button-clear'
|
|
1
|
+
export { default as buttonClear } from './button-clear'
|
|
2
|
+
export { default as uploraImage } from './uplora-image'
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { existsSync, promises as fsp } from 'node:fs'
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
import { defineCommand } from 'citty'
|
|
4
|
+
import { consola } from 'consola'
|
|
5
|
+
import { resolve } from 'pathe'
|
|
6
|
+
import { camelCase, kebabCase, splitByCase, upperFirst } from 'scule'
|
|
7
|
+
import templates from '../../templates.mjs'
|
|
8
|
+
import { appendFile, sortFile } from '../../utils.mjs'
|
|
9
|
+
|
|
10
|
+
export default defineCommand({
|
|
11
|
+
meta: {
|
|
12
|
+
name: 'component',
|
|
13
|
+
description: 'Создать новый компонент',
|
|
14
|
+
},
|
|
15
|
+
args: {
|
|
16
|
+
name: {
|
|
17
|
+
type: 'positional',
|
|
18
|
+
required: true,
|
|
19
|
+
description: 'Имя компонента',
|
|
20
|
+
},
|
|
21
|
+
prose: {
|
|
22
|
+
type: 'boolean',
|
|
23
|
+
description: 'Создать компонент для контента',
|
|
24
|
+
},
|
|
25
|
+
template: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: 'Сгенерировать только шаблон',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
async setup({ args }) {
|
|
31
|
+
const name = args.name
|
|
32
|
+
if (!name) {
|
|
33
|
+
consola.error('`name` argument is missing!')
|
|
34
|
+
process.exit(1)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const path = resolve('.')
|
|
38
|
+
|
|
39
|
+
for (const template of Object.keys(templates)) {
|
|
40
|
+
if (args.template && template !== args.template) {
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { filename, contents } = templates[template](args)
|
|
45
|
+
if (!contents) {
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const filePath = resolve(path, filename)
|
|
50
|
+
|
|
51
|
+
if (existsSync(filePath)) {
|
|
52
|
+
consola.error(`🚨 ${filePath} уже существует!`)
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await fsp.writeFile(filePath, `${contents.trim()}\n`)
|
|
57
|
+
|
|
58
|
+
consola.success(`🪄 ${filePath} создан!`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (args.template) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const themePath = resolve(path, `src/theme/${args.prose ? 'prose/' : ''}index.ts`)
|
|
66
|
+
await appendFile(themePath, `export { default as ${camelCase(name)} } from './${kebabCase(name)}'`)
|
|
67
|
+
await sortFile(themePath)
|
|
68
|
+
|
|
69
|
+
if (!args.prose) {
|
|
70
|
+
const typesPath = resolve(path, 'src/runtime/types/index.ts')
|
|
71
|
+
await appendFile(typesPath, `export * from '../components/${splitByCase(name).map((p) => upperFirst(p)).join('')}.vue'`)
|
|
72
|
+
await sortFile(typesPath)
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
})
|
package/cli/index.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { defineCommand, runMain } from 'citty'
|
|
3
|
+
import make from './commands/make/index.mjs'
|
|
4
|
+
|
|
5
|
+
const main = defineCommand({
|
|
6
|
+
meta: {
|
|
7
|
+
name: 'cms',
|
|
8
|
+
description: 'Hywax CMS CLI',
|
|
9
|
+
},
|
|
10
|
+
subCommands: {
|
|
11
|
+
make,
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
runMain(main)
|
package/cli/package.json
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { camelCase, kebabCase, splitByCase, upperFirst } from 'scule'
|
|
2
|
+
|
|
3
|
+
function component({ name, prose }) {
|
|
4
|
+
const upperName = splitByCase(name).map((p) => upperFirst(p)).join('')
|
|
5
|
+
const camelName = camelCase(name)
|
|
6
|
+
const kebabName = kebabCase(name)
|
|
7
|
+
const path = 'cms'
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
filename: `src/runtime/components/${prose ? 'prose/' : ''}${upperName}.vue`,
|
|
11
|
+
contents: `
|
|
12
|
+
<template>
|
|
13
|
+
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
|
14
|
+
<slot />
|
|
15
|
+
</Primitive>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script lang="ts">
|
|
19
|
+
import type { AppConfig } from '@nuxt/schema'
|
|
20
|
+
import type { ComponentConfig } from '${prose ? '../../types' : '../types'}'
|
|
21
|
+
import theme from '#build/${path}/${prose ? 'prose/' : ''}${kebabName}'
|
|
22
|
+
import { computed, useAppConfig } from '#imports'
|
|
23
|
+
import { Primitive } from 'reka-ui'
|
|
24
|
+
import { tv } from '${prose ? '../../utils/tv' : '../utils/tv'}'
|
|
25
|
+
|
|
26
|
+
type ${upperName} = ComponentConfig<typeof theme, AppConfig, ${upperName}>
|
|
27
|
+
|
|
28
|
+
export interface ${upperName}Props {
|
|
29
|
+
as?: any
|
|
30
|
+
class?: any
|
|
31
|
+
ui?: ${upperName}['slots']
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ${upperName}Emits {
|
|
35
|
+
// 'update:modelValue': [string]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ${upperName}Slots {
|
|
39
|
+
default?: () => any
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<script setup lang="ts">
|
|
44
|
+
const props = defineProps<${upperName}Props>()
|
|
45
|
+
const emit = defineEmits<${upperName}Emits>()
|
|
46
|
+
defineSlots<${upperName}Slots>()
|
|
47
|
+
|
|
48
|
+
const appConfig = useAppConfig() as ${upperName}['AppConfig']
|
|
49
|
+
|
|
50
|
+
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.cms?.${camelName} || {}) })())
|
|
51
|
+
</script>
|
|
52
|
+
`,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function test({ name, prose }) {
|
|
57
|
+
const upperName = splitByCase(name).map((p) => upperFirst(p)).join('')
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
filename: `test/runtime/components/${upperName}.spec.ts`,
|
|
61
|
+
contents: prose
|
|
62
|
+
? undefined
|
|
63
|
+
: `
|
|
64
|
+
import type { ${upperName}Props, ${upperName}Slots } from '../../../src/runtime/components/${upperName}.vue'
|
|
65
|
+
import { describe, it, expect } from 'vitest'
|
|
66
|
+
import ${upperName} from '../../../src/runtime/components/${upperName}.vue'
|
|
67
|
+
import ComponentRender from '../../component-render'
|
|
68
|
+
|
|
69
|
+
describe('${upperName}', () => {
|
|
70
|
+
it.each([
|
|
71
|
+
// Props
|
|
72
|
+
['с алиасом', { props: { as: 'section' } }],
|
|
73
|
+
['с class', { props: { class: '' } }],
|
|
74
|
+
['с ui', { props: { ui: {} } }],
|
|
75
|
+
// Slots
|
|
76
|
+
['с дефолтным слотом', { slots: { default: () => 'Default slot' } }]
|
|
77
|
+
])('рендерит %s корректно', async (nameOrHtml: string, options: { props?: ${upperName}Props, slots?: Partial<${upperName}Slots> }) => {
|
|
78
|
+
const html = await ComponentRender(nameOrHtml, options, ${upperName})
|
|
79
|
+
expect(html).toMatchSnapshot()
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
`,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function theme({ name, prose }) {
|
|
87
|
+
const kebabName = kebabCase(name)
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
filename: `src/theme/${prose ? 'prose/' : ''}${kebabName}.ts`,
|
|
91
|
+
contents: `
|
|
92
|
+
export default {
|
|
93
|
+
slots: {
|
|
94
|
+
root: '',
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
`,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default { component, test, theme }
|
package/cli/utils.mjs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { promises as fsp } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
export async function sortFile(path) {
|
|
4
|
+
const file = await fsp.readFile(path, 'utf-8')
|
|
5
|
+
|
|
6
|
+
const lines = file.trim().split('\n').sort()
|
|
7
|
+
|
|
8
|
+
await fsp.writeFile(path, `${lines.join('\n')}\n`)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function appendFile(path, contents) {
|
|
12
|
+
const file = await fsp.readFile(path, 'utf-8')
|
|
13
|
+
|
|
14
|
+
if (!file.includes(contents)) {
|
|
15
|
+
await fsp.writeFile(path, `${file.trim()}\n${contents}\n`)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function normalizeLocale(locale) {
|
|
20
|
+
if (!locale) {
|
|
21
|
+
return ''
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (locale.includes('_')) {
|
|
25
|
+
return locale.split('_')
|
|
26
|
+
.map((part, index) => index === 0 ? part.toLowerCase() : part.toUpperCase())
|
|
27
|
+
.join('-')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return locale.toLowerCase()
|
|
31
|
+
}
|
package/dist/module.d.mts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
2
|
export * from '../dist/runtime/types/index.js';
|
|
3
3
|
|
|
4
|
+
declare module '@nuxt/schema' {
|
|
5
|
+
interface ConfigSchema {
|
|
6
|
+
public?: {
|
|
7
|
+
cms: {
|
|
8
|
+
fluxorUrl: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
4
14
|
interface ModuleOptions {
|
|
5
15
|
/**
|
|
6
16
|
* Name of the module
|
|
@@ -12,6 +22,11 @@ interface ModuleOptions {
|
|
|
12
22
|
* @defaultValue 'C'
|
|
13
23
|
*/
|
|
14
24
|
prefix?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Fluxor URL
|
|
27
|
+
* @defaultValue 'https://fluxor.uplora.ru'
|
|
28
|
+
*/
|
|
29
|
+
fluxorUrl?: string;
|
|
15
30
|
}
|
|
16
31
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
17
32
|
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { addTypeTemplate, addTemplate, defineNuxtModule, hasNuxtModule, installModule, createResolver, addComponentsDir, addImportsDir } from '@nuxt/kit';
|
|
1
|
+
import { addTypeTemplate, addTemplate, defineNuxtModule, hasNuxtModule, installModule, createResolver, addComponentsDir, addImportsDir, addPlugin } from '@nuxt/kit';
|
|
2
2
|
import { defu } from 'defu';
|
|
3
3
|
import 'node:url';
|
|
4
4
|
import { kebabCase } from 'scule';
|
|
5
5
|
|
|
6
6
|
const name = "@hywax/cms";
|
|
7
|
-
const version = "0.0.
|
|
7
|
+
const version = "0.0.3";
|
|
8
8
|
|
|
9
9
|
const buttonClear = {
|
|
10
10
|
slots: {
|
|
@@ -12,9 +12,19 @@ const buttonClear = {
|
|
|
12
12
|
}
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
const uploraImage$1 = {
|
|
16
|
+
slots: {
|
|
17
|
+
root: "relative grid grid-cols-[100%] grid-rows-[100%] overflow-hidden",
|
|
18
|
+
lqip: "bg-cover bg-no-repeat bg-center col-[1] row-[1]",
|
|
19
|
+
picture: "aspect-3/2 col-[1] row-[1]",
|
|
20
|
+
img: "w-full h-full block object-cover"
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
15
24
|
const theme = {
|
|
16
25
|
__proto__: null,
|
|
17
|
-
buttonClear: buttonClear
|
|
26
|
+
buttonClear: buttonClear,
|
|
27
|
+
uploraImage: uploraImage$1
|
|
18
28
|
};
|
|
19
29
|
|
|
20
30
|
const uploraImage = {
|
|
@@ -125,9 +135,13 @@ const module = defineNuxtModule({
|
|
|
125
135
|
},
|
|
126
136
|
defaults: {
|
|
127
137
|
name: "cms",
|
|
128
|
-
prefix: "C"
|
|
138
|
+
prefix: "C",
|
|
139
|
+
fluxorUrl: "https://fluxor.uplora.ru"
|
|
129
140
|
},
|
|
130
141
|
async setup(options, nuxt) {
|
|
142
|
+
nuxt.options.runtimeConfig.public.cms = defu(nuxt.options.runtimeConfig.public.cms || {}, {
|
|
143
|
+
fluxorUrl: options.fluxorUrl
|
|
144
|
+
});
|
|
131
145
|
nuxt.options.appConfig.ui = defu(nuxt.options.appConfig.ui || {}, {
|
|
132
146
|
icons
|
|
133
147
|
});
|
|
@@ -177,6 +191,7 @@ const module = defineNuxtModule({
|
|
|
177
191
|
});
|
|
178
192
|
addImportsDir(resolve("./runtime/composables"));
|
|
179
193
|
addTemplates(options);
|
|
194
|
+
addPlugin(resolve("./runtime/plugins/api.ts"));
|
|
180
195
|
}
|
|
181
196
|
});
|
|
182
197
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<figure :class="ui.root({ class: [props.ui?.root, props.class] })">
|
|
3
|
+
<div
|
|
4
|
+
v-if="lqip"
|
|
5
|
+
:class="ui.lqip({ class: props.ui?.lqip })"
|
|
6
|
+
:style="{
|
|
7
|
+
backgroundImage: `url(${lqip})`
|
|
8
|
+
}"
|
|
9
|
+
/>
|
|
10
|
+
<picture :class="ui.picture({ class: props.ui?.picture })">
|
|
11
|
+
<template v-if="sources.length">
|
|
12
|
+
<source
|
|
13
|
+
v-for="(source, key) in sources"
|
|
14
|
+
:key="key"
|
|
15
|
+
:srcset="source.srcset"
|
|
16
|
+
:type="source.type"
|
|
17
|
+
>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<img
|
|
21
|
+
ref="imageRef"
|
|
22
|
+
v-bind="imageAttrs"
|
|
23
|
+
:src="image.img"
|
|
24
|
+
:srcset="image?.srcset"
|
|
25
|
+
:alt="alt"
|
|
26
|
+
:loading="loading"
|
|
27
|
+
:class="ui.img({ class: props.ui?.img })"
|
|
28
|
+
>
|
|
29
|
+
</picture>
|
|
30
|
+
</figure>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script>
|
|
34
|
+
import theme from "#build/cms/uplora-image";
|
|
35
|
+
import { computed, onMounted, useAppConfig, useHead, useNuxtApp, useTemplateRef } from "#imports";
|
|
36
|
+
import { buildUploraImage } from "../utils";
|
|
37
|
+
import { tv } from "../utils/tv";
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<script setup>
|
|
41
|
+
const props = defineProps({
|
|
42
|
+
id: { type: String, required: true },
|
|
43
|
+
alt: { type: String, required: true },
|
|
44
|
+
formats: { type: Array, required: false },
|
|
45
|
+
sizes: { type: Array, required: false },
|
|
46
|
+
lqip: { type: String, required: false },
|
|
47
|
+
loading: { type: String, required: false, default: "lazy" },
|
|
48
|
+
preload: { type: [Boolean, Object], required: false },
|
|
49
|
+
nonce: { type: String, required: false },
|
|
50
|
+
imgAttrs: { type: Object, required: false },
|
|
51
|
+
class: { type: null, required: false },
|
|
52
|
+
ui: { type: null, required: false }
|
|
53
|
+
});
|
|
54
|
+
const emit = defineEmits(["load", "error"]);
|
|
55
|
+
const nuxtApp = useNuxtApp();
|
|
56
|
+
const initialLoad = nuxtApp.isHydrating;
|
|
57
|
+
const image = computed(() => buildUploraImage({
|
|
58
|
+
id: props.id,
|
|
59
|
+
formats: props.formats,
|
|
60
|
+
sizes: props.sizes
|
|
61
|
+
}));
|
|
62
|
+
const sources = computed(() => {
|
|
63
|
+
if (image.value.sources.length > 1) {
|
|
64
|
+
return image.value.sources.slice(1);
|
|
65
|
+
}
|
|
66
|
+
return [];
|
|
67
|
+
});
|
|
68
|
+
if (import.meta.server && props.preload) {
|
|
69
|
+
useHead({ link: () => {
|
|
70
|
+
if (!image.value.img) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const link = {
|
|
74
|
+
rel: "preload",
|
|
75
|
+
as: "image",
|
|
76
|
+
imagesrcset: image.value.srcset,
|
|
77
|
+
nonce: props.nonce,
|
|
78
|
+
...typeof props.preload !== "boolean" && props.preload?.fetchPriority ? { fetchpriority: props.preload.fetchPriority } : {}
|
|
79
|
+
};
|
|
80
|
+
return [link];
|
|
81
|
+
} });
|
|
82
|
+
}
|
|
83
|
+
const imageRef = useTemplateRef("imageRef");
|
|
84
|
+
const imageAttrs = computed(() => ({
|
|
85
|
+
...props.imgAttrs || {},
|
|
86
|
+
...import.meta.server ? { onerror: "this.setAttribute('data-error', 1)" } : {}
|
|
87
|
+
}));
|
|
88
|
+
onMounted(() => {
|
|
89
|
+
if (!imageRef.value) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (imageRef.value.complete && initialLoad) {
|
|
93
|
+
if (imageRef.value.getAttribute("data-error")) {
|
|
94
|
+
emit("error", new Event("error"));
|
|
95
|
+
} else {
|
|
96
|
+
emit("load", new Event("load"));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
imageRef.value.onload = (event) => {
|
|
100
|
+
emit("load", event);
|
|
101
|
+
};
|
|
102
|
+
imageRef.value.onerror = (event) => {
|
|
103
|
+
emit("error", event);
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
const appConfig = useAppConfig();
|
|
107
|
+
const ui = computed(() => tv({ extend: tv(theme), ...appConfig.cms?.uploraImage || {} })());
|
|
108
|
+
</script>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AppConfig } from '@nuxt/schema';
|
|
2
|
+
import type { ImgHTMLAttributes } from 'vue';
|
|
3
|
+
import type { ComponentConfig, ImageFormat, ImageSize } from '../types';
|
|
4
|
+
import theme from '#build/cms/uplora-image';
|
|
5
|
+
export type UploraImage = ComponentConfig<typeof theme, AppConfig, 'uploraImage'>;
|
|
6
|
+
export interface UploraImageProps {
|
|
7
|
+
id: string;
|
|
8
|
+
alt: string;
|
|
9
|
+
formats?: ImageFormat[];
|
|
10
|
+
sizes?: ImageSize[];
|
|
11
|
+
lqip?: string;
|
|
12
|
+
loading?: 'lazy' | 'eager';
|
|
13
|
+
preload?: boolean | {
|
|
14
|
+
fetchPriority: 'auto' | 'high' | 'low';
|
|
15
|
+
};
|
|
16
|
+
nonce?: string;
|
|
17
|
+
imgAttrs?: ImgHTMLAttributes;
|
|
18
|
+
class?: any;
|
|
19
|
+
ui?: UploraImage['slots'];
|
|
20
|
+
}
|
|
21
|
+
export interface UploraImageEmits {
|
|
22
|
+
load: [Event];
|
|
23
|
+
error: [string | Event];
|
|
24
|
+
}
|
|
25
|
+
declare const _default: import("vue").DefineComponent<UploraImageProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
26
|
+
error: (args_0: string | Event) => any;
|
|
27
|
+
load: (args_0: Event) => any;
|
|
28
|
+
}, string, import("vue").PublicProps, Readonly<UploraImageProps> & Readonly<{
|
|
29
|
+
onError?: ((args_0: string | Event) => any) | undefined;
|
|
30
|
+
onLoad?: ((args_0: Event) => any) | undefined;
|
|
31
|
+
}>, {
|
|
32
|
+
loading: "lazy" | "eager";
|
|
33
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
34
|
+
export default _default;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AvailableRouterMethod, NitroFetchRequest } from 'nitropack';
|
|
2
|
+
import type { AsyncData, FetchResult, UseFetchOptions } from 'nuxt/app';
|
|
3
|
+
import type { DefaultAsyncDataValue } from 'nuxt/app/defaults';
|
|
4
|
+
import type { FetchError } from 'ofetch';
|
|
5
|
+
import type { Ref } from 'vue';
|
|
6
|
+
type PickFrom<T, K extends Array<string>> = T extends Array<any> ? T : T extends Record<string, any> ? keyof T extends K[number] ? T : K[number] extends never ? T : Pick<T, K[number]> : T;
|
|
7
|
+
type KeysOf<T> = Array<T extends T ? (keyof T extends string ? keyof T : never) : never>;
|
|
8
|
+
/**
|
|
9
|
+
* На данный момент нет возможности использовать `useFetch` со своим $fetch,
|
|
10
|
+
* поэтому приходится использовать костыль.
|
|
11
|
+
* https://github.com/nuxt/nuxt/issues/14736
|
|
12
|
+
*/
|
|
13
|
+
export declare function useApi<ResT = void, ErrorT = FetchError, ReqT extends NitroFetchRequest = NitroFetchRequest, Method extends AvailableRouterMethod<ReqT> = ResT extends void ? 'get' extends AvailableRouterMethod<ReqT> ? 'get' : AvailableRouterMethod<ReqT> : AvailableRouterMethod<ReqT>, _ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT, DataT = _ResT, PickKeys extends KeysOf<DataT> = KeysOf<DataT>, DefaultT = DefaultAsyncDataValue>(request: Ref<ReqT> | ReqT | (() => ReqT), opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | DefaultAsyncDataValue>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useFetch, useNuxtApp } from "#imports";
|
|
2
|
+
import { defu } from "defu";
|
|
3
|
+
export function useApi(request, opts) {
|
|
4
|
+
const $api = useNuxtApp().$api;
|
|
5
|
+
return useFetch(
|
|
6
|
+
request,
|
|
7
|
+
defu(
|
|
8
|
+
{
|
|
9
|
+
$fetch: $api
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
...opts
|
|
13
|
+
}
|
|
14
|
+
)
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ComputedRef, MaybeRefOrGetter } from 'vue';
|
|
2
|
+
import type { SEO } from '../types';
|
|
3
|
+
interface SEOStats {
|
|
4
|
+
progress: number;
|
|
5
|
+
color: 'error' | 'warning' | 'success';
|
|
6
|
+
}
|
|
7
|
+
interface UseSEOStatsReturn {
|
|
8
|
+
title: ComputedRef<SEOStats>;
|
|
9
|
+
description: ComputedRef<SEOStats>;
|
|
10
|
+
}
|
|
11
|
+
export declare function useSEOStats(options: MaybeRefOrGetter<SEO>): UseSEOStatsReturn;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { computed, toValue } from "#imports";
|
|
2
|
+
function computeScore(length, recommendedLength, maxLength) {
|
|
3
|
+
if (length === 0) {
|
|
4
|
+
return 0;
|
|
5
|
+
}
|
|
6
|
+
if (length < recommendedLength) {
|
|
7
|
+
return length / recommendedLength * 100;
|
|
8
|
+
}
|
|
9
|
+
if (length > maxLength) {
|
|
10
|
+
return Math.max(0, 100 - (length - maxLength) / maxLength * 100);
|
|
11
|
+
}
|
|
12
|
+
return 100;
|
|
13
|
+
}
|
|
14
|
+
function computeColor(score) {
|
|
15
|
+
if (score < 30) {
|
|
16
|
+
return "error";
|
|
17
|
+
}
|
|
18
|
+
if (score < 70) {
|
|
19
|
+
return "warning";
|
|
20
|
+
}
|
|
21
|
+
return "success";
|
|
22
|
+
}
|
|
23
|
+
export function useSEOStats(options) {
|
|
24
|
+
const title = computed(() => {
|
|
25
|
+
const value = toValue(options);
|
|
26
|
+
const score = computeScore(value.title.length, 45, 60);
|
|
27
|
+
return {
|
|
28
|
+
progress: Math.round(score),
|
|
29
|
+
color: computeColor(score)
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
const description = computed(() => {
|
|
33
|
+
const value = toValue(options);
|
|
34
|
+
const score = computeScore(value.description.length, 130, 160);
|
|
35
|
+
return {
|
|
36
|
+
progress: Math.round(score),
|
|
37
|
+
color: computeColor(score)
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
title,
|
|
42
|
+
description
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
declare const _default: import("#app").Plugin<{
|
|
2
|
+
api: import("nitropack/types").$Fetch<unknown, import("nitropack/types").NitroFetchRequest>;
|
|
3
|
+
}> & import("#app").ObjectPlugin<{
|
|
4
|
+
api: import("nitropack/types").$Fetch<unknown, import("nitropack/types").NitroFetchRequest>;
|
|
5
|
+
}>;
|
|
6
|
+
export default _default;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineNuxtPlugin, navigateTo, useAppConfig, useToast } from "#imports";
|
|
2
|
+
export default defineNuxtPlugin({
|
|
3
|
+
name: "cms-api",
|
|
4
|
+
async setup() {
|
|
5
|
+
const toast = useToast();
|
|
6
|
+
const appConfig = useAppConfig();
|
|
7
|
+
const api = $fetch.create({
|
|
8
|
+
async onResponseError({ response }) {
|
|
9
|
+
toast.add({
|
|
10
|
+
title: "\u041E\u0448\u0438\u0431\u043A\u0430",
|
|
11
|
+
description: response._data.message ?? "\u0427\u0442\u043E-\u0442\u043E \u043F\u043E\u0448\u043B\u043E \u043D\u0435 \u0442\u0430\u043A",
|
|
12
|
+
color: "error",
|
|
13
|
+
icon: appConfig.ui.icons.close
|
|
14
|
+
});
|
|
15
|
+
if (response.status === 401) {
|
|
16
|
+
await navigateTo({ name: "index" });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
provide: {
|
|
22
|
+
api
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ImageExtension, ImageMimeType } from '@uplora/formats';
|
|
2
|
+
export type ImageFormat = ImageExtension;
|
|
3
|
+
/**
|
|
4
|
+
* Размер изображения
|
|
5
|
+
*/
|
|
6
|
+
export interface ImageSize {
|
|
7
|
+
/**
|
|
8
|
+
* Ширина
|
|
9
|
+
*
|
|
10
|
+
* @example 1024
|
|
11
|
+
*/
|
|
12
|
+
width?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Высота
|
|
15
|
+
*
|
|
16
|
+
* @example 1024
|
|
17
|
+
*/
|
|
18
|
+
height?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Описание размера
|
|
21
|
+
*
|
|
22
|
+
* @example 2x, 512px
|
|
23
|
+
*/
|
|
24
|
+
descriptor: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Источник изображения
|
|
28
|
+
*/
|
|
29
|
+
export interface ImageSource {
|
|
30
|
+
/**
|
|
31
|
+
* Image URL
|
|
32
|
+
*
|
|
33
|
+
* @example https://uplora.ru/fgu1yd2ncj05iogcooz31u55
|
|
34
|
+
*/
|
|
35
|
+
img: string;
|
|
36
|
+
/**
|
|
37
|
+
* Image srcset
|
|
38
|
+
*
|
|
39
|
+
* @example https://uplora.ru/fgu1yd2ncj05iogcooz31u55/-/format/jpg 1x, https://uplora.ru/fgu1yd2ncj05iogcooz31u55/-/format/jpg 2x
|
|
40
|
+
*/
|
|
41
|
+
srcset?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Image mime type
|
|
44
|
+
*
|
|
45
|
+
* @example image/jpeg
|
|
46
|
+
*/
|
|
47
|
+
type?: ImageMimeType;
|
|
48
|
+
}
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Filters } from '@uplora/serializer';
|
|
2
|
+
import type { ImageFormat, ImageSize, ImageSource } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Создает функцию для получения URL изображения из Uplora
|
|
5
|
+
*/
|
|
6
|
+
export declare function createUploraImageResolver(): (id: string, filters?: Filters) => string;
|
|
7
|
+
export interface BuildUploraImageOptions {
|
|
8
|
+
id: string;
|
|
9
|
+
formats?: ImageFormat[];
|
|
10
|
+
sizes?: ImageSize[];
|
|
11
|
+
}
|
|
12
|
+
export interface BuildUploraImageReturn {
|
|
13
|
+
img: string;
|
|
14
|
+
original: string;
|
|
15
|
+
sources: ImageSource[];
|
|
16
|
+
srcset?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Создает URL изображения из Uplora
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildUploraImage(options: BuildUploraImageOptions): BuildUploraImageReturn;
|
|
22
|
+
export interface GenerateImageSizesOptions {
|
|
23
|
+
width: number;
|
|
24
|
+
minWidth?: number;
|
|
25
|
+
step?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Генерирует размеры изображения
|
|
29
|
+
*/
|
|
30
|
+
export declare function generateImageSizes(options: GenerateImageSizesOptions): ImageSize[];
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useRuntimeConfig } from "#imports";
|
|
2
|
+
import { imagesExtensionsToMimeTypes } from "@uplora/formats";
|
|
3
|
+
import { serialize } from "@uplora/serializer";
|
|
4
|
+
export function createUploraImageResolver() {
|
|
5
|
+
const runtimeConfig = useRuntimeConfig();
|
|
6
|
+
const { fluxorUrl } = runtimeConfig.public.cms;
|
|
7
|
+
return (id, filters) => {
|
|
8
|
+
const serializeFilters = filters ? serialize(filters) : "";
|
|
9
|
+
return `${fluxorUrl}/${id}${serializeFilters ? `/${serializeFilters}` : ""}`;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function buildUploraImage(options) {
|
|
13
|
+
const resolver = createUploraImageResolver();
|
|
14
|
+
const original = resolver(options.id);
|
|
15
|
+
const sizes = options.sizes ?? [{ descriptor: "1x" }];
|
|
16
|
+
const sources = [];
|
|
17
|
+
function makeSrcset(format) {
|
|
18
|
+
if (sizes && sizes.length > 1) {
|
|
19
|
+
return sizes.map((size) => `${resolver(options.id, { resize: { width: size.width, height: size.height }, format })} ${size.descriptor}`).join(", ");
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (options.formats) {
|
|
24
|
+
for (const format of options.formats) {
|
|
25
|
+
const srcset = makeSrcset(format);
|
|
26
|
+
sources.push({
|
|
27
|
+
img: resolver(options.id, {
|
|
28
|
+
resize: {
|
|
29
|
+
width: sizes[0]?.width,
|
|
30
|
+
height: sizes[0]?.height
|
|
31
|
+
},
|
|
32
|
+
format
|
|
33
|
+
}),
|
|
34
|
+
...srcset ? { srcset } : {},
|
|
35
|
+
type: imagesExtensionsToMimeTypes[format]
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
const srcset = makeSrcset();
|
|
40
|
+
sources.push({
|
|
41
|
+
img: resolver(options.id, {
|
|
42
|
+
resize: {
|
|
43
|
+
width: sizes[0]?.width,
|
|
44
|
+
height: sizes[0]?.height
|
|
45
|
+
}
|
|
46
|
+
}),
|
|
47
|
+
...srcset ? { srcset } : {}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
img: original,
|
|
52
|
+
original,
|
|
53
|
+
...sources.length > 0 ? {
|
|
54
|
+
img: sources[0]?.img,
|
|
55
|
+
srcset: sources[0]?.srcset
|
|
56
|
+
} : {},
|
|
57
|
+
sources
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function generateImageSizes(options) {
|
|
61
|
+
const { width, minWidth = 640, step = 0.7 } = options;
|
|
62
|
+
const sizes = [];
|
|
63
|
+
let currentWidth = width;
|
|
64
|
+
while (true) {
|
|
65
|
+
const normalizedWidth = Math.round(currentWidth);
|
|
66
|
+
sizes.push({
|
|
67
|
+
width: normalizedWidth,
|
|
68
|
+
descriptor: `${Math.round(normalizedWidth * step)}w`
|
|
69
|
+
});
|
|
70
|
+
currentWidth *= step;
|
|
71
|
+
if (currentWidth < minWidth) {
|
|
72
|
+
const normalizedWidth2 = Math.round(currentWidth);
|
|
73
|
+
sizes.push({
|
|
74
|
+
width: normalizedWidth2,
|
|
75
|
+
descriptor: `${Math.round(normalizedWidth2 * step)}w`
|
|
76
|
+
});
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return sizes;
|
|
81
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export * from './image';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./image.js";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hywax/cms",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.3",
|
|
5
5
|
"description": "Hywax CMS. ⚠️ This package is intended for internal use only.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -44,10 +44,14 @@
|
|
|
44
44
|
]
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
|
+
"bin": {
|
|
48
|
+
"cms": "./cli/index.mjs"
|
|
49
|
+
},
|
|
47
50
|
"style": "./dist/runtime/index.css",
|
|
48
51
|
"files": [
|
|
49
52
|
".nuxt/cms",
|
|
50
53
|
".nuxt/cms.css",
|
|
54
|
+
"cli",
|
|
51
55
|
"dist"
|
|
52
56
|
],
|
|
53
57
|
"dependencies": {
|
|
@@ -56,6 +60,8 @@
|
|
|
56
60
|
"@nuxt/ui-pro": "^3.1.3",
|
|
57
61
|
"@nuxtjs/mdc": "^0.17.0",
|
|
58
62
|
"@unpress/mdc-prosemirror": "0.1.12-beta.2",
|
|
63
|
+
"@uplora/formats": "^0.1.0",
|
|
64
|
+
"@uplora/serializer": "^0.1.3",
|
|
59
65
|
"@vueuse/nuxt": "^13.4.0",
|
|
60
66
|
"defu": "^6.1.4",
|
|
61
67
|
"nuxt-auth-utils": "^0.5.20",
|
|
@@ -82,6 +88,9 @@
|
|
|
82
88
|
"vitest": "^3.2.3",
|
|
83
89
|
"vue-tsc": "^2.2.10"
|
|
84
90
|
},
|
|
91
|
+
"resolutions": {
|
|
92
|
+
"@hywax/cms": "workspace:*"
|
|
93
|
+
},
|
|
85
94
|
"lint-staged": {
|
|
86
95
|
"*.ts": "pnpm run lint"
|
|
87
96
|
},
|