@xleddyl/nuxt-cms 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +5 -4
  2. package/dist/module.d.mts +37 -30
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +123 -35
  5. package/dist/runtime/app/components/cms/Alert.d.vue.ts +1 -2
  6. package/dist/runtime/app/components/cms/Alert.vue +2 -3
  7. package/dist/runtime/app/components/cms/Alert.vue.d.ts +1 -2
  8. package/dist/runtime/app/components/cms/BlocksField.vue +2 -2
  9. package/dist/runtime/app/components/cms/Button.d.vue.ts +1 -1
  10. package/dist/runtime/app/components/cms/Button.vue.d.ts +1 -1
  11. package/dist/runtime/app/components/cms/ConfirmModal.vue +1 -1
  12. package/dist/runtime/app/components/cms/EntryDrawer.vue +1 -1
  13. package/dist/runtime/app/components/cms/MediaField.vue +2 -2
  14. package/dist/runtime/app/components/cms/MediaGallery.vue +3 -3
  15. package/dist/runtime/app/components/cms/MediaUpload.vue +9 -4
  16. package/dist/runtime/app/components/cms/Modal.d.vue.ts +4 -4
  17. package/dist/runtime/app/components/cms/Modal.vue +4 -4
  18. package/dist/runtime/app/components/cms/Modal.vue.d.ts +4 -4
  19. package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +0 -1
  20. package/dist/runtime/app/components/cms/PageHeader.vue +0 -4
  21. package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +0 -1
  22. package/dist/runtime/app/components/cms/RichTextField.vue +1 -1
  23. package/dist/runtime/app/components/cms/Toaster.vue +6 -0
  24. package/dist/runtime/app/components/cms/TranslatableField.vue +19 -8
  25. package/dist/runtime/app/composables/cms-runtime.d.ts +1 -0
  26. package/dist/runtime/app/layouts/cms-admin.vue +2 -2
  27. package/dist/runtime/app/pages/admin-collection.vue +12 -7
  28. package/dist/runtime/app/pages/admin-entry.vue +2 -2
  29. package/dist/runtime/app/pages/admin-login.vue +5 -5
  30. package/dist/runtime/assets/main.css +1 -1
  31. package/dist/runtime/server/api/collection.get.d.ts +1 -3
  32. package/dist/runtime/server/api/collection.get.js +3 -2
  33. package/dist/runtime/server/api/collection.post.js +3 -3
  34. package/dist/runtime/server/api/collection.put.js +13 -3
  35. package/dist/runtime/server/api/item.get.js +2 -2
  36. package/dist/runtime/server/api/item.put.js +7 -2
  37. package/dist/runtime/server/api/media-presign.post.js +3 -9
  38. package/dist/runtime/server/utils/graphql.js +13 -1
  39. package/dist/runtime/server/utils/media.d.ts +2 -0
  40. package/dist/runtime/server/utils/media.js +9 -1
  41. package/dist/runtime/server/utils/registry.d.ts +2 -0
  42. package/dist/runtime/server/utils/registry.js +7 -0
  43. package/dist/runtime/shared/index.d.ts +11 -1
  44. package/dist/runtime/shared/index.js +70 -1
  45. package/dist/runtime/shared/validation.js +1 -1
  46. package/dist/types.d.mts +1 -1
  47. package/package.json +4 -2
package/README.md CHANGED
@@ -20,10 +20,6 @@ Register the module and configure it under the `cms` key in `nuxt.config.ts`:
20
20
  export default defineNuxtConfig({
21
21
  modules: ['@xleddyl/nuxt-cms'],
22
22
  cms: {
23
- database: {
24
- driver: 'sqlite', // 'sqlite' | 'postgres' (with `url`) | 'libsql' (Turso/remote, with `url` + `authToken`)
25
- path: 'data/cms.db',
26
- },
27
23
  i18n: {
28
24
  locales: ['en', 'it'],
29
25
  defaultLocale: 'en',
@@ -32,6 +28,11 @@ export default defineNuxtConfig({
32
28
  })
33
29
  ```
34
30
 
31
+ `database` defaults to SQLite (`{ driver: 'sqlite', path: 'data/cms.db' }`) and can be omitted
32
+ entirely. Switch it to `{ driver: 'postgres', url: '...' }` or
33
+ `{ driver: 'libsql', url: '...', authToken: '...' }` (Turso/remote); see
34
+ [Configuration](docs/configuration.md) for every option.
35
+
35
36
  Then declare your content types in a `cms.config.ts` at the project root with `defineCmsConfig()`.
36
37
 
37
38
  ### Disabling the CMS
package/dist/module.d.mts CHANGED
@@ -1,41 +1,48 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
- import { MediaStorageMode } from '../dist/runtime/shared/index.js';
3
-
4
- type Dialect = 'sqlite' | 'postgres';
5
- type Driver = Dialect | 'libsql';
6
2
 
3
+ type ModuleOptionsDatabase = {
4
+ driver?: 'sqlite';
5
+ path?: string;
6
+ } | {
7
+ driver: 'postgres';
8
+ url?: string;
9
+ } | {
10
+ driver: 'libsql';
11
+ url?: string;
12
+ authToken?: string;
13
+ };
14
+ type ModuleOptionsMedia = {
15
+ storage?: 's3';
16
+ endpoint?: string;
17
+ region?: string;
18
+ bucket?: string;
19
+ accessKeyId?: string;
20
+ secretAccessKey?: string;
21
+ publicBaseUrl?: string;
22
+ presignExpiry?: number;
23
+ maxFileSize?: number;
24
+ } | {
25
+ storage: 'local';
26
+ publicBaseUrl: string;
27
+ };
7
28
  interface ModuleOptions {
8
29
  enabled?: boolean;
9
- configPath: string;
10
- admin: {
11
- email: string;
12
- password: string;
13
- };
14
- database: {
15
- driver?: Driver;
16
- path?: string;
17
- url?: string;
18
- authToken?: string;
19
- };
20
- media: {
21
- storage: MediaStorageMode;
22
- endpoint: string;
23
- region: string;
24
- bucket: string;
25
- publicBaseUrl: string;
26
- presignExpiry: number;
27
- accessKeyId: string;
28
- secretAccessKey: string;
30
+ configPath?: string;
31
+ admin?: {
32
+ email?: string;
33
+ password?: string;
29
34
  };
30
- i18n: {
31
- locales: string[];
32
- defaultLocale: string;
35
+ database?: ModuleOptionsDatabase;
36
+ media?: ModuleOptionsMedia;
37
+ i18n?: {
38
+ locales?: string[];
39
+ defaultLocale?: string;
33
40
  };
34
- graphql: {
35
- maxDepth: number;
41
+ graphql?: {
42
+ maxDepth?: number;
36
43
  };
37
44
  }
38
45
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
39
46
 
40
47
  export { _default as default };
41
- export type { ModuleOptions };
48
+ export type { ModuleOptions, ModuleOptionsDatabase, ModuleOptionsMedia };
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xleddyl/nuxt-cms",
3
3
  "configKey": "cms",
4
- "version": "0.1.27",
4
+ "version": "0.1.29",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -10,7 +10,7 @@ import { introspectionFromSchema, buildSchema } from 'graphql';
10
10
  import { minifyIntrospection, outputIntrospectionFile } from 'gql.tada/internal';
11
11
  import { createJiti } from 'jiti';
12
12
  import { typeName, blockTypeName, blockUnionName, renderGraphqlSdl } from '../dist/runtime/shared/graphql-sdl.js';
13
- import { isMultiSelect, isTranslatableField } from '../dist/runtime/shared/index.js';
13
+ import { isMultiSelect, isTranslatableField, isTranslatableMediaField, DEFAULT_MEDIA_MAX_FILE_SIZE } from '../dist/runtime/shared/index.js';
14
14
 
15
15
  const IDENTIFIER = /^[a-z_]\w*$/i;
16
16
  const RESERVED_ENTRY_KEYS = ["admin", "auth", "login", "media", "graphql", "cms_media"];
@@ -96,8 +96,10 @@ function validateConfig(config, i18n) {
96
96
  columnNames.add(column);
97
97
  }
98
98
  if (field.translatable) {
99
- if (field.type !== "text" && field.type !== "richtext")
100
- errors.push(`${fat}: translatable is only supported on text and richtext fields`);
99
+ if (field.type !== "text" && field.type !== "richtext" && field.type !== "media")
100
+ errors.push(
101
+ `${fat}: translatable is only supported on text, richtext and media fields`
102
+ );
101
103
  if (!locales.length)
102
104
  errors.push(`${fat}: translatable requires cms.i18n.locales in nuxt.config`);
103
105
  }
@@ -207,7 +209,7 @@ function columnExpr(key, field, dialect) {
207
209
  const col = snakeCase(key);
208
210
  let expr;
209
211
  if (isTranslatableField(field)) {
210
- expr = jsonExpr(col, dialect);
212
+ expr = isTranslatableMediaField(field) ? `text('${col}')` : jsonExpr(col, dialect);
211
213
  if (field.required) expr += ".notNull()";
212
214
  return ` ${key}: ${expr},`;
213
215
  }
@@ -304,7 +306,7 @@ function renderSchemaFile(config, dialect, resolveImport = (s) => s) {
304
306
  if (pg && fields.some((f) => f.type === "date")) core.add("date");
305
307
  if (pg && fields.some((f) => f.type === "boolean")) core.add("boolean");
306
308
  if (pg && fields.some(
307
- (f) => f.type === "json" || f.type === "blocks" || isTranslatableField(f) || isMultiSelect(f)
309
+ (f) => f.type === "json" || f.type === "blocks" || isTranslatableField(f) && !isTranslatableMediaField(f) || isMultiSelect(f)
308
310
  ))
309
311
  core.add("jsonb");
310
312
  if (pg) core.add("timestamp");
@@ -428,6 +430,64 @@ function resolveCmsEnabled(explicit, envValue) {
428
430
  return true;
429
431
  }
430
432
 
433
+ function resolveDatabaseOptions(database) {
434
+ if (database?.driver === "postgres") {
435
+ return { driver: "postgres", path: "data/cms.db", url: database.url ?? "", authToken: "" };
436
+ }
437
+ if (database?.driver === "libsql") {
438
+ return {
439
+ driver: "libsql",
440
+ path: "data/cms.db",
441
+ url: database.url ?? "",
442
+ authToken: database.authToken ?? ""
443
+ };
444
+ }
445
+ return { driver: "sqlite", path: database?.path ?? "data/cms.db", url: "", authToken: "" };
446
+ }
447
+ function resolveMediaOptions(media) {
448
+ if (media?.storage === "local") {
449
+ return {
450
+ storage: "local",
451
+ endpoint: "",
452
+ region: "auto",
453
+ bucket: "",
454
+ accessKeyId: "",
455
+ secretAccessKey: "",
456
+ presignExpiry: 600,
457
+ maxFileSize: DEFAULT_MEDIA_MAX_FILE_SIZE,
458
+ publicBaseUrl: media.publicBaseUrl
459
+ };
460
+ }
461
+ return {
462
+ storage: "s3",
463
+ endpoint: media?.endpoint ?? "",
464
+ region: media?.region ?? "auto",
465
+ bucket: media?.bucket ?? "",
466
+ accessKeyId: media?.accessKeyId ?? "",
467
+ secretAccessKey: media?.secretAccessKey ?? "",
468
+ presignExpiry: media?.presignExpiry ?? 600,
469
+ maxFileSize: media?.maxFileSize ?? DEFAULT_MEDIA_MAX_FILE_SIZE,
470
+ publicBaseUrl: media?.publicBaseUrl ?? ""
471
+ };
472
+ }
473
+ function resolveModuleOptions(options) {
474
+ return {
475
+ configPath: options.configPath ?? "cms.config",
476
+ admin: {
477
+ email: options.admin?.email ?? "",
478
+ password: options.admin?.password ?? ""
479
+ },
480
+ database: resolveDatabaseOptions(options.database),
481
+ media: resolveMediaOptions(options.media),
482
+ i18n: {
483
+ locales: options.i18n?.locales ?? [],
484
+ defaultLocale: options.i18n?.defaultLocale ?? "en"
485
+ },
486
+ graphql: {
487
+ maxDepth: options.graphql?.maxDepth ?? 8
488
+ }
489
+ };
490
+ }
431
491
  const module$1 = defineNuxtModule({
432
492
  meta: {
433
493
  name: "@xleddyl/nuxt-cms",
@@ -445,9 +505,7 @@ const module$1 = defineNuxtModule({
445
505
  },
446
506
  database: {
447
507
  driver: "sqlite",
448
- path: "data/cms.db",
449
- url: "",
450
- authToken: ""
508
+ path: "data/cms.db"
451
509
  },
452
510
  media: {
453
511
  storage: "s3",
@@ -456,6 +514,7 @@ const module$1 = defineNuxtModule({
456
514
  bucket: "",
457
515
  publicBaseUrl: "",
458
516
  presignExpiry: 600,
517
+ maxFileSize: DEFAULT_MEDIA_MAX_FILE_SIZE,
459
518
  accessKeyId: "",
460
519
  secretAccessKey: ""
461
520
  },
@@ -470,6 +529,7 @@ const module$1 = defineNuxtModule({
470
529
  async setup(options, nuxt) {
471
530
  const resolver = createResolver(import.meta.url);
472
531
  const logger = useLogger("nuxt-cms");
532
+ const resolved = resolveModuleOptions(options);
473
533
  if (!resolveCmsEnabled(options.enabled, process.env[CMS_ENABLED_ENV])) {
474
534
  const stub = resolver.resolve("./runtime/app/composables/cms-query-disabled");
475
535
  addImports([
@@ -477,16 +537,38 @@ const module$1 = defineNuxtModule({
477
537
  { name: "$cmsQuery", from: stub }
478
538
  ]);
479
539
  nuxt.options.runtimeConfig.public.cms = {
480
- mediaBaseUrl: options.media.publicBaseUrl,
481
- mediaStorage: options.media.storage,
482
- i18n: options.i18n
540
+ mediaBaseUrl: resolved.media.publicBaseUrl,
541
+ mediaStorage: resolved.media.storage,
542
+ mediaMaxFileSize: resolved.media.maxFileSize,
543
+ i18n: resolved.i18n
483
544
  };
484
545
  logger.info(
485
546
  "[nuxt-cms] disabled: registering no-op useCms/$cmsQuery stubs, skipping admin, server and database setup"
486
547
  );
487
548
  return;
488
549
  }
489
- const configPath = await resolvePath(options.configPath, { cwd: nuxt.options.rootDir });
550
+ if ((resolved.database.driver === "postgres" || resolved.database.driver === "libsql") && !resolved.database.url && !process.env.NUXT_CMS_DATABASE_URL) {
551
+ logger.warn(
552
+ `[nuxt-cms] database.driver is '${resolved.database.driver}' but no url is configured (database.url or NUXT_CMS_DATABASE_URL); the app will fail to connect unless one is provided before the server starts.`
553
+ );
554
+ }
555
+ if (!Number.isInteger(resolved.media.maxFileSize) || resolved.media.maxFileSize <= 0) {
556
+ throw new Error(
557
+ `[nuxt-cms] media.maxFileSize must be a positive integer number of bytes, got ${resolved.media.maxFileSize}`
558
+ );
559
+ }
560
+ const s3KeysConfigured = [
561
+ resolved.media.endpoint,
562
+ resolved.media.bucket,
563
+ resolved.media.accessKeyId,
564
+ resolved.media.secretAccessKey
565
+ ].some(Boolean);
566
+ if (resolved.media.storage === "s3" && s3KeysConfigured && !resolved.media.publicBaseUrl && !process.env.NUXT_PUBLIC_CMS_MEDIA_BASE_URL) {
567
+ logger.warn(
568
+ "[nuxt-cms] media.storage is 's3' with credentials configured but no publicBaseUrl (media.publicBaseUrl or NUXT_PUBLIC_CMS_MEDIA_BASE_URL); uploaded media URLs will be null."
569
+ );
570
+ }
571
+ const configPath = await resolvePath(resolved.configPath, { cwd: nuxt.options.rootDir });
490
572
  nuxt.options.alias["#nuxt-cms"] = resolver.resolve("./runtime/shared/index");
491
573
  nuxt.options.watch.push(configPath);
492
574
  let cmsConfig = {};
@@ -499,11 +581,11 @@ const module$1 = defineNuxtModule({
499
581
  cmsConfig = await jiti.import(configPath, { default: true });
500
582
  } else {
501
583
  logger.warn(
502
- `[nuxt-cms] Config file not found: ${configPath}. Using an empty registry \u2014 create a ${options.configPath}.ts with defineCmsConfig().`
584
+ `[nuxt-cms] Config file not found: ${configPath}. Using an empty registry \u2014 create a ${resolved.configPath}.ts with defineCmsConfig().`
503
585
  );
504
586
  nuxt.options.alias["#cms-config"] = resolver.resolve("./runtime/shared/empty-config");
505
587
  }
506
- const configErrors = validateConfig(cmsConfig, options.i18n);
588
+ const configErrors = validateConfig(cmsConfig, resolved.i18n);
507
589
  if (configErrors.length) {
508
590
  for (const error of configErrors) logger.error(error);
509
591
  throw new Error(
@@ -523,7 +605,7 @@ const module$1 = defineNuxtModule({
523
605
  write: true,
524
606
  getContents: () => renderSchemaFile(
525
607
  cmsConfig,
526
- options.database.driver === "postgres" ? "postgres" : "sqlite",
608
+ resolved.database.driver === "postgres" ? "postgres" : "sqlite",
527
609
  resolveImport
528
610
  )
529
611
  });
@@ -588,11 +670,11 @@ const module$1 = defineNuxtModule({
588
670
  { name: "$cmsQuery", from: resolver.resolve("./runtime/app/composables/cms-query") }
589
671
  ]);
590
672
  const {
591
- driver = "sqlite",
592
- path: dbPath = "data/cms.db",
593
- url: databaseUrl = "",
594
- authToken: databaseAuthToken = ""
595
- } = options.database;
673
+ driver,
674
+ path: dbPath,
675
+ url: databaseUrl,
676
+ authToken: databaseAuthToken
677
+ } = resolved.database;
596
678
  nuxt.options.alias["#cms-db"] = resolver.resolve(
597
679
  driver === "postgres" ? "./runtime/server/utils/db-postgres" : driver === "libsql" ? "./runtime/server/utils/db-libsql" : "./runtime/server/utils/db-sqlite"
598
680
  );
@@ -645,11 +727,11 @@ const module$1 = defineNuxtModule({
645
727
  });
646
728
  }
647
729
  const publicDir = resolve(nuxt.options.rootDir, nuxt.options.dir?.public ?? "public");
648
- const mediaLocalRoot = options.media.storage === "local" && options.media.publicBaseUrl.startsWith("/") ? join(publicDir, ...options.media.publicBaseUrl.split("/").filter(Boolean)) : "";
730
+ const mediaLocalRoot = resolved.media.storage === "local" && resolved.media.publicBaseUrl.startsWith("/") ? join(publicDir, ...resolved.media.publicBaseUrl.split("/").filter(Boolean)) : "";
649
731
  const existingConfig = nuxt.options.runtimeConfig.cms ?? {};
650
732
  nuxt.options.runtimeConfig.cms = {
651
- adminEmail: options.admin.email,
652
- adminPassword: options.admin.password,
733
+ adminEmail: resolved.admin.email,
734
+ adminPassword: resolved.admin.password,
653
735
  databaseUrl,
654
736
  databaseAuthToken,
655
737
  dbPath: resolvedDbPath,
@@ -657,25 +739,27 @@ const module$1 = defineNuxtModule({
657
739
  ...existingConfig,
658
740
  graphql: {
659
741
  graphiql: nuxt.options.dev,
660
- maxDepth: options.graphql.maxDepth,
742
+ maxDepth: resolved.graphql.maxDepth,
661
743
  ...existingConfig.graphql ?? {}
662
744
  },
663
745
  media: {
664
- storage: options.media.storage,
665
- endpoint: options.media.endpoint,
666
- region: options.media.region,
667
- bucket: options.media.bucket,
668
- presignExpiry: options.media.presignExpiry,
669
- accessKeyId: options.media.accessKeyId,
670
- secretAccessKey: options.media.secretAccessKey,
746
+ storage: resolved.media.storage,
747
+ endpoint: resolved.media.endpoint,
748
+ region: resolved.media.region,
749
+ bucket: resolved.media.bucket,
750
+ presignExpiry: resolved.media.presignExpiry,
751
+ maxFileSize: resolved.media.maxFileSize,
752
+ accessKeyId: resolved.media.accessKeyId,
753
+ secretAccessKey: resolved.media.secretAccessKey,
671
754
  localRoot: mediaLocalRoot,
672
755
  ...existingConfig.media ?? {}
673
756
  }
674
757
  };
675
758
  nuxt.options.runtimeConfig.public.cms = {
676
- mediaBaseUrl: options.media.publicBaseUrl,
677
- mediaStorage: options.media.storage,
678
- i18n: options.i18n
759
+ mediaBaseUrl: resolved.media.publicBaseUrl,
760
+ mediaStorage: resolved.media.storage,
761
+ mediaMaxFileSize: resolved.media.maxFileSize,
762
+ i18n: resolved.i18n
679
763
  };
680
764
  addTypeTemplate(
681
765
  {
@@ -702,7 +786,11 @@ const module$1 = defineNuxtModule({
702
786
  );
703
787
  addVitePlugin(tailwindcss());
704
788
  addVitePlugin(svgLoader({ defaultImport: "url", svgoConfig: { plugins: ["prefixIds"] } }));
705
- nuxt.options.css.push(resolver.resolve("./runtime/assets/main.css"));
789
+ nuxt.options.css.push(
790
+ resolveImport("@fontsource-variable/hanken-grotesk/index.css"),
791
+ resolveImport("@fontsource/fragment-mono/index.css"),
792
+ resolver.resolve("./runtime/assets/main.css")
793
+ );
706
794
  nuxt.hook("app:templates", (app) => {
707
795
  app.layouts["cms-admin"] = {
708
796
  name: "cms-admin",
@@ -1,6 +1,5 @@
1
1
  type __VLS_Props = {
2
- color?: 'error';
3
- variant?: string;
2
+ color?: 'error' | 'success' | 'warning' | 'neutral';
4
3
  title?: string;
5
4
  };
6
5
  declare var __VLS_1: {};
@@ -1,6 +1,6 @@
1
1
  <template>
2
- <div class="cms-alert" :class="`cms-alert-${color ?? 'error'}`">
3
- <p v-if="title" class="font-medium">{{ title }}</p>
2
+ <div class="cms-alert" :class="`cms-alert-${color ?? 'neutral'}`" role="alert">
3
+ <p v-if="title" class="cms-alert-title">{{ title }}</p>
4
4
  <slot />
5
5
  </div>
6
6
  </template>
@@ -8,7 +8,6 @@
8
8
  <script setup>
9
9
  defineProps({
10
10
  color: { type: String, required: false },
11
- variant: { type: String, required: false },
12
11
  title: { type: String, required: false }
13
12
  });
14
13
  </script>
@@ -1,6 +1,5 @@
1
1
  type __VLS_Props = {
2
- color?: 'error';
3
- variant?: string;
2
+ color?: 'error' | 'success' | 'warning' | 'neutral';
4
3
  title?: string;
5
4
  };
6
5
  declare var __VLS_1: {};
@@ -8,7 +8,7 @@
8
8
  <div class="flex items-center gap-1">
9
9
  <button
10
10
  type="button"
11
- class="cms-kicker flex grow items-center gap-1.5 text-left"
11
+ class="cms-block-label flex grow items-center gap-1.5 text-left"
12
12
  :aria-label="isCollapsed(item) ? 'Expand block' : 'Collapse block'"
13
13
  @click="toggleCollapsed(item)"
14
14
  >
@@ -69,7 +69,7 @@
69
69
  </template>
70
70
  </div>
71
71
  <CmsDropdownMenu :items="addItems" class="self-start">
72
- <CmsButton label="Add block" icon="plus" variant="subtle" />
72
+ <CmsButton label="Add block" icon="plus" variant="soft" />
73
73
  </CmsDropdownMenu>
74
74
  </div>
75
75
  </template>
@@ -6,7 +6,7 @@ type __VLS_Props = {
6
6
  trailingIcon?: boolean;
7
7
  size?: 'xs' | 'sm' | 'md' | 'lg';
8
8
  color?: 'primary' | 'neutral' | 'error' | 'success';
9
- variant?: 'solid' | 'subtle' | 'soft' | 'ghost';
9
+ variant?: 'solid' | 'soft' | 'ghost';
10
10
  disabled?: boolean;
11
11
  loading?: boolean;
12
12
  block?: boolean;
@@ -6,7 +6,7 @@ type __VLS_Props = {
6
6
  trailingIcon?: boolean;
7
7
  size?: 'xs' | 'sm' | 'md' | 'lg';
8
8
  color?: 'primary' | 'neutral' | 'error' | 'success';
9
- variant?: 'solid' | 'subtle' | 'soft' | 'ghost';
9
+ variant?: 'solid' | 'soft' | 'ghost';
10
10
  disabled?: boolean;
11
11
  loading?: boolean;
12
12
  block?: boolean;
@@ -9,7 +9,7 @@
9
9
  <div class="cms-form">
10
10
  <p class="text-sm">{{ state.message }}</p>
11
11
  <div class="cms-actions is-end">
12
- <CmsButton label="Cancel" variant="subtle" color="neutral" @click="finish(false)" />
12
+ <CmsButton label="Cancel" variant="soft" color="neutral" @click="finish(false)" />
13
13
  <CmsButton
14
14
  :label="state.confirmLabel ?? 'Confirm'"
15
15
  color="error"
@@ -34,7 +34,7 @@
34
34
  type="submit"
35
35
  :form="FORM_ID"
36
36
  :label="published ? 'Make draft' : 'Publish'"
37
- variant="subtle"
37
+ variant="soft"
38
38
  :loading="saving"
39
39
  :disabled="loading"
40
40
  @click="togglePublished"
@@ -17,7 +17,7 @@
17
17
  <CmsButton
18
18
  label="Replace"
19
19
  icon="arrow-path"
20
- variant="subtle"
20
+ variant="soft"
21
21
  color="neutral"
22
22
  size="xs"
23
23
  @click="openGallery"
@@ -25,7 +25,7 @@
25
25
  <CmsButton
26
26
  label="Remove"
27
27
  icon="trash"
28
- variant="subtle"
28
+ variant="soft"
29
29
  color="error"
30
30
  size="xs"
31
31
  @click="clear"
@@ -53,7 +53,7 @@
53
53
  <CmsButton
54
54
  label="New folder"
55
55
  icon="folder-plus"
56
- variant="subtle"
56
+ variant="soft"
57
57
  size="sm"
58
58
  @click="openNewFolder"
59
59
  />
@@ -120,7 +120,7 @@
120
120
  </div>
121
121
 
122
122
  <CmsEmptyState v-else-if="loadError" icon="exclamation-triangle" title="Could not load media">
123
- <CmsButton label="Retry" icon="arrow-path" variant="subtle" @click="reload" />
123
+ <CmsButton label="Retry" icon="arrow-path" variant="soft" @click="reload" />
124
124
  </CmsEmptyState>
125
125
 
126
126
  <CmsEmptyState
@@ -140,7 +140,7 @@
140
140
  v-if="!readOnly"
141
141
  label="Upload"
142
142
  icon="plus"
143
- variant="subtle"
143
+ variant="soft"
144
144
  @click="uploadOpen = true"
145
145
  />
146
146
  </CmsEmptyState>
@@ -36,7 +36,9 @@
36
36
  </template>
37
37
 
38
38
  <script setup>
39
+ import { formatFileSize } from "#nuxt-cms";
39
40
  import { computed, ref } from "#imports";
41
+ import { useCmsRuntime } from "../../composables/cms-runtime";
40
42
  import { useCmsToast } from "../../composables/cms-toast";
41
43
  const props = defineProps({
42
44
  multiple: { type: Boolean, required: false },
@@ -46,6 +48,7 @@ const props = defineProps({
46
48
  });
47
49
  const emit = defineEmits(["uploaded"]);
48
50
  const toast = useCmsToast();
51
+ const { mediaMaxFileSize } = useCmsRuntime();
49
52
  async function imageDimensions(file) {
50
53
  if (!file.type.startsWith("image/")) return {};
51
54
  try {
@@ -74,7 +77,6 @@ function matchesAccept(file) {
74
77
  return file.type.toLowerCase() === pattern;
75
78
  });
76
79
  }
77
- const MAX_FILE_SIZE = 10 * 1024 * 1024;
78
80
  async function uploadOne(file) {
79
81
  const presign = await $fetch("/api/cms/admin/media/presign", {
80
82
  method: "POST",
@@ -106,11 +108,14 @@ async function uploadOne(file) {
106
108
  }
107
109
  async function handleFiles(list) {
108
110
  let files = Array.from(list).filter(matchesAccept);
109
- const tooLarge = files.filter((file) => file.size > MAX_FILE_SIZE);
111
+ const tooLarge = files.filter((file) => file.size > mediaMaxFileSize);
110
112
  for (const file of tooLarge) {
111
- toast.add({ title: `File too large (max 10 MB): ${file.name}`, color: "error" });
113
+ toast.add({
114
+ title: `File too large (max ${formatFileSize(mediaMaxFileSize)}): ${file.name}`,
115
+ color: "error"
116
+ });
112
117
  }
113
- files = files.filter((file) => file.size <= MAX_FILE_SIZE);
118
+ files = files.filter((file) => file.size <= mediaMaxFileSize);
114
119
  if (!props.multiple) files = files.slice(0, 1);
115
120
  if (!files.length || uploading.value) return;
116
121
  uploading.value = true;
@@ -6,13 +6,13 @@ type __VLS_ModelProps = {
6
6
  'open'?: boolean;
7
7
  };
8
8
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
9
- declare var __VLS_7: {}, __VLS_9: {}, __VLS_11: {};
9
+ declare var __VLS_13: {}, __VLS_15: {}, __VLS_17: {};
10
10
  type __VLS_Slots = {} & {
11
- header?: (props: typeof __VLS_7) => any;
11
+ header?: (props: typeof __VLS_13) => any;
12
12
  } & {
13
- body?: (props: typeof __VLS_9) => any;
13
+ body?: (props: typeof __VLS_15) => any;
14
14
  } & {
15
- default?: (props: typeof __VLS_11) => any;
15
+ default?: (props: typeof __VLS_17) => any;
16
16
  };
17
17
  declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
18
18
  "after:leave": () => any;
@@ -2,10 +2,10 @@
2
2
  <Teleport to="body">
3
3
  <!-- Wrapped in .cms-scope so the vendored Tailwind utilities still apply
4
4
  once teleported outside the admin layout root. -->
5
- <div v-if="open" class="cms-scope">
6
- <div class="cms-overlay" @click.self="close">
5
+ <Transition name="cms-modal">
6
+ <div v-if="open" class="cms-scope cms-overlay" @click.self="close">
7
7
  <div
8
- class="cms-modal cms-rise"
8
+ class="cms-modal"
9
9
  :class="size ? `is-${size}` : void 0"
10
10
  role="dialog"
11
11
  aria-modal="true"
@@ -22,7 +22,7 @@
22
22
  </div>
23
23
  </div>
24
24
  </div>
25
- </div>
25
+ </Transition>
26
26
  </Teleport>
27
27
  </template>
28
28
 
@@ -6,13 +6,13 @@ type __VLS_ModelProps = {
6
6
  'open'?: boolean;
7
7
  };
8
8
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
9
- declare var __VLS_7: {}, __VLS_9: {}, __VLS_11: {};
9
+ declare var __VLS_13: {}, __VLS_15: {}, __VLS_17: {};
10
10
  type __VLS_Slots = {} & {
11
- header?: (props: typeof __VLS_7) => any;
11
+ header?: (props: typeof __VLS_13) => any;
12
12
  } & {
13
- body?: (props: typeof __VLS_9) => any;
13
+ body?: (props: typeof __VLS_15) => any;
14
14
  } & {
15
- default?: (props: typeof __VLS_11) => any;
15
+ default?: (props: typeof __VLS_17) => any;
16
16
  };
17
17
  declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
18
18
  "after:leave": () => any;
@@ -1,5 +1,4 @@
1
1
  type __VLS_Props = {
2
- kicker?: string;
3
2
  title: string;
4
3
  };
5
4
  declare var __VLS_1: {}, __VLS_3: {};
@@ -1,9 +1,6 @@
1
1
  <template>
2
2
  <header class="cms-page-header">
3
3
  <div class="cms-page-heading">
4
- <div v-if="kicker" class="cms-kicker">
5
- {{ kicker }}
6
- </div>
7
4
  <div class="cms-actions">
8
5
  <h1 class="cms-title cms-title-md">
9
6
  {{ title }}
@@ -17,7 +14,6 @@
17
14
 
18
15
  <script setup>
19
16
  defineProps({
20
- kicker: { type: String, required: false },
21
17
  title: { type: String, required: true }
22
18
  });
23
19
  </script>