includio-cms 0.24.1 → 0.26.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 (349) hide show
  1. package/API.md +57 -4
  2. package/CHANGELOG.md +53 -0
  3. package/DOCS.md +1 -1
  4. package/README.md +2 -0
  5. package/ROADMAP.md +6 -0
  6. package/dist/admin/client/account/lang.d.ts +1 -0
  7. package/dist/admin/client/account/lang.js +4 -2
  8. package/dist/admin/client/account/profile-section.svelte +2 -2
  9. package/dist/admin/client/account/security-section.svelte +27 -4
  10. package/dist/admin/client/account/sessions-section.svelte +1 -1
  11. package/dist/admin/client/admin/admin-after-login-layout-content.svelte +1 -1
  12. package/dist/admin/client/admin/dashboard-page.svelte +34 -10
  13. package/dist/admin/client/collection/bulk-actions-bar.svelte +86 -44
  14. package/dist/admin/client/collection/bulk-actions-bar.svelte.d.ts +3 -1
  15. package/dist/admin/client/collection/collection-entries.svelte +52 -36
  16. package/dist/admin/client/collection/collection-entries.svelte.d.ts +3 -0
  17. package/dist/admin/client/collection/collection.svelte +28 -14
  18. package/dist/admin/client/collection/collection.svelte.d.ts +3 -0
  19. package/dist/admin/client/collection/data-table.svelte +279 -130
  20. package/dist/admin/client/collection/data-table.svelte.d.ts +11 -0
  21. package/dist/admin/client/collection/date-cell.svelte +4 -4
  22. package/dist/admin/client/collection/row-actions.svelte +2 -1
  23. package/dist/admin/client/collection/sortable-header.svelte +33 -9
  24. package/dist/admin/client/collection/state-display.svelte +102 -0
  25. package/dist/admin/client/collection/state-display.svelte.d.ts +12 -0
  26. package/dist/admin/client/collection/status-badge.svelte +99 -11
  27. package/dist/admin/client/collection/status-badge.svelte.d.ts +15 -1
  28. package/dist/admin/client/collection/table-pagination.svelte +21 -6
  29. package/dist/admin/client/collection/table-toolbar.svelte +105 -80
  30. package/dist/admin/client/collection/table-toolbar.svelte.d.ts +11 -8
  31. package/dist/admin/client/entry/entry-form.svelte +36 -11
  32. package/dist/admin/client/entry/entry-form.svelte.d.ts +1 -0
  33. package/dist/admin/client/entry/entry-header.svelte +22 -15
  34. package/dist/admin/client/entry/entry-header.svelte.d.ts +1 -0
  35. package/dist/admin/client/entry/entry.svelte +269 -165
  36. package/dist/admin/client/entry/header/a11y-header-badge.svelte +47 -0
  37. package/dist/admin/client/entry/header/a11y-header-badge.svelte.d.ts +8 -0
  38. package/dist/admin/client/entry/header/publish-panel.svelte +69 -13
  39. package/dist/admin/client/entry/header/save-indicator.svelte +57 -28
  40. package/dist/admin/client/entry/header/save-indicator.svelte.d.ts +1 -0
  41. package/dist/admin/client/entry/header/status-badge.svelte +60 -15
  42. package/dist/admin/client/entry/header/status-badge.svelte.d.ts +1 -2
  43. package/dist/admin/client/entry/header/version-history-sheet.svelte +1 -1
  44. package/dist/admin/client/entry/hybrid/hybrid-layout.svelte +74 -23
  45. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +1 -1
  46. package/dist/admin/client/entry/utils.d.ts +14 -0
  47. package/dist/admin/client/entry/utils.js +28 -0
  48. package/dist/admin/client/form/form-submission/form-submission.svelte +2 -2
  49. package/dist/admin/client/form/form-submissions.svelte +143 -194
  50. package/dist/admin/client/form/form-submissions.svelte.d.ts +2 -0
  51. package/dist/admin/client/login/lang.d.ts +3 -0
  52. package/dist/admin/client/login/lang.js +10 -4
  53. package/dist/admin/client/login/login-form.svelte +8 -1
  54. package/dist/admin/client/login/reset-password-page.svelte +24 -3
  55. package/dist/admin/client/login/schema.d.ts +14 -2
  56. package/dist/admin/client/login/schema.js +19 -8
  57. package/dist/admin/client/maintenance/maintenance-page.svelte +16 -17
  58. package/dist/admin/client/media/media-page.svelte +1 -1
  59. package/dist/admin/client/shop/coupon-edit-page.svelte +117 -13
  60. package/dist/admin/client/shop/coupon-form.svelte +282 -138
  61. package/dist/admin/client/shop/coupon-form.svelte.d.ts +1 -9
  62. package/dist/admin/client/shop/coupon-new-page.svelte +40 -10
  63. package/dist/admin/client/shop/coupon-new-page.svelte.d.ts +2 -17
  64. package/dist/admin/client/shop/coupon-schema.d.ts +28 -0
  65. package/dist/admin/client/shop/coupon-schema.js +53 -0
  66. package/dist/admin/client/shop/coupons-list-page.svelte +262 -118
  67. package/dist/admin/client/shop/coupons-list-page.svelte.d.ts +16 -1
  68. package/dist/admin/client/shop/shipping-method-edit-page.svelte +108 -59
  69. package/dist/admin/client/shop/shipping-method-form.svelte +36 -9
  70. package/dist/admin/client/shop/shipping-method-new-page.svelte +44 -13
  71. package/dist/admin/client/shop/shipping-methods-list-page.svelte +101 -59
  72. package/dist/admin/client/shop/shop-order-detail-page.svelte +113 -84
  73. package/dist/admin/client/shop/shop-orders-list-page.svelte +302 -152
  74. package/dist/admin/client/shop/shop-orders-list-page.svelte.d.ts +18 -1
  75. package/dist/admin/client/shop/shop-products-list-page.svelte +355 -118
  76. package/dist/admin/client/shop/shop-products-list-page.svelte.d.ts +19 -1
  77. package/dist/admin/client/users/accept-invite-page.svelte +24 -3
  78. package/dist/admin/client/users/create-user-dialog.svelte +3 -8
  79. package/dist/admin/client/users/lang.d.ts +2 -0
  80. package/dist/admin/client/users/lang.js +4 -0
  81. package/dist/admin/client/users/pending-invitations.svelte +2 -9
  82. package/dist/admin/client/users/user-name-cell.svelte +20 -0
  83. package/dist/admin/client/users/user-name-cell.svelte.d.ts +9 -0
  84. package/dist/admin/client/users/user-role-badge.svelte +16 -0
  85. package/dist/admin/client/users/user-role-badge.svelte.d.ts +7 -0
  86. package/dist/admin/client/users/user-row-actions.svelte +72 -0
  87. package/dist/admin/client/users/user-row-actions.svelte.d.ts +20 -0
  88. package/dist/admin/client/users/user-sessions-sheet.svelte +2 -11
  89. package/dist/admin/client/users/users-page.svelte +283 -497
  90. package/dist/admin/client/users/users-page.svelte.d.ts +12 -1
  91. package/dist/admin/components/dashboard/form-submissions-widget.svelte +59 -74
  92. package/dist/admin/components/dashboard/recent-activity.svelte +17 -5
  93. package/dist/admin/components/dashboard/recent-entries.svelte +19 -7
  94. package/dist/admin/components/dialogs/confirmation-dialog.svelte +105 -0
  95. package/dist/admin/components/dialogs/confirmation-dialog.svelte.d.ts +13 -0
  96. package/dist/admin/components/fields/block-picker-modal.svelte +6 -0
  97. package/dist/admin/components/fields/blocks-field.svelte +46 -1
  98. package/dist/admin/components/fields/boolean-field.svelte +1 -1
  99. package/dist/admin/components/fields/field-renderer.svelte +23 -21
  100. package/dist/admin/components/fields/file-field.svelte +344 -30
  101. package/dist/admin/components/fields/media-field.svelte +16 -2
  102. package/dist/admin/components/fields/radio-field.svelte +22 -0
  103. package/dist/admin/components/fields/relation-field.svelte +123 -97
  104. package/dist/admin/components/fields/relation-picker-dialog.svelte +2 -2
  105. package/dist/admin/components/fields/seo-field.svelte +60 -30
  106. package/dist/admin/components/fields/shop-field.svelte +9 -4
  107. package/dist/admin/components/fields/simple-array-field.svelte +321 -151
  108. package/dist/admin/components/fields/simple-array-field.svelte.d.ts +3 -0
  109. package/dist/admin/components/fields/slug-field.svelte +146 -21
  110. package/dist/admin/components/fields/text-field-wrapper.svelte +37 -20
  111. package/dist/admin/components/fields/text-field.svelte +7 -2
  112. package/dist/admin/components/fields/url-field-wrapper.svelte +10 -0
  113. package/dist/admin/components/fields/url-field.svelte +36 -23
  114. package/dist/admin/components/forms/form-error-summary.svelte +143 -0
  115. package/dist/admin/components/forms/form-error-summary.svelte.d.ts +27 -0
  116. package/dist/admin/components/layout/app-sidebar.svelte +7 -2
  117. package/dist/admin/components/layout/detail-page-shell.svelte +71 -0
  118. package/dist/admin/components/layout/detail-page-shell.svelte.d.ts +24 -0
  119. package/dist/admin/components/layout/lang.d.ts +5 -0
  120. package/dist/admin/components/layout/lang.js +10 -0
  121. package/dist/admin/components/layout/layout-renderer.svelte +71 -2
  122. package/dist/admin/components/layout/layout-renderer.svelte.d.ts +1 -0
  123. package/dist/admin/components/layout/layout-tabs.svelte +172 -0
  124. package/dist/admin/components/layout/layout-tabs.svelte.d.ts +24 -0
  125. package/dist/admin/components/layout/nav-breadcrumbs.svelte +25 -7
  126. package/dist/admin/components/layout/nav-collections.svelte +23 -36
  127. package/dist/admin/components/layout/nav-forms.svelte +19 -35
  128. package/dist/admin/components/layout/nav-main.svelte +3 -28
  129. package/dist/admin/components/layout/nav-search.svelte +70 -2
  130. package/dist/admin/components/layout/nav-section.svelte +77 -0
  131. package/dist/admin/components/layout/nav-section.svelte.d.ts +22 -0
  132. package/dist/admin/components/layout/nav-shop.svelte +3 -27
  133. package/dist/admin/components/layout/nav-singletons.svelte +16 -28
  134. package/dist/admin/components/layout/page-header.stories.svelte +93 -0
  135. package/dist/admin/components/layout/page-header.stories.svelte.d.ts +27 -0
  136. package/dist/admin/components/layout/page-header.svelte +68 -0
  137. package/dist/admin/components/layout/page-header.svelte.d.ts +17 -0
  138. package/dist/admin/components/layout/site-header.svelte +9 -0
  139. package/dist/admin/components/layout/site-header.svelte.d.ts +2 -17
  140. package/dist/admin/components/media/file/file-name-input.svelte +6 -2
  141. package/dist/admin/components/media/file/file-preview.svelte +130 -17
  142. package/dist/admin/components/media/file-upload.svelte +16 -7
  143. package/dist/admin/components/media/file-upload.svelte.d.ts +1 -0
  144. package/dist/admin/components/media/files-list.svelte +153 -53
  145. package/dist/admin/components/media/files-list.svelte.d.ts +1 -0
  146. package/dist/admin/components/media/media-library.svelte +577 -198
  147. package/dist/admin/components/media/media-library.svelte.d.ts +4 -0
  148. package/dist/admin/components/media/media-selector.svelte +4 -2
  149. package/dist/admin/components/media/media-selector.svelte.d.ts +1 -0
  150. package/dist/admin/components/media/tag-sidebar.svelte +4 -4
  151. package/dist/admin/components/tiptap/FigureNodeView.svelte +10 -0
  152. package/dist/admin/components/tiptap/bubble-menu.svelte +104 -0
  153. package/dist/admin/components/tiptap/bubble-menu.svelte.d.ts +19 -0
  154. package/dist/admin/components/tiptap/content-editor.svelte +28 -24
  155. package/dist/admin/components/tiptap/editor-toolbar.svelte +7 -7
  156. package/dist/admin/components/tiptap/extensions.js +5 -1
  157. package/dist/admin/components/tiptap/image-dialog.svelte +5 -1
  158. package/dist/admin/components/tiptap/link-dialog.svelte +2 -0
  159. package/dist/admin/components/tiptap/tiptap-editor.svelte +18 -20
  160. package/dist/admin/components/tiptap/video-dialog.svelte +1 -1
  161. package/dist/admin/i18n/errors.d.ts +140 -0
  162. package/dist/admin/i18n/errors.js +151 -0
  163. package/dist/admin/remote/entry.remote.d.ts +59 -4
  164. package/dist/admin/remote/entry.remote.js +239 -62
  165. package/dist/admin/remote/shop.remote.d.ts +37 -32
  166. package/dist/admin/remote/shop.remote.js +9 -2
  167. package/dist/admin/shared/password-generate.d.ts +6 -0
  168. package/dist/admin/shared/password-generate.js +40 -0
  169. package/dist/admin/shared/password-schema.d.ts +6 -0
  170. package/dist/admin/shared/password-schema.js +10 -3
  171. package/dist/admin/styles/admin.css +23 -6
  172. package/dist/admin/styles/tokens.md +244 -0
  173. package/dist/admin/utils/accordionActivation.d.ts +13 -0
  174. package/dist/admin/utils/accordionActivation.js +35 -0
  175. package/dist/admin/utils/entryLabel.d.ts +23 -0
  176. package/dist/admin/utils/entryLabel.js +51 -12
  177. package/dist/admin/utils/field-a11y.d.ts +29 -0
  178. package/dist/admin/utils/field-a11y.js +23 -0
  179. package/dist/admin/utils/fieldPathElement.d.ts +9 -0
  180. package/dist/admin/utils/fieldPathElement.js +18 -0
  181. package/dist/admin/utils/fileDisplay.d.ts +10 -0
  182. package/dist/admin/utils/fileDisplay.js +26 -0
  183. package/dist/admin/utils/flattenFormErrors.d.ts +19 -0
  184. package/dist/admin/utils/flattenFormErrors.js +102 -0
  185. package/dist/admin/utils/formatters.d.ts +12 -0
  186. package/dist/admin/utils/{formatDate.js → formatters.js} +23 -2
  187. package/dist/admin/utils/scrollWithin.d.ts +9 -0
  188. package/dist/admin/utils/scrollWithin.js +32 -0
  189. package/dist/admin/utils/tabActivation.d.ts +12 -0
  190. package/dist/admin/utils/tabActivation.js +24 -0
  191. package/dist/cms/runtime/schema.d.ts +1 -0
  192. package/dist/cms/runtime/schema.js +1 -0
  193. package/dist/cms/runtime/types.d.ts +80 -7
  194. package/dist/components/ui/accordion/accordion-content.svelte +17 -3
  195. package/dist/components/ui/accordion/accordion.stories.svelte +21 -1
  196. package/dist/components/ui/alert/alert.stories.svelte +14 -0
  197. package/dist/components/ui/alert-dialog/alert-dialog.stories.svelte +45 -0
  198. package/dist/components/ui/alert-dialog/alert-dialog.stories.svelte.d.ts +27 -0
  199. package/dist/components/ui/avatar/avatar.stories.svelte +27 -0
  200. package/dist/components/ui/badge/badge.stories.svelte +15 -0
  201. package/dist/components/ui/breadcrumb/breadcrumb.stories.svelte +47 -0
  202. package/dist/components/ui/breadcrumb/breadcrumb.svelte +1 -1
  203. package/dist/components/ui/button/button.stories.svelte +53 -6
  204. package/dist/components/ui/button/button.svelte +39 -5
  205. package/dist/components/ui/button/button.svelte.d.ts +4 -0
  206. package/dist/components/ui/button-group/button-group.stories.svelte +44 -0
  207. package/dist/components/ui/button-group/button-group.stories.svelte.d.ts +27 -0
  208. package/dist/components/ui/calendar/calendar.stories.svelte +36 -0
  209. package/dist/components/ui/calendar/calendar.stories.svelte.d.ts +27 -0
  210. package/dist/components/ui/card/card.stories.svelte +7 -0
  211. package/dist/components/ui/carousel/carousel.stories.svelte +43 -0
  212. package/dist/components/ui/carousel/carousel.stories.svelte.d.ts +27 -0
  213. package/dist/components/ui/checkbox/checkbox.stories.svelte +67 -0
  214. package/dist/components/ui/checkbox/checkbox.stories.svelte.d.ts +27 -0
  215. package/dist/components/ui/checkbox/checkbox.svelte +3 -3
  216. package/dist/components/ui/command/command.stories.svelte +18 -0
  217. package/dist/components/ui/data-table/data-table.stories.svelte +61 -0
  218. package/dist/components/ui/data-table/data-table.stories.svelte.d.ts +18 -0
  219. package/dist/components/ui/dialog/dialog-content.svelte +5 -0
  220. package/dist/components/ui/dialog/dialog-content.svelte.d.ts +2 -0
  221. package/dist/components/ui/dialog/dialog.stories.svelte +35 -0
  222. package/dist/components/ui/dropdown-menu/dropdown-menu.stories.svelte +74 -0
  223. package/dist/components/ui/dropdown-menu/dropdown-menu.stories.svelte.d.ts +27 -0
  224. package/dist/components/ui/field/field-context.svelte.d.ts +22 -0
  225. package/dist/components/ui/field/field-context.svelte.js +9 -0
  226. package/dist/components/ui/field/field-control.svelte +18 -0
  227. package/dist/components/ui/field/field-control.svelte.d.ts +8 -0
  228. package/dist/components/ui/field/field-description.svelte +12 -0
  229. package/dist/components/ui/field/field-error.svelte +14 -6
  230. package/dist/components/ui/field/field-label.svelte +10 -0
  231. package/dist/components/ui/field/field.stories.svelte +95 -9
  232. package/dist/components/ui/field/field.svelte +57 -0
  233. package/dist/components/ui/field/field.svelte.d.ts +2 -0
  234. package/dist/components/ui/field/index.d.ts +3 -1
  235. package/dist/components/ui/field/index.js +4 -2
  236. package/dist/components/ui/form/form-field-errors.svelte +1 -1
  237. package/dist/components/ui/form/form.stories.svelte +25 -0
  238. package/dist/components/ui/form/form.stories.svelte.d.ts +26 -0
  239. package/dist/components/ui/input/input.stories.svelte +26 -0
  240. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  241. package/dist/components/ui/input-group/input-group.stories.svelte +43 -0
  242. package/dist/components/ui/input-group/input-group.stories.svelte.d.ts +27 -0
  243. package/dist/components/ui/item/item.stories.svelte +61 -0
  244. package/dist/components/ui/item/item.stories.svelte.d.ts +27 -0
  245. package/dist/components/ui/label/label.stories.svelte +7 -0
  246. package/dist/components/ui/live-region/index.d.ts +1 -0
  247. package/dist/components/ui/live-region/index.js +1 -0
  248. package/dist/components/ui/live-region/live-region-demo.svelte +32 -0
  249. package/dist/components/ui/live-region/live-region-demo.svelte.d.ts +7 -0
  250. package/dist/components/ui/live-region/live-region.stories.svelte +23 -0
  251. package/dist/components/ui/live-region/live-region.stories.svelte.d.ts +26 -0
  252. package/dist/components/ui/live-region/live-region.svelte +12 -0
  253. package/dist/components/ui/live-region/live-region.svelte.d.ts +8 -0
  254. package/dist/components/ui/popover/popover.stories.svelte +34 -0
  255. package/dist/components/ui/radio-group/radio-group.stories.svelte +58 -0
  256. package/dist/components/ui/radio-group/radio-group.stories.svelte.d.ts +27 -0
  257. package/dist/components/ui/resizable/resizable.stories.svelte +56 -0
  258. package/dist/components/ui/resizable/resizable.stories.svelte.d.ts +27 -0
  259. package/dist/components/ui/select/select.stories.svelte +49 -0
  260. package/dist/components/ui/separator/separator.stories.svelte +18 -0
  261. package/dist/components/ui/sheet/sheet.stories.svelte +34 -0
  262. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  263. package/dist/components/ui/sidebar/sidebar-menu-button.svelte +1 -0
  264. package/dist/components/ui/sidebar/sidebar-trigger.svelte +1 -1
  265. package/dist/components/ui/sidebar/sidebar.stories.svelte +72 -0
  266. package/dist/components/ui/sidebar/sidebar.stories.svelte.d.ts +27 -0
  267. package/dist/components/ui/skeleton/skeleton.stories.svelte +39 -0
  268. package/dist/components/ui/skeleton/skeleton.stories.svelte.d.ts +27 -0
  269. package/dist/components/ui/skeleton/skeleton.svelte +6 -0
  270. package/dist/components/ui/sonner/index.d.ts +1 -1
  271. package/dist/components/ui/sonner/index.js +1 -1
  272. package/dist/components/ui/sonner/sonner.stories.svelte +7 -0
  273. package/dist/components/ui/sonner/sonner.svelte +17 -1
  274. package/dist/components/ui/sonner/sonner.svelte.d.ts +6 -0
  275. package/dist/components/ui/spinner/spinner.stories.svelte +30 -0
  276. package/dist/components/ui/spinner/spinner.stories.svelte.d.ts +27 -0
  277. package/dist/components/ui/switch/switch.stories.svelte +56 -0
  278. package/dist/components/ui/switch/switch.stories.svelte.d.ts +27 -0
  279. package/dist/components/ui/table/table-cell.svelte +1 -1
  280. package/dist/components/ui/table/table-head.svelte +1 -1
  281. package/dist/components/ui/table/table.stories.svelte +68 -0
  282. package/dist/components/ui/table/table.stories.svelte.d.ts +27 -0
  283. package/dist/components/ui/table/table.svelte +1 -1
  284. package/dist/components/ui/tabs/tabs.stories.svelte +48 -0
  285. package/dist/components/ui/tabs/tabs.stories.svelte.d.ts +27 -0
  286. package/dist/components/ui/textarea/textarea.stories.svelte +21 -0
  287. package/dist/components/ui/toggle/toggle.stories.svelte +23 -0
  288. package/dist/components/ui/toggle-group/toggle-group.stories.svelte +43 -0
  289. package/dist/components/ui/tooltip/tooltip.stories.svelte +46 -6
  290. package/dist/core/fields/fieldSchemaToTs.d.ts +7 -0
  291. package/dist/core/fields/fieldSchemaToTs.js +234 -90
  292. package/dist/core/fields/layoutUtils.d.ts +4 -1
  293. package/dist/core/fields/layoutUtils.js +41 -4
  294. package/dist/core/fields/resolveSeo.d.ts +70 -0
  295. package/dist/core/fields/resolveSeo.js +88 -0
  296. package/dist/core/fields/seoFieldDescriptor.d.ts +43 -0
  297. package/dist/core/fields/seoFieldDescriptor.js +74 -0
  298. package/dist/core/fields/slugPath.d.ts +13 -0
  299. package/dist/core/fields/slugPath.js +32 -0
  300. package/dist/core/fields/urlUtils.d.ts +8 -0
  301. package/dist/core/fields/urlUtils.js +27 -0
  302. package/dist/core/index.d.ts +1 -0
  303. package/dist/core/index.js +1 -0
  304. package/dist/core/server/entries/operations/create.js +13 -0
  305. package/dist/core/server/entries/operations/get.d.ts +7 -0
  306. package/dist/core/server/entries/operations/get.js +10 -6
  307. package/dist/core/server/entries/operations/slugUniqueness.d.ts +37 -0
  308. package/dist/core/server/entries/operations/slugUniqueness.js +116 -0
  309. package/dist/core/server/entries/operations/update.d.ts +6 -1
  310. package/dist/core/server/entries/operations/update.js +24 -1
  311. package/dist/core/server/fields/slugResolver.d.ts +3 -13
  312. package/dist/core/server/fields/slugResolver.js +8 -37
  313. package/dist/core/server/generator/fields.js +10 -17
  314. package/dist/core/server/generator/formFields.js +2 -1
  315. package/dist/core/server/generator/generator.js +4 -4
  316. package/dist/core/server/generator/utils.d.ts +1 -0
  317. package/dist/core/server/generator/utils.js +4 -0
  318. package/dist/paraglide/messages/_index.d.ts +3 -36
  319. package/dist/paraglide/messages/_index.js +3 -71
  320. package/dist/paraglide/messages/hello_world.d.ts +5 -0
  321. package/dist/paraglide/messages/hello_world.js +33 -0
  322. package/dist/paraglide/messages/login_hello.d.ts +16 -0
  323. package/dist/paraglide/messages/login_hello.js +34 -0
  324. package/dist/paraglide/messages/login_please_login.d.ts +16 -0
  325. package/dist/paraglide/messages/login_please_login.js +34 -0
  326. package/dist/shop/server/orders.d.ts +1 -0
  327. package/dist/shop/server/orders.js +14 -0
  328. package/dist/shop/server/shop-data.d.ts +2 -0
  329. package/dist/shop/server/shop-data.js +20 -5
  330. package/dist/sveltekit/server/handle.js +17 -0
  331. package/dist/types/cms.schema.js +4 -2
  332. package/dist/types/fields.d.ts +35 -0
  333. package/dist/types/index.d.ts +1 -1
  334. package/dist/types/layout.d.ts +35 -2
  335. package/dist/updates/0.26.0/index.d.ts +2 -0
  336. package/dist/updates/0.26.0/index.js +51 -0
  337. package/dist/updates/index.js +3 -1
  338. package/package.json +29 -7
  339. package/dist/admin/client/collection/empty-state.svelte +0 -28
  340. package/dist/admin/client/collection/empty-state.svelte.d.ts +0 -9
  341. package/dist/admin/client/form/submission-status-badge.svelte +0 -41
  342. package/dist/admin/client/form/submission-status-badge.svelte.d.ts +0 -7
  343. package/dist/admin/components/media/file-preview.svelte +0 -51
  344. package/dist/admin/components/media/file-preview.svelte.d.ts +0 -6
  345. package/dist/admin/utils/formatDate.d.ts +0 -5
  346. package/dist/paraglide/messages/en.d.ts +0 -5
  347. package/dist/paraglide/messages/en.js +0 -14
  348. package/dist/paraglide/messages/pl.d.ts +0 -5
  349. package/dist/paraglide/messages/pl.js +0 -14
@@ -1,4 +1,29 @@
1
1
  import { z } from 'zod';
2
+ import { buildSeoZodShape } from './seoFieldDescriptor.js';
3
+ import { isValidUrlValue } from './urlUtils.js';
4
+ /**
5
+ * Localized generic validation messages. `generateZodSchemaFromField` gets the
6
+ * preferred language via `options.messageLanguage` (the interface language on
7
+ * the client); server-side / seed schemas leave it undefined → Polish.
8
+ */
9
+ function validationMessages(messageLanguage) {
10
+ const en = messageLanguage === 'en';
11
+ return {
12
+ required: en ? 'This field is required' : 'Pole wymagane',
13
+ minChars: (n) => (en ? `At least ${n} characters` : `Minimum ${n} znaków`),
14
+ maxChars: (n) => (en ? `At most ${n} characters` : `Maksimum ${n} znaków`),
15
+ invalidFormat: en ? 'Invalid format' : 'Nieprawidłowy format',
16
+ numberRequired: en ? 'A number is required' : 'Wymagana liczba',
17
+ minValue: (n) => `Minimum: ${n}`,
18
+ maxValue: (n) => (en ? `Maximum: ${n}` : `Maksimum: ${n}`),
19
+ exactItems: (n) => en ? `Exactly ${n} items required` : `Wymagane dokładnie ${n} elementów`,
20
+ minItems: (n) => (en ? `At least ${n} items` : `Minimum ${n} elementów`),
21
+ maxItems: (n) => (en ? `At most ${n} items` : `Maksimum ${n} elementów`),
22
+ invalidUrl: en ? 'Invalid URL' : 'Nieprawidłowy adres URL',
23
+ unknownBlock: (t) => (en ? `Unknown block type: ${t}` : `Nieznany typ bloku: ${t}`),
24
+ invalidBlock: en ? 'Invalid block data' : 'Nieprawidłowe dane bloku'
25
+ };
26
+ }
2
27
  let _customFieldsOverride = null;
3
28
  /**
4
29
  * Set custom fields map for client-side use (where getCMS() is unavailable).
@@ -27,20 +52,39 @@ export function generateZodSchemaFromField(field, languages, options = {
27
52
  parentRequired: true
28
53
  }) {
29
54
  const localized = options.localized !== false;
55
+ const msg = validationMessages(options.messageLanguage);
30
56
  switch (field.type) {
31
57
  case 'text': {
32
58
  let schema = z.string();
33
59
  if (field.required && options.parentRequired && field.minLength === undefined)
34
- schema = schema.min(1, { message: 'Pole wymagane' });
60
+ schema = schema.min(1, { message: msg.required });
35
61
  if (field.minLength !== undefined)
36
- schema = schema.min(field.minLength, { message: `Minimum ${field.minLength} znaków` });
62
+ schema = schema.min(field.minLength, { message: msg.minChars(field.minLength) });
37
63
  if (field.maxLength !== undefined)
38
- schema = schema.max(field.maxLength, { message: `Maksimum ${field.maxLength} znaków` });
39
- if (field.pattern !== undefined)
40
- schema = schema.regex(new RegExp(field.pattern), { message: 'Nieprawidłowy format' });
41
- if (!localized)
42
- return schema;
43
- return z.object(Object.fromEntries(languages.map((lang) => [lang, schema])));
64
+ schema = schema.max(field.maxLength, { message: msg.maxChars(field.maxLength) });
65
+ if (field.pattern !== undefined) {
66
+ // Localized = `Partial<Record<lang, string>> | string` (union),
67
+ // so TS can't index it by a free `string`. Resolve via a local
68
+ // view to pick the preferred-language message, with a fallback
69
+ // to the first available entry.
70
+ const localized = field.patternError;
71
+ const patternMsg = (typeof localized === 'object' &&
72
+ localized &&
73
+ options.messageLanguage &&
74
+ localized[options.messageLanguage]) ||
75
+ (typeof localized === 'object' && localized && Object.values(localized)[0]) ||
76
+ (typeof localized === 'string' && localized) ||
77
+ msg.invalidFormat;
78
+ schema = schema.regex(new RegExp(field.pattern), { message: patternMsg });
79
+ }
80
+ if (!localized) {
81
+ return field.required ? schema : schema.optional().default('');
82
+ }
83
+ const localizedSchema = z.object(Object.fromEntries(languages.map((lang) => [lang, schema])));
84
+ if (field.required)
85
+ return localizedSchema;
86
+ const emptyLang = Object.fromEntries(languages.map((lang) => [lang, '']));
87
+ return localizedSchema.optional().default(emptyLang);
44
88
  }
45
89
  case 'content': {
46
90
  const markSchema = z.object({
@@ -64,7 +108,8 @@ export function generateZodSchemaFromField(field, languages, options = {
64
108
  for (const blockDef of field.inlineBlocks) {
65
109
  blockSchemas.set(blockDef.slug, generateZodSchemaFromFields(blockDef.fields, languages, {
66
110
  parentRequired: false,
67
- localized: false
111
+ localized: false,
112
+ messageLanguage: options.messageLanguage
68
113
  }));
69
114
  }
70
115
  docSchema = z
@@ -83,7 +128,7 @@ export function generateZodSchemaFromField(field, languages, options = {
83
128
  if (!schema) {
84
129
  ctx.addIssue({
85
130
  code: z.ZodIssueCode.custom,
86
- message: `Nieznany typ bloku: ${blockType}`,
131
+ message: msg.unknownBlock(blockType),
87
132
  path: ['content', i, 'attrs', 'blockType']
88
133
  });
89
134
  continue;
@@ -96,7 +141,7 @@ export function generateZodSchemaFromField(field, languages, options = {
96
141
  catch {
97
142
  ctx.addIssue({
98
143
  code: z.ZodIssueCode.custom,
99
- message: 'Nieprawidłowe dane bloku',
144
+ message: msg.invalidBlock,
100
145
  path: ['content', i, 'attrs', 'blockData']
101
146
  });
102
147
  continue;
@@ -119,59 +164,81 @@ export function generateZodSchemaFromField(field, languages, options = {
119
164
  return z.object(Object.fromEntries(languages.map((lang) => [lang, docSchema])));
120
165
  }
121
166
  case 'number': {
122
- let schema = z.number({ message: 'Wymagana liczba' });
167
+ let schema = z.number({ message: msg.numberRequired });
123
168
  if (field.min !== undefined)
124
- schema = schema.min(field.min, { message: `Minimum: ${field.min}` });
169
+ schema = schema.min(field.min, { message: msg.minValue(field.min) });
125
170
  if (field.max !== undefined)
126
- schema = schema.max(field.max, { message: `Maksimum: ${field.max}` });
127
- return schema;
171
+ schema = schema.max(field.max, { message: msg.maxValue(field.max) });
172
+ return field.required ? schema : schema.optional();
128
173
  }
129
174
  case 'boolean':
130
- return z.boolean();
175
+ // A non-required toggle absent from the data must not fail
176
+ // validation — treat missing as `false` (matches the UI toggle's
177
+ // off state). Same class of bug as non-required select/radio.
178
+ return field.required ? z.boolean() : z.boolean().optional().default(false);
131
179
  case 'date':
132
- case 'datetime':
133
- return z.string().datetime();
180
+ case 'datetime': {
181
+ const dt = z.string().datetime();
182
+ if (field.required)
183
+ return dt;
184
+ // Empty string is the editor's "no value" state; accept it and
185
+ // default to '' so missing fields don't fail validation.
186
+ return z.union([z.literal(''), dt]).optional().default('');
187
+ }
134
188
  case 'file':
135
189
  case 'media': {
136
190
  const singleSchema = field.required
137
- ? z.string().uuid({ message: 'Pole wymagane' })
138
- : z.string();
139
- return field.multiple ? z.array(singleSchema).default([]) : singleSchema;
191
+ ? z.string().uuid({ message: msg.required })
192
+ : z.string().optional().default('');
193
+ return field.multiple ? z.array(z.string()).default([]) : singleSchema;
140
194
  }
141
195
  case 'select':
142
196
  case 'radio': {
143
197
  const enumValues = field.options.map((o) => o.value);
144
198
  const enumObject = z.enum(enumValues);
145
199
  if (field.type === 'select' && field.multiple) {
146
- return z.array(enumObject);
200
+ return z.array(enumObject).default([]);
147
201
  }
148
- return field.required ? enumObject : enumObject.or(z.literal(''));
202
+ // Non-required: union of enum + '' sentinel. Superforms requires a
203
+ // multi-type union to carry a default, so anchor it on the '' sentinel.
204
+ return field.required ? enumObject : enumObject.or(z.literal('')).default('');
149
205
  }
150
206
  case 'checkboxes': {
151
- const checkValues = field.options.map((o) => JSON.stringify(o.value)).join(', ');
152
- return z.array(z.enum([checkValues])).default([]);
207
+ const enumValues = field.options.map((o) => o.value);
208
+ return z.array(z.enum(enumValues)).default([]);
153
209
  }
154
210
  case 'relation': {
155
- let baseSchema = z.string();
211
+ if (field.multiple) {
212
+ return z.array(z.string()).default([]);
213
+ }
156
214
  if (field.required) {
157
- baseSchema = baseSchema.uuid({ message: 'Pole wymagane' });
215
+ return z.string().uuid({ message: msg.required });
158
216
  }
159
- return field.multiple ? z.array(baseSchema).default([]) : baseSchema;
217
+ return z.string().optional().default('');
160
218
  }
161
219
  case 'object': {
220
+ // Children's `required` is enforced when the object itself is required OR
221
+ // a parent context (e.g. a blocks item) says it must be validated.
162
222
  const data = generateZodSchemaFromFields(field.fields, languages, {
163
- parentRequired: field.required,
164
- localized: options.localized
223
+ parentRequired: !!field.required || options.parentRequired === true,
224
+ localized: options.localized,
225
+ messageLanguage: options.messageLanguage
165
226
  });
227
+ // An optional object stays `.partial()` (a non-required field may be
228
+ // absent); a present field that is `required` still has to satisfy its
229
+ // `.min(1)`/etc. constraint.
166
230
  const base = !field.required ? data.partial() : data;
167
231
  return base.extend({
168
232
  _slug: z.literal(field.slug)
169
233
  });
170
234
  }
171
235
  case 'blocks': {
236
+ // Each block item, once added, should have its own required fields filled
237
+ // (validation runs only on publish, drafts are unvalidated).
172
238
  const schemas = field.of.map((f) => generateZodSchemaFromField(f, languages, {
173
- parentRequired: field.required,
174
- localized: options.localized
239
+ parentRequired: true,
240
+ localized: options.localized,
241
+ messageLanguage: options.messageLanguage
175
242
  }));
176
243
  const itemSchema = schemas.length > 1
177
244
  ? z.discriminatedUnion('_slug', schemas)
@@ -182,17 +249,17 @@ export function generateZodSchemaFromField(field, languages, options = {
182
249
  field.minItems === field.maxItems &&
183
250
  field.minItems > 0) {
184
251
  schema = schema.length(field.minItems, {
185
- message: `Wymagane dokładnie ${field.minItems} elementów`
252
+ message: msg.exactItems(field.minItems)
186
253
  });
187
254
  }
188
255
  else {
189
256
  if (field.minItems !== undefined)
190
257
  schema = schema.min(field.minItems, {
191
- message: `Minimum ${field.minItems} elementów`
258
+ message: msg.minItems(field.minItems)
192
259
  });
193
260
  if (field.maxItems !== undefined)
194
261
  schema = schema.max(field.maxItems, {
195
- message: `Maksimum ${field.maxItems} elementów`
262
+ message: msg.maxItems(field.maxItems)
196
263
  });
197
264
  }
198
265
  return schema.default([]); // Default to empty array if not required
@@ -208,53 +275,83 @@ export function generateZodSchemaFromField(field, languages, options = {
208
275
  case 'number':
209
276
  itemSchema = z.number();
210
277
  break;
211
- case 'url':
212
- itemSchema = z.object({
213
- id: z.string().optional(),
214
- url: z.record(z.string(), z.string()),
215
- text: z.record(z.string(), z.string()).optional(),
216
- newTab: z.boolean().optional()
217
- });
278
+ case 'url': {
279
+ // Harmonized with the standalone url field: same shape, same
280
+ // superRefine. Each item carries url/id/text/newTab/rel,
281
+ // respects `localized` like the rest of the schema, and
282
+ // rejects malformed values via isValidUrlValue.
283
+ if (!localized) {
284
+ itemSchema = z
285
+ .object({
286
+ url: z.string().default(''),
287
+ id: z.string().optional(),
288
+ text: z.string().default('').optional(),
289
+ newTab: z.boolean().optional(),
290
+ rel: z.string().optional()
291
+ })
292
+ .superRefine((data, ctx) => {
293
+ const isLinked = typeof data.id === 'string' && data.id.length > 0;
294
+ if (isLinked)
295
+ return;
296
+ if (!isValidUrlValue(data.url ?? '')) {
297
+ ctx.addIssue({
298
+ code: z.ZodIssueCode.custom,
299
+ message: msg.invalidUrl,
300
+ path: ['url']
301
+ });
302
+ }
303
+ });
304
+ }
305
+ else {
306
+ const langString = z.string().default('');
307
+ itemSchema = z
308
+ .object({
309
+ url: z.object(Object.fromEntries(languages.map((lang) => [lang, langString]))),
310
+ id: z.string().optional(),
311
+ text: z
312
+ .object(Object.fromEntries(languages.map((lang) => [lang, langString])))
313
+ .optional(),
314
+ newTab: z.boolean().optional(),
315
+ rel: z.string().optional()
316
+ })
317
+ .superRefine((data, ctx) => {
318
+ const isLinked = typeof data.id === 'string' && data.id.length > 0;
319
+ if (isLinked)
320
+ return;
321
+ for (const lang of languages) {
322
+ const val = data.url[lang] ?? '';
323
+ if (!isValidUrlValue(val)) {
324
+ ctx.addIssue({
325
+ code: z.ZodIssueCode.custom,
326
+ message: msg.invalidUrl,
327
+ path: ['url', lang]
328
+ });
329
+ }
330
+ }
331
+ });
332
+ }
218
333
  break;
334
+ }
219
335
  }
220
336
  let schema = z.array(itemSchema);
221
337
  if (field.minItems !== undefined)
222
338
  schema = schema.min(field.minItems, {
223
- message: `Minimum ${field.minItems} elementów`
339
+ message: msg.minItems(field.minItems)
224
340
  });
225
341
  if (field.maxItems !== undefined)
226
342
  schema = schema.max(field.maxItems, {
227
- message: `Maksimum ${field.maxItems} elementów`
343
+ message: msg.maxItems(field.maxItems)
228
344
  });
229
345
  return schema.default([]);
230
346
  }
231
347
  case 'slug': {
232
- let schema = z.string();
233
- if (field.required)
234
- schema = schema.min(1, { message: 'Pole wymagane' });
235
- return schema;
348
+ if (field.required) {
349
+ return z.string().min(1, { message: msg.required });
350
+ }
351
+ return z.string().optional().default('');
236
352
  }
237
353
  case 'seo': {
238
- if (!localized) {
239
- return z.object({
240
- slug: z.string(),
241
- canonicalUrl: z.string().optional(),
242
- title: z.string(),
243
- description: z.string(),
244
- keywords: z.string().optional(),
245
- ogImage: z.string(),
246
- customCode: z.string().optional()
247
- });
248
- }
249
- return z.object({
250
- slug: z.object(Object.fromEntries(languages.map((lang) => [lang, z.string()]))),
251
- canonicalUrl: z.object(Object.fromEntries(languages.map((lang) => [lang, z.string().optional()]))),
252
- title: z.object(Object.fromEntries(languages.map((lang) => [lang, z.string()]))),
253
- description: z.object(Object.fromEntries(languages.map((lang) => [lang, z.string()]))),
254
- keywords: z.object(Object.fromEntries(languages.map((lang) => [lang, z.string().optional()]))),
255
- ogImage: z.string(),
256
- customCode: z.object(Object.fromEntries(languages.map((lang) => [lang, z.string().optional()])))
257
- });
354
+ return z.object(buildSeoZodShape(languages, localized));
258
355
  }
259
356
  case 'shop': {
260
357
  // Shop data is stored in shop_products table (keyed by entry_id), not in entry.data.
@@ -271,7 +368,36 @@ export function generateZodSchemaFromField(field, languages, options = {
271
368
  flatShape.text = z.string().default('');
272
369
  if (field.newTab)
273
370
  flatShape.newTab = z.boolean().optional();
274
- return z.object(flatShape);
371
+ const flatSchema = z.object(flatShape).superRefine((data, ctx) => {
372
+ const isLinked = typeof data.id === 'string' && data.id.length > 0;
373
+ if (isLinked)
374
+ return;
375
+ const url = typeof data.url === 'string' ? data.url : '';
376
+ if (field.required && url.trim() === '') {
377
+ ctx.addIssue({
378
+ code: z.ZodIssueCode.too_small,
379
+ minimum: 1,
380
+ type: 'string',
381
+ inclusive: true,
382
+ message: msg.required,
383
+ path: ['url']
384
+ });
385
+ return;
386
+ }
387
+ if (!isValidUrlValue(url)) {
388
+ ctx.addIssue({
389
+ code: z.ZodIssueCode.custom,
390
+ message: msg.invalidUrl,
391
+ path: ['url']
392
+ });
393
+ }
394
+ });
395
+ if (field.required)
396
+ return flatSchema;
397
+ const flatDefault = { url: '' };
398
+ if (field.text)
399
+ flatDefault.text = '';
400
+ return flatSchema.optional().default(flatDefault);
275
401
  }
276
402
  const langString = z.string().default('');
277
403
  const urlShape = {
@@ -284,27 +410,39 @@ export function generateZodSchemaFromField(field, languages, options = {
284
410
  if (field.newTab) {
285
411
  urlShape.newTab = z.boolean().optional();
286
412
  }
287
- const schema = z.object(urlShape);
288
- if (field.required) {
289
- return schema.superRefine((data, ctx) => {
290
- const isLinked = data.id != null && data.id.length > 0;
291
- if (isLinked)
292
- return;
293
- for (const lang of languages) {
294
- if (!data.url[lang]) {
295
- ctx.addIssue({
296
- code: z.ZodIssueCode.too_small,
297
- minimum: 1,
298
- type: 'string',
299
- inclusive: true,
300
- message: 'Pole wymagane',
301
- path: ['url', lang]
302
- });
303
- }
413
+ const localizedSchema = z.object(urlShape).superRefine((data, ctx) => {
414
+ const isLinked = data.id != null && data.id.length > 0;
415
+ if (isLinked)
416
+ return;
417
+ for (const lang of languages) {
418
+ const val = data.url[lang] ?? '';
419
+ if (field.required && val.trim() === '') {
420
+ ctx.addIssue({
421
+ code: z.ZodIssueCode.too_small,
422
+ minimum: 1,
423
+ type: 'string',
424
+ inclusive: true,
425
+ message: msg.required,
426
+ path: ['url', lang]
427
+ });
428
+ continue;
304
429
  }
305
- });
306
- }
307
- return schema;
430
+ if (!isValidUrlValue(val)) {
431
+ ctx.addIssue({
432
+ code: z.ZodIssueCode.custom,
433
+ message: msg.invalidUrl,
434
+ path: ['url', lang]
435
+ });
436
+ }
437
+ }
438
+ });
439
+ if (field.required)
440
+ return localizedSchema;
441
+ const emptyLangMap = Object.fromEntries(languages.map((l) => [l, '']));
442
+ const localizedDefault = { url: emptyLangMap };
443
+ if (field.text)
444
+ localizedDefault.text = emptyLangMap;
445
+ return localizedSchema.optional().default(localizedDefault);
308
446
  }
309
447
  case 'custom': {
310
448
  const customDef = getCustomFieldDef(field.fieldType);
@@ -317,9 +455,15 @@ export function generateZodSchemaFromField(field, languages, options = {
317
455
  }
318
456
  }
319
457
  export function generateZodSchemaFromFields(fields, languages, options = {}) {
458
+ // Top-level fields are required-validated by default (consistent with the
459
+ // single-field default). Nested callers (object/blocks/content) always pass
460
+ // an explicit `parentRequired`; entry-level callers only pass `localized`,
461
+ // so without this merge a top-level `required` text field never got
462
+ // `.min(1)` and empty values slipped through validation.
463
+ const resolvedOptions = { parentRequired: true, ...options };
320
464
  return z.object({
321
465
  ...Object.fromEntries(fields.map((f) => {
322
- let schema = generateZodSchemaFromField(f, languages, options);
466
+ let schema = generateZodSchemaFromField(f, languages, resolvedOptions);
323
467
  // Fields with showWhen are conditionally visible — make them optional
324
468
  // so hidden fields don't block validation
325
469
  if (f.showWhen) {
@@ -31,9 +31,12 @@ export declare function getDistributedObjectSlugs(nodes: LayoutNode[], fields: F
31
31
  */
32
32
  export declare function buildFormPath(dotPath: string): string;
33
33
  export interface LayoutValidationError {
34
- type: 'missing_field' | 'duplicate_field' | 'depth_exceeded' | 'columns_mismatch';
34
+ type: 'missing_field' | 'duplicate_field' | 'depth_exceeded' | 'columns_mismatch' | 'nested_tabs' | 'invalid_tabs_structure';
35
35
  message: string;
36
36
  }
37
+ /** Expand layout field references (incl. dot-notation & whole objects) into
38
+ * concrete leaf paths. Used by the renderer to map a tab subtree → its fields. */
39
+ export declare function collectLeafPathsForNodes(nodes: LayoutNode[], fields: Field[]): string[];
37
40
  /** Validate layout against fields — returns errors or empty array.
38
41
  * Supports dot-notation paths (e.g. 'hero.title'). */
39
42
  export declare function validateLayout(nodes: LayoutNode[], fields: Field[]): LayoutValidationError[];
@@ -109,6 +109,15 @@ export function buildFormPath(dotPath) {
109
109
  function columnCount(ratio) {
110
110
  return ratio.split(' ').length;
111
111
  }
112
+ /** Expand layout field references (incl. dot-notation & whole objects) into
113
+ * concrete leaf paths. Used by the renderer to map a tab subtree → its fields. */
114
+ export function collectLeafPathsForNodes(nodes, fields) {
115
+ const leaves = [];
116
+ for (const ref of collectFieldSlugs(nodes)) {
117
+ leaves.push(...expandToLeafPaths(ref, fields));
118
+ }
119
+ return leaves;
120
+ }
112
121
  /** Validate layout against fields — returns errors or empty array.
113
122
  * Supports dot-notation paths (e.g. 'hero.title'). */
114
123
  export function validateLayout(nodes, fields) {
@@ -140,13 +149,41 @@ export function validateLayout(nodes, fields) {
140
149
  seenLeaves.add(leaf);
141
150
  }
142
151
  }
143
- // Check depth + columns
144
- function walk(nodeList, depth) {
152
+ // Check depth + columns + tabs structure.
153
+ // `tabs`/`tab` are structural wrappers — they do NOT count toward visual
154
+ // nesting depth, but `tabs` may not be nested inside another `tab`/`tabs`.
155
+ function walk(nodeList, depth, insideTabs) {
145
156
  if (depth > 2) {
146
157
  errors.push({ type: 'depth_exceeded', message: `Nesting depth ${depth} exceeds max of 2` });
147
158
  return;
148
159
  }
149
160
  for (const node of nodeList) {
161
+ if (node.type === 'tabs') {
162
+ if (insideTabs) {
163
+ errors.push({ type: 'nested_tabs', message: 'Nested tabs are not allowed' });
164
+ }
165
+ for (const child of node.children) {
166
+ if (child.type !== 'tab') {
167
+ errors.push({
168
+ type: 'invalid_tabs_structure',
169
+ message: `"tabs" children must be "tab" nodes, got "${child.type}"`
170
+ });
171
+ }
172
+ }
173
+ for (const child of node.children) {
174
+ if (child.type === 'tab' && isLayoutBranch(child)) {
175
+ walk(child.children, depth, true);
176
+ }
177
+ }
178
+ continue;
179
+ }
180
+ if (node.type === 'tab') {
181
+ errors.push({
182
+ type: 'invalid_tabs_structure',
183
+ message: '"tab" is only valid as a direct child of "tabs"'
184
+ });
185
+ continue;
186
+ }
150
187
  if (node.type === 'columns') {
151
188
  const expected = columnCount(node.ratio);
152
189
  const actual = node.children?.length ?? 0;
@@ -158,11 +195,11 @@ export function validateLayout(nodes, fields) {
158
195
  }
159
196
  }
160
197
  if (isLayoutBranch(node)) {
161
- walk(node.children, depth + 1);
198
+ walk(node.children, depth + 1, insideTabs);
162
199
  }
163
200
  }
164
201
  }
165
- walk(nodes, 0);
202
+ walk(nodes, 0, false);
166
203
  return errors;
167
204
  }
168
205
  /** Expand a preset into LayoutNode[] */
@@ -0,0 +1,70 @@
1
+ import type { SeoFieldData } from '../../types/fields.js';
2
+ import type { MediaFile } from '../../types/media.js';
3
+ /**
4
+ * Normalized SEO data — localized values flattened by `language`. A stable
5
+ * read-path over the v1.0-frozen `entry.seo.*` contract. Use it to build a
6
+ * custom `<head>` or to feed the `<Seo>` component.
7
+ *
8
+ * @remarks
9
+ * Every field is optional, even though `slug` and `title` are *required* in
10
+ * {@link SeoFieldData}: a missing, partial, or garbage `entry.seo` resolves to
11
+ * an all-`undefined` shape rather than throwing.
12
+ *
13
+ * `ogImage` is special. The frozen SEO Zod schema treats it as a flat
14
+ * `z.string()` (never per-language localized), so at rest it is a media-UUID
15
+ * string. After server media-hydration it becomes a {@link MediaFile} object.
16
+ * `resolveSeo` therefore passes `ogImage` through unchanged — it may be a
17
+ * `MediaFile` (hydrated) or a string (raw), and is never language-flattened.
18
+ *
19
+ * @public
20
+ */
21
+ export type ResolvedSeo = {
22
+ [K in keyof SeoFieldData]: K extends 'ogImage' ? string | MediaFile | undefined : string | undefined;
23
+ };
24
+ /**
25
+ * Resolve `entry.seo` into flat fields, flattening localized objects by
26
+ * `language`.
27
+ *
28
+ * For each of the six text keys: a `string` value passes through; a localized
29
+ * `{ lang: string }` object resolves to `obj[language]` (or `undefined` if the
30
+ * requested language is missing); anything else resolves to `undefined`.
31
+ *
32
+ * `ogImage` is exempt from flattening: it passes through unchanged as a string
33
+ * or a {@link MediaFile} object (only `null`/`undefined` → `undefined`).
34
+ *
35
+ * The name `resolveSeo` is a deliberate, v1.0-frozen choice — it aligns with
36
+ * the `resolveEntryUrl` / `resolveMedia*` resolver family.
37
+ *
38
+ * @param entry - The (possibly resolved/hydrated) entry, or any object with an
39
+ * optional `seo` property. A non-object or missing `seo` yields all-`undefined`.
40
+ * @param language - The active language code. Required to flatten localized
41
+ * `{ lang: string }` values; localized fields resolve to `undefined` without it.
42
+ * @returns A {@link ResolvedSeo} — all keys present, each `string`/`MediaFile`
43
+ * (only `ogImage`) or `undefined`.
44
+ * @public
45
+ * @example
46
+ * ```svelte
47
+ * <script lang="ts">
48
+ * import { resolveSeo } from 'includio-cms/core';
49
+ * import { Seo } from 'includio-cms';
50
+ * let { entry, language } = $props();
51
+ * const seo = $derived(resolveSeo(entry, language));
52
+ * </script>
53
+ *
54
+ * <svelte:head>
55
+ * {#if seo.title}<title>{seo.title}</title>{/if}
56
+ * {#if seo.canonicalUrl}<link rel="canonical" href={seo.canonicalUrl} />{/if}
57
+ * </svelte:head>
58
+ *
59
+ * <Seo
60
+ * title={seo.title}
61
+ * description={seo.description}
62
+ * keywords={seo.keywords}
63
+ * ogImage={seo.ogImage}
64
+ * canonicalUrl={seo.canonicalUrl}
65
+ * />
66
+ * ```
67
+ */
68
+ export declare function resolveSeo(entry: {
69
+ seo?: Record<string, unknown>;
70
+ } | Record<string, unknown>, language?: string): ResolvedSeo;