@xleddyl/nuxt-cms 0.1.61 → 0.2.1

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 (147) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +133 -14
  3. package/dist/runtime/app/components/cms/BlocksField.vue +86 -10
  4. package/dist/runtime/app/components/cms/BrandMark.d.vue.ts +6 -0
  5. package/dist/runtime/app/components/cms/BrandMark.vue +19 -0
  6. package/dist/runtime/app/components/cms/BrandMark.vue.d.ts +6 -0
  7. package/dist/runtime/app/components/cms/Checkbox.d.vue.ts +11 -0
  8. package/dist/runtime/app/components/cms/Checkbox.vue +21 -0
  9. package/dist/runtime/app/components/cms/Checkbox.vue.d.ts +11 -0
  10. package/dist/runtime/app/components/cms/Drawer.vue +5 -9
  11. package/dist/runtime/app/components/cms/EmptyState.vue +4 -2
  12. package/dist/runtime/app/components/cms/EntryDrawer.vue +12 -3
  13. package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +1 -0
  14. package/dist/runtime/app/components/cms/EntryForm.vue +104 -30
  15. package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +1 -0
  16. package/dist/runtime/app/components/cms/FieldInput.vue +20 -5
  17. package/dist/runtime/app/components/cms/FormField.d.vue.ts +5 -3
  18. package/dist/runtime/app/components/cms/FormField.vue +8 -2
  19. package/dist/runtime/app/components/cms/FormField.vue.d.ts +5 -3
  20. package/dist/runtime/app/components/cms/Icon.vue +1 -1
  21. package/dist/runtime/app/components/cms/Input.d.vue.ts +0 -1
  22. package/dist/runtime/app/components/cms/Input.vue +7 -4
  23. package/dist/runtime/app/components/cms/Input.vue.d.ts +0 -1
  24. package/dist/runtime/app/components/cms/LocaleSwitch.vue +1 -1
  25. package/dist/runtime/app/components/cms/MediaFolderPicker.d.vue.ts +0 -1
  26. package/dist/runtime/app/components/cms/MediaFolderPicker.vue +1 -3
  27. package/dist/runtime/app/components/cms/MediaFolderPicker.vue.d.ts +0 -1
  28. package/dist/runtime/app/components/cms/MediaGallery.vue +24 -10
  29. package/dist/runtime/app/components/cms/MediaUpload.d.vue.ts +1 -0
  30. package/dist/runtime/app/components/cms/MediaUpload.vue +71 -41
  31. package/dist/runtime/app/components/cms/MediaUpload.vue.d.ts +1 -0
  32. package/dist/runtime/app/components/cms/Modal.vue +5 -9
  33. package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +5 -3
  34. package/dist/runtime/app/components/cms/PageHeader.vue +16 -6
  35. package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +5 -3
  36. package/dist/runtime/app/components/cms/PasswordInput.d.vue.ts +17 -0
  37. package/dist/runtime/app/components/cms/PasswordInput.vue +81 -0
  38. package/dist/runtime/app/components/cms/PasswordInput.vue.d.ts +17 -0
  39. package/dist/runtime/app/components/cms/RelationField.vue +7 -2
  40. package/dist/runtime/app/components/cms/SelectMenu.d.vue.ts +0 -1
  41. package/dist/runtime/app/components/cms/SelectMenu.vue +4 -11
  42. package/dist/runtime/app/components/cms/SelectMenu.vue.d.ts +0 -1
  43. package/dist/runtime/app/components/cms/SettingsAccount.d.vue.ts +3 -0
  44. package/dist/runtime/app/components/cms/SettingsAccount.vue +138 -0
  45. package/dist/runtime/app/components/cms/SettingsAccount.vue.d.ts +3 -0
  46. package/dist/runtime/app/components/cms/SettingsColumns.d.vue.ts +16 -0
  47. package/dist/runtime/app/components/cms/SettingsColumns.vue +102 -0
  48. package/dist/runtime/app/components/cms/SettingsColumns.vue.d.ts +16 -0
  49. package/dist/runtime/app/components/cms/SettingsEntry.d.vue.ts +6 -0
  50. package/dist/runtime/app/components/cms/SettingsEntry.vue +126 -0
  51. package/dist/runtime/app/components/cms/SettingsEntry.vue.d.ts +6 -0
  52. package/dist/runtime/app/components/cms/SettingsLayout.d.vue.ts +16 -0
  53. package/dist/runtime/app/components/cms/SettingsLayout.vue +177 -0
  54. package/dist/runtime/app/components/cms/SettingsLayout.vue.d.ts +16 -0
  55. package/dist/runtime/app/components/cms/SettingsModal.d.vue.ts +16 -0
  56. package/dist/runtime/app/components/cms/SettingsModal.vue +174 -0
  57. package/dist/runtime/app/components/cms/SettingsModal.vue.d.ts +16 -0
  58. package/dist/runtime/app/components/cms/SettingsUsers.d.vue.ts +3 -0
  59. package/dist/runtime/app/components/cms/SettingsUsers.vue +274 -0
  60. package/dist/runtime/app/components/cms/SettingsUsers.vue.d.ts +3 -0
  61. package/dist/runtime/app/components/cms/Spinner.vue +2 -2
  62. package/dist/runtime/app/components/cms/Table.d.vue.ts +6 -8
  63. package/dist/runtime/app/components/cms/Table.vue +42 -47
  64. package/dist/runtime/app/components/cms/Table.vue.d.ts +6 -8
  65. package/dist/runtime/app/components/cms/Textarea.d.vue.ts +0 -1
  66. package/dist/runtime/app/components/cms/Textarea.vue +1 -2
  67. package/dist/runtime/app/components/cms/Textarea.vue.d.ts +0 -1
  68. package/dist/runtime/app/components/cms/Toaster.vue +1 -1
  69. package/dist/runtime/app/components/cms/WelcomeModal.d.vue.ts +3 -0
  70. package/dist/runtime/app/components/cms/WelcomeModal.vue +69 -0
  71. package/dist/runtime/app/components/cms/WelcomeModal.vue.d.ts +3 -0
  72. package/dist/runtime/app/composables/cms-account.d.ts +11 -0
  73. package/dist/runtime/app/composables/cms-account.js +23 -0
  74. package/dist/runtime/app/composables/cms-entry-disabled.js +1 -1
  75. package/dist/runtime/app/composables/cms-media-keys.d.ts +1 -0
  76. package/dist/runtime/app/composables/cms-media-keys.js +5 -0
  77. package/dist/runtime/app/composables/cms-media-library.js +3 -0
  78. package/dist/runtime/app/composables/cms-media-uploader.d.ts +11 -2
  79. package/dist/runtime/app/composables/cms-media-uploader.js +139 -12
  80. package/dist/runtime/app/composables/cms-overlay.d.ts +4 -0
  81. package/dist/runtime/app/composables/cms-overlay.js +23 -0
  82. package/dist/runtime/app/composables/cms-settings.d.ts +12 -0
  83. package/dist/runtime/app/composables/cms-settings.js +59 -0
  84. package/dist/runtime/app/composables/cms-theme.d.ts +9 -0
  85. package/dist/runtime/app/composables/cms-theme.js +27 -0
  86. package/dist/runtime/app/layouts/cms-admin.d.vue.ts +2 -2
  87. package/dist/runtime/app/layouts/cms-admin.vue +71 -17
  88. package/dist/runtime/app/layouts/cms-admin.vue.d.ts +2 -2
  89. package/dist/runtime/app/pages/admin-collection.vue +127 -101
  90. package/dist/runtime/app/pages/admin-entry.vue +2 -0
  91. package/dist/runtime/app/pages/admin-login.vue +18 -3
  92. package/dist/runtime/app/pages/admin-media.vue +5 -1
  93. package/dist/runtime/app/utils/ui.d.ts +2 -0
  94. package/dist/runtime/app/utils/ui.js +18 -0
  95. package/dist/runtime/assets/main.css +1 -1
  96. package/dist/runtime/generate.js +1 -1
  97. package/dist/runtime/server/api/account-password.put.d.ts +4 -0
  98. package/dist/runtime/server/api/account-password.put.js +38 -0
  99. package/dist/runtime/server/api/account-welcome.post.d.ts +4 -0
  100. package/dist/runtime/server/api/account-welcome.post.js +14 -0
  101. package/dist/runtime/server/api/account.put.d.ts +3 -0
  102. package/dist/runtime/server/api/account.put.js +25 -0
  103. package/dist/runtime/server/api/collection.get.js +1 -1
  104. package/dist/runtime/server/api/media.delete.js +2 -1
  105. package/dist/runtime/server/api/media.post.js +2 -1
  106. package/dist/runtime/server/api/media.put.js +2 -1
  107. package/dist/runtime/server/api/settings.delete.d.ts +4 -0
  108. package/dist/runtime/server/api/settings.delete.js +12 -0
  109. package/dist/runtime/server/api/settings.get.d.ts +3 -0
  110. package/dist/runtime/server/api/settings.get.js +15 -0
  111. package/dist/runtime/server/api/settings.put.d.ts +3 -0
  112. package/dist/runtime/server/api/settings.put.js +14 -0
  113. package/dist/runtime/server/api/user.delete.d.ts +4 -0
  114. package/dist/runtime/server/api/user.delete.js +12 -0
  115. package/dist/runtime/server/api/user.put.d.ts +3 -0
  116. package/dist/runtime/server/api/user.put.js +27 -0
  117. package/dist/runtime/server/api/users.get.d.ts +3 -0
  118. package/dist/runtime/server/api/users.get.js +11 -0
  119. package/dist/runtime/server/api/users.post.d.ts +3 -0
  120. package/dist/runtime/server/api/users.post.js +36 -0
  121. package/dist/runtime/server/plugins/session-validate.d.ts +2 -0
  122. package/dist/runtime/server/plugins/session-validate.js +13 -0
  123. package/dist/runtime/server/routes/auth/login.post.js +20 -10
  124. package/dist/runtime/server/utils/graphql-depth.js +12 -5
  125. package/dist/runtime/server/utils/graphql.js +2 -2
  126. package/dist/runtime/server/utils/media-folders.js +10 -2
  127. package/dist/runtime/server/utils/media.d.ts +1 -0
  128. package/dist/runtime/server/utils/media.js +4 -1
  129. package/dist/runtime/server/utils/migrate.js +1 -1
  130. package/dist/runtime/server/utils/password.d.ts +3 -0
  131. package/dist/runtime/server/utils/password.js +31 -0
  132. package/dist/runtime/server/utils/require-admin.d.ts +4 -2
  133. package/dist/runtime/server/utils/require-admin.js +32 -28
  134. package/dist/runtime/server/utils/same-origin.d.ts +2 -0
  135. package/dist/runtime/server/utils/same-origin.js +24 -0
  136. package/dist/runtime/server/utils/settings.d.ts +16 -0
  137. package/dist/runtime/server/utils/settings.js +31 -0
  138. package/dist/runtime/server/utils/users.d.ts +15 -0
  139. package/dist/runtime/server/utils/users.js +34 -0
  140. package/dist/runtime/shared/index.d.ts +13 -1
  141. package/dist/runtime/shared/index.js +2 -0
  142. package/dist/runtime/shared/layout.d.ts +33 -0
  143. package/dist/runtime/shared/layout.js +127 -0
  144. package/dist/runtime/shared/users.d.ts +18 -0
  145. package/dist/runtime/shared/users.js +1 -0
  146. package/dist/runtime/shared/validation.js +5 -4
  147. package/package.json +3 -3
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.61",
4
+ "version": "0.2.1",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
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 { pageColumnFields, isTranslatableField, isTranslatableMediaField, isMultiSelect, pageAllFields, PAGE_PATH_FIELD, pageRoutes, mobileMediaKey, fieldConditions, pageFieldsTableName, pageMediaTableName, isRequiredField, pageFields, isPrivateField, entryFieldsFor, mediaTypeFilter, DEFAULT_MEDIA_MAX_FILE_SIZE, expandMobileMedia } from '../dist/runtime/shared/index.js';
14
+ import { pageColumnFields, isTranslatableField, isTranslatableMediaField, isMultiSelect, pageAllFields, PAGE_PATH_FIELD, pageRoutes, layoutErrors, listErrors, mobileMediaKey, fieldConditions, pageFieldsTableName, pageMediaTableName, isRequiredField, pageFields, isPrivateField, entryFieldsFor, mediaTypeFilter, DEFAULT_MEDIA_MAX_FILE_SIZE, expandMobileMedia } 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';
@@ -80,7 +80,17 @@ function renderMigrationsTypes(typesPath) {
80
80
  }
81
81
 
82
82
  const IDENTIFIER = /^[a-z_]\w*$/i;
83
- const RESERVED_ENTRY_KEYS = ["admin", "auth", "login", "media", "graphql", "cms_media"];
83
+ const RESERVED_ENTRY_KEYS = [
84
+ "admin",
85
+ "auth",
86
+ "login",
87
+ "media",
88
+ "graphql",
89
+ "settings",
90
+ "cms_media",
91
+ "cms_settings",
92
+ "cms_users"
93
+ ];
84
94
  const RESERVED_COLUMNS = ["id", "status", "created_at", "updated_at"];
85
95
  const TITLE_FIELD_TYPES = ["text", "slug", "email", "number", "date", "select"];
86
96
  const TRANSLATABLE_FIELD_TYPES = ["text", "richtext", "media"];
@@ -225,6 +235,12 @@ function validateConfig(config, i18n) {
225
235
  }
226
236
  }
227
237
  const allFields = entry.kind === "page" ? pageAllFields(entry) : entry.fields ?? {};
238
+ errors.push(...layoutErrors(at, allFields, entry.layout));
239
+ errors.push(...listErrors(at, allFields, entry.list, entry.drafts ? ["status"] : []));
240
+ if (entry.list && entry.kind !== "collection")
241
+ errors.push(`${at}: list needs kind 'collection'`);
242
+ if (entry.icon !== void 0 && (typeof entry.icon !== "string" || !entry.icon))
243
+ errors.push(`${at}: icon must be a non-empty string`);
228
244
  const columnNames = /* @__PURE__ */ new Set();
229
245
  for (const [key, field] of Object.entries(allFields)) {
230
246
  const fat = `${at}, field '${key}'`;
@@ -364,7 +380,7 @@ function validateConfig(config, i18n) {
364
380
  errors.push(`${fat}: relation target '${field.to}' must be a collection`);
365
381
  if (!entry.table && target.table)
366
382
  errors.push(
367
- `${fat}: relation target '${field.to}' uses a custom table \u2014 derived relations can only target derived entries`
383
+ `${fat}: relation target '${field.to}' uses a custom table: derived relations can only target derived entries`
368
384
  );
369
385
  }
370
386
  if (field.cardinality && !["many-to-one", "one-to-one", "many-to-many"].includes(field.cardinality)) {
@@ -383,7 +399,11 @@ function validateConfig(config, i18n) {
383
399
  }
384
400
  }
385
401
  }
386
- const sqlTableNames = /* @__PURE__ */ new Map([["cms_media", "the built-in media table"]]);
402
+ const sqlTableNames = /* @__PURE__ */ new Map([
403
+ ["cms_media", "the built-in media table"],
404
+ ["cms_settings", "the built-in settings table"],
405
+ ["cms_users", "the built-in users table"]
406
+ ]);
387
407
  for (const [name, entry] of Object.entries(config)) {
388
408
  if (entry.table) continue;
389
409
  const registerTable = (sqlName, source) => {
@@ -535,6 +555,30 @@ function pageRowsTableExprs(name, dialect) {
535
555
  rowsTable(pageMediaTableName(name), `mediaKey: text('media_key').notNull()`)
536
556
  ];
537
557
  }
558
+ function settingsTableExpr(dialect) {
559
+ const pg = dialect === "postgres";
560
+ const tableFn = pg ? "pgTable" : "sqliteTable";
561
+ return `export const cms_settings = ${tableFn}('cms_settings', {
562
+ key: text('key').primaryKey(),
563
+ value: text('value').notNull(),
564
+ updatedAt: ${nowExpr(pg, "updated_at")},
565
+ })`;
566
+ }
567
+ function usersTableExpr(dialect) {
568
+ const pg = dialect === "postgres";
569
+ const tableFn = pg ? "pgTable" : "sqliteTable";
570
+ return `export const cms_users = ${tableFn}('cms_users', {
571
+ id: text('id').primaryKey(),
572
+ email: text('email').notNull().unique(),
573
+ name: text('name'),
574
+ role: text('role').notNull().default('admin'),
575
+ passwordHash: text('password_hash').notNull(),
576
+ mustChangePassword: ${pg ? `boolean('must_change_password')` : `integer('must_change_password', { mode: 'boolean' })`}.notNull().default(true),
577
+ lastLoginAt: text('last_login_at'),
578
+ createdAt: ${nowExpr(pg, "created_at")},
579
+ updatedAt: ${nowExpr(pg, "updated_at")},
580
+ })`;
581
+ }
538
582
  function mediaTableExpr(dialect) {
539
583
  const pg = dialect === "postgres";
540
584
  const tableFn = pg ? "pgTable" : "sqliteTable";
@@ -561,7 +605,7 @@ function renderSchemaFile(config, dialect, resolveImport = (s) => s) {
561
605
  if (fields.some((f) => f.type === "number" && !f.integer))
562
606
  core.add(pg ? "doublePrecision" : "real");
563
607
  if (pg && fields.some((f) => f.type === "date")) core.add("date");
564
- if (pg && fields.some((f) => f.type === "boolean")) core.add("boolean");
608
+ if (pg) core.add("boolean");
565
609
  if (pg && fields.some(
566
610
  (f) => f.type === "json" || f.type === "blocks" || isTranslatableField(f) && !isTranslatableMediaField(f) || isMultiSelect(f)
567
611
  ))
@@ -581,9 +625,14 @@ function renderSchemaFile(config, dialect, resolveImport = (s) => s) {
581
625
  const rows = rowsEntries.flatMap(([name]) => pageRowsTableExprs(name, dialect));
582
626
  return `${imports.join("\n")}
583
627
 
584
- ${[mediaTableExpr(dialect), ...tables, ...joins, ...rows].join(
585
- "\n\n"
586
- )}
628
+ ${[
629
+ mediaTableExpr(dialect),
630
+ settingsTableExpr(dialect),
631
+ usersTableExpr(dialect),
632
+ ...tables,
633
+ ...joins,
634
+ ...rows
635
+ ].join("\n\n")}
587
636
  `;
588
637
  }
589
638
 
@@ -786,7 +835,12 @@ ${lines.join(
786
835
  function pageTypesTs(name, entry) {
787
836
  const auto = `${typeName(name)}Auto`;
788
837
  const lines = pageRoutes(entry).map((route) => {
789
- const keys = ["id", "path", "updatedAt", ...Object.keys(pageFields(entry, route.path))];
838
+ const keys = [
839
+ "id",
840
+ "path",
841
+ "updatedAt",
842
+ ...Object.entries(pageFields(entry, route.path)).filter(([, field]) => !isPrivateField(field)).map(([key]) => key)
843
+ ];
790
844
  return ` ${JSON.stringify(route.path)}: Pick<${auto}, ${quotedKeys(keys)}>`;
791
845
  });
792
846
  return [
@@ -978,7 +1032,7 @@ async function loadCmsConfig(nuxt, resolver, configPathOption, i18n, logger) {
978
1032
  cmsConfig = expandMobileMedia(await jiti.import(configPath, { default: true }));
979
1033
  } else {
980
1034
  logger.warn(
981
- `[nuxt-cms] Config file not found: ${configPath}. Using an empty registry \u2014 create a ${configPathOption}.ts with defineCmsConfig().`
1035
+ `[nuxt-cms] Config file not found: ${configPath}. Using an empty registry. Create a ${configPathOption}.ts with defineCmsConfig().`
982
1036
  );
983
1037
  nuxt.options.alias["#cms-config"] = resolver.resolve("./runtime/shared/empty-config");
984
1038
  }
@@ -1239,7 +1293,13 @@ const module$1 = defineNuxtModule({
1239
1293
  ` dialect: '${dialect}',`,
1240
1294
  ` schema: '${toPosix(schemaTemplate.dst)}',`,
1241
1295
  ` out: '${toPosix(relativeMigrationsDir)}',`,
1242
- driver === "postgres" ? ` dbCredentials: { url: process.env.NUXT_CMS_DATABASE_URL ?? '${databaseUrl}' },` : driver === "libsql" ? ` dbCredentials: { url: process.env.NUXT_CMS_DATABASE_URL ?? '${databaseUrl || `file:${toPosix(resolvedDbPath)}`}', authToken: (process.env.NUXT_CMS_DATABASE_AUTH_TOKEN ?? '${databaseAuthToken}') || undefined },` : driver === "d1" ? `` : ` dbCredentials: { url: '${toPosix(resolvedDbPath)}' },`,
1296
+ driver === "postgres" ? ` dbCredentials: { url: process.env.NUXT_CMS_DATABASE_URL ?? ${JSON.stringify(
1297
+ databaseUrl
1298
+ )} },` : driver === "libsql" ? ` dbCredentials: { url: process.env.NUXT_CMS_DATABASE_URL ?? ${JSON.stringify(
1299
+ databaseUrl || `file:${toPosix(resolvedDbPath)}`
1300
+ )}, authToken: (process.env.NUXT_CMS_DATABASE_AUTH_TOKEN ?? ${JSON.stringify(
1301
+ databaseAuthToken
1302
+ )}) || undefined },` : driver === "d1" ? `` : ` dbCredentials: { url: ${JSON.stringify(toPosix(resolvedDbPath))} },`,
1243
1303
  `}`,
1244
1304
  ``
1245
1305
  ].join("\n")
@@ -1256,7 +1316,7 @@ const module$1 = defineNuxtModule({
1256
1316
  const migrations = await collectMigrations(migrationsDir);
1257
1317
  if (!migrations && !nuxt.options.dev && Object.keys(cmsConfig).length) {
1258
1318
  logger.warn(
1259
- `[nuxt-cms] No migrations found at ${migrationsDir}. The CMS tables will not be created \u2014 run the dev server once to generate them and commit ${toPosix(
1319
+ `[nuxt-cms] No migrations found at ${migrationsDir}. The CMS tables will not be created. Run the dev server once to generate them and commit ${toPosix(
1260
1320
  relativeMigrationsDir
1261
1321
  )}.`
1262
1322
  );
@@ -1302,6 +1362,7 @@ const module$1 = defineNuxtModule({
1302
1362
  driver === "postgres" ? "./runtime/server/plugins/migrate-postgres" : driver === "libsql" ? "./runtime/server/plugins/migrate-libsql" : driver === "d1" ? "./runtime/server/plugins/d1-binding" : "./runtime/server/plugins/migrate-sqlite"
1303
1363
  )
1304
1364
  );
1365
+ addServerPlugin(resolver.resolve("./runtime/server/plugins/session-validate"));
1305
1366
  if (!nuxt.options.dev) {
1306
1367
  addServerPlugin(resolver.resolve("./runtime/server/plugins/session-check"));
1307
1368
  }
@@ -1382,13 +1443,21 @@ const module$1 = defineNuxtModule({
1382
1443
  "",
1383
1444
  "declare module '#auth-utils' {",
1384
1445
  " interface User {",
1446
+ " id?: string",
1385
1447
  " email: string",
1448
+ " name?: string | null",
1449
+ " role?: 'superadmin' | 'admin'",
1450
+ " firstLogin?: boolean",
1451
+ " passwordStamp?: string",
1386
1452
  " }",
1387
1453
  "}",
1388
1454
  "",
1389
1455
  "declare module '#imports' {",
1390
1456
  " function getUserSession(event: H3Event): Promise<{ user?: import('#auth-utils').User }>",
1391
1457
  " function setUserSession(event: H3Event, session: { user: import('#auth-utils').User }): Promise<unknown>",
1458
+ " function replaceUserSession(event: H3Event, session: { user: import('#auth-utils').User }): Promise<unknown>",
1459
+ " function clearUserSession(event: H3Event): Promise<boolean>",
1460
+ " const sessionHooks: { hook(name: 'fetch', fn: (session: { user?: import('#auth-utils').User }, event: H3Event) => void | Promise<void>): () => void }",
1392
1461
  " function useStorage(base?: string): {",
1393
1462
  " getItem<T>(key: string): Promise<T | null>",
1394
1463
  " setItem<T>(key: string, value: T, options?: { ttl?: number }): Promise<void>",
@@ -1406,8 +1475,8 @@ const module$1 = defineNuxtModule({
1406
1475
  addVitePlugin(tailwindcss());
1407
1476
  addVitePlugin(svgLoader({ defaultImport: "url", svgoConfig: { plugins: ["prefixIds"] } }));
1408
1477
  nuxt.options.css.push(
1409
- resolveImport("@fontsource-variable/hanken-grotesk/index.css"),
1410
- resolveImport("@fontsource/fragment-mono/index.css"),
1478
+ resolveImport("@fontsource-variable/geist/index.css"),
1479
+ resolveImport("@fontsource-variable/geist-mono/index.css"),
1411
1480
  resolver.resolve("./runtime/assets/main.css")
1412
1481
  );
1413
1482
  nuxt.hook("app:templates", (app) => {
@@ -1526,6 +1595,56 @@ const module$1 = defineNuxtModule({
1526
1595
  handler: resolver.resolve("./runtime/server/routes/media-file")
1527
1596
  });
1528
1597
  }
1598
+ addServerHandler({
1599
+ route: "/api/cms/users",
1600
+ method: "get",
1601
+ handler: resolver.resolve("./runtime/server/api/users.get")
1602
+ });
1603
+ addServerHandler({
1604
+ route: "/api/cms/users",
1605
+ method: "post",
1606
+ handler: resolver.resolve("./runtime/server/api/users.post")
1607
+ });
1608
+ addServerHandler({
1609
+ route: "/api/cms/users/:id",
1610
+ method: "put",
1611
+ handler: resolver.resolve("./runtime/server/api/user.put")
1612
+ });
1613
+ addServerHandler({
1614
+ route: "/api/cms/users/:id",
1615
+ method: "delete",
1616
+ handler: resolver.resolve("./runtime/server/api/user.delete")
1617
+ });
1618
+ addServerHandler({
1619
+ route: "/api/cms/account",
1620
+ method: "put",
1621
+ handler: resolver.resolve("./runtime/server/api/account.put")
1622
+ });
1623
+ addServerHandler({
1624
+ route: "/api/cms/account/password",
1625
+ method: "put",
1626
+ handler: resolver.resolve("./runtime/server/api/account-password.put")
1627
+ });
1628
+ addServerHandler({
1629
+ route: "/api/cms/account/welcome",
1630
+ method: "post",
1631
+ handler: resolver.resolve("./runtime/server/api/account-welcome.post")
1632
+ });
1633
+ addServerHandler({
1634
+ route: "/api/cms/admin/settings",
1635
+ method: "get",
1636
+ handler: resolver.resolve("./runtime/server/api/settings.get")
1637
+ });
1638
+ addServerHandler({
1639
+ route: "/api/cms/admin/settings/:key",
1640
+ method: "put",
1641
+ handler: resolver.resolve("./runtime/server/api/settings.put")
1642
+ });
1643
+ addServerHandler({
1644
+ route: "/api/cms/admin/settings/:key",
1645
+ method: "delete",
1646
+ handler: resolver.resolve("./runtime/server/api/settings.delete")
1647
+ });
1529
1648
  const api = "/api/cms/admin/:collection";
1530
1649
  addServerHandler({
1531
1650
  route: api,
@@ -9,20 +9,25 @@
9
9
  'is-muted': isHidden(item),
10
10
  'is-dragging': dragIndex === index,
11
11
  'is-drop-target': dropIndex === index && dragIndex !== index,
12
- 'is-missing': hasMissingMedia(item)
12
+ 'is-missing': hasMissingMedia(item),
13
+ 'is-media-only': mediaOnly
13
14
  }"
14
15
  draggable="true"
16
+ :tabindex="mediaOnly ? 0 : void 0"
17
+ :aria-label="mediaOnly ? `${barTitle(item)}, use the arrow keys to reorder` : void 0"
15
18
  @dragstart="onDragStart(index, $event)"
16
19
  @dragover.prevent="dropIndex = index"
17
20
  @dragleave="onDragLeave(index)"
18
21
  @drop.prevent="onDrop(index)"
19
22
  @dragend="onDragEnd"
23
+ @keydown.self="onTileKey($event, index)"
20
24
  >
21
- <button
22
- type="button"
25
+ <component
26
+ :is="mediaOnly ? 'div' : 'button'"
27
+ :type="mediaOnly ? void 0 : 'button'"
23
28
  class="cms-block-preview"
24
- :aria-label="`Edit ${labelOf(item)}`"
25
- @click="openEditor(index)"
29
+ :aria-label="mediaOnly ? void 0 : `Edit ${labelOf(item)}`"
30
+ @click="onPreviewClick(index)"
26
31
  >
27
32
  <img
28
33
  v-if="previewOf(item)?.kind === 'image'"
@@ -43,9 +48,28 @@
43
48
  <CmsIcon v-else :name="iconOf(item)" class="size-7" />
44
49
 
45
50
  <CmsIcon v-if="isHidden(item)" name="eye-slash" class="cms-block-hidden size-4" />
46
- </button>
47
51
 
48
- <div class="cms-block-bar">
52
+ <template v-if="mediaOnly">
53
+ <span
54
+ v-if="hasMissingMedia(item)"
55
+ class="cms-block-missing"
56
+ title="Not found in the media library"
57
+ >
58
+ <CmsIcon name="exclamation-triangle" class="size-3.5" />
59
+ </span>
60
+ <button
61
+ type="button"
62
+ class="cms-block-remove"
63
+ :aria-label="`Remove ${barTitle(item)}`"
64
+ title="Remove"
65
+ @click.stop="remove(index)"
66
+ >
67
+ <CmsIcon name="x-mark" class="size-3.5" />
68
+ </button>
69
+ </template>
70
+ </component>
71
+
72
+ <div v-if="!mediaOnly" class="cms-block-bar">
49
73
  <button
50
74
  type="button"
51
75
  class="cms-block-grip"
@@ -84,10 +108,22 @@
84
108
  </CmsDropdownMenu>
85
109
  <button v-else type="button" class="cms-block-add" @click="addFirst">
86
110
  <CmsIcon name="plus" class="size-5" />
87
- <span>Add</span>
111
+ <span>{{ mediaOnly ? addMediaLabel : "Add" }}</span>
88
112
  </button>
89
113
  </div>
90
114
 
115
+ <CmsModal v-if="mediaOnly" v-model:open="pickerOpen" title="Media" size="lg">
116
+ <template #body>
117
+ <CmsMediaGallery
118
+ v-model:folder="pickerFolder"
119
+ selectable
120
+ :media-type="mediaOnly.field.mediaType"
121
+ :accept="mediaOnly.field.accept"
122
+ @select="addMedia"
123
+ />
124
+ </template>
125
+ </CmsModal>
126
+
91
127
  <CmsModal v-model:open="editorOpen" :title="editorTitle">
92
128
  <template #body>
93
129
  <div class="flex flex-col gap-4">
@@ -95,6 +131,7 @@
95
131
  v-for="(blockField, blockKey) in editorFields"
96
132
  :key="blockKey"
97
133
  :label="blockField.label"
134
+ :icon="fieldIcon(blockField)"
98
135
  :required="blockField.required"
99
136
  >
100
137
  <CmsFieldInput
@@ -157,9 +194,10 @@ import {
157
194
  mediaTypeForKey,
158
195
  pickTranslatedMedia
159
196
  } from "#nuxt-cms";
160
- import { computed, ref } from "#imports";
197
+ import { computed, ref, useState } from "#imports";
161
198
  import { useCmsMediaKeys } from "../../composables/cms-media-keys";
162
199
  import { useCmsRuntime } from "../../composables/cms-runtime";
200
+ import { fieldIcon } from "../../utils/ui";
163
201
  const props = defineProps({
164
202
  field: { type: Object, required: true },
165
203
  locale: { type: String, required: false }
@@ -169,6 +207,29 @@ const { mediaBaseUrl, i18n } = useCmsRuntime();
169
207
  const mediaKeys = useCmsMediaKeys();
170
208
  const blocks = computed(() => props.field.blocks ?? {});
171
209
  const items = computed(() => Array.isArray(model.value) ? model.value : []);
210
+ const mediaOnly = computed(() => {
211
+ const types = Object.entries(blocks.value);
212
+ if (types.length !== 1) return null;
213
+ const [type, block] = types[0];
214
+ const fields = Object.entries(block.fields);
215
+ if (fields.length !== 1) return null;
216
+ const [key, field] = fields[0];
217
+ if (field.type !== "media" || isTranslatableMediaField(field)) return null;
218
+ return { type, key, field };
219
+ });
220
+ const addMediaLabel = computed(() => {
221
+ const types = [mediaOnly.value?.field.mediaType ?? []].flat();
222
+ return types.length === 1 && types[0] === "image" ? "Add photo" : "Add media";
223
+ });
224
+ const pickerOpen = ref(false);
225
+ const pickerFolder = useState("cms-media-picker-folder", () => null);
226
+ function addMedia(media) {
227
+ const target = mediaOnly.value;
228
+ if (!target) return;
229
+ mediaKeys.remember(media.key);
230
+ model.value = [...items.value, { type: target.type, [target.key]: media.key }];
231
+ pickerOpen.value = false;
232
+ }
172
233
  let uidCounter = 0;
173
234
  const uids = /* @__PURE__ */ new WeakMap();
174
235
  function uidFor(item) {
@@ -245,10 +306,25 @@ function add(type) {
245
306
  const block = blocks.value[type];
246
307
  if (!block) return;
247
308
  const empty = Object.fromEntries(Object.keys(block.fields).map((k) => [k, null]));
309
+ const index = items.value.length;
248
310
  model.value = [...items.value, { type, ...empty }];
249
- openEditor(items.value.length - 1);
311
+ openEditor(index);
312
+ }
313
+ function onTileKey(event, index) {
314
+ if (!mediaOnly.value) return;
315
+ const action = event.key === "ArrowLeft" ? () => move(index, -1) : event.key === "ArrowRight" ? () => move(index, 1) : event.key === "Delete" || event.key === "Backspace" ? () => remove(index) : null;
316
+ if (!action) return;
317
+ event.preventDefault();
318
+ action();
319
+ }
320
+ function onPreviewClick(index) {
321
+ if (!mediaOnly.value) openEditor(index);
250
322
  }
251
323
  function addFirst() {
324
+ if (mediaOnly.value) {
325
+ pickerOpen.value = true;
326
+ return;
327
+ }
252
328
  const first = Object.keys(blocks.value)[0];
253
329
  if (first) add(first);
254
330
  }
@@ -0,0 +1,6 @@
1
+ type __VLS_Props = {
2
+ size?: 'md' | 'lg';
3
+ };
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
5
+ declare const _default: typeof __VLS_export;
6
+ export default _default;
@@ -0,0 +1,19 @@
1
+ <template>
2
+ <span class="cms-brand-mark" :class="{ 'is-lg': size === 'lg' }" aria-hidden="true">
3
+ <svg viewBox="0 0 16 16" fill="none">
4
+ <path
5
+ d="M4.25 12.25V7.5a3.75 3.75 0 0 1 7.5 0v4.75"
6
+ stroke="currentColor"
7
+ stroke-width="2"
8
+ stroke-linecap="round"
9
+ />
10
+ <circle cx="8" cy="12.25" r="1.25" fill="currentColor" />
11
+ </svg>
12
+ </span>
13
+ </template>
14
+
15
+ <script setup>
16
+ defineProps({
17
+ size: { type: String, required: false }
18
+ });
19
+ </script>
@@ -0,0 +1,6 @@
1
+ type __VLS_Props = {
2
+ size?: 'md' | 'lg';
3
+ };
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
5
+ declare const _default: typeof __VLS_export;
6
+ export default _default;
@@ -0,0 +1,11 @@
1
+ type __VLS_Props = {
2
+ checked?: boolean;
3
+ indeterminate?: boolean;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ toggle: () => any;
7
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
8
+ onToggle?: (() => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
@@ -0,0 +1,21 @@
1
+ <template>
2
+ <button
3
+ type="button"
4
+ role="checkbox"
5
+ :aria-checked="indeterminate ? 'mixed' : checked"
6
+ class="cms-checkbox"
7
+ :class="{ 'is-on': checked || indeterminate }"
8
+ @click.stop="emit('toggle')"
9
+ >
10
+ <CmsIcon v-if="indeterminate" name="minus" class="size-3" />
11
+ <CmsIcon v-else-if="checked" name="check" class="size-3" />
12
+ </button>
13
+ </template>
14
+
15
+ <script setup>
16
+ defineProps({
17
+ checked: { type: Boolean, required: false },
18
+ indeterminate: { type: Boolean, required: false }
19
+ });
20
+ const emit = defineEmits(["toggle"]);
21
+ </script>
@@ -0,0 +1,11 @@
1
+ type __VLS_Props = {
2
+ checked?: boolean;
3
+ indeterminate?: boolean;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ toggle: () => any;
7
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
8
+ onToggle?: (() => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
@@ -35,6 +35,7 @@
35
35
 
36
36
  <script setup>
37
37
  import { onBeforeUnmount, watch } from "#imports";
38
+ import { useCmsOverlay } from "../../composables/cms-overlay";
38
39
  defineProps({
39
40
  title: { type: String, required: false }
40
41
  });
@@ -43,23 +44,18 @@ const emit = defineEmits(["close"]);
43
44
  function close() {
44
45
  emit("close");
45
46
  }
46
- function onKeydown(event) {
47
- if (event.key === "Escape") close();
48
- }
47
+ const overlay = useCmsOverlay(close);
49
48
  watch(open, (value) => {
50
49
  if (import.meta.server) return;
51
50
  if (value) {
52
- document.addEventListener("keydown", onKeydown);
51
+ overlay.activate();
53
52
  document.body.style.overflow = "hidden";
54
53
  } else {
55
- document.removeEventListener("keydown", onKeydown);
54
+ overlay.deactivate();
56
55
  document.body.style.overflow = "";
57
56
  }
58
57
  });
59
58
  onBeforeUnmount(() => {
60
- if (import.meta.client) {
61
- document.removeEventListener("keydown", onKeydown);
62
- document.body.style.overflow = "";
63
- }
59
+ if (import.meta.client) document.body.style.overflow = "";
64
60
  });
65
61
  </script>
@@ -1,7 +1,9 @@
1
1
  <template>
2
2
  <div class="cms-empty" :class="{ 'is-fill': fill }">
3
- <CmsIcon :name="icon" class="cms-empty-icon size-6" />
4
- <p class="cms-title cms-title-sm">{{ title }}<span class="cms-accent">.</span></p>
3
+ <span class="cms-empty-icon">
4
+ <CmsIcon :name="icon" class="size-5" />
5
+ </span>
6
+ <p class="cms-title cms-title-sm">{{ title }}</p>
5
7
  <p v-if="body" class="cms-subtitle max-w-md">
6
8
  {{ body }}
7
9
  </p>
@@ -11,6 +11,7 @@
11
11
  v-model="formState"
12
12
  :fields="config.fields"
13
13
  :tabs="config.tabs"
14
+ :entry-name="collection"
14
15
  :drafts="drafts"
15
16
  :form-id="FORM_ID"
16
17
  :loading="saving"
@@ -120,16 +121,23 @@ function revertStatus() {
120
121
  if (!drafts.value) return;
121
122
  formState.value.status = JSON.parse(snapshot.value).status ?? null;
122
123
  }
124
+ let loadRequest = 0;
123
125
  async function load() {
124
- if (props.entryId === null) {
126
+ const request = ++loadRequest;
127
+ const id = props.entryId;
128
+ if (id === null) {
129
+ loading.value = false;
125
130
  setState(emptyState());
126
131
  return;
127
132
  }
133
+ const isStale = () => request !== loadRequest || props.entryId !== id || !open.value;
128
134
  loading.value = true;
129
135
  try {
130
- const entry = await cmsApi(`${endpoint.value}/${props.entryId}`);
136
+ const entry = await cmsApi(`${endpoint.value}/${id}`);
137
+ if (isStale()) return;
131
138
  setState(pickFields(entry));
132
139
  } catch (error) {
140
+ if (isStale()) return;
133
141
  toast.add({
134
142
  title: "Could not load entry",
135
143
  description: errorMessage(error),
@@ -137,7 +145,7 @@ async function load() {
137
145
  });
138
146
  open.value = false;
139
147
  } finally {
140
- loading.value = false;
148
+ if (request === loadRequest) loading.value = false;
141
149
  }
142
150
  }
143
151
  watch(
@@ -164,6 +172,7 @@ async function save() {
164
172
  emit("saved");
165
173
  open.value = false;
166
174
  } catch (error) {
175
+ revertStatus();
167
176
  toast.add({
168
177
  title: "Save failed",
169
178
  description: errorMessage(error),
@@ -2,6 +2,7 @@ import type { CmsEntry, CmsTab } from '#nuxt-cms';
2
2
  type __VLS_Props = {
3
3
  fields: CmsEntry['fields'];
4
4
  tabs?: CmsTab[];
5
+ entryName?: string;
5
6
  drafts?: boolean;
6
7
  loading?: boolean;
7
8
  formId?: string;