@xleddyl/nuxt-cms 0.1.17 → 0.1.20
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/README.md +16 -0
- package/dist/module.d.mts +1 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +45 -7
- package/dist/runtime/app/components/cms/Button.d.vue.ts +3 -2
- package/dist/runtime/app/components/cms/Button.vue +16 -3
- package/dist/runtime/app/components/cms/Button.vue.d.ts +3 -2
- package/dist/runtime/app/components/cms/FieldInput.vue +5 -2
- package/dist/runtime/app/components/cms/Icon.vue +6 -1
- package/dist/runtime/app/composables/cms-query-disabled.d.ts +6 -0
- package/dist/runtime/app/composables/cms-query-disabled.js +10 -0
- package/dist/runtime/app/layouts/cms-admin.vue +4 -6
- package/dist/runtime/app/pages/admin-collection.vue +2 -15
- package/dist/runtime/app/pages/admin-login.vue +2 -3
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/shared/graphql-sdl.js +3 -0
- package/dist/runtime/shared/index.d.ts +3 -0
- package/dist/runtime/shared/index.js +3 -0
- package/dist/runtime/shared/validation.js +5 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -34,12 +34,28 @@ export default defineNuxtConfig({
|
|
|
34
34
|
|
|
35
35
|
Then declare your content types in a `cms.config.ts` at the project root with `defineCmsConfig()`.
|
|
36
36
|
|
|
37
|
+
### Disabling the CMS
|
|
38
|
+
|
|
39
|
+
Keep the module in `modules[]` at all times and turn it off with the `enabled` option or the
|
|
40
|
+
`NUXT_CMS_ENABLED` env var. When disabled the module registers no-op `useCms` / `$cmsQuery` stubs and
|
|
41
|
+
nothing else, so components can call them unconditionally and simply render their empty states:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
export default defineNuxtConfig({
|
|
45
|
+
modules: ['@xleddyl/nuxt-cms'],
|
|
46
|
+
cms: { enabled: false },
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
See [Configuration](docs/configuration.md#enabled) for the resolution order.
|
|
51
|
+
|
|
37
52
|
### Environment variables
|
|
38
53
|
|
|
39
54
|
Every secret maps to runtime config, so it can be set as an env var instead of in `nuxt.config.ts`:
|
|
40
55
|
|
|
41
56
|
| Variable | Required | Purpose |
|
|
42
57
|
| --- | --- | --- |
|
|
58
|
+
| `NUXT_CMS_ENABLED` | no | set to `0` / `false` to disable the CMS (default enabled) |
|
|
43
59
|
| `NUXT_CMS_ADMIN_EMAIL` | yes | admin login email |
|
|
44
60
|
| `NUXT_CMS_ADMIN_PASSWORD` | yes | admin login password |
|
|
45
61
|
| `NUXT_SESSION_PASSWORD` | in production | session encryption key (32+ chars) |
|
package/dist/module.d.mts
CHANGED
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -3,14 +3,14 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { isAbsolute, resolve, relative, join, dirname } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { defineNuxtModule, createResolver, useLogger, resolvePath, addTemplate,
|
|
6
|
+
import { defineNuxtModule, createResolver, useLogger, addImports, resolvePath, addTemplate, addServerPlugin, addTypeTemplate, addVitePlugin, addLayout, addComponentsDir, addRouteMiddleware, extendPages, addServerHandler } from '@nuxt/kit';
|
|
7
7
|
import tailwindcss from '@tailwindcss/vite';
|
|
8
8
|
import svgLoader from 'vite-svg-loader';
|
|
9
9
|
import { introspectionFromSchema, buildSchema } from 'graphql';
|
|
10
10
|
import { minifyIntrospection, outputIntrospectionFile } from 'gql.tada/internal';
|
|
11
11
|
import { createJiti } from 'jiti';
|
|
12
12
|
import { typeName, blockTypeName, blockUnionName, renderGraphqlSdl } from '../dist/runtime/shared/graphql-sdl.js';
|
|
13
|
-
import { isTranslatableField } from '../dist/runtime/shared/index.js';
|
|
13
|
+
import { isTranslatableField, isMultiSelect } from '../dist/runtime/shared/index.js';
|
|
14
14
|
|
|
15
15
|
const IDENTIFIER = /^[a-z_]\w*$/i;
|
|
16
16
|
const RESERVED_ENTRY_KEYS = ["admin", "auth", "login", "media", "graphql", "cms_media"];
|
|
@@ -129,6 +129,9 @@ function validateConfig(config, i18n) {
|
|
|
129
129
|
if (blockField.type === "select" && !blockField.options?.length) {
|
|
130
130
|
errors.push(`${bfat}: select requires a non-empty options array`);
|
|
131
131
|
}
|
|
132
|
+
if (blockField.type === "select" && blockField.multiple) {
|
|
133
|
+
errors.push(`${bfat}: multiple select is not supported inside blocks`);
|
|
134
|
+
}
|
|
132
135
|
}
|
|
133
136
|
}
|
|
134
137
|
}
|
|
@@ -209,6 +212,9 @@ function columnExpr(key, field, dialect) {
|
|
|
209
212
|
case "blocks":
|
|
210
213
|
expr = jsonExpr(col, dialect);
|
|
211
214
|
break;
|
|
215
|
+
case "select":
|
|
216
|
+
expr = field.multiple ? jsonExpr(col, dialect) : `text('${col}')`;
|
|
217
|
+
break;
|
|
212
218
|
case "slug":
|
|
213
219
|
expr = `text('${col}').unique()`;
|
|
214
220
|
break;
|
|
@@ -284,7 +290,9 @@ function renderSchemaFile(config, dialect, resolveImport = (s) => s) {
|
|
|
284
290
|
core.add(pg ? "doublePrecision" : "real");
|
|
285
291
|
if (pg && fields.some((f) => f.type === "date")) core.add("date");
|
|
286
292
|
if (pg && fields.some((f) => f.type === "boolean")) core.add("boolean");
|
|
287
|
-
if (pg && fields.some(
|
|
293
|
+
if (pg && fields.some(
|
|
294
|
+
(f) => f.type === "json" || f.type === "blocks" || isTranslatableField(f) || isMultiSelect(f)
|
|
295
|
+
))
|
|
288
296
|
core.add("jsonb");
|
|
289
297
|
if (pg) core.add("timestamp");
|
|
290
298
|
if (fields.some(isManyToMany)) core.add("primaryKey");
|
|
@@ -327,6 +335,10 @@ function fieldTsType(config, entryName, key, field) {
|
|
|
327
335
|
return field.required && !config[field.to]?.drafts ? target : `${target} | null`;
|
|
328
336
|
}
|
|
329
337
|
if (field.type === "media") return "CmsMedia | null";
|
|
338
|
+
if (field.type === "select" && field.multiple) {
|
|
339
|
+
const union = field.options.map((o) => JSON.stringify(o)).join(" | ");
|
|
340
|
+
return `(${union})[]`;
|
|
341
|
+
}
|
|
330
342
|
if (field.type === "blocks") {
|
|
331
343
|
const union = blockUnionName(entryName, key);
|
|
332
344
|
return field.required ? `${union}[]` : `${union}[] | null`;
|
|
@@ -393,14 +405,25 @@ function renderTypesFile(config) {
|
|
|
393
405
|
`;
|
|
394
406
|
}
|
|
395
407
|
|
|
408
|
+
const CMS_ENABLED_ENV = "NUXT_CMS_ENABLED";
|
|
409
|
+
function resolveCmsEnabled(explicit, envValue) {
|
|
410
|
+
if (typeof explicit === "boolean") return explicit;
|
|
411
|
+
if (typeof envValue === "string") {
|
|
412
|
+
const normalized = envValue.trim().toLowerCase();
|
|
413
|
+
return normalized === "1" || normalized === "true";
|
|
414
|
+
}
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
|
|
396
418
|
const module$1 = defineNuxtModule({
|
|
397
419
|
meta: {
|
|
398
420
|
name: "@xleddyl/nuxt-cms",
|
|
399
421
|
configKey: "cms"
|
|
400
422
|
},
|
|
401
|
-
moduleDependencies:
|
|
402
|
-
|
|
403
|
-
|
|
423
|
+
moduleDependencies: (nuxt) => resolveCmsEnabled(
|
|
424
|
+
nuxt.options.cms?.enabled,
|
|
425
|
+
process.env[CMS_ENABLED_ENV]
|
|
426
|
+
) ? { "nuxt-auth-utils": {} } : {},
|
|
404
427
|
defaults: {
|
|
405
428
|
configPath: "cms.config",
|
|
406
429
|
admin: {
|
|
@@ -433,6 +456,21 @@ const module$1 = defineNuxtModule({
|
|
|
433
456
|
async setup(options, nuxt) {
|
|
434
457
|
const resolver = createResolver(import.meta.url);
|
|
435
458
|
const logger = useLogger("nuxt-cms");
|
|
459
|
+
if (!resolveCmsEnabled(options.enabled, process.env[CMS_ENABLED_ENV])) {
|
|
460
|
+
const stub = resolver.resolve("./runtime/app/composables/cms-query-disabled");
|
|
461
|
+
addImports([
|
|
462
|
+
{ name: "useCms", from: stub },
|
|
463
|
+
{ name: "$cmsQuery", from: stub }
|
|
464
|
+
]);
|
|
465
|
+
nuxt.options.runtimeConfig.public.cms = {
|
|
466
|
+
mediaBaseUrl: options.media.publicBaseUrl,
|
|
467
|
+
i18n: options.i18n
|
|
468
|
+
};
|
|
469
|
+
logger.info(
|
|
470
|
+
"[nuxt-cms] disabled: registering no-op useCms/$cmsQuery stubs, skipping admin, server and database setup"
|
|
471
|
+
);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
436
474
|
const configPath = await resolvePath(options.configPath, { cwd: nuxt.options.rootDir });
|
|
437
475
|
nuxt.options.alias["#nuxt-cms"] = resolver.resolve("./runtime/shared/index");
|
|
438
476
|
nuxt.options.watch.push(configPath);
|
|
@@ -556,7 +594,7 @@ const module$1 = defineNuxtModule({
|
|
|
556
594
|
`export default {`,
|
|
557
595
|
` dialect: '${dialect}',`,
|
|
558
596
|
` schema: '${toPosix(schemaTemplate.dst)}',`,
|
|
559
|
-
` out: '${toPosix(
|
|
597
|
+
` out: '${toPosix(relativeMigrationsDir)}',`,
|
|
560
598
|
driver === "postgres" ? ` dbCredentials: { url: process.env.NUXT_CMS_DATABASE_URL ?? '${databaseUrl}' },` : driver === "libsql" ? ` dbCredentials: { url: process.env.NUXT_CMS_DATABASE_URL ?? '${databaseUrl || `file:${toPosix(resolvedDbPath)}`}', authToken: (process.env.NUXT_CMS_DATABASE_AUTH_TOKEN ?? '${databaseAuthToken}') || undefined },` : ` dbCredentials: { url: '${toPosix(resolvedDbPath)}' },`,
|
|
561
599
|
`}`,
|
|
562
600
|
``
|
|
@@ -3,6 +3,7 @@ type __VLS_Props = {
|
|
|
3
3
|
type?: 'button' | 'submit' | 'reset';
|
|
4
4
|
form?: string;
|
|
5
5
|
icon?: string;
|
|
6
|
+
trailingIcon?: boolean;
|
|
6
7
|
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
7
8
|
color?: 'primary' | 'neutral' | 'error' | 'success';
|
|
8
9
|
variant?: 'solid' | 'subtle' | 'soft' | 'ghost';
|
|
@@ -10,9 +11,9 @@ type __VLS_Props = {
|
|
|
10
11
|
loading?: boolean;
|
|
11
12
|
block?: boolean;
|
|
12
13
|
};
|
|
13
|
-
declare var
|
|
14
|
+
declare var __VLS_6: {};
|
|
14
15
|
type __VLS_Slots = {} & {
|
|
15
|
-
default?: (props: typeof
|
|
16
|
+
default?: (props: typeof __VLS_6) => any;
|
|
16
17
|
};
|
|
17
18
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
18
19
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -11,18 +11,30 @@
|
|
|
11
11
|
{ 'is-block': block }
|
|
12
12
|
]"
|
|
13
13
|
>
|
|
14
|
-
<CmsIcon
|
|
15
|
-
|
|
14
|
+
<CmsIcon
|
|
15
|
+
v-if="iconName && !trailingIcon"
|
|
16
|
+
:name="iconName"
|
|
17
|
+
class="size-4 shrink-0"
|
|
18
|
+
:class="{ 'animate-spin': loading }"
|
|
19
|
+
/>
|
|
16
20
|
<slot>{{ label }}</slot>
|
|
21
|
+
<CmsIcon
|
|
22
|
+
v-if="iconName && trailingIcon"
|
|
23
|
+
:name="iconName"
|
|
24
|
+
class="size-4 shrink-0"
|
|
25
|
+
:class="{ 'animate-spin': loading }"
|
|
26
|
+
/>
|
|
17
27
|
</button>
|
|
18
28
|
</template>
|
|
19
29
|
|
|
20
30
|
<script setup>
|
|
21
|
-
|
|
31
|
+
import { computed } from "#imports";
|
|
32
|
+
const props = defineProps({
|
|
22
33
|
label: { type: String, required: false },
|
|
23
34
|
type: { type: String, required: false },
|
|
24
35
|
form: { type: String, required: false },
|
|
25
36
|
icon: { type: String, required: false },
|
|
37
|
+
trailingIcon: { type: Boolean, required: false },
|
|
26
38
|
size: { type: String, required: false },
|
|
27
39
|
color: { type: String, required: false },
|
|
28
40
|
variant: { type: String, required: false },
|
|
@@ -30,4 +42,5 @@ defineProps({
|
|
|
30
42
|
loading: { type: Boolean, required: false },
|
|
31
43
|
block: { type: Boolean, required: false }
|
|
32
44
|
});
|
|
45
|
+
const iconName = computed(() => props.loading ? "arrow-path" : props.icon);
|
|
33
46
|
</script>
|
|
@@ -3,6 +3,7 @@ type __VLS_Props = {
|
|
|
3
3
|
type?: 'button' | 'submit' | 'reset';
|
|
4
4
|
form?: string;
|
|
5
5
|
icon?: string;
|
|
6
|
+
trailingIcon?: boolean;
|
|
6
7
|
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
7
8
|
color?: 'primary' | 'neutral' | 'error' | 'success';
|
|
8
9
|
variant?: 'solid' | 'subtle' | 'soft' | 'ghost';
|
|
@@ -10,9 +11,9 @@ type __VLS_Props = {
|
|
|
10
11
|
loading?: boolean;
|
|
11
12
|
block?: boolean;
|
|
12
13
|
};
|
|
13
|
-
declare var
|
|
14
|
+
declare var __VLS_6: {};
|
|
14
15
|
type __VLS_Slots = {} & {
|
|
15
|
-
default?: (props: typeof
|
|
16
|
+
default?: (props: typeof __VLS_6) => any;
|
|
16
17
|
};
|
|
17
18
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
18
19
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
<CmsSelectMenu
|
|
22
22
|
v-else-if="field.type === 'select'"
|
|
23
23
|
v-model="selValue"
|
|
24
|
+
:multiple="field.multiple"
|
|
24
25
|
:items="selectItems"
|
|
25
26
|
placeholder="Select…"
|
|
26
27
|
size="lg"
|
|
@@ -63,7 +64,7 @@ const props = defineProps({
|
|
|
63
64
|
});
|
|
64
65
|
const model = defineModel({ type: null, ...{ required: true } });
|
|
65
66
|
const selectItems = computed(() => [
|
|
66
|
-
...props.field.required ? [] : [{ label: "\u2014", value: null }],
|
|
67
|
+
...props.field.required || props.field.multiple ? [] : [{ label: "\u2014", value: null }],
|
|
67
68
|
...(props.field.options ?? []).map((option) => ({ label: option, value: option }))
|
|
68
69
|
]);
|
|
69
70
|
function proxy(fromModel, toModel = (v) => v) {
|
|
@@ -79,7 +80,9 @@ const str = proxy(
|
|
|
79
80
|
(v) => v === "" ? null : v
|
|
80
81
|
);
|
|
81
82
|
const strOrNull = proxy((v) => v);
|
|
82
|
-
const selValue = proxy(
|
|
83
|
+
const selValue = proxy(
|
|
84
|
+
(v) => props.field.multiple ? v ?? [] : v ?? null
|
|
85
|
+
);
|
|
83
86
|
const num = proxy(
|
|
84
87
|
(v) => v ?? void 0,
|
|
85
88
|
(v) => typeof v === "number" && !Number.isNaN(v) ? v : null
|
|
@@ -7,11 +7,16 @@ import { defineAsyncComponent, shallowRef, watch } from "#imports";
|
|
|
7
7
|
const props = defineProps({
|
|
8
8
|
name: { type: String, required: true }
|
|
9
9
|
});
|
|
10
|
+
const icons = import.meta.glob("../../../assets/svg/*.svg", {
|
|
11
|
+
query: "?component",
|
|
12
|
+
import: "default"
|
|
13
|
+
});
|
|
10
14
|
const icon = shallowRef();
|
|
11
15
|
watch(
|
|
12
16
|
() => props.name,
|
|
13
17
|
(name) => {
|
|
14
|
-
|
|
18
|
+
const loader = name ? icons[`../../../assets/svg/${name}.svg`] : void 0;
|
|
19
|
+
icon.value = loader ? defineAsyncComponent(loader) : void 0;
|
|
15
20
|
},
|
|
16
21
|
{ immediate: true }
|
|
17
22
|
);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AsyncData } from 'nuxt/app';
|
|
2
|
+
type CmsDisabledResult = Record<string, any>;
|
|
3
|
+
type CmsDisabledVariables = Record<string, any>;
|
|
4
|
+
export declare function $cmsQuery<const Q extends string>(query: Q, variables?: CmsDisabledVariables): Promise<CmsDisabledResult>;
|
|
5
|
+
export declare function useCms<const Q extends string>(query: Q, variables?: CmsDisabledVariables): AsyncData<CmsDisabledResult | undefined, Error | undefined>;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useAsyncData } from "#imports";
|
|
2
|
+
export async function $cmsQuery(query, variables) {
|
|
3
|
+
return {};
|
|
4
|
+
}
|
|
5
|
+
export function useCms(query, variables) {
|
|
6
|
+
return useAsyncData(
|
|
7
|
+
`cms-gql:${query}:${JSON.stringify(variables ?? {})}`,
|
|
8
|
+
async () => null
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
<div class="cms-display text-[22px] font-semibold text-(--ui-text-highlighted)">
|
|
9
9
|
nuxt<span class="text-(--cms-fern)">·</span>cms
|
|
10
10
|
</div>
|
|
11
|
-
<div class="cms-kicker mt-1.5">content studio</div>
|
|
12
11
|
</div>
|
|
13
12
|
|
|
14
13
|
<nav class="flex flex-1 flex-col gap-6 overflow-y-auto">
|
|
@@ -33,16 +32,15 @@
|
|
|
33
32
|
</nav>
|
|
34
33
|
|
|
35
34
|
<div class="flex flex-col gap-3 border-t border-(--cms-line) px-3 pt-5">
|
|
36
|
-
<div class="truncate text-sm font-medium text-(--ui-text-muted)">
|
|
37
|
-
{{ user?.email }}
|
|
38
|
-
</div>
|
|
39
35
|
<CmsButton
|
|
40
36
|
label="Sign out"
|
|
41
37
|
icon="arrow-right-on-rectangle"
|
|
38
|
+
trailing-icon
|
|
39
|
+
block
|
|
42
40
|
variant="ghost"
|
|
43
41
|
color="neutral"
|
|
44
42
|
size="xs"
|
|
45
|
-
class="
|
|
43
|
+
class="rounded-full"
|
|
46
44
|
@click="logout"
|
|
47
45
|
/>
|
|
48
46
|
</div>
|
|
@@ -64,7 +62,7 @@
|
|
|
64
62
|
import { computed, navigateTo, useRoute, useUserSession } from "#imports";
|
|
65
63
|
import cmsConfig from "#cms-config";
|
|
66
64
|
const route = useRoute();
|
|
67
|
-
const {
|
|
65
|
+
const { clear } = useUserSession();
|
|
68
66
|
const links = Object.entries(cmsConfig).map(([name, entry]) => ({
|
|
69
67
|
name,
|
|
70
68
|
label: entry.label,
|
|
@@ -71,15 +71,12 @@
|
|
|
71
71
|
@select="onSelect"
|
|
72
72
|
>
|
|
73
73
|
<template v-if="drafts" #status-cell="{ row }">
|
|
74
|
-
<
|
|
75
|
-
type="button"
|
|
74
|
+
<span
|
|
76
75
|
class="cms-badge"
|
|
77
76
|
:class="row.original.status === 'published' ? 'is-published' : 'is-draft'"
|
|
78
|
-
:title="row.original.status === 'published' ? 'Make draft' : 'Publish'"
|
|
79
|
-
@click.stop="toggleStatus(row.original)"
|
|
80
77
|
>
|
|
81
78
|
{{ row.original.status === "published" ? "Published" : "Draft" }}
|
|
82
|
-
</
|
|
79
|
+
</span>
|
|
83
80
|
</template>
|
|
84
81
|
<template #actions-cell="{ row }">
|
|
85
82
|
<div class="flex justify-end">
|
|
@@ -312,16 +309,6 @@ const kicker = computed(
|
|
|
312
309
|
function onSelect(_event, row) {
|
|
313
310
|
navigateTo(`/cms/${name}/${row.original.id}`);
|
|
314
311
|
}
|
|
315
|
-
async function toggleStatus(row) {
|
|
316
|
-
const next = row.status === "published" ? "draft" : "published";
|
|
317
|
-
await submit(async () => {
|
|
318
|
-
const fresh = await cmsApi(`${endpoint}/${row.id}`);
|
|
319
|
-
await cmsApi(`${endpoint}/${row.id}`, {
|
|
320
|
-
method: "PUT",
|
|
321
|
-
body: { ...pickFields(fresh), status: next }
|
|
322
|
-
});
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
312
|
function openCreate() {
|
|
326
313
|
navigateTo(`/cms/${name}/new`);
|
|
327
314
|
}
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
import { definePageMeta, navigateTo, ref, useUserSession } from "#imports";
|
|
56
56
|
import { CMS_FIELD_UI, errorMessage } from "../utils/ui";
|
|
57
57
|
definePageMeta({ layout: false });
|
|
58
|
-
const { loggedIn
|
|
58
|
+
const { loggedIn } = useUserSession();
|
|
59
59
|
if (loggedIn.value) {
|
|
60
60
|
await navigateTo("/cms", { replace: true });
|
|
61
61
|
}
|
|
@@ -67,8 +67,7 @@ async function login() {
|
|
|
67
67
|
error.value = null;
|
|
68
68
|
try {
|
|
69
69
|
await $fetch("/api/cms/auth/login", { method: "POST", body: state.value });
|
|
70
|
-
|
|
71
|
-
await navigateTo("/cms", { replace: true });
|
|
70
|
+
window.location.replace("/cms");
|
|
72
71
|
} catch (e) {
|
|
73
72
|
const status = e.statusCode;
|
|
74
73
|
error.value = status === 401 ? "Invalid email or password." : errorMessage(e) ?? "Login failed. Please try again.";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
@import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600&family=Hanken+Grotesk:ital,wght@0,300..800;1,300..800&family=Fragment+Mono:ital@0;1&display=swap");@layer theme, base, components, utilities;@import "tailwindcss/theme.css" layer(theme);.cms-scope{@import "tailwindcss/preflight.css" layer(base);@import "tailwindcss/utilities.css" layer(utilities) source(none)}@source "../app";.cms-scope,body:has(.cms-scope){--cms-paper:#f5f5f5;--cms-surface:#fff;--cms-field:#f0f0f0;--cms-ink:#171717;--cms-line:#e5e5e5;--cms-line-strong:#d4d4d4;--cms-fern:#171717;--cms-sage:#ebebeb;--cms-sage-ink:#171717;--cms-shadow-soft:none;color-scheme:light;--ui-text-dimmed:#a3a3a3;--ui-text-muted:#737373;--ui-text-toned:#525252;--ui-text:#404040;--ui-text-highlighted:var(--cms-ink);--ui-text-inverted:#fff;--ui-bg:var(--cms-surface);--ui-bg-muted:var(--cms-paper);--ui-bg-elevated:var(--cms-field);--ui-bg-accented:#e0e0e0;--ui-bg-inverted:var(--cms-ink);--ui-border:var(--cms-line);--ui-border-muted:var(--cms-line);--ui-border-accented:var(--cms-line-strong);--ui-border-inverted:var(--cms-ink);--ui-primary:var(--cms-ink);--ui-error:#dc2626;--ui-radius:0.4rem;color:var(--ui-text);font-family:Hanken Grotesk,ui-sans-serif,system-ui,sans-serif;margin:0;padding:0;-webkit-font-smoothing:antialiased}body:has(.cms-scope) ::-moz-selection{background:var(--cms-sage);color:var(--cms-sage-ink)}body:has(.cms-scope) ::selection{background:var(--cms-sage);color:var(--cms-sage-ink)}.cms-display{font-family:Poppins,ui-sans-serif,system-ui,sans-serif;letter-spacing:-.02em}.cms-kicker{color:var(--ui-text-dimmed);font-family:Fragment Mono,ui-monospace,monospace;font-size:.875rem;letter-spacing:.16em;line-height:1.4;text-transform:uppercase}body:has(.cms-scope) :is(button,a,[role=button],[role=menuitem],[role=option],.__clickable):not(:disabled,[aria-disabled=true]){@apply cursor-pointer transition-all duration-200 select-none}body:has(.cms-scope) :is(button,a,[role=button],[role=menuitem],[role=option],.__clickable):not(:disabled,[aria-disabled=true]):active{@apply transition-none;scale:.99}.cms-canvas{background-color:var(--cms-paper)}.cms-grain:before{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='a'%3E%3CfeTurbulence baseFrequency='.8' numOctaves='2' stitchTiles='stitch' type='fractalNoise'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)'/%3E%3C/svg%3E");content:"";inset:0;mix-blend-mode:multiply;opacity:.035;pointer-events:none;position:fixed;z-index:50}.cms-card{background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:1rem;box-shadow:var(--cms-shadow-soft)}.cms-empty{background:rgba(253,252,250,.5);border:1.5px dashed var(--cms-line-strong);border-radius:1rem}.cms-navlink{align-items:center;border-radius:.75rem;color:var(--ui-text-toned);display:flex;font-size:.875rem;font-weight:500;gap:.625rem;padding:.5rem .75rem;transition:background-color .15s ease,color .15s ease}.cms-navlink:hover{background:rgba(38,33,25,.05);color:var(--ui-text-highlighted)}.cms-navlink.is-active{background:var(--cms-sage);color:var(--cms-sage-ink);font-weight:600}.cms-navlink.is-active .cms-navlink-icon{color:var(--cms-fern)}.cms-badge{border:1px solid transparent;border-radius:9999px;cursor:pointer;font-family:Fragment Mono,ui-monospace,monospace;font-size:.875rem;letter-spacing:.12em;padding:.25rem .625rem;text-transform:uppercase;transition:background-color .15s ease,border-color .15s ease}.cms-badge.is-published{background:var(--cms-sage);color:var(--cms-sage-ink)}.cms-badge.is-published:hover{background:#e0e0e0}.cms-badge.is-draft{background:transparent;border:1px dashed var(--cms-line-strong);color:var(--ui-text-muted)}.cms-badge.is-draft:hover{background:rgba(38,33,25,.04)}.cms-dropzone{align-items:center;background:rgba(253,252,250,.5);border:1.5px dashed var(--cms-line-strong);border-radius:1rem;color:var(--ui-text-muted);cursor:pointer;display:flex;flex-direction:column;gap:.5rem;justify-content:center;padding:2rem 1.5rem;transition:border-color .15s ease,background-color .15s ease,color .15s ease;width:100%}.cms-dropzone.is-over,.cms-dropzone:hover{background:var(--cms-sage);border-color:var(--cms-fern);color:var(--cms-sage-ink)}.cms-dropzone.is-dense{border-radius:.75rem;flex-direction:row;gap:.625rem;padding:.875rem 1.25rem}.cms-dropzone.is-busy{opacity:.7;pointer-events:none}.cms-pill{background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:9999px;color:var(--ui-text-dimmed);cursor:pointer;font-family:Fragment Mono,ui-monospace,monospace;font-size:.875rem;letter-spacing:.12em;padding:.3rem .8rem;transition:background-color .15s ease,color .15s ease}.cms-pill:hover{background:rgba(38,33,25,.05);color:var(--ui-text)}.cms-pill.is-active{background:var(--cms-sage);color:var(--cms-sage-ink)}@keyframes cms-rise{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}.cms-rise{animation:cms-rise .55s cubic-bezier(.22,1,.36,1) both}@media (prefers-reduced-motion:reduce){.cms-rise{animation:none}}.cms-richtext{border:1px solid var(--cms-line);border-radius:.75rem;overflow:hidden}.cms-richtext-toolbar{align-items:center;border-bottom:1px solid var(--cms-line);display:flex;flex-wrap:wrap;gap:2px;padding:4px}.cms-richtext-body{font-size:.875rem;min-height:9rem;outline:none;padding:.75rem 1rem}.cms-richtext-body h2{font-size:1.25rem;font-weight:600;margin:.75em 0 .35em}.cms-richtext-body h3{font-size:1.1rem;font-weight:600;margin:.75em 0 .35em}.cms-richtext-body p{margin:.35em 0}.cms-richtext-body ul{list-style:disc;padding-left:1.25rem}.cms-richtext-body ol{list-style:decimal;padding-left:1.25rem}.cms-richtext-body blockquote{border-left:3px solid var(--cms-line);color:var(--ui-text-muted);padding-left:.75rem}.cms-btn{align-items:center;border:1px solid transparent;border-radius:var(--ui-radius);display:inline-flex;font-weight:500;gap:.375rem;justify-content:center;line-height:1;transition:background-color .15s ease,border-color .15s ease,color .15s ease,opacity .15s ease;white-space:nowrap}.cms-btn:disabled,.cms-btn[aria-disabled=true]{opacity:.5;pointer-events:none}.cms-btn.is-block{width:100%}.cms-btn-xs{font-size:.875rem;padding:.25rem .5rem}.cms-btn-sm{font-size:.875rem;padding:.375rem .625rem}.cms-btn-md{font-size:.875rem;padding:.5rem .875rem}.cms-btn-lg{font-size:.9375rem;padding:.625rem 1rem}.cms-btn-primary.cms-btn-solid{background:var(--ui-primary);color:var(--ui-text-inverted)}.cms-btn-primary.cms-btn-solid:hover{background:color-mix(in oklab,var(--ui-primary) 88%,#000)}.cms-btn-primary.cms-btn-subtle{background:var(--cms-sage);color:var(--cms-sage-ink)}.cms-btn-primary.cms-btn-subtle:hover{background:#e0e0e0}.cms-btn-primary.cms-btn-soft{background:var(--cms-sage);color:var(--cms-sage-ink)}.cms-btn-neutral.cms-btn-solid{background:var(--ui-bg-inverted);color:var(--ui-text-inverted)}.cms-btn-neutral.cms-btn-subtle{background:var(--ui-bg-elevated);color:var(--ui-text-highlighted)}.cms-btn-neutral.cms-btn-subtle:hover{background:var(--ui-bg-accented)}.cms-btn-neutral.cms-btn-soft{background:var(--ui-bg-elevated);color:var(--ui-text-highlighted)}.cms-btn-neutral.cms-btn-ghost{color:var(--ui-text-toned)}.cms-btn-neutral.cms-btn-ghost:hover{background:rgba(38,33,25,.06);color:var(--ui-text-highlighted)}.cms-btn-error.cms-btn-solid{background:var(--ui-error);color:#fff}.cms-btn-error.cms-btn-solid:hover{background:color-mix(in oklab,var(--ui-error) 88%,#000)}.cms-btn-error.cms-btn-subtle{background:color-mix(in oklab,var(--ui-error) 12%,transparent);color:var(--ui-error)}.cms-btn-error.cms-btn-ghost{color:var(--ui-error)}.cms-btn-error.cms-btn-ghost:hover{background:color-mix(in oklab,var(--ui-error) 10%,transparent)}.cms-btn-success.cms-btn-solid{background:var(--cms-fern);color:var(--ui-text-inverted)}.cms-field{background:var(--ui-bg);border:1px solid var(--cms-line-strong);border-radius:var(--ui-radius);color:var(--ui-text-highlighted);transition:border-color .15s ease,box-shadow .15s ease;width:100%}.cms-field::-moz-placeholder{color:var(--ui-text-dimmed)}.cms-field::placeholder{color:var(--ui-text-dimmed)}.cms-field:focus,.cms-field:focus-within{border-color:var(--ui-primary);box-shadow:0 0 0 3px color-mix(in oklab,var(--ui-primary) 18%,transparent);outline:none}.cms-field.is-error{border-color:var(--ui-error)}.cms-field-sm{font-size:.875rem;padding:.375rem .625rem}.cms-field-md{font-size:.875rem;padding:.5rem .75rem}.cms-field-lg{font-size:.9375rem;padding:.625rem .875rem}.cms-input-wrap{align-items:center;display:flex;position:relative}.cms-input-wrap .cms-field{width:100%}.cms-input-lead,.cms-input-trail{align-items:center;color:var(--ui-text-dimmed);display:flex;position:absolute}.cms-input-lead{left:.625rem}.cms-input-trail{right:.375rem}.cms-input-wrap.has-lead .cms-field{padding-left:2rem}.cms-switch{align-items:center;background:var(--cms-line-strong);border-radius:9999px;display:inline-flex;flex-shrink:0;height:1.25rem;position:relative;transition:background-color .15s ease;width:2.25rem}.cms-switch.is-on{background:var(--ui-primary)}.cms-switch:after{background:#fff;border-radius:9999px;content:"";height:.875rem;left:.1875rem;position:absolute;transition:transform .15s ease;width:.875rem}.cms-switch.is-on:after{transform:translateX(1rem)}.cms-overlay{align-items:center;backdrop-filter:blur(2px);background:rgba(38,33,25,.32);display:flex;inset:0;justify-content:center;overflow-y:auto;padding:2rem 1rem;position:fixed;z-index:60}.cms-modal{background:var(--ui-bg);border:1px solid var(--cms-line);border-radius:1rem;box-shadow:var(--cms-shadow-soft);max-width:32rem;width:100%}.cms-modal-header{padding:1.25rem 1.5rem 0}.cms-modal-body{padding:1.25rem 1.5rem 1.5rem}.cms-menu{background:var(--ui-bg);border:1px solid var(--cms-line);border-radius:.75rem;box-shadow:var(--cms-shadow-soft);margin-top:.375rem;min-width:11rem;padding:.25rem;position:absolute;z-index:50}.cms-menu.align-end{right:0}.cms-menu-item{align-items:center;border-radius:.5rem;color:var(--ui-text);cursor:pointer;display:flex;font-size:.875rem;gap:.5rem;padding:.4rem .6rem;text-align:left;width:100%}.cms-menu-item:hover{background:var(--ui-bg-elevated)}.cms-menu-check{color:var(--ui-primary);margin-left:auto}.cms-popover,.cms-selectmenu-panel{background:var(--ui-bg);border:1px solid var(--cms-line);border-radius:.75rem;box-shadow:var(--cms-shadow-soft);margin-top:.375rem;position:absolute;z-index:50}.cms-selectmenu-panel{left:0;max-height:15rem;overflow-y:auto;padding:.25rem;right:0}.cms-tag{align-items:center;background:var(--ui-bg-elevated);border-radius:9999px;display:inline-flex;font-size:.875rem;gap:.25rem;padding:.125rem .5rem}.cms-table{border-collapse:collapse;text-align:left;width:100%}.cms-alert{border-radius:.75rem;font-size:.875rem;padding:.75rem 1rem}.cms-alert-error{background:color-mix(in oklab,var(--ui-error) 10%,transparent);color:var(--ui-error)}.cms-toaster{bottom:1rem;display:flex;flex-direction:column;gap:.5rem;max-width:calc(100vw - 2rem);position:fixed;right:1rem;width:20rem;z-index:80}.cms-toast{background:var(--ui-bg);border:1px solid var(--cms-line);border-radius:.75rem;box-shadow:var(--cms-shadow-soft);padding:.75rem .875rem}.cms-toast.is-success{border-left:3px solid var(--cms-fern)}.cms-toast.is-error{border-left:3px solid var(--ui-error)}.cms-pagination{align-items:center;display:flex;gap:.25rem}.cms-page-btn{border-radius:.5rem;color:var(--ui-text-toned);font-size:.875rem;height:2rem;min-width:2rem;padding:0 .5rem}.cms-page-btn:hover:not(:disabled){background:var(--ui-bg-elevated)}.cms-page-btn.is-active{background:var(--cms-sage);color:var(--cms-sage-ink);font-weight:600}.cms-page-btn:disabled{opacity:.4}
|
|
1
|
+
@import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600&family=Hanken+Grotesk:ital,wght@0,300..800;1,300..800&family=Fragment+Mono:ital@0;1&display=swap");@layer theme, base, components, utilities;@import "tailwindcss/theme.css" layer(theme);.cms-scope{@import "tailwindcss/preflight.css" layer(base);@import "tailwindcss/utilities.css" layer(utilities) source(none)}@source "../app";.cms-scope,body:has(.cms-scope){--cms-paper:#f5f5f5;--cms-surface:#fff;--cms-field:#f0f0f0;--cms-ink:#171717;--cms-line:#e5e5e5;--cms-line-strong:#d4d4d4;--cms-fern:#171717;--cms-sage:#ebebeb;--cms-sage-ink:#171717;--cms-shadow-soft:none;color-scheme:light;--ui-text-dimmed:#a3a3a3;--ui-text-muted:#737373;--ui-text-toned:#525252;--ui-text:#404040;--ui-text-highlighted:var(--cms-ink);--ui-text-inverted:#fff;--ui-bg:var(--cms-surface);--ui-bg-muted:var(--cms-paper);--ui-bg-elevated:var(--cms-field);--ui-bg-accented:#e0e0e0;--ui-bg-inverted:var(--cms-ink);--ui-border:var(--cms-line);--ui-border-muted:var(--cms-line);--ui-border-accented:var(--cms-line-strong);--ui-border-inverted:var(--cms-ink);--ui-primary:var(--cms-ink);--ui-error:#dc2626;--ui-radius:0.4rem;color:var(--ui-text);font-family:Hanken Grotesk,ui-sans-serif,system-ui,sans-serif;margin:0;padding:0;-webkit-font-smoothing:antialiased}body:has(.cms-scope) ::-moz-selection{background:var(--cms-sage);color:var(--cms-sage-ink)}body:has(.cms-scope) ::selection{background:var(--cms-sage);color:var(--cms-sage-ink)}.cms-display{font-family:Poppins,ui-sans-serif,system-ui,sans-serif;letter-spacing:-.02em}.cms-kicker{color:var(--ui-text-dimmed);font-family:Fragment Mono,ui-monospace,monospace;font-size:.875rem;letter-spacing:.16em;line-height:1.4;text-transform:uppercase}.cms-scope thead .cms-kicker{font-size:.6875rem;letter-spacing:.1em}body:has(.cms-scope) :is(button,a,[role=button],[role=menuitem],[role=option],.__clickable):not(:disabled,[aria-disabled=true]){@apply cursor-pointer transition-all duration-200 select-none}body:has(.cms-scope) :is(button,a,[role=button],[role=menuitem],[role=option],.__clickable):not(:disabled,[aria-disabled=true]):active{@apply transition-none;scale:.99}.cms-canvas{background-color:var(--cms-paper)}.cms-grain:before{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='a'%3E%3CfeTurbulence baseFrequency='.8' numOctaves='2' stitchTiles='stitch' type='fractalNoise'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)'/%3E%3C/svg%3E");content:"";inset:0;mix-blend-mode:multiply;opacity:.035;pointer-events:none;position:fixed;z-index:50}.cms-card{background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:1rem;box-shadow:var(--cms-shadow-soft)}.cms-empty{background:rgba(253,252,250,.5);border:1.5px dashed var(--cms-line-strong);border-radius:1rem}.cms-navlink{align-items:center;border-radius:.75rem;color:var(--ui-text-toned);display:flex;font-size:.875rem;font-weight:500;gap:.625rem;padding:.5rem .75rem;transition:background-color .15s ease,color .15s ease}.cms-navlink:hover{background:rgba(38,33,25,.05);color:var(--ui-text-highlighted)}.cms-navlink.is-active{background:var(--cms-sage);color:var(--cms-sage-ink);font-weight:600}.cms-navlink.is-active .cms-navlink-icon{color:var(--cms-fern)}.cms-badge{border:1px solid transparent;border-radius:9999px;font-family:Fragment Mono,ui-monospace,monospace;font-size:.6875rem;letter-spacing:.1em;padding:.1875rem .5rem;text-transform:uppercase;transition:background-color .15s ease,border-color .15s ease}.cms-badge.is-published{background:var(--cms-sage);color:var(--cms-sage-ink)}.cms-badge.is-draft{background:transparent;border:1px dashed var(--cms-line-strong);color:var(--ui-text-muted)}.cms-dropzone{align-items:center;background:rgba(253,252,250,.5);border:1.5px dashed var(--cms-line-strong);border-radius:1rem;color:var(--ui-text-muted);cursor:pointer;display:flex;flex-direction:column;gap:.5rem;justify-content:center;padding:2rem 1.5rem;transition:border-color .15s ease,background-color .15s ease,color .15s ease;width:100%}.cms-dropzone.is-over,.cms-dropzone:hover{background:var(--cms-sage);border-color:var(--cms-fern);color:var(--cms-sage-ink)}.cms-dropzone.is-dense{border-radius:.75rem;flex-direction:row;gap:.625rem;padding:.875rem 1.25rem}.cms-dropzone.is-busy{opacity:.7;pointer-events:none}.cms-pill{background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:9999px;color:var(--ui-text-dimmed);cursor:pointer;font-family:Fragment Mono,ui-monospace,monospace;font-size:.875rem;letter-spacing:.12em;padding:.3rem .8rem;transition:background-color .15s ease,color .15s ease}.cms-pill:hover{background:rgba(38,33,25,.05);color:var(--ui-text)}.cms-pill.is-active{background:var(--cms-sage);color:var(--cms-sage-ink)}@keyframes cms-rise{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}.cms-rise{animation:cms-rise .55s cubic-bezier(.22,1,.36,1) both}@media (prefers-reduced-motion:reduce){.cms-rise{animation:none}}.cms-richtext{border:1px solid var(--cms-line);border-radius:.75rem;overflow:hidden}.cms-richtext-toolbar{align-items:center;border-bottom:1px solid var(--cms-line);display:flex;flex-wrap:wrap;gap:2px;padding:4px}.cms-richtext-body{font-size:.875rem;min-height:9rem;outline:none;padding:.75rem 1rem}.cms-richtext-body h2{font-size:1.25rem;font-weight:600;margin:.75em 0 .35em}.cms-richtext-body h3{font-size:1.1rem;font-weight:600;margin:.75em 0 .35em}.cms-richtext-body p{margin:.35em 0}.cms-richtext-body ul{list-style:disc;padding-left:1.25rem}.cms-richtext-body ol{list-style:decimal;padding-left:1.25rem}.cms-richtext-body blockquote{border-left:3px solid var(--cms-line);color:var(--ui-text-muted);padding-left:.75rem}.cms-btn{align-items:center;border:1px solid transparent;border-radius:var(--ui-radius);display:inline-flex;font-weight:500;gap:.375rem;justify-content:space-between;line-height:1;transition:background-color .15s ease,border-color .15s ease,color .15s ease,opacity .15s ease;white-space:nowrap}.cms-btn:disabled,.cms-btn[aria-disabled=true]{opacity:.5;pointer-events:none}.cms-btn.is-block{width:100%}.cms-btn-xs{font-size:.875rem;padding:.25rem .5rem}.cms-btn-sm{font-size:.875rem;padding:.375rem .625rem}.cms-btn-md{font-size:.875rem;padding:.5rem .875rem}.cms-btn-lg{font-size:.9375rem;padding:.625rem 1rem}.cms-btn-primary.cms-btn-solid{background:var(--ui-primary);color:var(--ui-text-inverted)}.cms-btn-primary.cms-btn-solid:hover{background:color-mix(in oklab,var(--ui-primary) 88%,#000)}.cms-btn-primary.cms-btn-subtle{background:var(--cms-sage);color:var(--cms-sage-ink)}.cms-btn-primary.cms-btn-subtle:hover{background:#e0e0e0}.cms-btn-primary.cms-btn-soft{background:var(--cms-sage);color:var(--cms-sage-ink)}.cms-btn-neutral.cms-btn-solid{background:var(--ui-bg-inverted);color:var(--ui-text-inverted)}.cms-btn-neutral.cms-btn-subtle{background:var(--ui-bg-elevated);color:var(--ui-text-highlighted)}.cms-btn-neutral.cms-btn-subtle:hover{background:var(--ui-bg-accented)}.cms-btn-neutral.cms-btn-soft{background:var(--ui-bg-elevated);color:var(--ui-text-highlighted)}.cms-btn-neutral.cms-btn-ghost{color:var(--ui-text-toned)}.cms-btn-neutral.cms-btn-ghost:hover{background:rgba(38,33,25,.06);color:var(--ui-text-highlighted)}.cms-btn-error.cms-btn-solid{background:var(--ui-error);color:#fff}.cms-btn-error.cms-btn-solid:hover{background:color-mix(in oklab,var(--ui-error) 88%,#000)}.cms-btn-error.cms-btn-subtle{background:color-mix(in oklab,var(--ui-error) 12%,transparent);color:var(--ui-error)}.cms-btn-error.cms-btn-ghost{color:var(--ui-error)}.cms-btn-error.cms-btn-ghost:hover{background:color-mix(in oklab,var(--ui-error) 10%,transparent)}.cms-btn-success.cms-btn-solid{background:var(--cms-fern);color:var(--ui-text-inverted)}.cms-field{background:var(--ui-bg);border:1px solid var(--cms-line-strong);border-radius:var(--ui-radius);color:var(--ui-text-highlighted);transition:border-color .15s ease,box-shadow .15s ease;width:100%}.cms-field::-moz-placeholder{color:var(--ui-text-dimmed)}.cms-field::placeholder{color:var(--ui-text-dimmed)}.cms-field:focus,.cms-field:focus-within{border-color:var(--ui-primary);box-shadow:0 0 0 3px color-mix(in oklab,var(--ui-primary) 18%,transparent);outline:none}.cms-field.is-error{border-color:var(--ui-error)}.cms-field-sm{font-size:.875rem;padding:.375rem .625rem}.cms-field-md{font-size:.875rem;padding:.5rem .75rem}.cms-field-lg{font-size:.9375rem;padding:.625rem .875rem}.cms-input-wrap{align-items:center;display:flex;position:relative}.cms-input-wrap .cms-field{width:100%}.cms-input-lead,.cms-input-trail{align-items:center;color:var(--ui-text-dimmed);display:flex;position:absolute}.cms-input-lead{left:.625rem}.cms-input-trail{right:.375rem}.cms-input-wrap.has-lead .cms-field{padding-left:2rem}.cms-switch{align-items:center;background:var(--cms-line-strong);border-radius:9999px;display:inline-flex;flex-shrink:0;height:1.25rem;position:relative;transition:background-color .15s ease;width:2.25rem}.cms-switch.is-on{background:var(--ui-primary)}.cms-switch:after{background:#fff;border-radius:9999px;content:"";height:.875rem;left:.1875rem;position:absolute;transition:transform .15s ease;width:.875rem}.cms-switch.is-on:after{transform:translateX(1rem)}.cms-overlay{align-items:center;backdrop-filter:blur(2px);background:rgba(38,33,25,.32);display:flex;inset:0;justify-content:center;overflow-y:auto;padding:2rem 1rem;position:fixed;z-index:60}.cms-modal{background:var(--ui-bg);border:1px solid var(--cms-line);border-radius:1rem;box-shadow:var(--cms-shadow-soft);max-width:32rem;width:100%}.cms-modal-header{padding:1.25rem 1.5rem 0}.cms-modal-body{padding:1.25rem 1.5rem 1.5rem}.cms-menu{background:var(--ui-bg);border:1px solid var(--cms-line);border-radius:.75rem;box-shadow:var(--cms-shadow-soft);margin-top:.375rem;min-width:11rem;padding:.25rem;position:absolute;z-index:50}.cms-menu.align-end{right:0}.cms-menu-item{align-items:center;border-radius:.5rem;color:var(--ui-text);cursor:pointer;display:flex;font-size:.875rem;gap:.5rem;padding:.4rem .6rem;text-align:left;width:100%}.cms-menu-item:hover{background:var(--ui-bg-elevated)}.cms-menu-check{color:var(--ui-primary);margin-left:auto}.cms-popover,.cms-selectmenu-panel{background:var(--ui-bg);border:1px solid var(--cms-line);border-radius:.75rem;box-shadow:var(--cms-shadow-soft);margin-top:.375rem;position:absolute;z-index:50}.cms-selectmenu-panel{left:0;max-height:15rem;overflow-y:auto;padding:.25rem;right:0}.cms-tag{align-items:center;background:var(--ui-bg-elevated);border-radius:9999px;display:inline-flex;font-size:.875rem;gap:.25rem;padding:.125rem .5rem}.cms-table{border-collapse:collapse;text-align:left;width:100%}.cms-alert{border-radius:.75rem;font-size:.875rem;padding:.75rem 1rem}.cms-alert-error{background:color-mix(in oklab,var(--ui-error) 10%,transparent);color:var(--ui-error)}.cms-toaster{bottom:1rem;display:flex;flex-direction:column;gap:.5rem;max-width:calc(100vw - 2rem);position:fixed;right:1rem;width:20rem;z-index:80}.cms-toast{background:var(--ui-bg);border:1px solid var(--cms-line);border-radius:.75rem;box-shadow:var(--cms-shadow-soft);padding:.75rem .875rem}.cms-toast.is-success{border-left:3px solid var(--cms-fern)}.cms-toast.is-error{border-left:3px solid var(--ui-error)}.cms-pagination{align-items:center;display:flex;gap:.25rem}.cms-page-btn{border-radius:.5rem;color:var(--ui-text-toned);font-size:.875rem;height:2rem;min-width:2rem;padding:0 .5rem}.cms-page-btn:hover:not(:disabled){background:var(--ui-bg-elevated)}.cms-page-btn.is-active{background:var(--cms-sage);color:var(--cms-sage-ink);font-weight:600}.cms-page-btn:disabled{opacity:.4}
|
|
@@ -27,6 +27,8 @@ function filterScalarFor(field) {
|
|
|
27
27
|
return null;
|
|
28
28
|
case "blocks":
|
|
29
29
|
return null;
|
|
30
|
+
case "select":
|
|
31
|
+
return field.multiple ? null : "StringFilter";
|
|
30
32
|
case "relation":
|
|
31
33
|
return field.cardinality === "many-to-many" ? null : "StringFilter";
|
|
32
34
|
case "number":
|
|
@@ -54,6 +56,7 @@ function fieldSdl(config, entryName, key, field) {
|
|
|
54
56
|
return ` ${key}: ${target}${nonNull ? "!" : ""}`;
|
|
55
57
|
}
|
|
56
58
|
if (field.type === "media") return ` ${key}: CmsMedia`;
|
|
59
|
+
if (field.type === "select" && field.multiple) return ` ${key}: [String!]!`;
|
|
57
60
|
if (field.type === "blocks")
|
|
58
61
|
return ` ${key}: [${blockUnionName(entryName, key)}!]${field.required ? "!" : ""}`;
|
|
59
62
|
return ` ${key}: ${scalarFor(field)}${field.required ? "!" : ""}`;
|
|
@@ -39,6 +39,7 @@ export interface FieldConfig {
|
|
|
39
39
|
integer?: boolean;
|
|
40
40
|
translatable?: boolean;
|
|
41
41
|
options?: string[];
|
|
42
|
+
multiple?: boolean;
|
|
42
43
|
from?: string;
|
|
43
44
|
blocks?: Record<string, BlockConfig>;
|
|
44
45
|
mediaType?: MediaType;
|
|
@@ -48,6 +49,7 @@ export interface FieldConfig {
|
|
|
48
49
|
onDelete?: 'set null' | 'cascade' | 'restrict';
|
|
49
50
|
}
|
|
50
51
|
export declare function isTranslatableField(field: FieldConfig): boolean;
|
|
52
|
+
export declare function isMultiSelect(field: FieldConfig): boolean;
|
|
51
53
|
export declare function translatableFieldKeys(entry: CmsEntry): string[];
|
|
52
54
|
export interface CmsEntry {
|
|
53
55
|
id: string;
|
|
@@ -92,6 +94,7 @@ export interface SlugFieldInput extends FieldInputBase {
|
|
|
92
94
|
export interface SelectFieldInput extends FieldInputBase {
|
|
93
95
|
type: 'select';
|
|
94
96
|
options: string[];
|
|
97
|
+
multiple?: boolean;
|
|
95
98
|
}
|
|
96
99
|
export interface JsonFieldInput extends FieldInputBase {
|
|
97
100
|
type: 'json';
|
|
@@ -47,6 +47,9 @@ export function slugify(value) {
|
|
|
47
47
|
export function isTranslatableField(field) {
|
|
48
48
|
return !!field.translatable && (field.type === "text" || field.type === "richtext");
|
|
49
49
|
}
|
|
50
|
+
export function isMultiSelect(field) {
|
|
51
|
+
return field.type === "select" && !!field.multiple;
|
|
52
|
+
}
|
|
50
53
|
export function translatableFieldKeys(entry) {
|
|
51
54
|
return Object.entries(entry.fields).filter(([, field]) => isTranslatableField(field)).map(([key]) => key);
|
|
52
55
|
}
|
|
@@ -58,6 +58,11 @@ export function buildEntrySchema(entry, i18n, messages) {
|
|
|
58
58
|
shape[key] = field.required ? list.min(1, m.required) : list.nullish().transform((v) => v ?? []);
|
|
59
59
|
continue;
|
|
60
60
|
}
|
|
61
|
+
if (field.type === "select" && field.multiple) {
|
|
62
|
+
const list = z.array(z.enum(field.options));
|
|
63
|
+
shape[key] = field.required ? list.min(1, m.required) : list.nullish().transform((v) => v ?? []);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
61
66
|
if (field.type === "blocks") {
|
|
62
67
|
const list = blocksSchema(field, m);
|
|
63
68
|
shape[key] = field.required ? list.min(1, m.required) : list.nullish().transform((v) => v ?? null);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xleddyl/nuxt-cms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "Lightweight CMS that ships with your Nuxt app: runs on the Nitro server, content types defined in code, /cms admin panel, GraphQL API, SQLite or Postgres. No external CMS needed!",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Edoardo Alberti (https://github.com/xleddyl)",
|
|
@@ -103,6 +103,6 @@
|
|
|
103
103
|
"test": "vitest run",
|
|
104
104
|
"test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
|
|
105
105
|
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,md,html,json,sql,vue}\" --log-level error",
|
|
106
|
-
"release": "changelogen --release
|
|
106
|
+
"release": "changelogen --release && git push origin main && git push origin v$(node -p \"require('./package.json').version\")"
|
|
107
107
|
}
|
|
108
108
|
}
|