@xleddyl/nuxt-cms 0.1.55 → 0.1.57
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/dist/module.json +1 -1
- package/dist/module.mjs +57 -5
- package/dist/runtime/app/components/cms/BlocksField.vue +19 -1
- package/dist/runtime/app/components/cms/MediaField.vue +14 -1
- package/dist/runtime/app/composables/cms-media-keys.d.ts +4 -0
- package/dist/runtime/app/composables/cms-media-keys.js +21 -0
- package/dist/runtime/app/composables/cms-query-disabled.d.ts +3 -6
- package/dist/runtime/app/composables/cms-query.js +57 -4
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/server/api/collection.get.js +2 -1
- package/dist/runtime/server/api/item.get.js +6 -4
- package/dist/runtime/server/api/item.put.js +5 -4
- package/dist/runtime/server/routes/graphql.js +2 -0
- package/dist/runtime/server/utils/db-d1.d.ts +2 -0
- package/dist/runtime/server/utils/db-d1.js +5 -0
- package/dist/runtime/server/utils/db-libsql.d.ts +2 -0
- package/dist/runtime/server/utils/db-libsql.js +5 -0
- package/dist/runtime/server/utils/db-postgres.d.ts +2 -0
- package/dist/runtime/server/utils/db-postgres.js +8 -0
- package/dist/runtime/server/utils/db-sqlite.d.ts +2 -0
- package/dist/runtime/server/utils/db-sqlite.js +8 -0
- package/dist/runtime/server/utils/graphql.js +3 -4
- package/dist/runtime/server/utils/media-check.d.ts +2 -0
- package/dist/runtime/server/utils/media-check.js +20 -0
- package/dist/runtime/server/utils/media-references.d.ts +10 -0
- package/dist/runtime/server/utils/media-references.js +50 -0
- package/dist/runtime/server/utils/page-rows.d.ts +51 -0
- package/dist/runtime/server/utils/page-rows.js +223 -0
- package/dist/runtime/server/utils/page-storage.d.ts +11 -0
- package/dist/runtime/server/utils/page-storage.js +42 -0
- package/dist/runtime/shared/index.d.ts +7 -0
- package/dist/runtime/shared/index.js +21 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { minifyIntrospection, outputIntrospectionFile } from 'gql.tada/internal'
|
|
|
11
11
|
import { createJiti } from 'jiti';
|
|
12
12
|
import { typeName, blockTypeName, blocksFieldTypeName, renderGraphqlSdl } from '../dist/runtime/shared/graphql-sdl.js';
|
|
13
13
|
import { migrationsDirFor } from '../dist/runtime/shared/migrations-dir.js';
|
|
14
|
-
import { isTranslatableField, isTranslatableMediaField, isMultiSelect, pageAllFields, PAGE_PATH_FIELD, pageRoutes, fieldConditions, isRequiredField, pageFields, isPrivateField, entryFieldsFor, mediaTypeFilter, DEFAULT_MEDIA_MAX_FILE_SIZE } from '../dist/runtime/shared/index.js';
|
|
14
|
+
import { pageColumnFields, isTranslatableField, isTranslatableMediaField, isMultiSelect, pageAllFields, PAGE_PATH_FIELD, pageRoutes, fieldConditions, pageFieldsTableName, pageMediaTableName, isRequiredField, pageFields, isPrivateField, entryFieldsFor, mediaTypeFilter, DEFAULT_MEDIA_MAX_FILE_SIZE } from '../dist/runtime/shared/index.js';
|
|
15
15
|
import { scanMediaDirectory, readMediaFileMeta } from '../dist/runtime/server/utils/media-sync.js';
|
|
16
16
|
import { createHash } from 'node:crypto';
|
|
17
17
|
import { readFile } from 'node:fs/promises';
|
|
@@ -181,9 +181,11 @@ function validateConfig(config, i18n) {
|
|
|
181
181
|
declared.set(key, field.type);
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
|
+
errors.push(...pageColumnsErrors(at, entry));
|
|
184
185
|
} else if (entry.overrides || entry.routes || entry.include || entry.exclude) {
|
|
185
186
|
errors.push(`${at}: routes, include, exclude and overrides need kind 'page'`);
|
|
186
187
|
}
|
|
188
|
+
if (entry.kind !== "page" && entry.columns) errors.push(`${at}: columns need kind 'page'`);
|
|
187
189
|
if (entry.drafts && entry.kind !== "collection")
|
|
188
190
|
errors.push(`${at}: drafts are only supported on collections`);
|
|
189
191
|
const tabIds = /* @__PURE__ */ new Set();
|
|
@@ -386,6 +388,10 @@ function validateConfig(config, i18n) {
|
|
|
386
388
|
}
|
|
387
389
|
};
|
|
388
390
|
registerTable(name, `entry '${name}'`);
|
|
391
|
+
if (entry.kind === "page") {
|
|
392
|
+
registerTable(pageFieldsTableName(name), `the field rows table of '${name}'`);
|
|
393
|
+
registerTable(pageMediaTableName(name), `the media rows table of '${name}'`);
|
|
394
|
+
}
|
|
389
395
|
for (const [key, field] of Object.entries(entry.fields ?? {})) {
|
|
390
396
|
if (isManyToMany(field))
|
|
391
397
|
registerTable(`${name}_${snakeCase(key)}`, `the join table of '${name}.${key}'`);
|
|
@@ -393,6 +399,35 @@ function validateConfig(config, i18n) {
|
|
|
393
399
|
}
|
|
394
400
|
return errors;
|
|
395
401
|
}
|
|
402
|
+
function pageColumnsErrors(at, entry) {
|
|
403
|
+
const errors = [];
|
|
404
|
+
if (entry.table) errors.push(`${at}: page entries cannot use a custom table`);
|
|
405
|
+
const columns = /* @__PURE__ */ new Set();
|
|
406
|
+
for (const key of entry.columns ?? []) {
|
|
407
|
+
const cat = `${at}, column '${key}'`;
|
|
408
|
+
if (columns.has(key)) errors.push(`${cat}: listed more than once`);
|
|
409
|
+
columns.add(key);
|
|
410
|
+
if (!Object.hasOwn(entry.fields ?? {}, key)) {
|
|
411
|
+
errors.push(`${cat}: not a field declared for every page in fields`);
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
for (const [path, override] of Object.entries(entry.overrides ?? {})) {
|
|
415
|
+
if (Object.hasOwn(override, key) && !override[key])
|
|
416
|
+
errors.push(`${cat}: removed from '${path}', a column must exist on every page`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
for (const [key, field] of Object.entries(pageAllFields(entry))) {
|
|
420
|
+
const fat = `${at}, field '${key}'`;
|
|
421
|
+
if (isManyToMany(field)) {
|
|
422
|
+
errors.push(`${fat}: many-to-many relations are not supported on pages`);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (columns.has(key)) continue;
|
|
426
|
+
if (field.type === "relation" || field.type === "slug")
|
|
427
|
+
errors.push(`${fat}: ${field.type} fields on pages need to be listed in columns`);
|
|
428
|
+
}
|
|
429
|
+
return errors;
|
|
430
|
+
}
|
|
396
431
|
function snakeCase(key) {
|
|
397
432
|
return key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
398
433
|
}
|
|
@@ -455,7 +490,7 @@ function tableExpr(name, entry, dialect) {
|
|
|
455
490
|
lines.push(` id: text('id').primaryKey(),`);
|
|
456
491
|
if (entry.kind === "page") lines.push(` path: text('path').notNull().unique(),`);
|
|
457
492
|
for (const [key, field] of Object.entries(
|
|
458
|
-
entry.kind === "page" ?
|
|
493
|
+
entry.kind === "page" ? pageColumnFields(entry) : entry.fields
|
|
459
494
|
)) {
|
|
460
495
|
if (isManyToMany(field)) continue;
|
|
461
496
|
lines.push(columnExpr(key, field, dialect));
|
|
@@ -478,6 +513,19 @@ function joinTableExpr(name, key, field, dialect) {
|
|
|
478
513
|
position: integer('position').notNull().default(0),
|
|
479
514
|
}, table => [primaryKey({ columns: [table.sourceId, table.targetId] })])`;
|
|
480
515
|
}
|
|
516
|
+
function pageRowsTableExprs(name, dialect) {
|
|
517
|
+
const tableFn = dialect === "postgres" ? "pgTable" : "sqliteTable";
|
|
518
|
+
const rowsTable = (table, valueLine) => `export const ${table} = ${tableFn}('${table}', {
|
|
519
|
+
pageId: text('page_id').notNull().references(() => ${name}.id, { onDelete: 'cascade' }),
|
|
520
|
+
key: text('key').notNull(),
|
|
521
|
+
position: integer('position').notNull().default(0),
|
|
522
|
+
${valueLine},
|
|
523
|
+
}, table => [primaryKey({ columns: [table.pageId, table.key, table.position] })])`;
|
|
524
|
+
return [
|
|
525
|
+
rowsTable(pageFieldsTableName(name), `value: text('value').notNull()`),
|
|
526
|
+
rowsTable(pageMediaTableName(name), `mediaKey: text('media_key').notNull()`)
|
|
527
|
+
];
|
|
528
|
+
}
|
|
481
529
|
function mediaTableExpr(dialect) {
|
|
482
530
|
const pg = dialect === "postgres";
|
|
483
531
|
const tableFn = pg ? "pgTable" : "sqliteTable";
|
|
@@ -495,7 +543,10 @@ function mediaTableExpr(dialect) {
|
|
|
495
543
|
}
|
|
496
544
|
function renderSchemaFile(config, dialect, resolveImport = (s) => s) {
|
|
497
545
|
const derived = Object.entries(config).filter(([, entry]) => !entry.table);
|
|
498
|
-
const fields = derived.flatMap(
|
|
546
|
+
const fields = derived.flatMap(
|
|
547
|
+
([, entry]) => Object.values(entry.kind === "page" ? pageColumnFields(entry) : entry.fields)
|
|
548
|
+
);
|
|
549
|
+
const rowsEntries = derived.filter(([, entry]) => entry.kind === "page");
|
|
499
550
|
const pg = dialect === "postgres";
|
|
500
551
|
const core = /* @__PURE__ */ new Set(["integer", "text", pg ? "pgTable" : "sqliteTable"]);
|
|
501
552
|
if (fields.some((f) => f.type === "number" && !f.integer))
|
|
@@ -507,7 +558,7 @@ function renderSchemaFile(config, dialect, resolveImport = (s) => s) {
|
|
|
507
558
|
))
|
|
508
559
|
core.add("jsonb");
|
|
509
560
|
if (pg) core.add("timestamp");
|
|
510
|
-
if (fields.some(isManyToMany)) core.add("primaryKey");
|
|
561
|
+
if (fields.some(isManyToMany) || rowsEntries.length) core.add("primaryKey");
|
|
511
562
|
const imports = [
|
|
512
563
|
...pg ? [] : [`import { sql } from '${resolveImport("drizzle-orm")}'`],
|
|
513
564
|
`import { ${[...core].sort().join(", ")} } from '${resolveImport(
|
|
@@ -518,9 +569,10 @@ function renderSchemaFile(config, dialect, resolveImport = (s) => s) {
|
|
|
518
569
|
const joins = derived.flatMap(
|
|
519
570
|
([name, entry]) => Object.entries(entry.kind === "page" ? pageAllFields(entry) : entry.fields).filter(([, field]) => isManyToMany(field)).map(([key, field]) => joinTableExpr(name, key, field, dialect))
|
|
520
571
|
);
|
|
572
|
+
const rows = rowsEntries.flatMap(([name]) => pageRowsTableExprs(name, dialect));
|
|
521
573
|
return `${imports.join("\n")}
|
|
522
574
|
|
|
523
|
-
${[mediaTableExpr(dialect), ...tables, ...joins].join(
|
|
575
|
+
${[mediaTableExpr(dialect), ...tables, ...joins, ...rows].join(
|
|
524
576
|
"\n\n"
|
|
525
577
|
)}
|
|
526
578
|
`;
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
:class="{
|
|
9
9
|
'is-muted': isHidden(item),
|
|
10
10
|
'is-dragging': dragIndex === index,
|
|
11
|
-
'is-drop-target': dropIndex === index && dragIndex !== index
|
|
11
|
+
'is-drop-target': dropIndex === index && dragIndex !== index,
|
|
12
|
+
'is-missing': hasMissingMedia(item)
|
|
12
13
|
}"
|
|
13
14
|
draggable="true"
|
|
14
15
|
@dragstart="onDragStart(index, $event)"
|
|
@@ -55,6 +56,13 @@
|
|
|
55
56
|
<CmsIcon name="bars-2" class="size-4" />
|
|
56
57
|
</button>
|
|
57
58
|
<span class="cms-block-bar-name" :title="barTitle(item)">{{ barLabel(item) }}</span>
|
|
59
|
+
<span
|
|
60
|
+
v-if="hasMissingMedia(item)"
|
|
61
|
+
class="cms-media-missing-icon"
|
|
62
|
+
title="Not found in the media library"
|
|
63
|
+
>
|
|
64
|
+
<CmsIcon name="exclamation-triangle" class="size-4" />
|
|
65
|
+
</span>
|
|
58
66
|
<div class="cms-block-bar-actions">
|
|
59
67
|
<CmsButton
|
|
60
68
|
icon="trash"
|
|
@@ -150,6 +158,7 @@ import {
|
|
|
150
158
|
pickTranslatedMedia
|
|
151
159
|
} from "#nuxt-cms";
|
|
152
160
|
import { computed, ref } from "#imports";
|
|
161
|
+
import { useCmsMediaKeys } from "../../composables/cms-media-keys";
|
|
153
162
|
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
154
163
|
const props = defineProps({
|
|
155
164
|
field: { type: Object, required: true },
|
|
@@ -157,6 +166,7 @@ const props = defineProps({
|
|
|
157
166
|
});
|
|
158
167
|
const model = defineModel({ type: [Array, null], ...{ required: true } });
|
|
159
168
|
const { mediaBaseUrl, i18n } = useCmsRuntime();
|
|
169
|
+
const mediaKeys = useCmsMediaKeys();
|
|
160
170
|
const blocks = computed(() => props.field.blocks ?? {});
|
|
161
171
|
const items = computed(() => Array.isArray(model.value) ? model.value : []);
|
|
162
172
|
let uidCounter = 0;
|
|
@@ -192,6 +202,14 @@ function mediaKeyOf(item) {
|
|
|
192
202
|
}
|
|
193
203
|
return null;
|
|
194
204
|
}
|
|
205
|
+
function hasMissingMedia(item) {
|
|
206
|
+
return Object.entries(blockOf(item)?.fields ?? {}).some(([key, field]) => {
|
|
207
|
+
if (field.type !== "media") return false;
|
|
208
|
+
const raw = item[key];
|
|
209
|
+
const values = raw && typeof raw === "object" ? Object.values(raw) : [raw];
|
|
210
|
+
return values.some((value) => mediaKeys.isMissing(value));
|
|
211
|
+
});
|
|
212
|
+
}
|
|
195
213
|
function previewOf(item) {
|
|
196
214
|
const key = mediaKeyOf(item);
|
|
197
215
|
if (!key) return null;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div
|
|
3
|
+
v-if="model"
|
|
4
|
+
class="cms-card cms-block-tile cms-media-single"
|
|
5
|
+
:class="{ 'is-missing': missing }"
|
|
6
|
+
>
|
|
3
7
|
<button
|
|
4
8
|
type="button"
|
|
5
9
|
class="cms-block-preview"
|
|
@@ -44,6 +48,11 @@
|
|
|
44
48
|
<span>Choose</span>
|
|
45
49
|
</button>
|
|
46
50
|
|
|
51
|
+
<p v-if="missing" class="cms-media-missing">
|
|
52
|
+
<CmsIcon name="exclamation-triangle" class="size-4" />
|
|
53
|
+
<span>Not found in the media library</span>
|
|
54
|
+
</p>
|
|
55
|
+
|
|
47
56
|
<CmsModal v-model:open="galleryOpen" title="Media" size="lg">
|
|
48
57
|
<template #body>
|
|
49
58
|
<CmsMediaGallery selectable :media-type="mediaType" :accept="accept" @select="onSelect" />
|
|
@@ -54,6 +63,7 @@
|
|
|
54
63
|
<script setup>
|
|
55
64
|
import { computed, ref } from "#imports";
|
|
56
65
|
import { mediaFilename, mediaIconFor, mediaPublicUrl, mediaTypeForKey } from "#nuxt-cms";
|
|
66
|
+
import { useCmsMediaKeys } from "../../composables/cms-media-keys";
|
|
57
67
|
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
58
68
|
defineProps({
|
|
59
69
|
mediaType: { type: null, required: false },
|
|
@@ -61,8 +71,10 @@ defineProps({
|
|
|
61
71
|
});
|
|
62
72
|
const model = defineModel({ type: [String, null], ...{ required: true } });
|
|
63
73
|
const { mediaBaseUrl } = useCmsRuntime();
|
|
74
|
+
const mediaKeys = useCmsMediaKeys();
|
|
64
75
|
const galleryOpen = ref(false);
|
|
65
76
|
function onSelect(item) {
|
|
77
|
+
mediaKeys.remember(item.key);
|
|
66
78
|
model.value = item.key;
|
|
67
79
|
galleryOpen.value = false;
|
|
68
80
|
}
|
|
@@ -72,6 +84,7 @@ function openGallery() {
|
|
|
72
84
|
function clear() {
|
|
73
85
|
model.value = null;
|
|
74
86
|
}
|
|
87
|
+
const missing = computed(() => mediaKeys.isMissing(model.value));
|
|
75
88
|
const url = computed(() => model.value ? mediaPublicUrl(mediaBaseUrl, model.value) : null);
|
|
76
89
|
const kind = computed(() => model.value ? mediaTypeForKey(model.value) : "file");
|
|
77
90
|
const icon = computed(() => mediaIconFor(kind.value));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ref } from "#imports";
|
|
2
|
+
const known = ref(null);
|
|
3
|
+
let loading = null;
|
|
4
|
+
function load() {
|
|
5
|
+
loading ??= $fetch("/api/cms/admin/media").then((result) => {
|
|
6
|
+
known.value = result.source.kind === "none" ? null : new Set(result.items.map((item) => item.key));
|
|
7
|
+
}).catch(() => {
|
|
8
|
+
known.value = null;
|
|
9
|
+
});
|
|
10
|
+
return loading;
|
|
11
|
+
}
|
|
12
|
+
export function useCmsMediaKeys() {
|
|
13
|
+
if (import.meta.client) void load();
|
|
14
|
+
function isMissing(key) {
|
|
15
|
+
return typeof key === "string" && key !== "" && !!known.value && !known.value.has(key);
|
|
16
|
+
}
|
|
17
|
+
function remember(key) {
|
|
18
|
+
if (known.value) known.value = /* @__PURE__ */ new Set([...known.value, key]);
|
|
19
|
+
}
|
|
20
|
+
return { isMissing, remember };
|
|
21
|
+
}
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import type { AsyncData
|
|
1
|
+
import type { AsyncData } from 'nuxt/app';
|
|
2
|
+
import type { CmsAsyncDataOptions } from './cms-query.js';
|
|
2
3
|
type CmsDisabledResult = Record<string, any>;
|
|
3
4
|
type CmsDisabledVariables = Record<string, any>;
|
|
4
|
-
export
|
|
5
|
-
key?: string;
|
|
6
|
-
default?: () => DefaultT;
|
|
7
|
-
}
|
|
5
|
+
export type { CmsAsyncDataOptions };
|
|
8
6
|
export declare function cmsQueryKey(query: string, variables?: unknown): string;
|
|
9
7
|
export declare function $cmsQuery<const Q extends string>(query: Q, variables?: CmsDisabledVariables): Promise<CmsDisabledResult>;
|
|
10
8
|
export declare function useCms<const Q extends string, DefaultT = undefined>(query: Q, variables?: CmsDisabledVariables, options?: CmsAsyncDataOptions<CmsDisabledResult, DefaultT>): AsyncData<CmsDisabledResult | DefaultT | undefined, Error | undefined>;
|
|
11
|
-
export {};
|
|
@@ -1,13 +1,66 @@
|
|
|
1
1
|
import { useAsyncData } from "#imports";
|
|
2
|
+
import { CMS_GRAPHQL_BATCH_LIMIT } from "../../shared/index.js";
|
|
2
3
|
const ENDPOINT = "/api/cms/graphql";
|
|
4
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
5
|
+
let queue = [];
|
|
6
|
+
let scheduled = false;
|
|
7
|
+
function responseError(response) {
|
|
8
|
+
return new Error(
|
|
9
|
+
response?.errors?.map((e) => e.message).join("; ") || "Invalid batched GraphQL response"
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
async function sendBatch(batch) {
|
|
13
|
+
try {
|
|
14
|
+
if (batch.length === 1) {
|
|
15
|
+
batch[0].resolve(
|
|
16
|
+
await $fetch(ENDPOINT, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
body: batch[0].body
|
|
19
|
+
})
|
|
20
|
+
);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const responses = await $fetch(
|
|
24
|
+
ENDPOINT,
|
|
25
|
+
{ method: "POST", body: batch.map((pending) => pending.body) }
|
|
26
|
+
);
|
|
27
|
+
if (!Array.isArray(responses)) throw responseError(responses);
|
|
28
|
+
batch.forEach((pending, index) => {
|
|
29
|
+
const response = responses[index];
|
|
30
|
+
if (response) pending.resolve(response);
|
|
31
|
+
else pending.reject(responseError(response));
|
|
32
|
+
});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
for (const pending of batch) pending.reject(error);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function flushQueue() {
|
|
38
|
+
const pending = queue;
|
|
39
|
+
queue = [];
|
|
40
|
+
scheduled = false;
|
|
41
|
+
for (let index = 0; index < pending.length; index += CMS_GRAPHQL_BATCH_LIMIT) {
|
|
42
|
+
void sendBatch(pending.slice(index, index + CMS_GRAPHQL_BATCH_LIMIT));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function requestGraphql(body) {
|
|
46
|
+
const key = JSON.stringify(body);
|
|
47
|
+
const existing = inFlight.get(key);
|
|
48
|
+
if (existing) return existing;
|
|
49
|
+
const request = new Promise((resolve, reject) => {
|
|
50
|
+
queue.push({ body, resolve, reject });
|
|
51
|
+
if (!scheduled) {
|
|
52
|
+
scheduled = true;
|
|
53
|
+
setTimeout(flushQueue, 0);
|
|
54
|
+
}
|
|
55
|
+
}).finally(() => inFlight.delete(key));
|
|
56
|
+
inFlight.set(key, request);
|
|
57
|
+
return request;
|
|
58
|
+
}
|
|
3
59
|
export function cmsQueryKey(query, variables) {
|
|
4
60
|
return `cms-gql:${query}:${JSON.stringify(variables ?? {})}`;
|
|
5
61
|
}
|
|
6
62
|
export async function $cmsQuery(query, variables) {
|
|
7
|
-
const res = await
|
|
8
|
-
method: "POST",
|
|
9
|
-
body: { query, variables }
|
|
10
|
-
});
|
|
63
|
+
const res = await requestGraphql({ query, variables });
|
|
11
64
|
if (res.errors?.length) {
|
|
12
65
|
throw new Error(res.errors.map((e) => e.message).join("; "));
|
|
13
66
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
@layer theme, base, components, utilities;@import "tailwindcss/theme.css" layer(theme) theme(inline);.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-canvas:#1b1b1e;--cms-surface:#222226;--cms-field:#2a2a2f;--cms-elevated:#2f2f35;--cms-line:#35353b;--cms-line-strong:#46464e;--cms-text:#ededef;--cms-text-toned:#c2c2c8;--cms-text-muted:#9a9aa3;--cms-text-dimmed:#6e6e78;--cms-fern:#8fae8b;--cms-error:#f0857d;--cms-success:#7fb083;--cms-warning:#d4ad76;--cms-font-ui:"Hanken Grotesk Variable","Hanken Grotesk",ui-sans-serif,system-ui,sans-serif;--cms-font-mono:"Fragment Mono",ui-monospace,SFMono-Regular,Menlo,monospace;--cms-radius-sm:6px;--cms-radius-md:8px;--cms-radius-lg:12px;--cms-shadow-overlay:0 8px 24px rgba(0,0,0,.35);--cms-scrim:rgba(0,0,0,.5);--cms-duration:120ms;--cms-duration-lg:250ms;--cms-ease:cubic-bezier(0.2,0,0,1);color-scheme:dark;--ui-text-dimmed:var(--cms-text-dimmed);--ui-text-muted:var(--cms-text-muted);--ui-text-toned:var(--cms-text-toned);--ui-text:var(--cms-text-toned);--ui-text-highlighted:var(--cms-text);--ui-text-inverted:var(--cms-canvas);--ui-bg:var(--cms-surface);--ui-bg-muted:var(--cms-canvas);--ui-bg-elevated:var(--cms-field);--ui-bg-accented:var(--cms-elevated);--ui-bg-inverted:var(--cms-text);--ui-border:var(--cms-line);--ui-border-muted:var(--cms-line);--ui-border-accented:var(--cms-line-strong);--ui-border-inverted:var(--cms-text);--ui-primary:var(--cms-text);--ui-accent:var(--cms-fern);--ui-error:var(--cms-error);--ui-success:var(--cms-success);--ui-warning:var(--cms-warning);--ui-radius:var(--cms-radius-md);color:var(--ui-text);font-family:var(--cms-font-ui);margin:0;padding:0;-webkit-font-smoothing:antialiased}body:has(.cms-scope) ::-moz-selection{background:color-mix(in oklab,var(--cms-fern) 35%,transparent);color:var(--ui-text-highlighted)}body:has(.cms-scope) ::selection{background:color-mix(in oklab,var(--cms-fern) 35%,transparent);color:var(--ui-text-highlighted)}body:has(.cms-scope) :is(.cms-scope,.cms-scope *){scrollbar-color:var(--cms-line-strong) transparent;scrollbar-width:thin}body:has(.cms-scope) :is(.cms-scope,.cms-scope *)::-webkit-scrollbar{height:10px;width:10px}body:has(.cms-scope) :is(.cms-scope,.cms-scope *)::-webkit-scrollbar-track{background:transparent}body:has(.cms-scope) :is(.cms-scope,.cms-scope *)::-webkit-scrollbar-thumb{background:var(--cms-line-strong);background-clip:content-box;border:3px solid transparent;border-radius:9999px}body:has(.cms-scope) :is(.cms-scope,.cms-scope *)::-webkit-scrollbar-thumb:hover{background:color-mix(in oklab,var(--cms-line-strong) 70%,var(--cms-text));background-clip:content-box}.cms-display,.cms-title{font-family:var(--cms-font-ui);font-weight:650;letter-spacing:-.01em}.cms-title{color:var(--ui-text-highlighted)}.cms-title-sm{font-size:1.125rem;line-height:1.4}.cms-title-md{font-size:1.5rem;line-height:1.25}.cms-title-lg{font-size:1.875rem;line-height:1.2}.cms-subtitle{color:var(--ui-text-muted);font-size:.875rem}.cms-accent{color:var(--cms-fern)}.cms-label{color:var(--ui-text-muted);font-size:.75rem;font-weight:500;line-height:1.4}body:has(.cms-scope) :is(button,a,[role=button],[role=menuitem],[role=option],.__clickable):not(:disabled,[aria-disabled=true]){@apply cursor-pointer select-none}body:has(.cms-scope) :is(button,a,[role=button],[role=menuitem],[role=option]):focus-visible{outline:2px solid var(--cms-fern);outline-offset:2px}.cms-canvas{background-color:var(--cms-canvas)}.cms-card{background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg)}.cms-panel{padding:1.75rem}body:has(.cms-scope),html:has(.cms-scope){overscroll-behavior-y:none}.cms-shell{display:flex;min-height:100vh}.cms-sidebar{background:var(--cms-surface);border-right:1px solid var(--cms-line);display:flex;flex-direction:column;flex-shrink:0;gap:1.75rem;height:100vh;padding:1.75rem 1rem;position:sticky;top:0;width:16rem}.cms-sidebar-brand{color:var(--ui-text-highlighted);font-family:var(--cms-font-ui);font-size:1.25rem;font-weight:650;letter-spacing:-.01em;padding-inline:.75rem}.cms-sidebar-nav{display:flex;flex:1;flex-direction:column;gap:1.5rem;overflow-y:auto;overscroll-behavior-y:none}.cms-sidebar-group{display:flex;flex-direction:column;gap:.25rem}.cms-sidebar-group>.cms-label{margin-bottom:.5rem;padding-inline:.75rem}.cms-sidebar-footer{border-top:1px solid var(--cms-line);display:flex;flex-direction:column;gap:.75rem;padding-top:1rem}.cms-sidebar-footer .cms-btn.is-block{color:var(--ui-text-muted);justify-content:space-between;padding-inline:.75rem}.cms-sidebar-footer .cms-btn.is-block:hover{background:var(--cms-field);color:var(--ui-text)}.cms-main{flex:1;height:100vh;min-width:0;overflow-y:auto;overscroll-behavior-y:none;padding:2.5rem}.cms-main-inner{margin-inline:auto;max-width:64rem;min-height:100%;width:100%}.cms-main-inner,.cms-page{display:flex;flex-direction:column}.cms-page{gap:1.75rem}.cms-page.is-fill{flex:1;min-height:0}.cms-page-header{align-items:flex-end;display:flex;gap:1rem;justify-content:space-between}.cms-page-heading{display:flex;flex-direction:column;gap:.5rem}.cms-breadcrumb{align-items:center;color:var(--ui-text-muted);display:flex;font-size:.75rem;font-weight:500;gap:.375rem;line-height:1.4}.cms-breadcrumb-link{border-radius:.25rem;padding:.0625rem .125rem;transition:color .15s ease}.cms-breadcrumb-link:hover{color:var(--ui-text-highlighted)}.cms-breadcrumb-separator{color:var(--ui-text-dimmed)}.cms-tree-row{align-items:center;display:flex;gap:.75rem;padding:.625rem 1rem;transition:background-color .15s ease}.cms-tree-row:hover{background-color:var(--ui-bg-elevated)}.cms-tree-branch{color:var(--ui-text-dimmed);flex-shrink:0;transform:scaleY(-1) rotate(90deg);width:1rem}.cms-toolbar{align-items:center;display:flex;gap:1.5rem}.cms-toolbar-actions{align-items:center;display:flex;flex-shrink:0;gap:.5rem;margin-left:auto}.cms-actions{align-items:center;display:flex;gap:.75rem}.cms-actions.is-end{justify-content:flex-end}.cms-spinner{color:var(--ui-text-dimmed);flex:1;justify-content:center;padding-block:2.5rem}.cms-empty,.cms-spinner{align-items:center;display:flex}.cms-empty{background:color-mix(in oklab,var(--cms-surface) 55%,transparent);border:1px dashed var(--cms-line-strong);border-radius:var(--cms-radius-lg);flex-direction:column;gap:.5rem;padding:3.5rem;text-align:center}.cms-empty.is-fill{flex:1;justify-content:center}.cms-empty-icon{color:var(--cms-fern);margin-bottom:.25rem}.cms-empty>.cms-btn{margin-top:.75rem}.cms-auth{align-items:center;display:flex;justify-content:center;min-height:100vh;padding:1.5rem}.cms-auth-panel{gap:2rem;max-width:24rem;width:100%}.cms-auth-header,.cms-auth-panel{display:flex;flex-direction:column}.cms-auth-header{align-items:center;gap:.75rem;text-align:center}.cms-auth-card{padding:1.75rem}.cms-navlink{align-items:center;border-radius:var(--cms-radius-md);color:var(--ui-text-toned);display:flex;font-size:.875rem;font-weight:500;gap:.625rem;padding:.5rem .75rem;transition:background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-navlink:hover{background:var(--cms-field);color:var(--ui-text-highlighted)}.cms-navlink.is-active{background:var(--cms-field);color:var(--cms-fern)}.cms-navlink-icon{color:var(--ui-text-dimmed)}.cms-navlink.is-active .cms-navlink-icon{color:var(--cms-fern)}.cms-badge{align-items:center;border:1px solid transparent;border-radius:var(--cms-radius-md);display:inline-flex;font-family:var(--cms-font-mono);font-size:.6875rem;letter-spacing:.08em;line-height:1.4;padding:.1875rem .5rem;pointer-events:none;text-transform:uppercase;transition:background-color var(--cms-duration) var(--cms-ease),border-color var(--cms-duration) var(--cms-ease);white-space:nowrap}.cms-badge.is-published{background:color-mix(in oklab,var(--cms-success) 18%,transparent);border-color:color-mix(in oklab,var(--cms-success) 32%,transparent);color:var(--cms-success)}.cms-badge.is-muted{background:var(--cms-field);border-color:var(--cms-line);color:var(--ui-text-muted)}.cms-badge.is-draft{background:transparent}.cms-badge.is-draft,.cms-dropzone{border:1px dashed var(--cms-line-strong);color:var(--ui-text-muted)}.cms-dropzone{align-items:center;background:color-mix(in oklab,var(--cms-surface) 55%,transparent);border-radius:var(--cms-radius-lg);cursor:pointer;display:flex;flex-direction:column;gap:.5rem;justify-content:center;padding:2rem 1.5rem;transition:border-color var(--cms-duration) var(--cms-ease),background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease);width:100%}.cms-dropzone.is-over,.cms-dropzone:hover{background:var(--cms-field);border-color:var(--cms-fern);color:var(--ui-text-highlighted)}.cms-dropzone.is-dense{flex-direction:row;gap:.625rem;padding:.875rem 1.25rem}.cms-dropzone.is-busy{opacity:.7;pointer-events:none}.cms-pill{align-items:center;background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:9999px;color:var(--ui-text-muted);cursor:pointer;display:inline-flex;flex:none;font-size:.8125rem;gap:.35rem;line-height:1.4;padding:.3rem .8rem;transition:background-color var(--cms-duration) var(--cms-ease),border-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease);white-space:nowrap}.cms-pill:hover{background:var(--cms-field);color:var(--ui-text-highlighted)}.cms-pill.is-active{background:var(--cms-field);border-color:color-mix(in oklab,var(--cms-fern) 45%,transparent);color:var(--cms-fern)}.cms-pill.is-empty{border-style:dashed}.cms-pill-group{align-items:center;display:inline-flex;gap:.125rem}.cms-pill-group .cms-pill{padding-right:.6rem}.cms-pill-discard{align-items:center;border-radius:9999px;color:var(--ui-text-dimmed);cursor:pointer;display:inline-flex;height:1.25rem;justify-content:center;transition:color var(--cms-duration) var(--cms-ease);width:1.25rem}.cms-pill-discard:hover{color:var(--ui-error)}.cms-folder-picker{align-items:center;display:flex;flex-wrap:wrap;gap:.375rem}.cms-folder-picker-input{width:11rem}.cms-form-hint{color:var(--ui-text-dimmed);font-size:.8125rem}.cms-dropzone-hint{align-items:center;color:var(--ui-text-dimmed);display:inline-flex;font-size:.75rem;gap:.3rem}.cms-block-label{color:var(--ui-text-toned);font-size:.875rem;font-weight:550;line-height:1.4;transition:color var(--cms-duration) var(--cms-ease)}.cms-block-label:hover{color:var(--ui-text-highlighted)}@keyframes cms-rise{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}.cms-rise{animation:cms-rise .3s var(--cms-ease) both}.cms-rise.is-delayed{animation-delay:90ms}@media (prefers-reduced-motion:reduce){.cms-rise{animation:none}}.cms-richtext{background:var(--cms-field);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);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{color:var(--ui-text-highlighted);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-strong);color:var(--ui-text-muted);padding-left:.75rem}.cms-btn{align-items:center;border:1px solid transparent;border-radius:var(--cms-radius-md);display:inline-flex;font-weight:500;gap:.375rem;justify-content:center;line-height:1;min-height:var(--cms-btn-size);transition:background-color var(--cms-duration) var(--cms-ease),border-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease),opacity var(--cms-duration) var(--cms-ease);white-space:nowrap}.cms-btn:active:not(:disabled,[aria-disabled=true]){scale:.99}.cms-btn:disabled,.cms-btn[aria-disabled=true]{opacity:.5;pointer-events:none}.cms-btn.is-block{width:100%}.cms-btn.is-icon{justify-content:center;padding-inline:0;width:var(--cms-btn-size)}.cms-btn-xs{--cms-btn-size:1.75rem;font-size:.875rem;padding:.25rem .625rem}.cms-btn-sm{--cms-btn-size:2rem;font-size:.875rem;padding:.25rem .75rem}.cms-btn-md{--cms-btn-size:2.25rem;font-size:.875rem;padding:.25rem .875rem}.cms-btn-lg{--cms-btn-size:2.625rem;font-size:.9375rem;padding:.375rem 1.125rem}.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) 82%,#fff)}.cms-btn-primary.cms-btn-solid:active{background:color-mix(in oklab,var(--ui-primary) 88%,#000)}.cms-btn-primary.cms-btn-soft{background:var(--cms-field);border-color:var(--cms-line);color:var(--ui-text-highlighted)}.cms-btn-primary.cms-btn-soft:hover{background:var(--cms-elevated);border-color:var(--cms-line-strong)}.cms-btn-primary.cms-btn-soft:active{background:var(--cms-field)}.cms-btn-primary.cms-btn-ghost{background:transparent;color:var(--ui-text-toned)}.cms-btn-primary.cms-btn-ghost:hover{background:var(--cms-field);color:var(--ui-text-highlighted)}.cms-btn-primary.cms-btn-ghost:active{background:var(--cms-elevated)}.cms-btn-neutral.cms-btn-solid{background:var(--cms-line-strong);color:var(--ui-text-highlighted)}.cms-btn-neutral.cms-btn-solid:hover{background:color-mix(in oklab,var(--cms-line-strong) 80%,var(--cms-text))}.cms-btn-neutral.cms-btn-solid:active{background:color-mix(in oklab,var(--cms-line-strong) 90%,#000)}.cms-btn-neutral.cms-btn-soft{background:var(--cms-field);border-color:var(--cms-line);color:var(--ui-text-highlighted)}.cms-btn-neutral.cms-btn-soft:hover{background:var(--cms-elevated);border-color:var(--cms-line-strong)}.cms-btn-neutral.cms-btn-soft:active{background:var(--cms-field)}.cms-btn-neutral.cms-btn-ghost{background:transparent;color:var(--ui-text-toned)}.cms-btn-neutral.cms-btn-ghost:hover{background:var(--cms-field);color:var(--ui-text-highlighted)}.cms-btn-neutral.cms-btn-ghost:active{background:var(--cms-elevated)}.cms-btn-error.cms-btn-solid{background:var(--ui-error);color:var(--cms-canvas)}.cms-btn-error.cms-btn-solid:hover{background:color-mix(in oklab,var(--ui-error) 85%,#fff)}.cms-btn-error.cms-btn-solid:active{background:color-mix(in oklab,var(--ui-error) 88%,#000)}.cms-btn-error.cms-btn-soft{background:color-mix(in oklab,var(--ui-error) 16%,transparent);border-color:color-mix(in oklab,var(--ui-error) 28%,transparent);color:var(--ui-error)}.cms-btn-error.cms-btn-soft:hover{background:color-mix(in oklab,var(--ui-error) 24%,transparent);border-color:color-mix(in oklab,var(--ui-error) 40%,transparent)}.cms-btn-error.cms-btn-soft:active{background:color-mix(in oklab,var(--ui-error) 16%,transparent)}.cms-btn-error.cms-btn-ghost{background:transparent;color:var(--ui-error)}.cms-btn-error.cms-btn-ghost:hover{background:color-mix(in oklab,var(--ui-error) 14%,transparent)}.cms-btn-error.cms-btn-ghost:active{background:color-mix(in oklab,var(--ui-error) 22%,transparent)}.cms-btn-success.cms-btn-solid{background:var(--ui-success);color:var(--cms-canvas)}.cms-btn-success.cms-btn-solid:hover{background:color-mix(in oklab,var(--ui-success) 85%,#fff)}.cms-btn-success.cms-btn-solid:active{background:color-mix(in oklab,var(--ui-success) 88%,#000)}.cms-btn-success.cms-btn-soft{background:color-mix(in oklab,var(--ui-success) 16%,transparent);border-color:color-mix(in oklab,var(--ui-success) 28%,transparent);color:var(--ui-success)}.cms-btn-success.cms-btn-soft:hover{background:color-mix(in oklab,var(--ui-success) 24%,transparent);border-color:color-mix(in oklab,var(--ui-success) 40%,transparent)}.cms-btn-success.cms-btn-soft:active{background:color-mix(in oklab,var(--ui-success) 16%,transparent)}.cms-btn-success.cms-btn-ghost{background:transparent;color:var(--ui-success)}.cms-btn-success.cms-btn-ghost:hover{background:color-mix(in oklab,var(--ui-success) 14%,transparent)}.cms-btn-success.cms-btn-ghost:active{background:color-mix(in oklab,var(--ui-success) 22%,transparent)}.cms-form{display:flex;flex-direction:column;gap:1.25rem}.cms-tabs{align-items:center;border-bottom:1px solid var(--ui-border);display:flex;gap:.25rem;margin-bottom:.25rem;overflow-x:auto}.cms-tab{border-bottom:2px solid transparent;color:var(--ui-text-toned);cursor:pointer;font-size:.875rem;font-weight:500;margin-bottom:-1px;padding:.5rem .875rem;transition:color var(--cms-duration) var(--cms-ease),border-color var(--cms-duration) var(--cms-ease);white-space:nowrap}.cms-tab:hover{color:var(--ui-text-highlighted)}.cms-tab.is-active{border-bottom-color:var(--cms-fern);color:var(--cms-fern)}.cms-form-field{display:flex;flex-direction:column;gap:.375rem}.cms-form-label-row{align-items:center;display:flex;gap:.75rem;justify-content:space-between;min-height:1.75rem}.cms-form-label{color:var(--ui-text-toned);font-size:.875rem;font-weight:500}.cms-form-error,.cms-form-required{color:var(--ui-error)}.cms-form-error{font-size:.875rem}.cms-form-actions{align-items:center;display:flex;gap:.75rem;justify-content:flex-end;padding-top:.25rem}.cms-field{background:var(--cms-field);border:1px solid var(--cms-line);border-radius:var(--ui-radius);color:var(--ui-text-highlighted);line-height:1.25;transition:border-color var(--cms-duration) var(--cms-ease),box-shadow var(--cms-duration) var(--cms-ease);width:100%}textarea.cms-field{line-height:1.5;min-height:0;padding-bottom:.625rem;padding-top:.625rem;resize:vertical}.cms-field::-moz-placeholder{color:var(--ui-text-dimmed)}.cms-field::placeholder{color:var(--ui-text-dimmed)}.cms-field:hover:not(:focus,:focus-within){border-color:var(--cms-line-strong)}.cms-field:focus,.cms-field:focus-within{border-color:var(--cms-fern);box-shadow:0 0 0 3px color-mix(in oklab,var(--cms-fern) 25%,transparent);outline:none}.cms-field.is-error{border-color:var(--ui-error)}.cms-field.is-error:focus,.cms-field.is-error:focus-within{border-color:var(--ui-error);box-shadow:0 0 0 3px color-mix(in oklab,var(--ui-error) 25%,transparent)}.cms-field-sm{font-size:.875rem;min-height:2rem;padding:.25rem .625rem}.cms-field-md{font-size:.875rem;min-height:2.25rem;padding:.25rem .75rem}.cms-field-lg{font-size:.9375rem;min-height:2.625rem;padding:.375rem .875rem}.cms-select-chevron{align-items:center;cursor:pointer;display:inline-flex;transition:transform var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-select-chevron:hover{color:var(--ui-text-muted)}.cms-select-chevron.is-open{transform:rotate(180deg)}.cms-locale-switch{align-items:center;background:var(--cms-field);border:1px solid var(--cms-line);border-radius:var(--cms-radius-md);display:inline-flex;gap:2px;padding:2px}.cms-locale-option{border-radius:var(--cms-radius-sm);color:var(--ui-text-muted);cursor:pointer;font-family:var(--cms-font-mono);font-size:.6875rem;letter-spacing:.08em;line-height:1.4;padding:.125rem .5rem;text-transform:uppercase;transition:background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-locale-option:hover:not(.is-active){color:var(--ui-text-highlighted)}.cms-locale-option.is-empty{color:var(--ui-text-dimmed)}.cms-locale-option.is-active{background:var(--cms-elevated);color:var(--cms-fern)}.cms-input-wrap{align-items:center;display:flex;position:relative;width:100%}.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 var(--cms-duration) var(--cms-ease);width:2.25rem}.cms-switch.is-on{background:var(--cms-fern)}.cms-switch:after{background:var(--cms-text);border-radius:9999px;content:"";height:.875rem;left:.1875rem;position:absolute;transition:transform var(--cms-duration) var(--cms-ease);width:.875rem}.cms-switch.is-on:after{background:var(--cms-canvas);transform:translateX(1rem)}.cms-overlay{align-items:center;background:var(--cms-scrim);display:flex;inset:0;justify-content:center;overflow-y:auto;padding:2rem 1rem;position:fixed;z-index:60}.cms-modal{background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);display:flex;flex-direction:column;max-height:calc(100dvh - 4rem);max-width:32rem;width:100%}.cms-modal.is-sm{max-width:28rem}.cms-modal.is-lg{max-width:48rem}.cms-modal-header{padding:1.25rem 1.5rem 0}.cms-modal-body{flex:1;min-height:0;overflow-y:auto;padding:1.25rem 1.5rem 1.5rem}.cms-modal-enter-active,.cms-modal-leave-active{transition:opacity var(--cms-duration-lg) var(--cms-ease)}.cms-modal-enter-active .cms-modal,.cms-modal-leave-active .cms-modal{transition:transform var(--cms-duration-lg) var(--cms-ease),opacity var(--cms-duration-lg) var(--cms-ease);will-change:transform}.cms-modal-enter-from,.cms-modal-leave-to{opacity:0}.cms-modal-enter-from .cms-modal,.cms-modal-leave-to .cms-modal{opacity:0;transform:translateY(8px)}.cms-overlay-drawer{align-items:stretch;justify-content:flex-end;overflow:hidden;padding:0;z-index:55}.cms-drawer{background:var(--cms-surface);border-left:1px solid var(--cms-line);border-radius:var(--cms-radius-lg) 0 0 var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);display:flex;flex-direction:column;height:100%;max-width:46rem;width:100%}.cms-drawer-header{align-items:center;border-bottom:1px solid var(--cms-line);display:flex;gap:1rem;justify-content:space-between;padding:1.25rem 1.75rem}.cms-drawer-body{flex:1;min-height:0;overflow-y:auto;padding:1.75rem}.cms-drawer-footer{align-items:center;border-top:1px solid var(--cms-line);display:flex;gap:1rem;justify-content:space-between;padding:1.25rem 1.75rem}.cms-drawer-enter-active,.cms-drawer-leave-active{transition:opacity var(--cms-duration-lg) var(--cms-ease)}.cms-drawer-enter-active .cms-drawer,.cms-drawer-leave-active .cms-drawer{transition:transform var(--cms-duration-lg) var(--cms-ease);will-change:transform}.cms-drawer-enter-from,.cms-drawer-leave-to{opacity:0}.cms-drawer-enter-from .cms-drawer,.cms-drawer-leave-to .cms-drawer{transform:translateX(100%)}@media (prefers-reduced-motion:reduce){.cms-drawer-enter-active,.cms-drawer-enter-active .cms-drawer,.cms-drawer-leave-active,.cms-drawer-leave-active .cms-drawer,.cms-modal-enter-active,.cms-modal-enter-active .cms-modal,.cms-modal-leave-active,.cms-modal-leave-active .cms-modal{transition-duration:.01ms}}.cms-menu{background:var(--cms-elevated);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);margin-top:.375rem;min-width:12rem;padding:.25rem;position:absolute;z-index:50}.cms-menu.align-end{right:0}.cms-menu-item{align-items:center;border-radius:var(--cms-radius-sm);color:var(--ui-text);cursor:pointer;display:flex;font-size:.875rem;gap:.5rem;padding:.4rem .6rem;text-align:left;transition:background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease);width:100%}.cms-menu-item:hover{background:color-mix(in oklab,var(--cms-text) 8%,transparent);color:var(--ui-text-highlighted)}.cms-menu-check{color:var(--cms-fern);margin-left:auto}.cms-popover{background:var(--cms-elevated);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);margin-top:.375rem;position:absolute;z-index:50}.cms-selectmenu-input{background:transparent;color:var(--ui-text-highlighted);flex:1;min-width:4rem;outline:none}.cms-selectmenu-affix{color:var(--ui-text-dimmed);flex-shrink:0}.cms-selectmenu-panel{background:var(--cms-elevated);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);left:0;margin-top:.375rem;max-height:15rem;overflow-y:auto;padding:.25rem;position:absolute;right:0;z-index:50}.cms-tag{align-items:center;background:var(--cms-elevated);border:1px solid var(--cms-line);border-radius:9999px;color:var(--ui-text-highlighted);display:inline-flex;font-size:.8125rem;gap:.25rem;padding:.125rem .5rem}.cms-media-gallery{display:flex;flex-direction:column;gap:1.25rem}.cms-media-source{color:var(--ui-text-muted);font-size:.8125rem;margin-top:-.75rem}.cms-media-filters{align-items:center;display:flex;flex-wrap:wrap;gap:.25rem}.cms-media-filters-actions{align-items:center;display:flex;flex-shrink:0;gap:.5rem;margin-left:auto}.cms-media-filter-separator{background:var(--cms-line);flex:none;height:1rem;margin-inline:.25rem;width:1px}.cms-pill-count{color:var(--ui-text-dimmed);font-family:var(--cms-font-mono);font-size:.6875rem;margin-left:.25rem}.cms-blocks-grid{display:grid;gap:.75rem;grid-template-columns:repeat(2,minmax(0,1fr))}@media (min-width:640px){.cms-blocks-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:1024px){.cms-blocks-grid{grid-template-columns:repeat(4,minmax(0,1fr))}}.cms-block-tile{overflow:hidden;transition:opacity var(--cms-duration) var(--cms-ease)}.cms-block-tile.is-muted{opacity:.45}.cms-block-preview{align-items:center;aspect-ratio:4/3;background:var(--cms-field);color:var(--ui-text-dimmed);display:flex;justify-content:center;position:relative;width:100%}.cms-block-preview>img,.cms-block-preview>video{height:100%;inset:0;-o-object-fit:cover;object-fit:cover;position:absolute;width:100%}.cms-block-summary{color:var(--ui-text-muted);font-size:.75rem;line-height:1.5;padding:.75rem;text-align:left}.cms-block-hidden{color:var(--cms-text);filter:drop-shadow(0 1px 2px rgb(0 0 0/.6));left:.5rem;position:absolute;top:.5rem}.cms-block-tile.is-dragging{opacity:.4}.cms-block-tile.is-drop-target{outline:2px solid var(--cms-fern);outline-offset:2px}.cms-block-bar{gap:.25rem;padding:.375rem}.cms-block-bar,.cms-block-grip{align-items:center;display:flex}.cms-block-grip{color:var(--ui-text-dimmed);cursor:grab;justify-content:center;padding:.25rem;transition:color var(--cms-duration) var(--cms-ease)}.cms-block-grip:hover{color:var(--ui-text-muted)}.cms-block-tile.is-dragging .cms-block-grip{cursor:grabbing}.cms-block-bar-name{color:var(--ui-text-muted);flex:1;font-size:.75rem;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cms-block-bar-actions{align-items:center;display:flex;gap:.125rem}.cms-block-add{align-items:center;border:1px dashed var(--cms-line-strong);border-radius:var(--cms-radius-md);color:var(--ui-text-muted);display:flex;flex-direction:column;font-size:.8125rem;font-weight:500;gap:.375rem;justify-content:center;min-height:6rem;transition:border-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-block-add:hover{border-color:var(--cms-fern);color:var(--ui-text-highlighted)}.cms-media-grid{display:grid;gap:1rem;grid-template-columns:repeat(2,minmax(0,1fr))}@media (min-width:640px){.cms-media-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:1024px){.cms-media-grid:not(.is-compact){grid-template-columns:repeat(4,minmax(0,1fr))}}.cms-media-tile{overflow:hidden;text-align:left;transition:border-color var(--cms-duration) var(--cms-ease)}.cms-media-tile.is-selectable{cursor:pointer}.cms-media-tile.is-selectable:hover{border-color:var(--cms-line-strong)}.cms-media-preview{align-items:center;aspect-ratio:1;background:var(--cms-field);color:var(--ui-text-dimmed);display:flex;justify-content:center;position:relative}.cms-media-preview>img,.cms-media-preview>video{height:100%;inset:0;-o-object-fit:cover;object-fit:cover;position:absolute;width:100%}.cms-media-actions{display:flex;gap:.25rem;opacity:0;position:absolute;right:.5rem;top:.5rem;transition:opacity var(--cms-duration) var(--cms-ease)}.cms-media-actions:focus-within,.cms-media-tile:hover .cms-media-actions{opacity:1}.cms-media-info{background:linear-gradient(0deg,rgba(0,0,0,.75),transparent);bottom:0;color:var(--cms-text);font-family:var(--cms-font-mono);font-size:.75rem;left:0;letter-spacing:.08em;line-height:1.4;opacity:0;padding:1.5rem .75rem .5rem;pointer-events:none;position:absolute;right:0;text-transform:uppercase;transition:opacity var(--cms-duration) var(--cms-ease)}.cms-media-tile:focus-visible .cms-media-info,.cms-media-tile:hover .cms-media-info{opacity:1}.cms-media-meta{display:flex;flex-direction:column;gap:.25rem;padding:.625rem .875rem}.cms-media-name{color:var(--ui-text-highlighted);font-size:.875rem;font-weight:500}.cms-media-folder,.cms-media-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cms-media-folder{align-items:center;color:var(--ui-text-dimmed);display:inline-flex;font-size:.75rem;gap:.25rem}.cms-media-single{max-width:15rem;width:100%}.cms-media-thumb{background:var(--cms-field);border-radius:var(--cms-radius-sm);height:2.25rem;-o-object-fit:cover;object-fit:cover;width:2.25rem}.cms-media-thumb.is-fallback{align-items:center;color:var(--ui-text-dimmed);display:flex;justify-content:center}.cms-media-thumb-empty{color:var(--ui-text-dimmed)}.cms-table-shell{display:flex;flex:1;flex-direction:column;min-height:0;overflow:hidden}.cms-table-scroll{flex:1;min-height:0;overflow:auto}.cms-table{border-collapse:collapse;min-width:-moz-max-content;min-width:max-content;text-align:left;width:100%}.cms-table td,.cms-table th{white-space:nowrap}.cms-table thead{background:var(--cms-surface);position:sticky;top:0;z-index:10}.cms-table th{background:var(--cms-surface);box-shadow:inset 0 -1px 0 var(--cms-line);color:var(--ui-text-muted);font-family:var(--cms-font-mono);font-size:.6875rem;font-weight:400;letter-spacing:.08em;line-height:1.4;padding:.75rem 1.25rem;text-transform:uppercase;transition:background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-table td{color:var(--ui-text);font-size:.875rem;padding:.875rem 1.25rem}.cms-table tbody tr{border-bottom:1px solid var(--cms-line);transition:background-color var(--cms-duration) var(--cms-ease)}.cms-table tbody tr:last-child{border-bottom:0}.cms-table tbody tr:hover{background:var(--cms-field)}.cms-table-cell-actions{display:flex;justify-content:flex-end}.cms-table th[draggable=true]{cursor:grab;-webkit-user-select:none;-moz-user-select:none;user-select:none}.cms-table th[draggable=true]:active{cursor:grabbing}.cms-table th.is-dragging{opacity:.4}.cms-table th.is-drop-target{background:var(--cms-field);box-shadow:inset 0 -1px 0 var(--cms-line),inset 0 0 0 2px color-mix(in oklab,var(--cms-fern) 55%,transparent)}.cms-th-label{align-items:center;display:inline-flex;gap:.35rem}.cms-table th.is-sortable{cursor:pointer}.cms-table th.is-sortable:hover,.cms-table th.is-sorted{color:var(--ui-text-highlighted)}.cms-table th.is-sorted{background:var(--cms-field)}.cms-th-sort{flex:none;opacity:0;transition:opacity var(--cms-duration) var(--cms-ease)}.cms-table th.is-sortable:hover .cms-th-sort,.cms-table th.is-sorted .cms-th-sort{opacity:1}.cms-alert{border:1px solid transparent;border-radius:var(--cms-radius-md);display:flex;flex-direction:column;font-size:.875rem;gap:.125rem;padding:.75rem 1rem}.cms-alert-title{font-weight:550}.cms-alert-error{background:color-mix(in oklab,var(--ui-error) 14%,transparent);border-color:color-mix(in oklab,var(--ui-error) 26%,transparent);color:var(--ui-error)}.cms-alert-success{background:color-mix(in oklab,var(--ui-success) 14%,transparent);border-color:color-mix(in oklab,var(--ui-success) 26%,transparent);color:var(--ui-success)}.cms-alert-warning{background:color-mix(in oklab,var(--ui-warning) 14%,transparent);border-color:color-mix(in oklab,var(--ui-warning) 26%,transparent);color:var(--ui-warning)}.cms-alert-neutral{background:var(--cms-field);border-color:var(--cms-line);color:var(--ui-text-toned)}.cms-alert-neutral .cms-alert-title{color:var(--ui-text-highlighted)}.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(--cms-elevated);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);padding:.75rem .875rem}.cms-toast-row{align-items:flex-start;display:flex;gap:.625rem}.cms-toast-icon{color:var(--ui-text-muted);flex-shrink:0;margin-top:.0625rem}.cms-toast.is-success .cms-toast-icon{color:var(--ui-success)}.cms-toast.is-error .cms-toast-icon{color:var(--ui-error)}.cms-toast-title{color:var(--ui-text-highlighted);font-size:.875rem;font-weight:500}.cms-toast-description{color:var(--ui-text-muted);font-size:.875rem;margin-top:.125rem}.cms-toast-dismiss{color:var(--ui-text-dimmed);flex-shrink:0;transition:color var(--cms-duration) var(--cms-ease)}.cms-toast-dismiss:hover{color:var(--ui-text-highlighted)}.cms-pagination{align-items:center;display:flex;gap:.25rem;justify-content:center}.cms-page-btn{border-radius:var(--cms-radius-sm);color:var(--ui-text-toned);font-size:.875rem;height:2rem;min-width:2rem;padding:0 .5rem;transition:background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-page-btn:hover:not(:disabled){background:var(--cms-field);color:var(--ui-text-highlighted)}.cms-page-btn.is-active{background:var(--cms-field);color:var(--cms-fern);font-weight:600}.cms-page-btn:disabled{opacity:.4}
|
|
1
|
+
@layer theme, base, components, utilities;@import "tailwindcss/theme.css" layer(theme) theme(inline);.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-canvas:#1b1b1e;--cms-surface:#222226;--cms-field:#2a2a2f;--cms-elevated:#2f2f35;--cms-line:#35353b;--cms-line-strong:#46464e;--cms-text:#ededef;--cms-text-toned:#c2c2c8;--cms-text-muted:#9a9aa3;--cms-text-dimmed:#6e6e78;--cms-fern:#8fae8b;--cms-error:#f0857d;--cms-success:#7fb083;--cms-warning:#d4ad76;--cms-font-ui:"Hanken Grotesk Variable","Hanken Grotesk",ui-sans-serif,system-ui,sans-serif;--cms-font-mono:"Fragment Mono",ui-monospace,SFMono-Regular,Menlo,monospace;--cms-radius-sm:6px;--cms-radius-md:8px;--cms-radius-lg:12px;--cms-shadow-overlay:0 8px 24px rgba(0,0,0,.35);--cms-scrim:rgba(0,0,0,.5);--cms-duration:120ms;--cms-duration-lg:250ms;--cms-ease:cubic-bezier(0.2,0,0,1);color-scheme:dark;--ui-text-dimmed:var(--cms-text-dimmed);--ui-text-muted:var(--cms-text-muted);--ui-text-toned:var(--cms-text-toned);--ui-text:var(--cms-text-toned);--ui-text-highlighted:var(--cms-text);--ui-text-inverted:var(--cms-canvas);--ui-bg:var(--cms-surface);--ui-bg-muted:var(--cms-canvas);--ui-bg-elevated:var(--cms-field);--ui-bg-accented:var(--cms-elevated);--ui-bg-inverted:var(--cms-text);--ui-border:var(--cms-line);--ui-border-muted:var(--cms-line);--ui-border-accented:var(--cms-line-strong);--ui-border-inverted:var(--cms-text);--ui-primary:var(--cms-text);--ui-accent:var(--cms-fern);--ui-error:var(--cms-error);--ui-success:var(--cms-success);--ui-warning:var(--cms-warning);--ui-radius:var(--cms-radius-md);color:var(--ui-text);font-family:var(--cms-font-ui);margin:0;padding:0;-webkit-font-smoothing:antialiased}body:has(.cms-scope) ::-moz-selection{background:color-mix(in oklab,var(--cms-fern) 35%,transparent);color:var(--ui-text-highlighted)}body:has(.cms-scope) ::selection{background:color-mix(in oklab,var(--cms-fern) 35%,transparent);color:var(--ui-text-highlighted)}body:has(.cms-scope) :is(.cms-scope,.cms-scope *){scrollbar-color:var(--cms-line-strong) transparent;scrollbar-width:thin}body:has(.cms-scope) :is(.cms-scope,.cms-scope *)::-webkit-scrollbar{height:10px;width:10px}body:has(.cms-scope) :is(.cms-scope,.cms-scope *)::-webkit-scrollbar-track{background:transparent}body:has(.cms-scope) :is(.cms-scope,.cms-scope *)::-webkit-scrollbar-thumb{background:var(--cms-line-strong);background-clip:content-box;border:3px solid transparent;border-radius:9999px}body:has(.cms-scope) :is(.cms-scope,.cms-scope *)::-webkit-scrollbar-thumb:hover{background:color-mix(in oklab,var(--cms-line-strong) 70%,var(--cms-text));background-clip:content-box}.cms-display,.cms-title{font-family:var(--cms-font-ui);font-weight:650;letter-spacing:-.01em}.cms-title{color:var(--ui-text-highlighted)}.cms-title-sm{font-size:1.125rem;line-height:1.4}.cms-title-md{font-size:1.5rem;line-height:1.25}.cms-title-lg{font-size:1.875rem;line-height:1.2}.cms-subtitle{color:var(--ui-text-muted);font-size:.875rem}.cms-accent{color:var(--cms-fern)}.cms-label{color:var(--ui-text-muted);font-size:.75rem;font-weight:500;line-height:1.4}body:has(.cms-scope) :is(button,a,[role=button],[role=menuitem],[role=option],.__clickable):not(:disabled,[aria-disabled=true]){@apply cursor-pointer select-none}body:has(.cms-scope) :is(button,a,[role=button],[role=menuitem],[role=option]):focus-visible{outline:2px solid var(--cms-fern);outline-offset:2px}.cms-canvas{background-color:var(--cms-canvas)}.cms-card{background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg)}.cms-panel{padding:1.75rem}body:has(.cms-scope),html:has(.cms-scope){overscroll-behavior-y:none}.cms-shell{display:flex;min-height:100vh}.cms-sidebar{background:var(--cms-surface);border-right:1px solid var(--cms-line);display:flex;flex-direction:column;flex-shrink:0;gap:1.75rem;height:100vh;padding:1.75rem 1rem;position:sticky;top:0;width:16rem}.cms-sidebar-brand{color:var(--ui-text-highlighted);font-family:var(--cms-font-ui);font-size:1.25rem;font-weight:650;letter-spacing:-.01em;padding-inline:.75rem}.cms-sidebar-nav{display:flex;flex:1;flex-direction:column;gap:1.5rem;overflow-y:auto;overscroll-behavior-y:none}.cms-sidebar-group{display:flex;flex-direction:column;gap:.25rem}.cms-sidebar-group>.cms-label{margin-bottom:.5rem;padding-inline:.75rem}.cms-sidebar-footer{border-top:1px solid var(--cms-line);display:flex;flex-direction:column;gap:.75rem;padding-top:1rem}.cms-sidebar-footer .cms-btn.is-block{color:var(--ui-text-muted);justify-content:space-between;padding-inline:.75rem}.cms-sidebar-footer .cms-btn.is-block:hover{background:var(--cms-field);color:var(--ui-text)}.cms-main{flex:1;height:100vh;min-width:0;overflow-y:auto;overscroll-behavior-y:none;padding:2.5rem}.cms-main-inner{margin-inline:auto;max-width:64rem;min-height:100%;width:100%}.cms-main-inner,.cms-page{display:flex;flex-direction:column}.cms-page{gap:1.75rem}.cms-page.is-fill{flex:1;min-height:0}.cms-page-header{align-items:flex-end;display:flex;gap:1rem;justify-content:space-between}.cms-page-heading{display:flex;flex-direction:column;gap:.5rem}.cms-breadcrumb{align-items:center;color:var(--ui-text-muted);display:flex;font-size:.75rem;font-weight:500;gap:.375rem;line-height:1.4}.cms-breadcrumb-link{border-radius:.25rem;padding:.0625rem .125rem;transition:color .15s ease}.cms-breadcrumb-link:hover{color:var(--ui-text-highlighted)}.cms-breadcrumb-separator{color:var(--ui-text-dimmed)}.cms-tree-row{align-items:center;display:flex;gap:.75rem;padding:.625rem 1rem;transition:background-color .15s ease}.cms-tree-row:hover{background-color:var(--ui-bg-elevated)}.cms-tree-branch{color:var(--ui-text-dimmed);flex-shrink:0;transform:scaleY(-1) rotate(90deg);width:1rem}.cms-toolbar{align-items:center;display:flex;gap:1.5rem}.cms-toolbar-actions{align-items:center;display:flex;flex-shrink:0;gap:.5rem;margin-left:auto}.cms-actions{align-items:center;display:flex;gap:.75rem}.cms-actions.is-end{justify-content:flex-end}.cms-spinner{color:var(--ui-text-dimmed);flex:1;justify-content:center;padding-block:2.5rem}.cms-empty,.cms-spinner{align-items:center;display:flex}.cms-empty{background:color-mix(in oklab,var(--cms-surface) 55%,transparent);border:1px dashed var(--cms-line-strong);border-radius:var(--cms-radius-lg);flex-direction:column;gap:.5rem;padding:3.5rem;text-align:center}.cms-empty.is-fill{flex:1;justify-content:center}.cms-empty-icon{color:var(--cms-fern);margin-bottom:.25rem}.cms-empty>.cms-btn{margin-top:.75rem}.cms-auth{align-items:center;display:flex;justify-content:center;min-height:100vh;padding:1.5rem}.cms-auth-panel{gap:2rem;max-width:24rem;width:100%}.cms-auth-header,.cms-auth-panel{display:flex;flex-direction:column}.cms-auth-header{align-items:center;gap:.75rem;text-align:center}.cms-auth-card{padding:1.75rem}.cms-navlink{align-items:center;border-radius:var(--cms-radius-md);color:var(--ui-text-toned);display:flex;font-size:.875rem;font-weight:500;gap:.625rem;padding:.5rem .75rem;transition:background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-navlink:hover{background:var(--cms-field);color:var(--ui-text-highlighted)}.cms-navlink.is-active{background:var(--cms-field);color:var(--cms-fern)}.cms-navlink-icon{color:var(--ui-text-dimmed)}.cms-navlink.is-active .cms-navlink-icon{color:var(--cms-fern)}.cms-badge{align-items:center;border:1px solid transparent;border-radius:var(--cms-radius-md);display:inline-flex;font-family:var(--cms-font-mono);font-size:.6875rem;letter-spacing:.08em;line-height:1.4;padding:.1875rem .5rem;pointer-events:none;text-transform:uppercase;transition:background-color var(--cms-duration) var(--cms-ease),border-color var(--cms-duration) var(--cms-ease);white-space:nowrap}.cms-badge.is-published{background:color-mix(in oklab,var(--cms-success) 18%,transparent);border-color:color-mix(in oklab,var(--cms-success) 32%,transparent);color:var(--cms-success)}.cms-badge.is-muted{background:var(--cms-field);border-color:var(--cms-line);color:var(--ui-text-muted)}.cms-badge.is-draft{background:transparent}.cms-badge.is-draft,.cms-dropzone{border:1px dashed var(--cms-line-strong);color:var(--ui-text-muted)}.cms-dropzone{align-items:center;background:color-mix(in oklab,var(--cms-surface) 55%,transparent);border-radius:var(--cms-radius-lg);cursor:pointer;display:flex;flex-direction:column;gap:.5rem;justify-content:center;padding:2rem 1.5rem;transition:border-color var(--cms-duration) var(--cms-ease),background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease);width:100%}.cms-dropzone.is-over,.cms-dropzone:hover{background:var(--cms-field);border-color:var(--cms-fern);color:var(--ui-text-highlighted)}.cms-dropzone.is-dense{flex-direction:row;gap:.625rem;padding:.875rem 1.25rem}.cms-dropzone.is-busy{opacity:.7;pointer-events:none}.cms-pill{align-items:center;background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:9999px;color:var(--ui-text-muted);cursor:pointer;display:inline-flex;flex:none;font-size:.8125rem;gap:.35rem;line-height:1.4;padding:.3rem .8rem;transition:background-color var(--cms-duration) var(--cms-ease),border-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease);white-space:nowrap}.cms-pill:hover{background:var(--cms-field);color:var(--ui-text-highlighted)}.cms-pill.is-active{background:var(--cms-field);border-color:color-mix(in oklab,var(--cms-fern) 45%,transparent);color:var(--cms-fern)}.cms-pill.is-empty{border-style:dashed}.cms-pill-group{align-items:center;display:inline-flex;gap:.125rem}.cms-pill-group .cms-pill{padding-right:.6rem}.cms-pill-discard{align-items:center;border-radius:9999px;color:var(--ui-text-dimmed);cursor:pointer;display:inline-flex;height:1.25rem;justify-content:center;transition:color var(--cms-duration) var(--cms-ease);width:1.25rem}.cms-pill-discard:hover{color:var(--ui-error)}.cms-folder-picker{align-items:center;display:flex;flex-wrap:wrap;gap:.375rem}.cms-folder-picker-input{width:11rem}.cms-form-hint{color:var(--ui-text-dimmed);font-size:.8125rem}.cms-dropzone-hint{align-items:center;color:var(--ui-text-dimmed);display:inline-flex;font-size:.75rem;gap:.3rem}.cms-block-label{color:var(--ui-text-toned);font-size:.875rem;font-weight:550;line-height:1.4;transition:color var(--cms-duration) var(--cms-ease)}.cms-block-label:hover{color:var(--ui-text-highlighted)}@keyframes cms-rise{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}.cms-rise{animation:cms-rise .3s var(--cms-ease) both}.cms-rise.is-delayed{animation-delay:90ms}@media (prefers-reduced-motion:reduce){.cms-rise{animation:none}}.cms-richtext{background:var(--cms-field);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);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{color:var(--ui-text-highlighted);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-strong);color:var(--ui-text-muted);padding-left:.75rem}.cms-btn{align-items:center;border:1px solid transparent;border-radius:var(--cms-radius-md);display:inline-flex;font-weight:500;gap:.375rem;justify-content:center;line-height:1;min-height:var(--cms-btn-size);transition:background-color var(--cms-duration) var(--cms-ease),border-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease),opacity var(--cms-duration) var(--cms-ease);white-space:nowrap}.cms-btn:active:not(:disabled,[aria-disabled=true]){scale:.99}.cms-btn:disabled,.cms-btn[aria-disabled=true]{opacity:.5;pointer-events:none}.cms-btn.is-block{width:100%}.cms-btn.is-icon{justify-content:center;padding-inline:0;width:var(--cms-btn-size)}.cms-btn-xs{--cms-btn-size:1.75rem;font-size:.875rem;padding:.25rem .625rem}.cms-btn-sm{--cms-btn-size:2rem;font-size:.875rem;padding:.25rem .75rem}.cms-btn-md{--cms-btn-size:2.25rem;font-size:.875rem;padding:.25rem .875rem}.cms-btn-lg{--cms-btn-size:2.625rem;font-size:.9375rem;padding:.375rem 1.125rem}.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) 82%,#fff)}.cms-btn-primary.cms-btn-solid:active{background:color-mix(in oklab,var(--ui-primary) 88%,#000)}.cms-btn-primary.cms-btn-soft{background:var(--cms-field);border-color:var(--cms-line);color:var(--ui-text-highlighted)}.cms-btn-primary.cms-btn-soft:hover{background:var(--cms-elevated);border-color:var(--cms-line-strong)}.cms-btn-primary.cms-btn-soft:active{background:var(--cms-field)}.cms-btn-primary.cms-btn-ghost{background:transparent;color:var(--ui-text-toned)}.cms-btn-primary.cms-btn-ghost:hover{background:var(--cms-field);color:var(--ui-text-highlighted)}.cms-btn-primary.cms-btn-ghost:active{background:var(--cms-elevated)}.cms-btn-neutral.cms-btn-solid{background:var(--cms-line-strong);color:var(--ui-text-highlighted)}.cms-btn-neutral.cms-btn-solid:hover{background:color-mix(in oklab,var(--cms-line-strong) 80%,var(--cms-text))}.cms-btn-neutral.cms-btn-solid:active{background:color-mix(in oklab,var(--cms-line-strong) 90%,#000)}.cms-btn-neutral.cms-btn-soft{background:var(--cms-field);border-color:var(--cms-line);color:var(--ui-text-highlighted)}.cms-btn-neutral.cms-btn-soft:hover{background:var(--cms-elevated);border-color:var(--cms-line-strong)}.cms-btn-neutral.cms-btn-soft:active{background:var(--cms-field)}.cms-btn-neutral.cms-btn-ghost{background:transparent;color:var(--ui-text-toned)}.cms-btn-neutral.cms-btn-ghost:hover{background:var(--cms-field);color:var(--ui-text-highlighted)}.cms-btn-neutral.cms-btn-ghost:active{background:var(--cms-elevated)}.cms-btn-error.cms-btn-solid{background:var(--ui-error);color:var(--cms-canvas)}.cms-btn-error.cms-btn-solid:hover{background:color-mix(in oklab,var(--ui-error) 85%,#fff)}.cms-btn-error.cms-btn-solid:active{background:color-mix(in oklab,var(--ui-error) 88%,#000)}.cms-btn-error.cms-btn-soft{background:color-mix(in oklab,var(--ui-error) 16%,transparent);border-color:color-mix(in oklab,var(--ui-error) 28%,transparent);color:var(--ui-error)}.cms-btn-error.cms-btn-soft:hover{background:color-mix(in oklab,var(--ui-error) 24%,transparent);border-color:color-mix(in oklab,var(--ui-error) 40%,transparent)}.cms-btn-error.cms-btn-soft:active{background:color-mix(in oklab,var(--ui-error) 16%,transparent)}.cms-btn-error.cms-btn-ghost{background:transparent;color:var(--ui-error)}.cms-btn-error.cms-btn-ghost:hover{background:color-mix(in oklab,var(--ui-error) 14%,transparent)}.cms-btn-error.cms-btn-ghost:active{background:color-mix(in oklab,var(--ui-error) 22%,transparent)}.cms-btn-success.cms-btn-solid{background:var(--ui-success);color:var(--cms-canvas)}.cms-btn-success.cms-btn-solid:hover{background:color-mix(in oklab,var(--ui-success) 85%,#fff)}.cms-btn-success.cms-btn-solid:active{background:color-mix(in oklab,var(--ui-success) 88%,#000)}.cms-btn-success.cms-btn-soft{background:color-mix(in oklab,var(--ui-success) 16%,transparent);border-color:color-mix(in oklab,var(--ui-success) 28%,transparent);color:var(--ui-success)}.cms-btn-success.cms-btn-soft:hover{background:color-mix(in oklab,var(--ui-success) 24%,transparent);border-color:color-mix(in oklab,var(--ui-success) 40%,transparent)}.cms-btn-success.cms-btn-soft:active{background:color-mix(in oklab,var(--ui-success) 16%,transparent)}.cms-btn-success.cms-btn-ghost{background:transparent;color:var(--ui-success)}.cms-btn-success.cms-btn-ghost:hover{background:color-mix(in oklab,var(--ui-success) 14%,transparent)}.cms-btn-success.cms-btn-ghost:active{background:color-mix(in oklab,var(--ui-success) 22%,transparent)}.cms-form{display:flex;flex-direction:column;gap:1.25rem}.cms-tabs{align-items:center;border-bottom:1px solid var(--ui-border);display:flex;gap:.25rem;margin-bottom:.25rem;overflow-x:auto}.cms-tab{border-bottom:2px solid transparent;color:var(--ui-text-toned);cursor:pointer;font-size:.875rem;font-weight:500;margin-bottom:-1px;padding:.5rem .875rem;transition:color var(--cms-duration) var(--cms-ease),border-color var(--cms-duration) var(--cms-ease);white-space:nowrap}.cms-tab:hover{color:var(--ui-text-highlighted)}.cms-tab.is-active{border-bottom-color:var(--cms-fern);color:var(--cms-fern)}.cms-form-field{display:flex;flex-direction:column;gap:.375rem}.cms-form-label-row{align-items:center;display:flex;gap:.75rem;justify-content:space-between;min-height:1.75rem}.cms-form-label{color:var(--ui-text-toned);font-size:.875rem;font-weight:500}.cms-form-error,.cms-form-required{color:var(--ui-error)}.cms-form-error{font-size:.875rem}.cms-form-actions{align-items:center;display:flex;gap:.75rem;justify-content:flex-end;padding-top:.25rem}.cms-field{background:var(--cms-field);border:1px solid var(--cms-line);border-radius:var(--ui-radius);color:var(--ui-text-highlighted);line-height:1.25;transition:border-color var(--cms-duration) var(--cms-ease),box-shadow var(--cms-duration) var(--cms-ease);width:100%}textarea.cms-field{line-height:1.5;min-height:0;padding-bottom:.625rem;padding-top:.625rem;resize:vertical}.cms-field::-moz-placeholder{color:var(--ui-text-dimmed)}.cms-field::placeholder{color:var(--ui-text-dimmed)}.cms-field:hover:not(:focus,:focus-within){border-color:var(--cms-line-strong)}.cms-field:focus,.cms-field:focus-within{border-color:var(--cms-fern);box-shadow:0 0 0 3px color-mix(in oklab,var(--cms-fern) 25%,transparent);outline:none}.cms-field.is-error{border-color:var(--ui-error)}.cms-field.is-error:focus,.cms-field.is-error:focus-within{border-color:var(--ui-error);box-shadow:0 0 0 3px color-mix(in oklab,var(--ui-error) 25%,transparent)}.cms-field-sm{font-size:.875rem;min-height:2rem;padding:.25rem .625rem}.cms-field-md{font-size:.875rem;min-height:2.25rem;padding:.25rem .75rem}.cms-field-lg{font-size:.9375rem;min-height:2.625rem;padding:.375rem .875rem}.cms-select-chevron{align-items:center;cursor:pointer;display:inline-flex;transition:transform var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-select-chevron:hover{color:var(--ui-text-muted)}.cms-select-chevron.is-open{transform:rotate(180deg)}.cms-locale-switch{align-items:center;background:var(--cms-field);border:1px solid var(--cms-line);border-radius:var(--cms-radius-md);display:inline-flex;gap:2px;padding:2px}.cms-locale-option{border-radius:var(--cms-radius-sm);color:var(--ui-text-muted);cursor:pointer;font-family:var(--cms-font-mono);font-size:.6875rem;letter-spacing:.08em;line-height:1.4;padding:.125rem .5rem;text-transform:uppercase;transition:background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-locale-option:hover:not(.is-active){color:var(--ui-text-highlighted)}.cms-locale-option.is-empty{color:var(--ui-text-dimmed)}.cms-locale-option.is-active{background:var(--cms-elevated);color:var(--cms-fern)}.cms-input-wrap{align-items:center;display:flex;position:relative;width:100%}.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 var(--cms-duration) var(--cms-ease);width:2.25rem}.cms-switch.is-on{background:var(--cms-fern)}.cms-switch:after{background:var(--cms-text);border-radius:9999px;content:"";height:.875rem;left:.1875rem;position:absolute;transition:transform var(--cms-duration) var(--cms-ease);width:.875rem}.cms-switch.is-on:after{background:var(--cms-canvas);transform:translateX(1rem)}.cms-overlay{align-items:center;background:var(--cms-scrim);display:flex;inset:0;justify-content:center;overflow-y:auto;padding:2rem 1rem;position:fixed;z-index:60}.cms-modal{background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);display:flex;flex-direction:column;max-height:calc(100dvh - 4rem);max-width:32rem;width:100%}.cms-modal.is-sm{max-width:28rem}.cms-modal.is-lg{max-width:48rem}.cms-modal-header{padding:1.25rem 1.5rem 0}.cms-modal-body{flex:1;min-height:0;overflow-y:auto;padding:1.25rem 1.5rem 1.5rem}.cms-modal-enter-active,.cms-modal-leave-active{transition:opacity var(--cms-duration-lg) var(--cms-ease)}.cms-modal-enter-active .cms-modal,.cms-modal-leave-active .cms-modal{transition:transform var(--cms-duration-lg) var(--cms-ease),opacity var(--cms-duration-lg) var(--cms-ease);will-change:transform}.cms-modal-enter-from,.cms-modal-leave-to{opacity:0}.cms-modal-enter-from .cms-modal,.cms-modal-leave-to .cms-modal{opacity:0;transform:translateY(8px)}.cms-overlay-drawer{align-items:stretch;justify-content:flex-end;overflow:hidden;padding:0;z-index:55}.cms-drawer{background:var(--cms-surface);border-left:1px solid var(--cms-line);border-radius:var(--cms-radius-lg) 0 0 var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);display:flex;flex-direction:column;height:100%;max-width:46rem;width:100%}.cms-drawer-header{align-items:center;border-bottom:1px solid var(--cms-line);display:flex;gap:1rem;justify-content:space-between;padding:1.25rem 1.75rem}.cms-drawer-body{flex:1;min-height:0;overflow-y:auto;padding:1.75rem}.cms-drawer-footer{align-items:center;border-top:1px solid var(--cms-line);display:flex;gap:1rem;justify-content:space-between;padding:1.25rem 1.75rem}.cms-drawer-enter-active,.cms-drawer-leave-active{transition:opacity var(--cms-duration-lg) var(--cms-ease)}.cms-drawer-enter-active .cms-drawer,.cms-drawer-leave-active .cms-drawer{transition:transform var(--cms-duration-lg) var(--cms-ease);will-change:transform}.cms-drawer-enter-from,.cms-drawer-leave-to{opacity:0}.cms-drawer-enter-from .cms-drawer,.cms-drawer-leave-to .cms-drawer{transform:translateX(100%)}@media (prefers-reduced-motion:reduce){.cms-drawer-enter-active,.cms-drawer-enter-active .cms-drawer,.cms-drawer-leave-active,.cms-drawer-leave-active .cms-drawer,.cms-modal-enter-active,.cms-modal-enter-active .cms-modal,.cms-modal-leave-active,.cms-modal-leave-active .cms-modal{transition-duration:.01ms}}.cms-menu{background:var(--cms-elevated);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);margin-top:.375rem;min-width:12rem;padding:.25rem;position:absolute;z-index:50}.cms-menu.align-end{right:0}.cms-menu-item{align-items:center;border-radius:var(--cms-radius-sm);color:var(--ui-text);cursor:pointer;display:flex;font-size:.875rem;gap:.5rem;padding:.4rem .6rem;text-align:left;transition:background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease);width:100%}.cms-menu-item:hover{background:color-mix(in oklab,var(--cms-text) 8%,transparent);color:var(--ui-text-highlighted)}.cms-menu-check{color:var(--cms-fern);margin-left:auto}.cms-popover{background:var(--cms-elevated);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);margin-top:.375rem;position:absolute;z-index:50}.cms-selectmenu-input{background:transparent;color:var(--ui-text-highlighted);flex:1;min-width:4rem;outline:none}.cms-selectmenu-affix{color:var(--ui-text-dimmed);flex-shrink:0}.cms-selectmenu-panel{background:var(--cms-elevated);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);left:0;margin-top:.375rem;max-height:15rem;overflow-y:auto;padding:.25rem;position:absolute;right:0;z-index:50}.cms-tag{align-items:center;background:var(--cms-elevated);border:1px solid var(--cms-line);border-radius:9999px;color:var(--ui-text-highlighted);display:inline-flex;font-size:.8125rem;gap:.25rem;padding:.125rem .5rem}.cms-media-gallery{display:flex;flex-direction:column;gap:1.25rem}.cms-media-source{color:var(--ui-text-muted);font-size:.8125rem;margin-top:-.75rem}.cms-media-filters{align-items:center;display:flex;flex-wrap:wrap;gap:.25rem}.cms-media-filters-actions{align-items:center;display:flex;flex-shrink:0;gap:.5rem;margin-left:auto}.cms-media-filter-separator{background:var(--cms-line);flex:none;height:1rem;margin-inline:.25rem;width:1px}.cms-pill-count{color:var(--ui-text-dimmed);font-family:var(--cms-font-mono);font-size:.6875rem;margin-left:.25rem}.cms-blocks-grid{display:grid;gap:.75rem;grid-template-columns:repeat(2,minmax(0,1fr))}@media (min-width:640px){.cms-blocks-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:1024px){.cms-blocks-grid{grid-template-columns:repeat(4,minmax(0,1fr))}}.cms-block-tile{overflow:hidden;transition:opacity var(--cms-duration) var(--cms-ease)}.cms-block-tile.is-muted{opacity:.45}.cms-block-preview{align-items:center;aspect-ratio:4/3;background:var(--cms-field);color:var(--ui-text-dimmed);display:flex;justify-content:center;position:relative;width:100%}.cms-block-preview>img,.cms-block-preview>video{height:100%;inset:0;-o-object-fit:cover;object-fit:cover;position:absolute;width:100%}.cms-block-summary{color:var(--ui-text-muted);font-size:.75rem;line-height:1.5;padding:.75rem;text-align:left}.cms-block-hidden{color:var(--cms-text);filter:drop-shadow(0 1px 2px rgb(0 0 0/.6));left:.5rem;position:absolute;top:.5rem}.cms-block-tile.is-dragging{opacity:.4}.cms-block-tile.is-drop-target{outline:2px solid var(--cms-fern);outline-offset:2px}.cms-block-bar{gap:.25rem;padding:.375rem}.cms-block-bar,.cms-block-grip{align-items:center;display:flex}.cms-block-grip{color:var(--ui-text-dimmed);cursor:grab;justify-content:center;padding:.25rem;transition:color var(--cms-duration) var(--cms-ease)}.cms-block-grip:hover{color:var(--ui-text-muted)}.cms-block-tile.is-dragging .cms-block-grip{cursor:grabbing}.cms-block-bar-name{color:var(--ui-text-muted);flex:1;font-size:.75rem;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cms-block-bar-actions{align-items:center;display:flex;gap:.125rem}.cms-block-add{align-items:center;border:1px dashed var(--cms-line-strong);border-radius:var(--cms-radius-md);color:var(--ui-text-muted);display:flex;flex-direction:column;font-size:.8125rem;font-weight:500;gap:.375rem;justify-content:center;min-height:6rem;transition:border-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-block-add:hover{border-color:var(--cms-fern);color:var(--ui-text-highlighted)}.cms-media-grid{display:grid;gap:1rem;grid-template-columns:repeat(2,minmax(0,1fr))}@media (min-width:640px){.cms-media-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:1024px){.cms-media-grid:not(.is-compact){grid-template-columns:repeat(4,minmax(0,1fr))}}.cms-media-tile{overflow:hidden;text-align:left;transition:border-color var(--cms-duration) var(--cms-ease)}.cms-media-tile.is-selectable{cursor:pointer}.cms-media-tile.is-selectable:hover{border-color:var(--cms-line-strong)}.cms-media-preview{align-items:center;aspect-ratio:1;background:var(--cms-field);color:var(--ui-text-dimmed);display:flex;justify-content:center;position:relative}.cms-media-preview>img,.cms-media-preview>video{height:100%;inset:0;-o-object-fit:cover;object-fit:cover;position:absolute;width:100%}.cms-media-actions{display:flex;gap:.25rem;opacity:0;position:absolute;right:.5rem;top:.5rem;transition:opacity var(--cms-duration) var(--cms-ease)}.cms-media-actions:focus-within,.cms-media-tile:hover .cms-media-actions{opacity:1}.cms-media-info{background:linear-gradient(0deg,rgba(0,0,0,.75),transparent);bottom:0;color:var(--cms-text);font-family:var(--cms-font-mono);font-size:.75rem;left:0;letter-spacing:.08em;line-height:1.4;opacity:0;padding:1.5rem .75rem .5rem;pointer-events:none;position:absolute;right:0;text-transform:uppercase;transition:opacity var(--cms-duration) var(--cms-ease)}.cms-media-tile:focus-visible .cms-media-info,.cms-media-tile:hover .cms-media-info{opacity:1}.cms-media-meta{display:flex;flex-direction:column;gap:.25rem;padding:.625rem .875rem}.cms-media-name{color:var(--ui-text-highlighted);font-size:.875rem;font-weight:500}.cms-media-folder,.cms-media-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cms-media-folder{align-items:center;color:var(--ui-text-dimmed);display:inline-flex;font-size:.75rem;gap:.25rem}.cms-media-single{max-width:15rem;width:100%}.cms-block-tile.is-missing{border-color:color-mix(in oklab,var(--ui-warning) 55%,transparent)}.cms-media-missing{align-items:center;font-size:.75rem;gap:.375rem}.cms-media-missing,.cms-media-missing-icon{color:var(--ui-warning);display:flex}.cms-media-thumb{background:var(--cms-field);border-radius:var(--cms-radius-sm);height:2.25rem;-o-object-fit:cover;object-fit:cover;width:2.25rem}.cms-media-thumb.is-fallback{align-items:center;color:var(--ui-text-dimmed);display:flex;justify-content:center}.cms-media-thumb-empty{color:var(--ui-text-dimmed)}.cms-table-shell{display:flex;flex:1;flex-direction:column;min-height:0;overflow:hidden}.cms-table-scroll{flex:1;min-height:0;overflow:auto}.cms-table{border-collapse:collapse;min-width:-moz-max-content;min-width:max-content;text-align:left;width:100%}.cms-table td,.cms-table th{white-space:nowrap}.cms-table thead{background:var(--cms-surface);position:sticky;top:0;z-index:10}.cms-table th{background:var(--cms-surface);box-shadow:inset 0 -1px 0 var(--cms-line);color:var(--ui-text-muted);font-family:var(--cms-font-mono);font-size:.6875rem;font-weight:400;letter-spacing:.08em;line-height:1.4;padding:.75rem 1.25rem;text-transform:uppercase;transition:background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-table td{color:var(--ui-text);font-size:.875rem;padding:.875rem 1.25rem}.cms-table tbody tr{border-bottom:1px solid var(--cms-line);transition:background-color var(--cms-duration) var(--cms-ease)}.cms-table tbody tr:last-child{border-bottom:0}.cms-table tbody tr:hover{background:var(--cms-field)}.cms-table-cell-actions{display:flex;justify-content:flex-end}.cms-table th[draggable=true]{cursor:grab;-webkit-user-select:none;-moz-user-select:none;user-select:none}.cms-table th[draggable=true]:active{cursor:grabbing}.cms-table th.is-dragging{opacity:.4}.cms-table th.is-drop-target{background:var(--cms-field);box-shadow:inset 0 -1px 0 var(--cms-line),inset 0 0 0 2px color-mix(in oklab,var(--cms-fern) 55%,transparent)}.cms-th-label{align-items:center;display:inline-flex;gap:.35rem}.cms-table th.is-sortable{cursor:pointer}.cms-table th.is-sortable:hover,.cms-table th.is-sorted{color:var(--ui-text-highlighted)}.cms-table th.is-sorted{background:var(--cms-field)}.cms-th-sort{flex:none;opacity:0;transition:opacity var(--cms-duration) var(--cms-ease)}.cms-table th.is-sortable:hover .cms-th-sort,.cms-table th.is-sorted .cms-th-sort{opacity:1}.cms-alert{border:1px solid transparent;border-radius:var(--cms-radius-md);display:flex;flex-direction:column;font-size:.875rem;gap:.125rem;padding:.75rem 1rem}.cms-alert-title{font-weight:550}.cms-alert-error{background:color-mix(in oklab,var(--ui-error) 14%,transparent);border-color:color-mix(in oklab,var(--ui-error) 26%,transparent);color:var(--ui-error)}.cms-alert-success{background:color-mix(in oklab,var(--ui-success) 14%,transparent);border-color:color-mix(in oklab,var(--ui-success) 26%,transparent);color:var(--ui-success)}.cms-alert-warning{background:color-mix(in oklab,var(--ui-warning) 14%,transparent);border-color:color-mix(in oklab,var(--ui-warning) 26%,transparent);color:var(--ui-warning)}.cms-alert-neutral{background:var(--cms-field);border-color:var(--cms-line);color:var(--ui-text-toned)}.cms-alert-neutral .cms-alert-title{color:var(--ui-text-highlighted)}.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(--cms-elevated);border:1px solid var(--cms-line);border-radius:var(--cms-radius-lg);box-shadow:var(--cms-shadow-overlay);padding:.75rem .875rem}.cms-toast-row{align-items:flex-start;display:flex;gap:.625rem}.cms-toast-icon{color:var(--ui-text-muted);flex-shrink:0;margin-top:.0625rem}.cms-toast.is-success .cms-toast-icon{color:var(--ui-success)}.cms-toast.is-error .cms-toast-icon{color:var(--ui-error)}.cms-toast-title{color:var(--ui-text-highlighted);font-size:.875rem;font-weight:500}.cms-toast-description{color:var(--ui-text-muted);font-size:.875rem;margin-top:.125rem}.cms-toast-dismiss{color:var(--ui-text-dimmed);flex-shrink:0;transition:color var(--cms-duration) var(--cms-ease)}.cms-toast-dismiss:hover{color:var(--ui-text-highlighted)}.cms-pagination{align-items:center;display:flex;gap:.25rem;justify-content:center}.cms-page-btn{border-radius:var(--cms-radius-sm);color:var(--ui-text-toned);font-size:.875rem;height:2rem;min-width:2rem;padding:0 .5rem;transition:background-color var(--cms-duration) var(--cms-ease),color var(--cms-duration) var(--cms-ease)}.cms-page-btn:hover:not(:disabled){background:var(--cms-field);color:var(--ui-text-highlighted)}.cms-page-btn.is-active{background:var(--cms-field);color:var(--cms-fern);font-weight:600}.cms-page-btn:disabled{opacity:.4}
|
|
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { useDb } from "#cms-db";
|
|
5
5
|
import { pageRoutes } from "../../shared/index.js";
|
|
6
6
|
import { decodeRows, getRegistryEntry, idColumn, tableColumns } from "../utils/registry.js";
|
|
7
|
+
import { readPages } from "../utils/page-storage.js";
|
|
7
8
|
import { attachManyToMany, relationTitles } from "../utils/relations.js";
|
|
8
9
|
import { requireAdmin } from "../utils/require-admin.js";
|
|
9
10
|
const querySchema = z.object({
|
|
@@ -31,7 +32,7 @@ export default defineEventHandler(async (event) => {
|
|
|
31
32
|
querySchema.parse
|
|
32
33
|
);
|
|
33
34
|
if (entry.kind === "page") {
|
|
34
|
-
const rows = await
|
|
35
|
+
const rows = await readPages(name, entry, table);
|
|
35
36
|
decodeRows(entry, rows);
|
|
36
37
|
const saved = new Map(rows.map((row) => [row.id, row]));
|
|
37
38
|
const items2 = pageRoutes(entry).map((route) => {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
parseId,
|
|
9
9
|
requirePageRoute
|
|
10
10
|
} from "../utils/registry.js";
|
|
11
|
+
import { readPages } from "../utils/page-storage.js";
|
|
11
12
|
import { attachManyToMany } from "../utils/relations.js";
|
|
12
13
|
import { requireAdmin } from "../utils/require-admin.js";
|
|
13
14
|
export default defineEventHandler(async (event) => {
|
|
@@ -17,13 +18,14 @@ export default defineEventHandler(async (event) => {
|
|
|
17
18
|
throw createError({ statusCode: 404, statusMessage: "Single objects have no items" });
|
|
18
19
|
}
|
|
19
20
|
const id = parseId(event);
|
|
20
|
-
const db = useDb();
|
|
21
|
-
const rows = await db.select().from(table).where(eq(idColumn(table), id)).limit(1);
|
|
22
21
|
if (entry.kind === "page") {
|
|
23
22
|
const route = requirePageRoute(entry, id);
|
|
24
|
-
|
|
25
|
-
return
|
|
23
|
+
const [page] = await readPages(name, entry, table, { id });
|
|
24
|
+
if (!page) return { id: route.key, path: route.path };
|
|
25
|
+
return decodeRows(entry, [page])[0];
|
|
26
26
|
}
|
|
27
|
+
const db = useDb();
|
|
28
|
+
const rows = await db.select().from(table).where(eq(idColumn(table), id)).limit(1);
|
|
27
29
|
if (!rows[0]) throw createError({ statusCode: 404, statusMessage: "Row not found" });
|
|
28
30
|
const [attached] = await attachManyToMany(db, name, entry, [rows[0]]);
|
|
29
31
|
return decodeRows(entry, [attached])[0];
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
requirePageRoute,
|
|
12
12
|
withUpdatedAt
|
|
13
13
|
} from "../utils/registry.js";
|
|
14
|
+
import { pageFields } from "../../shared/index.js";
|
|
15
|
+
import { assertMediaExists } from "../utils/media-check.js";
|
|
16
|
+
import { writePage } from "../utils/page-storage.js";
|
|
14
17
|
import {
|
|
15
18
|
assertRelationTargets,
|
|
16
19
|
attachManyToMany,
|
|
@@ -32,13 +35,11 @@ export default defineEventHandler(async (event) => {
|
|
|
32
35
|
if (entry.kind === "page") {
|
|
33
36
|
const route = requirePageRoute(entry, id);
|
|
34
37
|
const body2 = await readValidatedBody(event, buildValidator(entry, route.path).parse);
|
|
38
|
+
await assertMediaExists(pageFields(entry, route.path), body2);
|
|
35
39
|
const values2 = encodeColumnValues(entry, body2);
|
|
36
40
|
const set2 = withUpdatedAt(table, values2);
|
|
37
41
|
return mapConstraintErrors(
|
|
38
|
-
() =>
|
|
39
|
-
const [row] = await db.insert(table).values({ id: route.key, path: route.path, ...set2 }).onConflictDoUpdate({ target: idColumn(table), set: set2 }).returning();
|
|
40
|
-
return decodeRows(entry, [row])[0];
|
|
41
|
-
})
|
|
42
|
+
async () => decodeRows(entry, [await writePage(name, entry, table, route, set2)])[0]
|
|
42
43
|
);
|
|
43
44
|
}
|
|
44
45
|
const body = await readValidatedBody(event, buildValidator(entry).parse);
|
|
@@ -3,6 +3,7 @@ import { defineEventHandler } from "h3";
|
|
|
3
3
|
import { useRuntimeConfig } from "#imports";
|
|
4
4
|
import { buildCmsSchema, createGraphqlContext } from "../utils/graphql.js";
|
|
5
5
|
import { createBreadthRule, createDepthRule, createIntrospectionRule } from "../utils/graphql-depth.js";
|
|
6
|
+
import { CMS_GRAPHQL_BATCH_LIMIT } from "../../shared/index.js";
|
|
6
7
|
function buildYoga(options) {
|
|
7
8
|
return createYoga({
|
|
8
9
|
schema: buildCmsSchema(),
|
|
@@ -10,6 +11,7 @@ function buildYoga(options) {
|
|
|
10
11
|
landingPage: false,
|
|
11
12
|
graphiql: options.graphiql,
|
|
12
13
|
cors: false,
|
|
14
|
+
batching: { limit: CMS_GRAPHQL_BATCH_LIMIT },
|
|
13
15
|
context: () => createGraphqlContext(),
|
|
14
16
|
plugins: [
|
|
15
17
|
{
|
|
@@ -7,5 +7,7 @@ export declare function useDb(): import("drizzle-orm/d1").DrizzleD1Database<Reco
|
|
|
7
7
|
};
|
|
8
8
|
type Db = ReturnType<typeof useDb>;
|
|
9
9
|
export type CmsDb = Db;
|
|
10
|
+
export declare const cmsDialect: 'sqlite' | 'postgres';
|
|
10
11
|
export declare function withTransaction<T>(fn: (db: CmsDb) => Promise<T>): Promise<T>;
|
|
12
|
+
export declare function runBatch(build: (db: CmsDb) => PromiseLike<unknown>[]): Promise<unknown[]>;
|
|
11
13
|
export {};
|
|
@@ -21,6 +21,11 @@ export function useDb() {
|
|
|
21
21
|
if (!_db) _db = drizzle(useD1Binding());
|
|
22
22
|
return _db;
|
|
23
23
|
}
|
|
24
|
+
export const cmsDialect = "sqlite";
|
|
24
25
|
export function withTransaction(fn) {
|
|
25
26
|
return fn(useDb());
|
|
26
27
|
}
|
|
28
|
+
export function runBatch(build) {
|
|
29
|
+
const db = useDb();
|
|
30
|
+
return db.batch(build(db));
|
|
31
|
+
}
|
|
@@ -3,5 +3,7 @@ export declare function useDb(): import("drizzle-orm/libsql").LibSQLDatabase<Rec
|
|
|
3
3
|
};
|
|
4
4
|
type Db = ReturnType<typeof useDb>;
|
|
5
5
|
export type CmsDb = Db | Parameters<Parameters<Db['transaction']>[0]>[0];
|
|
6
|
+
export declare const cmsDialect: 'sqlite' | 'postgres';
|
|
6
7
|
export declare function withTransaction<T>(fn: (db: CmsDb) => Promise<T>): Promise<T>;
|
|
8
|
+
export declare function runBatch(build: (db: CmsDb) => PromiseLike<unknown>[]): Promise<unknown[]>;
|
|
7
9
|
export {};
|
|
@@ -20,6 +20,11 @@ export function useDb() {
|
|
|
20
20
|
}
|
|
21
21
|
return _db;
|
|
22
22
|
}
|
|
23
|
+
export const cmsDialect = "sqlite";
|
|
23
24
|
export function withTransaction(fn) {
|
|
24
25
|
return useDb().transaction((tx) => fn(tx));
|
|
25
26
|
}
|
|
27
|
+
export function runBatch(build) {
|
|
28
|
+
const db = useDb();
|
|
29
|
+
return db.batch(build(db));
|
|
30
|
+
}
|
|
@@ -3,5 +3,7 @@ export declare function useDb(): import("drizzle-orm/node-postgres").NodePgDatab
|
|
|
3
3
|
};
|
|
4
4
|
type Db = ReturnType<typeof useDb>;
|
|
5
5
|
export type CmsDb = Db | Parameters<Parameters<Db['transaction']>[0]>[0];
|
|
6
|
+
export declare const cmsDialect: 'sqlite' | 'postgres';
|
|
6
7
|
export declare function withTransaction<T>(fn: (db: CmsDb) => Promise<T>): Promise<T>;
|
|
8
|
+
export declare function runBatch(build: (db: CmsDb) => PromiseLike<unknown>[]): Promise<unknown[]>;
|
|
7
9
|
export {};
|
|
@@ -16,6 +16,14 @@ export function useDb() {
|
|
|
16
16
|
}
|
|
17
17
|
return _db;
|
|
18
18
|
}
|
|
19
|
+
export const cmsDialect = "postgres";
|
|
19
20
|
export function withTransaction(fn) {
|
|
20
21
|
return useDb().transaction((tx) => fn(tx));
|
|
21
22
|
}
|
|
23
|
+
export function runBatch(build) {
|
|
24
|
+
return withTransaction(async (db) => {
|
|
25
|
+
const results = [];
|
|
26
|
+
for (const statement of build(db)) results.push(await statement);
|
|
27
|
+
return results;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -3,4 +3,6 @@ export declare function useDb(): import("drizzle-orm/better-sqlite3").BetterSQLi
|
|
|
3
3
|
$client: Database.Database;
|
|
4
4
|
};
|
|
5
5
|
export type CmsDb = ReturnType<typeof useDb>;
|
|
6
|
+
export declare const cmsDialect: 'sqlite' | 'postgres';
|
|
6
7
|
export declare function withTransaction<T>(fn: (db: CmsDb) => Promise<T>): Promise<T>;
|
|
8
|
+
export declare function runBatch(build: (db: CmsDb) => PromiseLike<unknown>[]): Promise<unknown[]>;
|
|
@@ -16,6 +16,7 @@ export function useDb() {
|
|
|
16
16
|
}
|
|
17
17
|
return _db;
|
|
18
18
|
}
|
|
19
|
+
export const cmsDialect = "sqlite";
|
|
19
20
|
let txQueue = Promise.resolve();
|
|
20
21
|
export function withTransaction(fn) {
|
|
21
22
|
const db = useDb();
|
|
@@ -34,3 +35,10 @@ export function withTransaction(fn) {
|
|
|
34
35
|
});
|
|
35
36
|
return run;
|
|
36
37
|
}
|
|
38
|
+
export function runBatch(build) {
|
|
39
|
+
return withTransaction(async (db) => {
|
|
40
|
+
const results = [];
|
|
41
|
+
for (const statement of build(db)) results.push(await statement);
|
|
42
|
+
return results;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
typeName
|
|
39
39
|
} from "../../shared/graphql-sdl.js";
|
|
40
40
|
import { useMediaIndex } from "./media-index.js";
|
|
41
|
+
import { readPages } from "./page-storage.js";
|
|
41
42
|
import { getContentI18n, resolveTable, tableColumns } from "./registry.js";
|
|
42
43
|
const MAX_LIMIT = 100;
|
|
43
44
|
const DEFAULT_LIMIT = 50;
|
|
@@ -322,14 +323,12 @@ export function buildCmsSchema() {
|
|
|
322
323
|
if (entry.kind === "page") {
|
|
323
324
|
queryResolvers[name] = async (_, args) => {
|
|
324
325
|
const locale = resolveLocaleArg(args.locale);
|
|
325
|
-
const
|
|
326
|
-
const rows = await useDb().select().from(table).orderBy(asc(tableColumns(table).path));
|
|
326
|
+
const rows = await readPages(name, entry, tableFor(name), { orderByPath: true });
|
|
327
327
|
return rows.map((row) => localizeRow(entry, row, locale));
|
|
328
328
|
};
|
|
329
329
|
queryResolvers[`${name}ByPath`] = async (_, args) => {
|
|
330
330
|
const locale = resolveLocaleArg(args.locale);
|
|
331
|
-
const
|
|
332
|
-
const [row] = await useDb().select().from(table).where(eq(tableColumns(table).path, args.path)).limit(1);
|
|
331
|
+
const [row] = await readPages(name, entry, tableFor(name), { path: args.path });
|
|
333
332
|
return row ? localizeRow(entry, row, locale) : null;
|
|
334
333
|
};
|
|
335
334
|
continue;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useDb } from "#cms-db";
|
|
2
|
+
import { cms_media } from "#cms-tables";
|
|
3
|
+
import { useRuntimeConfig } from "#imports";
|
|
4
|
+
import { useMediaIndex } from "./media-index.js";
|
|
5
|
+
import { assertKnownMedia, mediaKeysInTable, mediaReferences } from "./media-references.js";
|
|
6
|
+
async function mediaLookup() {
|
|
7
|
+
const { media } = useRuntimeConfig().cms;
|
|
8
|
+
if (media.storage !== "local") {
|
|
9
|
+
return (keys) => mediaKeysInTable(useDb(), cms_media, keys);
|
|
10
|
+
}
|
|
11
|
+
const index = await useMediaIndex();
|
|
12
|
+
if (index.source.kind === "none") return null;
|
|
13
|
+
return async (keys) => new Set(keys.filter((key) => index.get(key)));
|
|
14
|
+
}
|
|
15
|
+
export async function assertMediaExists(fields, values) {
|
|
16
|
+
const references = mediaReferences(fields, values);
|
|
17
|
+
if (!references.length) return;
|
|
18
|
+
const lookup = await mediaLookup();
|
|
19
|
+
if (lookup) await assertKnownMedia(references, lookup);
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BaseSQLiteDatabase, SQLiteTable } from 'drizzle-orm/sqlite-core';
|
|
2
|
+
import type { FieldConfig } from '../../shared/index.js';
|
|
3
|
+
export interface MediaReference {
|
|
4
|
+
field: string;
|
|
5
|
+
key: string;
|
|
6
|
+
}
|
|
7
|
+
export type MediaLookup = (keys: string[]) => Promise<Set<string>>;
|
|
8
|
+
export declare function mediaReferences(fields: Record<string, FieldConfig>, values: Record<string, unknown>): MediaReference[];
|
|
9
|
+
export declare function assertKnownMedia(references: MediaReference[], lookup: MediaLookup): Promise<void>;
|
|
10
|
+
export declare function mediaKeysInTable(db: Pick<BaseSQLiteDatabase<'sync' | 'async', unknown>, 'select'>, table: SQLiteTable, keys: string[]): Promise<Set<string>>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { inArray } from "drizzle-orm";
|
|
2
|
+
import { createError } from "h3";
|
|
3
|
+
const LOOKUP_CHUNK = 90;
|
|
4
|
+
function mediaKeysOf(value) {
|
|
5
|
+
if (typeof value === "string") return value ? [value] : [];
|
|
6
|
+
if (!value || typeof value !== "object") return [];
|
|
7
|
+
return Object.values(value).filter(
|
|
8
|
+
(key) => typeof key === "string" && key !== ""
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
export function mediaReferences(fields, values) {
|
|
12
|
+
const references = [];
|
|
13
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
14
|
+
const value = values[key];
|
|
15
|
+
if (field.type === "media") {
|
|
16
|
+
for (const mediaKey of mediaKeysOf(value)) references.push({ field: key, key: mediaKey });
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (field.type !== "blocks" || !Array.isArray(value)) continue;
|
|
20
|
+
value.forEach((item, index) => {
|
|
21
|
+
const block = item ? field.blocks?.[String(item.type)] : void 0;
|
|
22
|
+
for (const [blockFieldKey, blockField] of Object.entries(block?.fields ?? {})) {
|
|
23
|
+
if (blockField.type !== "media") continue;
|
|
24
|
+
for (const mediaKey of mediaKeysOf(item?.[blockFieldKey])) {
|
|
25
|
+
references.push({ field: `${key}[${index}].${blockFieldKey}`, key: mediaKey });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return references;
|
|
31
|
+
}
|
|
32
|
+
export async function assertKnownMedia(references, lookup) {
|
|
33
|
+
if (!references.length) return;
|
|
34
|
+
const known = await lookup([...new Set(references.map((reference) => reference.key))]);
|
|
35
|
+
const missing = references.filter((reference) => !known.has(reference.key));
|
|
36
|
+
if (!missing.length) return;
|
|
37
|
+
throw createError({
|
|
38
|
+
statusCode: 400,
|
|
39
|
+
statusMessage: `Media not found in the library: ${missing.map((reference) => `'${reference.key}' (${reference.field})`).join(", ")}`
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export async function mediaKeysInTable(db, table, keys) {
|
|
43
|
+
const column = table.key;
|
|
44
|
+
const found = /* @__PURE__ */ new Set();
|
|
45
|
+
for (let index = 0; index < keys.length; index += LOOKUP_CHUNK) {
|
|
46
|
+
const rows = await db.select({ key: column }).from(table).where(inArray(column, keys.slice(index, index + LOOKUP_CHUNK)));
|
|
47
|
+
for (const row of rows) found.add(row.key);
|
|
48
|
+
}
|
|
49
|
+
return found;
|
|
50
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { SQL } from 'drizzle-orm';
|
|
2
|
+
import type { BaseSQLiteDatabase, SQLiteTable } from 'drizzle-orm/sqlite-core';
|
|
3
|
+
import type { CmsEntry, CmsPageRoute } from '../../shared/index.js';
|
|
4
|
+
export type PageDialect = 'sqlite' | 'postgres';
|
|
5
|
+
export type PageDb = Pick<BaseSQLiteDatabase<'sync' | 'async', unknown>, 'select' | 'insert' | 'delete'>;
|
|
6
|
+
export interface PageTables {
|
|
7
|
+
page: SQLiteTable;
|
|
8
|
+
fields: SQLiteTable;
|
|
9
|
+
media: SQLiteTable;
|
|
10
|
+
}
|
|
11
|
+
export interface PageFieldRow {
|
|
12
|
+
pageId: string;
|
|
13
|
+
key: string;
|
|
14
|
+
position: number;
|
|
15
|
+
value: string;
|
|
16
|
+
}
|
|
17
|
+
export interface PageMediaRow {
|
|
18
|
+
pageId: string;
|
|
19
|
+
key: string;
|
|
20
|
+
position: number;
|
|
21
|
+
mediaKey: string;
|
|
22
|
+
}
|
|
23
|
+
export interface EncodedPage {
|
|
24
|
+
columns: Record<string, unknown>;
|
|
25
|
+
fields: PageFieldRow[];
|
|
26
|
+
media: PageMediaRow[];
|
|
27
|
+
fieldKeys: string[];
|
|
28
|
+
mediaKeys: string[];
|
|
29
|
+
}
|
|
30
|
+
type Row = Record<string, unknown>;
|
|
31
|
+
export declare const PAGE_BLOCK_TYPE_KEY = "_type";
|
|
32
|
+
export declare function pageBlockKey(fieldKey: string, blockFieldKey: string): string;
|
|
33
|
+
export declare function encodePageRows(entry: CmsEntry, pageId: string, values: Row): EncodedPage;
|
|
34
|
+
export declare function decodePageRow(entry: CmsEntry, row: Row): Row;
|
|
35
|
+
export declare function decodePageRows(entry: CmsEntry, rows: Row[]): Row[];
|
|
36
|
+
export declare function pageSelection(dialect: PageDialect, tables: PageTables): {
|
|
37
|
+
$fields: SQL<unknown>;
|
|
38
|
+
$media: SQL<unknown>;
|
|
39
|
+
};
|
|
40
|
+
export declare function selectPages(db: PageDb, dialect: PageDialect, tables: PageTables): import("drizzle-orm/sqlite-core").SQLiteSelectBase<string, "async" | "sync", unknown, {
|
|
41
|
+
$fields: SQL<unknown>;
|
|
42
|
+
$media: SQL<unknown>;
|
|
43
|
+
}, "partial", Record<string, "not-null">, true, never, {
|
|
44
|
+
$fields: unknown;
|
|
45
|
+
$media: unknown;
|
|
46
|
+
}[], {
|
|
47
|
+
$fields: import("drizzle-orm").DrizzleTypeError<"You cannot reference this field without assigning it an alias first - use `.as(<alias>)`">;
|
|
48
|
+
$media: import("drizzle-orm").DrizzleTypeError<"You cannot reference this field without assigning it an alias first - use `.as(<alias>)`">;
|
|
49
|
+
}>;
|
|
50
|
+
export declare function pageWriteStatements(db: PageDb, dialect: PageDialect, tables: PageTables, entry: CmsEntry, route: Pick<CmsPageRoute, 'key' | 'path'>, values: Row): PromiseLike<unknown>[];
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { and, eq, getTableColumns, getTableName, inArray, sql } from "drizzle-orm";
|
|
2
|
+
import {
|
|
3
|
+
PAGE_PATH_FIELD,
|
|
4
|
+
isMultiSelect,
|
|
5
|
+
isTranslatableField,
|
|
6
|
+
isTranslatableMediaField,
|
|
7
|
+
pageAllFields,
|
|
8
|
+
pageRowFields
|
|
9
|
+
} from "../../shared/index.js";
|
|
10
|
+
export const PAGE_BLOCK_TYPE_KEY = "_type";
|
|
11
|
+
const FIELDS_SELECTION = "$fields";
|
|
12
|
+
const MEDIA_SELECTION = "$media";
|
|
13
|
+
const INSERT_CHUNK = 20;
|
|
14
|
+
const KEY_CHUNK = 90;
|
|
15
|
+
const PLAIN_STRING_TYPES = /* @__PURE__ */ new Set([
|
|
16
|
+
"text",
|
|
17
|
+
"richtext",
|
|
18
|
+
"email",
|
|
19
|
+
"slug",
|
|
20
|
+
"date",
|
|
21
|
+
"select",
|
|
22
|
+
"relation"
|
|
23
|
+
]);
|
|
24
|
+
function columnsOf(table) {
|
|
25
|
+
return table;
|
|
26
|
+
}
|
|
27
|
+
function isPlainString(field) {
|
|
28
|
+
return !!field && PLAIN_STRING_TYPES.has(field.type) && !isTranslatableField(field) && !isMultiSelect(field);
|
|
29
|
+
}
|
|
30
|
+
function parseJson(raw) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(raw);
|
|
33
|
+
} catch {
|
|
34
|
+
return raw;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function encodeValue(field, value) {
|
|
38
|
+
return isPlainString(field) ? String(value) : JSON.stringify(value);
|
|
39
|
+
}
|
|
40
|
+
function decodeValue(field, raw) {
|
|
41
|
+
return isPlainString(field) ? raw : parseJson(raw);
|
|
42
|
+
}
|
|
43
|
+
function encodeMedia(value) {
|
|
44
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
45
|
+
}
|
|
46
|
+
function decodeBlockMedia(field, raw) {
|
|
47
|
+
return isTranslatableMediaField(field) && raw.startsWith("{") ? parseJson(raw) : raw;
|
|
48
|
+
}
|
|
49
|
+
export function pageBlockKey(fieldKey, blockFieldKey) {
|
|
50
|
+
return `${fieldKey}.${blockFieldKey}`;
|
|
51
|
+
}
|
|
52
|
+
function blockOwnedKeys(key, field) {
|
|
53
|
+
const fields = /* @__PURE__ */ new Set([pageBlockKey(key, PAGE_BLOCK_TYPE_KEY)]);
|
|
54
|
+
const media = /* @__PURE__ */ new Set();
|
|
55
|
+
for (const block of Object.values(field.blocks ?? {})) {
|
|
56
|
+
for (const [blockFieldKey, blockField] of Object.entries(block.fields)) {
|
|
57
|
+
const blockKey = pageBlockKey(key, blockFieldKey);
|
|
58
|
+
if (blockField.type === "media") media.add(blockKey);
|
|
59
|
+
else fields.add(blockKey);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { fields: [...fields], media: [...media] };
|
|
63
|
+
}
|
|
64
|
+
export function encodePageRows(entry, pageId, values) {
|
|
65
|
+
const rowFields = pageRowFields(entry);
|
|
66
|
+
const encoded = { columns: {}, fields: [], media: [], fieldKeys: [], mediaKeys: [] };
|
|
67
|
+
const pushField = (key, position, value) => encoded.fields.push({ pageId, key, position, value });
|
|
68
|
+
const pushMedia = (key, position, value) => encoded.media.push({ pageId, key, position, mediaKey: encodeMedia(value) });
|
|
69
|
+
for (const [key, value] of Object.entries(values)) {
|
|
70
|
+
const field = rowFields[key];
|
|
71
|
+
if (!field) {
|
|
72
|
+
encoded.columns[key] = value;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (field.type === "blocks") {
|
|
76
|
+
const owned = blockOwnedKeys(key, field);
|
|
77
|
+
encoded.fieldKeys.push(...owned.fields);
|
|
78
|
+
encoded.mediaKeys.push(...owned.media);
|
|
79
|
+
if (!Array.isArray(value)) continue;
|
|
80
|
+
value.forEach((item, position) => {
|
|
81
|
+
const type = String(item.type);
|
|
82
|
+
const block = field.blocks?.[type];
|
|
83
|
+
pushField(pageBlockKey(key, PAGE_BLOCK_TYPE_KEY), position, type);
|
|
84
|
+
for (const [blockFieldKey, blockValue] of Object.entries(item)) {
|
|
85
|
+
if (blockFieldKey === "type" || blockValue == null) continue;
|
|
86
|
+
const blockField = block?.fields[blockFieldKey];
|
|
87
|
+
const blockKey = pageBlockKey(key, blockFieldKey);
|
|
88
|
+
if (blockField?.type === "media") pushMedia(blockKey, position, blockValue);
|
|
89
|
+
else pushField(blockKey, position, encodeValue(blockField, blockValue));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (field.type === "media") {
|
|
95
|
+
encoded.mediaKeys.push(key);
|
|
96
|
+
if (value != null) pushMedia(key, 0, value);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
encoded.fieldKeys.push(key);
|
|
100
|
+
if (value != null) pushField(key, 0, encodeValue(field, value));
|
|
101
|
+
}
|
|
102
|
+
return encoded;
|
|
103
|
+
}
|
|
104
|
+
function parseChildren(value) {
|
|
105
|
+
if (value == null) return [];
|
|
106
|
+
const rows = typeof value === "string" ? parseJson(value) : value;
|
|
107
|
+
return Array.isArray(rows) ? rows : [];
|
|
108
|
+
}
|
|
109
|
+
function indexChildren(rows) {
|
|
110
|
+
const index = /* @__PURE__ */ new Map();
|
|
111
|
+
for (const [key, position, value] of rows) {
|
|
112
|
+
if (key == null || value == null) continue;
|
|
113
|
+
let positions = index.get(key);
|
|
114
|
+
if (!positions) {
|
|
115
|
+
positions = /* @__PURE__ */ new Map();
|
|
116
|
+
index.set(key, positions);
|
|
117
|
+
}
|
|
118
|
+
positions.set(Number(position), String(value));
|
|
119
|
+
}
|
|
120
|
+
return index;
|
|
121
|
+
}
|
|
122
|
+
function decodeBlocks(key, field, fields, media) {
|
|
123
|
+
const types = fields.get(pageBlockKey(key, PAGE_BLOCK_TYPE_KEY));
|
|
124
|
+
if (!types?.size) return null;
|
|
125
|
+
return [...types.entries()].sort(([a], [b]) => a - b).map(([position, type]) => {
|
|
126
|
+
const item = { type };
|
|
127
|
+
for (const [blockFieldKey, blockField] of Object.entries(
|
|
128
|
+
field.blocks?.[type]?.fields ?? {}
|
|
129
|
+
)) {
|
|
130
|
+
const blockKey = pageBlockKey(key, blockFieldKey);
|
|
131
|
+
if (blockField.type === "media") {
|
|
132
|
+
const raw2 = media.get(blockKey)?.get(position);
|
|
133
|
+
item[blockFieldKey] = raw2 == null ? null : decodeBlockMedia(blockField, raw2);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const raw = fields.get(blockKey)?.get(position);
|
|
137
|
+
item[blockFieldKey] = raw == null ? null : decodeValue(blockField, raw);
|
|
138
|
+
}
|
|
139
|
+
return item;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function decodeRowField(key, field, fields, media) {
|
|
143
|
+
if (field.type === "blocks") return decodeBlocks(key, field, fields, media);
|
|
144
|
+
if (field.type === "media") return media.get(key)?.get(0) ?? null;
|
|
145
|
+
const raw = fields.get(key)?.get(0);
|
|
146
|
+
return raw == null ? null : decodeValue(field, raw);
|
|
147
|
+
}
|
|
148
|
+
export function decodePageRow(entry, row) {
|
|
149
|
+
const rowFields = pageRowFields(entry);
|
|
150
|
+
const fields = indexChildren(parseChildren(row[FIELDS_SELECTION]));
|
|
151
|
+
const media = indexChildren(parseChildren(row[MEDIA_SELECTION]));
|
|
152
|
+
const result = { id: row.id, [PAGE_PATH_FIELD]: row[PAGE_PATH_FIELD] };
|
|
153
|
+
for (const [key, field] of Object.entries(pageAllFields(entry))) {
|
|
154
|
+
result[key] = Object.hasOwn(rowFields, key) ? decodeRowField(key, field, fields, media) : row[key] ?? null;
|
|
155
|
+
}
|
|
156
|
+
for (const [key, value] of Object.entries(row)) {
|
|
157
|
+
if (key === FIELDS_SELECTION || key === MEDIA_SELECTION || Object.hasOwn(result, key))
|
|
158
|
+
continue;
|
|
159
|
+
result[key] = value;
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
export function decodePageRows(entry, rows) {
|
|
164
|
+
return rows.map((row) => decodePageRow(entry, row));
|
|
165
|
+
}
|
|
166
|
+
function qualified(table, column) {
|
|
167
|
+
return sql`${sql.identifier(getTableName(table))}.${sql.identifier(column.name)}`;
|
|
168
|
+
}
|
|
169
|
+
function childAggregate(dialect, page, table, valueKey) {
|
|
170
|
+
const columns = columnsOf(table);
|
|
171
|
+
const owner = qualified(table, columns.pageId);
|
|
172
|
+
const key = qualified(table, columns.key);
|
|
173
|
+
const position = qualified(table, columns.position);
|
|
174
|
+
const value = qualified(table, columns[valueKey]);
|
|
175
|
+
const pageId = qualified(page, columnsOf(page).id);
|
|
176
|
+
const from = sql.identifier(getTableName(table));
|
|
177
|
+
return dialect === "postgres" ? sql`(select coalesce(json_agg(json_build_array(${key}, ${position}, ${value})), '[]'::json) from ${from} where ${owner} = ${pageId})` : sql`(select json_group_array(json_array(${key}, ${position}, ${value})) from ${from} where ${owner} = ${pageId})`;
|
|
178
|
+
}
|
|
179
|
+
export function pageSelection(dialect, tables) {
|
|
180
|
+
return {
|
|
181
|
+
...getTableColumns(tables.page),
|
|
182
|
+
[FIELDS_SELECTION]: childAggregate(dialect, tables.page, tables.fields, "value"),
|
|
183
|
+
[MEDIA_SELECTION]: childAggregate(dialect, tables.page, tables.media, "mediaKey")
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export function selectPages(db, dialect, tables) {
|
|
187
|
+
return db.select(pageSelection(dialect, tables)).from(tables.page).$dynamic();
|
|
188
|
+
}
|
|
189
|
+
function chunks(items, size) {
|
|
190
|
+
const out = [];
|
|
191
|
+
for (let index = 0; index < items.length; index += size) {
|
|
192
|
+
out.push(items.slice(index, index + size));
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
export function pageWriteStatements(db, dialect, tables, entry, route, values) {
|
|
197
|
+
const encoded = encodePageRows(entry, route.key, values);
|
|
198
|
+
const pageId = columnsOf(tables.page).id;
|
|
199
|
+
const fieldColumns = columnsOf(tables.fields);
|
|
200
|
+
const mediaColumns = columnsOf(tables.media);
|
|
201
|
+
const set = encoded.columns;
|
|
202
|
+
const statements = [
|
|
203
|
+
db.insert(tables.page).values({ ...set, id: route.key, [PAGE_PATH_FIELD]: route.path }).onConflictDoUpdate({ target: pageId, set })
|
|
204
|
+
];
|
|
205
|
+
for (const keys of chunks(encoded.fieldKeys, KEY_CHUNK)) {
|
|
206
|
+
statements.push(
|
|
207
|
+
db.delete(tables.fields).where(and(eq(fieldColumns.pageId, route.key), inArray(fieldColumns.key, keys)))
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
for (const keys of chunks(encoded.mediaKeys, KEY_CHUNK)) {
|
|
211
|
+
statements.push(
|
|
212
|
+
db.delete(tables.media).where(and(eq(mediaColumns.pageId, route.key), inArray(mediaColumns.key, keys)))
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
for (const rows of chunks(encoded.fields, INSERT_CHUNK)) {
|
|
216
|
+
statements.push(db.insert(tables.fields).values(rows));
|
|
217
|
+
}
|
|
218
|
+
for (const rows of chunks(encoded.media, INSERT_CHUNK)) {
|
|
219
|
+
statements.push(db.insert(tables.media).values(rows));
|
|
220
|
+
}
|
|
221
|
+
statements.push(selectPages(db, dialect, tables).where(eq(pageId, route.key)).limit(1));
|
|
222
|
+
return statements;
|
|
223
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SQLiteTable } from 'drizzle-orm/sqlite-core';
|
|
2
|
+
import type { CmsEntry, CmsPageRoute } from '../../shared/index.js';
|
|
3
|
+
type Row = Record<string, unknown>;
|
|
4
|
+
export interface PageReadOptions {
|
|
5
|
+
id?: string;
|
|
6
|
+
path?: string;
|
|
7
|
+
orderByPath?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function readPages(name: string, entry: CmsEntry, table: SQLiteTable, options?: PageReadOptions): Promise<Row[]>;
|
|
10
|
+
export declare function writePage(name: string, entry: CmsEntry, table: SQLiteTable, route: Pick<CmsPageRoute, 'key' | 'path'>, set: Row): Promise<Row>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { asc, eq } from "drizzle-orm";
|
|
2
|
+
import { createError } from "h3";
|
|
3
|
+
import { cmsDialect, runBatch, useDb } from "#cms-db";
|
|
4
|
+
import * as cmsTables from "#cms-tables";
|
|
5
|
+
import { PAGE_PATH_FIELD, pageFieldsTableName, pageMediaTableName } from "../../shared/index.js";
|
|
6
|
+
import { decodePageRows, pageWriteStatements, selectPages } from "./page-rows.js";
|
|
7
|
+
import { idColumn, tableColumns } from "./registry.js";
|
|
8
|
+
function childTable(name) {
|
|
9
|
+
const table = cmsTables[name];
|
|
10
|
+
if (!table) throw createError({ statusCode: 500, statusMessage: `No table: ${name}` });
|
|
11
|
+
return table;
|
|
12
|
+
}
|
|
13
|
+
function pageTables(name, table) {
|
|
14
|
+
return {
|
|
15
|
+
page: table,
|
|
16
|
+
fields: childTable(pageFieldsTableName(name)),
|
|
17
|
+
media: childTable(pageMediaTableName(name))
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function pageFilter(table, options) {
|
|
21
|
+
if (options.id !== void 0) return eq(idColumn(table), options.id);
|
|
22
|
+
if (options.path !== void 0) return eq(tableColumns(table)[PAGE_PATH_FIELD], options.path);
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
25
|
+
export async function readPages(name, entry, table, options = {}) {
|
|
26
|
+
const db = useDb();
|
|
27
|
+
const where = pageFilter(table, options);
|
|
28
|
+
const single = options.id !== void 0 || options.path !== void 0;
|
|
29
|
+
const rows = selectPages(db, cmsDialect, pageTables(name, table));
|
|
30
|
+
if (where) rows.where(where);
|
|
31
|
+
if (options.orderByPath) rows.orderBy(asc(tableColumns(table)[PAGE_PATH_FIELD]));
|
|
32
|
+
if (single) rows.limit(1);
|
|
33
|
+
const result = await rows;
|
|
34
|
+
return decodePageRows(entry, result);
|
|
35
|
+
}
|
|
36
|
+
export async function writePage(name, entry, table, route, set) {
|
|
37
|
+
const tables = pageTables(name, table);
|
|
38
|
+
const results = await runBatch(
|
|
39
|
+
(db) => pageWriteStatements(db, cmsDialect, tables, entry, route, set)
|
|
40
|
+
);
|
|
41
|
+
return decodePageRows(entry, results.at(-1))[0];
|
|
42
|
+
}
|
|
@@ -96,6 +96,7 @@ export declare function translatableBlockFieldKeys(block: BlockConfig): string[]
|
|
|
96
96
|
export declare function hasTranslatableBlockFields(field: FieldConfig): boolean;
|
|
97
97
|
export declare function localizeBlock(field: FieldConfig, item: unknown, locale: string, defaultLocale: string): unknown;
|
|
98
98
|
export declare function localizeBlocks(field: FieldConfig, value: unknown, locale: string, defaultLocale: string): unknown;
|
|
99
|
+
export declare const CMS_GRAPHQL_BATCH_LIMIT = 20;
|
|
99
100
|
export type CmsEntryKind = 'collection' | 'single' | 'page';
|
|
100
101
|
export interface CmsPageRoute {
|
|
101
102
|
path: string;
|
|
@@ -116,6 +117,7 @@ export interface CmsEntry {
|
|
|
116
117
|
labels?: Record<string, string>;
|
|
117
118
|
overrides?: Record<string, Record<string, FieldConfig | null>>;
|
|
118
119
|
pages?: CmsPageRoute[];
|
|
120
|
+
columns?: string[];
|
|
119
121
|
tabs?: CmsTab[];
|
|
120
122
|
table?: CmsTable;
|
|
121
123
|
}
|
|
@@ -134,6 +136,10 @@ export declare function pageRouteOf(entry: CmsEntry, key: string): CmsPageRoute
|
|
|
134
136
|
export declare function pageOverrideFields(entry: CmsEntry, path: string): Record<string, FieldConfig | null>;
|
|
135
137
|
export declare function pageFields(entry: CmsEntry, path: string): Record<string, FieldConfig>;
|
|
136
138
|
export declare function pageAllFields(entry: CmsEntry): Record<string, FieldConfig>;
|
|
139
|
+
export declare function pageColumnFields(entry: CmsEntry): Record<string, FieldConfig>;
|
|
140
|
+
export declare function pageRowFields(entry: CmsEntry): Record<string, FieldConfig>;
|
|
141
|
+
export declare function pageFieldsTableName(name: string): string;
|
|
142
|
+
export declare function pageMediaTableName(name: string): string;
|
|
137
143
|
type EntryLike = Pick<CmsEntry, 'fields'> & Partial<Pick<CmsEntry, 'kind' | 'overrides'>>;
|
|
138
144
|
export declare function entryFieldsFor(entry: EntryLike, path?: string): Record<string, FieldConfig>;
|
|
139
145
|
export declare function typeName(name: string): string;
|
|
@@ -228,6 +234,7 @@ export interface CmsPageInput extends CmsEntryInputBase {
|
|
|
228
234
|
order?: string[];
|
|
229
235
|
labels?: Record<string, string>;
|
|
230
236
|
overrides?: Record<string, Record<string, CmsFieldInput | null>>;
|
|
237
|
+
columns?: string[];
|
|
231
238
|
}
|
|
232
239
|
export type CmsEntryInput = CmsCollectionInput | CmsSingleInput | CmsPageInput;
|
|
233
240
|
export type CmsConfigInput = Record<string, CmsEntryInput>;
|
|
@@ -191,6 +191,7 @@ export function localizeBlocks(field, value, locale, defaultLocale) {
|
|
|
191
191
|
if (!Array.isArray(value)) return value;
|
|
192
192
|
return value.map((item) => localizeBlock(field, item, locale, defaultLocale));
|
|
193
193
|
}
|
|
194
|
+
export const CMS_GRAPHQL_BATCH_LIMIT = 20;
|
|
194
195
|
export function entryTabs(entry) {
|
|
195
196
|
return entry.tabs?.length ? entry.tabs : [];
|
|
196
197
|
}
|
|
@@ -258,6 +259,26 @@ export function pageAllFields(entry) {
|
|
|
258
259
|
}
|
|
259
260
|
return fields;
|
|
260
261
|
}
|
|
262
|
+
export function pageColumnFields(entry) {
|
|
263
|
+
const fields = {};
|
|
264
|
+
for (const key of entry.columns ?? []) {
|
|
265
|
+
const field = entry.fields[key];
|
|
266
|
+
if (field) fields[key] = field;
|
|
267
|
+
}
|
|
268
|
+
return fields;
|
|
269
|
+
}
|
|
270
|
+
export function pageRowFields(entry) {
|
|
271
|
+
const columns = new Set(entry.columns ?? []);
|
|
272
|
+
return Object.fromEntries(
|
|
273
|
+
Object.entries(pageAllFields(entry)).filter(([key]) => !columns.has(key))
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
export function pageFieldsTableName(name) {
|
|
277
|
+
return `${name}_fields`;
|
|
278
|
+
}
|
|
279
|
+
export function pageMediaTableName(name) {
|
|
280
|
+
return `${name}_media`;
|
|
281
|
+
}
|
|
261
282
|
export function entryFieldsFor(entry, path) {
|
|
262
283
|
if (entry.kind !== "page") return entry.fields;
|
|
263
284
|
return path ? pageFields(entry, path) : pageAllFields(entry);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xleddyl/nuxt-cms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.57",
|
|
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)",
|