@xleddyl/nuxt-cms 0.1.61 → 0.2.0

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 (115) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +117 -9
  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/EmptyState.vue +4 -2
  11. package/dist/runtime/app/components/cms/EntryDrawer.vue +1 -0
  12. package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +1 -0
  13. package/dist/runtime/app/components/cms/EntryForm.vue +105 -30
  14. package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +1 -0
  15. package/dist/runtime/app/components/cms/FieldInput.vue +20 -5
  16. package/dist/runtime/app/components/cms/FormField.d.vue.ts +5 -3
  17. package/dist/runtime/app/components/cms/FormField.vue +8 -2
  18. package/dist/runtime/app/components/cms/FormField.vue.d.ts +5 -3
  19. package/dist/runtime/app/components/cms/Icon.vue +1 -1
  20. package/dist/runtime/app/components/cms/MediaGallery.vue +24 -10
  21. package/dist/runtime/app/components/cms/MediaUpload.d.vue.ts +1 -0
  22. package/dist/runtime/app/components/cms/MediaUpload.vue +71 -41
  23. package/dist/runtime/app/components/cms/MediaUpload.vue.d.ts +1 -0
  24. package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +5 -3
  25. package/dist/runtime/app/components/cms/PageHeader.vue +16 -6
  26. package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +5 -3
  27. package/dist/runtime/app/components/cms/PasswordInput.d.vue.ts +17 -0
  28. package/dist/runtime/app/components/cms/PasswordInput.vue +77 -0
  29. package/dist/runtime/app/components/cms/PasswordInput.vue.d.ts +17 -0
  30. package/dist/runtime/app/components/cms/SelectMenu.vue +3 -5
  31. package/dist/runtime/app/components/cms/SettingsAccount.d.vue.ts +3 -0
  32. package/dist/runtime/app/components/cms/SettingsAccount.vue +138 -0
  33. package/dist/runtime/app/components/cms/SettingsAccount.vue.d.ts +3 -0
  34. package/dist/runtime/app/components/cms/SettingsColumns.d.vue.ts +16 -0
  35. package/dist/runtime/app/components/cms/SettingsColumns.vue +102 -0
  36. package/dist/runtime/app/components/cms/SettingsColumns.vue.d.ts +16 -0
  37. package/dist/runtime/app/components/cms/SettingsEntry.d.vue.ts +6 -0
  38. package/dist/runtime/app/components/cms/SettingsEntry.vue +133 -0
  39. package/dist/runtime/app/components/cms/SettingsEntry.vue.d.ts +6 -0
  40. package/dist/runtime/app/components/cms/SettingsLayout.d.vue.ts +16 -0
  41. package/dist/runtime/app/components/cms/SettingsLayout.vue +177 -0
  42. package/dist/runtime/app/components/cms/SettingsLayout.vue.d.ts +16 -0
  43. package/dist/runtime/app/components/cms/SettingsModal.d.vue.ts +16 -0
  44. package/dist/runtime/app/components/cms/SettingsModal.vue +179 -0
  45. package/dist/runtime/app/components/cms/SettingsModal.vue.d.ts +16 -0
  46. package/dist/runtime/app/components/cms/SettingsUsers.d.vue.ts +3 -0
  47. package/dist/runtime/app/components/cms/SettingsUsers.vue +270 -0
  48. package/dist/runtime/app/components/cms/SettingsUsers.vue.d.ts +3 -0
  49. package/dist/runtime/app/components/cms/Spinner.vue +2 -2
  50. package/dist/runtime/app/components/cms/Table.d.vue.ts +7 -2
  51. package/dist/runtime/app/components/cms/Table.vue +38 -1
  52. package/dist/runtime/app/components/cms/Table.vue.d.ts +7 -2
  53. package/dist/runtime/app/components/cms/Toaster.vue +1 -1
  54. package/dist/runtime/app/components/cms/WelcomeModal.d.vue.ts +3 -0
  55. package/dist/runtime/app/components/cms/WelcomeModal.vue +69 -0
  56. package/dist/runtime/app/components/cms/WelcomeModal.vue.d.ts +3 -0
  57. package/dist/runtime/app/composables/cms-account.d.ts +11 -0
  58. package/dist/runtime/app/composables/cms-account.js +23 -0
  59. package/dist/runtime/app/composables/cms-media-uploader.d.ts +11 -1
  60. package/dist/runtime/app/composables/cms-media-uploader.js +138 -11
  61. package/dist/runtime/app/composables/cms-settings.d.ts +13 -0
  62. package/dist/runtime/app/composables/cms-settings.js +59 -0
  63. package/dist/runtime/app/composables/cms-theme.d.ts +9 -0
  64. package/dist/runtime/app/composables/cms-theme.js +27 -0
  65. package/dist/runtime/app/layouts/cms-admin.d.vue.ts +2 -2
  66. package/dist/runtime/app/layouts/cms-admin.vue +71 -17
  67. package/dist/runtime/app/layouts/cms-admin.vue.d.ts +2 -2
  68. package/dist/runtime/app/pages/admin-collection.vue +127 -101
  69. package/dist/runtime/app/pages/admin-entry.vue +1 -0
  70. package/dist/runtime/app/pages/admin-login.vue +18 -3
  71. package/dist/runtime/app/pages/admin-media.vue +5 -1
  72. package/dist/runtime/app/utils/ui.d.ts +2 -0
  73. package/dist/runtime/app/utils/ui.js +18 -0
  74. package/dist/runtime/assets/main.css +1 -1
  75. package/dist/runtime/server/api/account-password.put.d.ts +4 -0
  76. package/dist/runtime/server/api/account-password.put.js +38 -0
  77. package/dist/runtime/server/api/account-welcome.post.d.ts +4 -0
  78. package/dist/runtime/server/api/account-welcome.post.js +14 -0
  79. package/dist/runtime/server/api/account.put.d.ts +3 -0
  80. package/dist/runtime/server/api/account.put.js +25 -0
  81. package/dist/runtime/server/api/settings.delete.d.ts +4 -0
  82. package/dist/runtime/server/api/settings.delete.js +12 -0
  83. package/dist/runtime/server/api/settings.get.d.ts +3 -0
  84. package/dist/runtime/server/api/settings.get.js +15 -0
  85. package/dist/runtime/server/api/settings.put.d.ts +3 -0
  86. package/dist/runtime/server/api/settings.put.js +14 -0
  87. package/dist/runtime/server/api/user.delete.d.ts +4 -0
  88. package/dist/runtime/server/api/user.delete.js +12 -0
  89. package/dist/runtime/server/api/user.put.d.ts +3 -0
  90. package/dist/runtime/server/api/user.put.js +27 -0
  91. package/dist/runtime/server/api/users.get.d.ts +3 -0
  92. package/dist/runtime/server/api/users.get.js +11 -0
  93. package/dist/runtime/server/api/users.post.d.ts +3 -0
  94. package/dist/runtime/server/api/users.post.js +32 -0
  95. package/dist/runtime/server/plugins/session-validate.d.ts +2 -0
  96. package/dist/runtime/server/plugins/session-validate.js +13 -0
  97. package/dist/runtime/server/routes/auth/login.post.js +18 -8
  98. package/dist/runtime/server/utils/password.d.ts +3 -0
  99. package/dist/runtime/server/utils/password.js +31 -0
  100. package/dist/runtime/server/utils/require-admin.d.ts +6 -2
  101. package/dist/runtime/server/utils/require-admin.js +33 -28
  102. package/dist/runtime/server/utils/same-origin.d.ts +2 -0
  103. package/dist/runtime/server/utils/same-origin.js +24 -0
  104. package/dist/runtime/server/utils/settings.d.ts +16 -0
  105. package/dist/runtime/server/utils/settings.js +31 -0
  106. package/dist/runtime/server/utils/users.d.ts +15 -0
  107. package/dist/runtime/server/utils/users.js +34 -0
  108. package/dist/runtime/shared/index.d.ts +13 -1
  109. package/dist/runtime/shared/index.js +2 -0
  110. package/dist/runtime/shared/layout.d.ts +34 -0
  111. package/dist/runtime/shared/layout.js +130 -0
  112. package/dist/runtime/shared/users.d.ts +18 -0
  113. package/dist/runtime/shared/users.js +1 -0
  114. package/dist/runtime/shared/validation.js +5 -4
  115. 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.0",
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}'`;
@@ -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
 
@@ -1302,6 +1351,7 @@ const module$1 = defineNuxtModule({
1302
1351
  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
1352
  )
1304
1353
  );
1354
+ addServerPlugin(resolver.resolve("./runtime/server/plugins/session-validate"));
1305
1355
  if (!nuxt.options.dev) {
1306
1356
  addServerPlugin(resolver.resolve("./runtime/server/plugins/session-check"));
1307
1357
  }
@@ -1382,13 +1432,21 @@ const module$1 = defineNuxtModule({
1382
1432
  "",
1383
1433
  "declare module '#auth-utils' {",
1384
1434
  " interface User {",
1435
+ " id?: string",
1385
1436
  " email: string",
1437
+ " name?: string | null",
1438
+ " role?: 'superadmin' | 'admin'",
1439
+ " firstLogin?: boolean",
1440
+ " passwordStamp?: string",
1386
1441
  " }",
1387
1442
  "}",
1388
1443
  "",
1389
1444
  "declare module '#imports' {",
1390
1445
  " function getUserSession(event: H3Event): Promise<{ user?: import('#auth-utils').User }>",
1391
1446
  " function setUserSession(event: H3Event, session: { user: import('#auth-utils').User }): Promise<unknown>",
1447
+ " function replaceUserSession(event: H3Event, session: { user: import('#auth-utils').User }): Promise<unknown>",
1448
+ " function clearUserSession(event: H3Event): Promise<boolean>",
1449
+ " const sessionHooks: { hook(name: 'fetch', fn: (session: { user?: import('#auth-utils').User }, event: H3Event) => void | Promise<void>): () => void }",
1392
1450
  " function useStorage(base?: string): {",
1393
1451
  " getItem<T>(key: string): Promise<T | null>",
1394
1452
  " setItem<T>(key: string, value: T, options?: { ttl?: number }): Promise<void>",
@@ -1406,8 +1464,8 @@ const module$1 = defineNuxtModule({
1406
1464
  addVitePlugin(tailwindcss());
1407
1465
  addVitePlugin(svgLoader({ defaultImport: "url", svgoConfig: { plugins: ["prefixIds"] } }));
1408
1466
  nuxt.options.css.push(
1409
- resolveImport("@fontsource-variable/hanken-grotesk/index.css"),
1410
- resolveImport("@fontsource/fragment-mono/index.css"),
1467
+ resolveImport("@fontsource-variable/geist/index.css"),
1468
+ resolveImport("@fontsource-variable/geist-mono/index.css"),
1411
1469
  resolver.resolve("./runtime/assets/main.css")
1412
1470
  );
1413
1471
  nuxt.hook("app:templates", (app) => {
@@ -1526,6 +1584,56 @@ const module$1 = defineNuxtModule({
1526
1584
  handler: resolver.resolve("./runtime/server/routes/media-file")
1527
1585
  });
1528
1586
  }
1587
+ addServerHandler({
1588
+ route: "/api/cms/users",
1589
+ method: "get",
1590
+ handler: resolver.resolve("./runtime/server/api/users.get")
1591
+ });
1592
+ addServerHandler({
1593
+ route: "/api/cms/users",
1594
+ method: "post",
1595
+ handler: resolver.resolve("./runtime/server/api/users.post")
1596
+ });
1597
+ addServerHandler({
1598
+ route: "/api/cms/users/:id",
1599
+ method: "put",
1600
+ handler: resolver.resolve("./runtime/server/api/user.put")
1601
+ });
1602
+ addServerHandler({
1603
+ route: "/api/cms/users/:id",
1604
+ method: "delete",
1605
+ handler: resolver.resolve("./runtime/server/api/user.delete")
1606
+ });
1607
+ addServerHandler({
1608
+ route: "/api/cms/account",
1609
+ method: "put",
1610
+ handler: resolver.resolve("./runtime/server/api/account.put")
1611
+ });
1612
+ addServerHandler({
1613
+ route: "/api/cms/account/password",
1614
+ method: "put",
1615
+ handler: resolver.resolve("./runtime/server/api/account-password.put")
1616
+ });
1617
+ addServerHandler({
1618
+ route: "/api/cms/account/welcome",
1619
+ method: "post",
1620
+ handler: resolver.resolve("./runtime/server/api/account-welcome.post")
1621
+ });
1622
+ addServerHandler({
1623
+ route: "/api/cms/admin/settings",
1624
+ method: "get",
1625
+ handler: resolver.resolve("./runtime/server/api/settings.get")
1626
+ });
1627
+ addServerHandler({
1628
+ route: "/api/cms/admin/settings/:key",
1629
+ method: "put",
1630
+ handler: resolver.resolve("./runtime/server/api/settings.put")
1631
+ });
1632
+ addServerHandler({
1633
+ route: "/api/cms/admin/settings/:key",
1634
+ method: "delete",
1635
+ handler: resolver.resolve("./runtime/server/api/settings.delete")
1636
+ });
1529
1637
  const api = "/api/cms/admin/:collection";
1530
1638
  addServerHandler({
1531
1639
  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;
@@ -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"
@@ -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;