includio-cms 0.25.0 → 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
@@ -6,7 +6,16 @@
6
6
  } from 'sveltekit-superforms';
7
7
  import { onMount } from 'svelte';
8
8
  import Input from '../../../components/ui/input/input.svelte';
9
+ import Button from '../../../components/ui/button/button.svelte';
10
+ import { Switch } from '../../../components/ui/switch/index.js';
11
+ import RequiredLabel from './required-label.svelte';
12
+ import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
13
+ import { getLocalizedLabel } from '../../utils/collectionLabel.js';
9
14
  import slugify from '../../imports/slugify.js';
15
+ import { UseClipboard } from '../../../components/hooks/use-clipboard.svelte.js';
16
+ import { toast } from 'svelte-sonner';
17
+ import Copy from '@tabler/icons-svelte/icons/copy';
18
+ import Check from '@tabler/icons-svelte/icons/check';
10
19
  import type { SlugField } from '../../../types/fields.js';
11
20
 
12
21
  type Props = {
@@ -17,45 +26,28 @@
17
26
 
18
27
  let { field, form, path, ...props }: Props = $props();
19
28
 
29
+ const interfaceLanguage = useInterfaceLanguage();
30
+
20
31
  let effectivePattern = field.pattern || (field.sourceField ? `{${field.sourceField}}` : undefined);
21
32
  let pattern = effectivePattern;
22
33
 
23
34
  const { form: formData } = form;
24
-
25
35
  const { value } = formFieldProxy(form, path);
26
36
 
27
37
  let patternKeys = effectivePattern ? extractPatternKeys(effectivePattern) : [];
38
+ const clipboard = new UseClipboard();
28
39
 
29
40
  function extractPatternKeys(pattern: string): string[] {
30
41
  const matches = [...pattern.matchAll(/{([^}]+)}/g)];
31
42
  return matches.map(([, key]) => key);
32
43
  }
33
44
 
34
- onMount(() => {
35
- if (pattern) {
36
- patternKeys.map((key) => {
37
- const { value: valueStore } = formFieldProxy(
38
- form,
39
- key as FormPathLeaves<Record<string, unknown>>
40
- );
41
- valueStore.subscribe(() => {
42
- try {
43
- $value = generateSlug(pattern);
44
- } catch {
45
- // Pattern key not yet filled - ignore
46
- }
47
- });
48
- });
49
- }
50
- });
51
-
52
45
  function generateSlug(pattern: string) {
53
46
  const replacedPattern = pattern.replace(/{([^}]+)}/g, (_, key) => {
54
47
  const keyValue = $formData[key];
55
48
  if (keyValue === undefined) {
56
49
  throw new Error(`Key not found in form data: ${key}`);
57
50
  }
58
-
59
51
  return String(keyValue);
60
52
  });
61
53
 
@@ -65,6 +57,139 @@
65
57
  trim: true
66
58
  });
67
59
  }
60
+
61
+ // Auto/manual toggle, mirroring the SEO slug UX. Default mode is inferred
62
+ // from the current value: an empty/exact-match-with-pattern slug stays in
63
+ // auto mode (source-of-truth = title); anything diverging means the
64
+ // editor took ownership and we start in manual.
65
+ let autoMode = $state(
66
+ (() => {
67
+ if (!pattern) return false;
68
+ const current = typeof $value === 'string' ? $value : '';
69
+ if (current === '') return true;
70
+ try {
71
+ return current === generateSlug(pattern);
72
+ } catch {
73
+ return false;
74
+ }
75
+ })()
76
+ );
77
+
78
+ onMount(() => {
79
+ if (!pattern) return;
80
+ const unsubscribers = patternKeys.map((key) => {
81
+ const { value: valueStore } = formFieldProxy(
82
+ form,
83
+ key as FormPathLeaves<Record<string, unknown>>
84
+ );
85
+ return valueStore.subscribe(() => {
86
+ if (!autoMode) return;
87
+ try {
88
+ $value = generateSlug(pattern);
89
+ } catch {
90
+ // Pattern key not yet filled - ignore
91
+ }
92
+ });
93
+ });
94
+ return () => unsubscribers.forEach((u) => u());
95
+ });
96
+
97
+ function onSlugInput() {
98
+ // Manual edit while auto is on flips to manual silently — the toggle
99
+ // state reflects what the editor actually controls.
100
+ if (autoMode) autoMode = false;
101
+ }
102
+
103
+ function onAutoToggle(checked: boolean) {
104
+ autoMode = checked;
105
+ if (!checked || !pattern) return;
106
+ try {
107
+ $value = generateSlug(pattern);
108
+ } catch {
109
+ // Source field empty — keep current value, auto will fill once typed
110
+ }
111
+ }
112
+
113
+ const previewSlug = $derived(
114
+ typeof $value === 'string' && $value
115
+ ? slugify($value, { lower: true, strict: true, trim: true })
116
+ : ''
117
+ );
118
+ const showPreview = $derived(previewSlug && previewSlug !== $value);
119
+
120
+ async function copySlug() {
121
+ const text = typeof $value === 'string' ? $value : '';
122
+ if (!text) return;
123
+ const result = await clipboard.copy(text);
124
+ if (result === 'success') {
125
+ toast.success('Skopiowano slug');
126
+ } else {
127
+ toast.error('Nie udało się skopiować');
128
+ }
129
+ }
68
130
  </script>
69
131
 
70
- <Input {...props} bind:value={$value} type="text" placeholder={field.pattern} />
132
+ <!-- space-y-2 = 8px gap (matches other field types via form-field.svelte) -->
133
+ <div class="space-y-2">
134
+ {#if field.label || pattern}
135
+ <div class="flex items-center justify-between gap-3">
136
+ {#if field.label}
137
+ <RequiredLabel required={field.required}>
138
+ {getLocalizedLabel(field.label, interfaceLanguage.current)}
139
+ </RequiredLabel>
140
+ {:else}
141
+ <span></span>
142
+ {/if}
143
+ {#if pattern}
144
+ <div class="flex items-center gap-2">
145
+ <span class="text-muted-foreground text-sm font-medium">Auto</span>
146
+ <Switch bind:checked={autoMode} onCheckedChange={onAutoToggle} aria-label="Generuj slug automatycznie z tytułu" />
147
+ </div>
148
+ {/if}
149
+ </div>
150
+ {/if}
151
+
152
+ <div class="flex items-stretch gap-2">
153
+ <Input
154
+ {...props}
155
+ bind:value={$value}
156
+ oninput={onSlugInput}
157
+ type="text"
158
+ placeholder={field.pattern}
159
+ readonly={autoMode}
160
+ aria-readonly={autoMode}
161
+ class="flex-1"
162
+ />
163
+ {#if typeof $value === 'string' && $value}
164
+ <Button
165
+ type="button"
166
+ variant="outline"
167
+ size="icon"
168
+ aria-label="Skopiuj slug"
169
+ onclick={copySlug}
170
+ >
171
+ {#if clipboard.copied}
172
+ <Check class="size-4" />
173
+ {:else}
174
+ <Copy class="size-4" />
175
+ {/if}
176
+ </Button>
177
+ {/if}
178
+ </div>
179
+
180
+ {#if pattern}
181
+ <p class="text-muted-foreground text-xs">
182
+ {#if autoMode}
183
+ Slug generowany automatycznie z tytułu. Wyłącz „Auto", żeby edytować ręcznie.
184
+ {:else}
185
+ Slug ustawiony ręcznie. Włącz „Auto", żeby śledził tytuł.
186
+ {/if}
187
+ </p>
188
+ {/if}
189
+
190
+ {#if showPreview}
191
+ <p class="text-muted-foreground text-xs">
192
+ Wygenerowany: <code class="bg-muted rounded px-1 py-0.5">{previewSlug}</code>
193
+ </p>
194
+ {/if}
195
+ </div>
@@ -28,8 +28,13 @@
28
28
  const formData = form.form;
29
29
 
30
30
  const isText = $derived(field.type === 'text');
31
+ const patternHintText = $derived(
32
+ field.type === 'text' && field.patternHint
33
+ ? getLocalizedLabel(field.patternHint, interfaceLanguage.current)
34
+ : ''
35
+ );
31
36
  const hasConstraints = $derived(
32
- isText && (field.minLength !== undefined || field.maxLength !== undefined || field.pattern !== undefined)
37
+ isText && (field.minLength !== undefined || field.maxLength !== undefined || !!patternHintText)
33
38
  );
34
39
 
35
40
  function resolvePathValue(data: Record<string, unknown>, dotPath: string): unknown {
@@ -80,22 +85,33 @@
80
85
  {@const val = resolvePathValue($formData, path)}
81
86
  {@const charCount = typeof val === 'string' ? val.length : 0}
82
87
  {@const atLimit = field.type === 'text' && field.maxLength !== undefined && charCount >= field.maxLength}
83
- <div class="flex items-start justify-between gap-4">
84
- {#if field.description || hasConstraints}
85
- <Form.Description class="flex-1">
86
- {#if field.description}{getLocalizedLabel(field.description, interfaceLanguage.current)}{/if}
87
- {#if hasConstraints}
88
- {#if field.description && constraintHint()}<br />{/if}
89
- {#if constraintHint()}{constraintHint()}{/if}
90
- {#if field.type === 'text' && field.pattern}
91
- {#if constraintHint()} · {/if}Format: <code class="text-xs bg-muted px-1 py-0.5 rounded">{field.pattern}</code>
88
+ <!--
89
+ Error + description share the left cell of one flex row with the char
90
+ counter on the right — so the error sits right under THIS input instead
91
+ of dropping a row below the counter (where it reads as the next field's).
92
+ -->
93
+ <div class="flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between sm:gap-4">
94
+ <div class="order-2 flex-1 space-y-1 sm:order-1">
95
+ {#if field.description || hasConstraints}
96
+ <Form.Description>
97
+ {#if field.description}{getLocalizedLabel(field.description, interfaceLanguage.current)}{/if}
98
+ {#if hasConstraints}
99
+ {#if field.description && (constraintHint() || patternHintText)}<br />{/if}
100
+ {#if constraintHint()}{constraintHint()}{/if}
101
+ {#if patternHintText}
102
+ {#if constraintHint()} · {/if}{patternHintText}
103
+ {/if}
92
104
  {/if}
93
- {/if}
94
- </Form.Description>
95
- {:else}
96
- <div></div>
97
- {/if}
98
- <span class="shrink-0 text-xs {atLimit ? 'text-destructive' : 'text-muted-foreground'}" aria-live="polite">
105
+ </Form.Description>
106
+ {/if}
107
+ <Form.FieldErrors />
108
+ </div>
109
+ <span
110
+ class="order-1 self-end shrink-0 text-xs sm:order-2 sm:self-auto {atLimit
111
+ ? 'text-destructive'
112
+ : 'text-muted-foreground'}"
113
+ aria-live="polite"
114
+ >
99
115
  {#if field.type === 'text' && field.maxLength !== undefined}
100
116
  {charCount} / {field.maxLength}
101
117
  {:else if field.type === 'text' && field.minLength !== undefined}
@@ -105,9 +121,10 @@
105
121
  {/if}
106
122
  </span>
107
123
  </div>
108
- {:else if field.description}
109
- <Form.Description>{getLocalizedLabel(field.description, interfaceLanguage.current)}</Form.Description>
124
+ {:else}
125
+ {#if field.description}
126
+ <Form.Description>{getLocalizedLabel(field.description, interfaceLanguage.current)}</Form.Description>
127
+ {/if}
128
+ <Form.FieldErrors />
110
129
  {/if}
111
-
112
- <Form.FieldErrors />
113
130
  </Form.Field>
@@ -22,8 +22,13 @@
22
22
  });
23
23
  </script>
24
24
 
25
+ <!--
26
+ No hard `minlength`/`maxlength` attrs: silently blocking input is hostile to
27
+ cognitive/AT users (COGA, WCAG 3.3). The char counter + schema validation in
28
+ text-field-wrapper communicate the limit instead.
29
+ -->
25
30
  {#if field.multiline}
26
- <Textarea {...props} bind:value placeholder={getLocalizedLabel(field.placeholder, interfaceLanguage.current)} minlength={field.minLength} maxlength={field.maxLength} />
31
+ <Textarea {...props} bind:value placeholder={getLocalizedLabel(field.placeholder, interfaceLanguage.current)} />
27
32
  {:else}
28
- <Input {...props} bind:value type="text" placeholder={getLocalizedLabel(field.placeholder, interfaceLanguage.current)} minlength={field.minLength} maxlength={field.maxLength} />
33
+ <Input {...props} bind:value type="text" placeholder={getLocalizedLabel(field.placeholder, interfaceLanguage.current)} />
29
34
  {/if}
@@ -11,6 +11,7 @@
11
11
  type FormPathLeaves,
12
12
  type SuperForm
13
13
  } from 'sveltekit-superforms';
14
+ import * as Form from '../../../components/ui/form/index.js';
14
15
  import UrlFieldComponent from './url-field.svelte';
15
16
 
16
17
  type Props = {
@@ -59,4 +60,13 @@
59
60
 
60
61
  {#if fieldValid && $value}
61
62
  <UrlFieldComponent {field} bind:value={$value} {...props} />
63
+ <!--
64
+ Schema reports issues at `<path>.url` (the actual string field inside the
65
+ composite url object). The outer Form.Field in field-renderer is on `path`
66
+ (root), so its FieldErrors never sees the nested issue. A scoped sibling
67
+ Field surfaces it at the input.
68
+ -->
69
+ <Form.Field {form} name={`${path}.url` as FormPathLeaves<T>}>
70
+ <Form.FieldErrors />
71
+ </Form.Field>
62
72
  {/if}
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { SeoFieldData, UrlField, UrlFieldData } from '../../../types/fields.js';
2
+ import type { UrlField, UrlFieldData } from '../../../types/fields.js';
3
3
  import { getRemotes } from '../../../sveltekit/index.js';
4
4
  import type { Entry } from '../../../types/entries.js';
5
5
  import * as InputGroup from '../../../components/ui/input-group/index.js';
@@ -7,6 +7,7 @@
7
7
  import Checkbox from '../../../components/ui/checkbox/checkbox.svelte';
8
8
  import Label from '../../../components/ui/label/label.svelte';
9
9
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
10
+ import { getContentLanguage } from '../../state/content-language.svelte.js';
10
11
  import { getLocalizedLabel } from '../../utils/collectionLabel.js';
11
12
  import { debounce } from '../../utils/debounce.js';
12
13
  import { isExternalUrl } from '../../../core/fields/urlUtils.js';
@@ -20,6 +21,7 @@
20
21
 
21
22
  const remotes = getRemotes();
22
23
  const interfaceLanguage = useInterfaceLanguage();
24
+ const contentLanguage = getContentLanguage();
23
25
 
24
26
  const labels = {
25
27
  linkText: { en: 'Displayed link text', pl: 'Tekst wyświetlany jako link' },
@@ -38,11 +40,14 @@
38
40
  internal: { en: 'Internal', pl: 'Wewnętrzny' }
39
41
  };
40
42
 
41
- type EntryWithSeo = {
43
+ type LinkSuggestion = {
42
44
  _id: string;
43
45
  _slug: string;
44
- seo?: SeoFieldData;
46
+ _url?: string;
47
+ title: string;
48
+ slug: string;
45
49
  };
50
+ type LinkedDisplay = { _id: string; title: string; slug: string };
46
51
 
47
52
  type Props = {
48
53
  field: UrlField;
@@ -60,9 +65,9 @@
60
65
  // Autocomplete state
61
66
  let popoverOpen = $state(false);
62
67
  let activeIndex = $state(-1);
63
- let linkedEntry = $state<EntryWithSeo | null>(null);
68
+ let linkedEntry = $state<LinkedDisplay | null>(null);
64
69
  let searchTerm = $state('');
65
- let autocompleteSuggestions = $state<EntryWithSeo[]>([]);
70
+ let autocompleteSuggestions = $state<LinkSuggestion[]>([]);
66
71
 
67
72
  $effect(() => {
68
73
  const term = searchTerm;
@@ -73,16 +78,12 @@
73
78
  let cancelled = false;
74
79
  (async () => {
75
80
  try {
76
- const [slugResults, titleResults] = (await Promise.all([
77
- remotes.getEntries({ dataLike: { seo: { slug: term } } }),
78
- remotes.getEntries({ dataLike: { seo: { title: term } } })
79
- ])) as [EntryWithSeo[], EntryWithSeo[]];
81
+ const results = (await remotes.searchLinkableEntries({
82
+ term,
83
+ language: contentLanguage.current
84
+ })) as LinkSuggestion[];
80
85
  if (cancelled) return;
81
- const combined = [...slugResults, ...titleResults];
82
- const deduped = combined.filter(
83
- (entry, index, self) => index === self.findIndex((e) => e._id === entry._id)
84
- );
85
- autocompleteSuggestions = deduped.slice(0, 5);
86
+ autocompleteSuggestions = results;
86
87
  } catch {
87
88
  if (!cancelled) autocompleteSuggestions = [];
88
89
  }
@@ -107,9 +108,20 @@
107
108
  }
108
109
  });
109
110
 
110
- async function fetchLinkedEntry(id: string | undefined): Promise<EntryWithSeo | null> {
111
+ async function fetchLinkedEntry(id: string | undefined): Promise<LinkedDisplay | null> {
111
112
  if (!id) return null;
112
- return remotes.getEntry({ id }) as Promise<EntryWithSeo | null>;
113
+ const entry = (await remotes.getEntry({
114
+ id,
115
+ language: contentLanguage.current
116
+ })) as
117
+ | { _id: string; seo?: { title?: string; slug?: string } }
118
+ | null;
119
+ if (!entry) return null;
120
+ return {
121
+ _id: entry._id,
122
+ title: typeof entry.seo?.title === 'string' ? entry.seo.title : '',
123
+ slug: typeof entry.seo?.slug === 'string' ? entry.seo.slug : ''
124
+ };
113
125
  }
114
126
 
115
127
  const applySearchTerm = debounce((url: string) => {
@@ -130,9 +142,9 @@
130
142
  activeIndex = -1;
131
143
  }
132
144
 
133
- function linkEntry(entry: EntryWithSeo) {
145
+ function linkEntry(entry: LinkSuggestion) {
134
146
  value.id = entry._id;
135
- linkedEntry = entry;
147
+ linkedEntry = { _id: entry._id, title: entry.title, slug: entry.slug };
136
148
  searchTerm = '';
137
149
  popoverOpen = false;
138
150
  }
@@ -191,11 +203,11 @@
191
203
  <div class="flex min-w-0 flex-1 items-center gap-2 py-1.5">
192
204
  <Badge variant="secondary" class="max-w-full gap-1.5 truncate">
193
205
  <span class="truncate font-medium">
194
- {linkedEntry.seo?.title || linkedEntry._id}
206
+ {linkedEntry.title || linkedEntry._id}
195
207
  </span>
196
- {#if linkedEntry.seo?.slug}
208
+ {#if linkedEntry.slug}
197
209
  <span class="text-muted-foreground truncate text-[10px] font-normal">
198
- {linkedEntry.seo.slug}
210
+ {linkedEntry.slug}
199
211
  </span>
200
212
  {/if}
201
213
  </Badge>
@@ -217,6 +229,7 @@
217
229
  <GlobeIcon class="size-4" />
218
230
  </InputGroup.Addon>
219
231
  <InputGroup.Input
232
+ {...props}
220
233
  bind:value={value.url}
221
234
  type="text"
222
235
  placeholder={getLocalizedLabel(labels.placeholder, interfaceLanguage.current)}
@@ -264,10 +277,10 @@
264
277
  >
265
278
  <LinkIcon class="text-muted-foreground size-3.5 shrink-0" />
266
279
  <span class="truncate text-sm">
267
- {suggestion.seo?.title || getLocalizedLabel(labels.noTitle, interfaceLanguage.current)}
280
+ {suggestion.title || getLocalizedLabel(labels.noTitle, interfaceLanguage.current)}
268
281
  </span>
269
282
  <span class="text-muted-foreground ml-auto shrink-0 text-xs">
270
- {suggestion.seo?.slug || ''}
283
+ {suggestion.slug || suggestion._url || ''}
271
284
  </span>
272
285
  </button>
273
286
  {/each}
@@ -0,0 +1,143 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * @internal
4
+ * Form error summary banner — renders above forms when validation fails.
5
+ * Aria-live alert announces errors to screen readers; click anchor scrolls
6
+ * to `[data-field-path="…"]` and focuses the input inside.
7
+ *
8
+ * Used by entry-page (S8), coupon-form, shipping-method-form, shop-order-detail.
9
+ */
10
+ export type FormErrorEntry = {
11
+ /** Dot/bracket path matching `data-field-path` in the form. */
12
+ path: string;
13
+ /** Localized error message. */
14
+ message: string;
15
+ /** Optional human-readable field label; falls back to `path`. */
16
+ label?: string;
17
+ };
18
+ </script>
19
+
20
+ <script lang="ts">
21
+ import { tick } from 'svelte';
22
+ import AlertCircle from '@tabler/icons-svelte/icons/alert-circle';
23
+ import XIcon from '@tabler/icons-svelte/icons/x';
24
+ import { cn } from '../../../utils.js';
25
+ import { scrollIntoViewWithin } from '../../utils/scrollWithin.js';
26
+ import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
27
+ import type { InterfaceLanguage } from '../../../types/languages.js';
28
+
29
+ type Props = {
30
+ errors?: FormErrorEntry[];
31
+ title?: string;
32
+ onFocusField?: (path: string) => void;
33
+ /** When provided, shows a close button that calls this. */
34
+ onDismiss?: () => void;
35
+ class?: string;
36
+ };
37
+
38
+ let { errors = [], title, onFocusField, onDismiss, class: className }: Props = $props();
39
+
40
+ const interfaceLanguage = useInterfaceLanguage();
41
+
42
+ const lang: Record<InterfaceLanguage, { title: string; jumpTo: string; dismiss: string }> = {
43
+ pl: {
44
+ title: 'Sprawdź te pola, żebyśmy mogli zapisać',
45
+ jumpTo: 'Przejdź do pola',
46
+ dismiss: 'Zamknij'
47
+ },
48
+ en: {
49
+ title: 'Check these fields so we can save',
50
+ jumpTo: 'Jump to field',
51
+ dismiss: 'Dismiss'
52
+ }
53
+ };
54
+
55
+ const resolvedTitle = $derived(title ?? lang[interfaceLanguage.current].title);
56
+
57
+ let prevCount = $state(0);
58
+ let rootEl = $state<HTMLElement | null>(null);
59
+
60
+ $effect(() => {
61
+ const count = errors.length;
62
+ if (count > 0 && prevCount === 0) {
63
+ // Move focus to the summary itself (not to a field) so the user reviews
64
+ // the whole list first; `role="alert"` also announces it to AT. The
65
+ // user then clicks an entry to jump to a specific field.
66
+ tick().then(() => {
67
+ if (!rootEl) return;
68
+ rootEl.focus({ preventScroll: true });
69
+ scrollIntoViewWithin(rootEl);
70
+ });
71
+ }
72
+ prevCount = count;
73
+ });
74
+
75
+ function focusField(path: string | undefined) {
76
+ if (!path) return;
77
+ if (typeof document === 'undefined') return;
78
+ const target = document.querySelector<HTMLElement>(`[data-field-path="${cssEscape(path)}"]`);
79
+ if (!target) {
80
+ onFocusField?.(path);
81
+ return;
82
+ }
83
+ scrollIntoViewWithin(target);
84
+ const focusable = target.querySelector<HTMLElement>(
85
+ 'input, textarea, select, [contenteditable="true"], [tabindex]:not([tabindex="-1"])'
86
+ );
87
+ focusable?.focus({ preventScroll: true });
88
+ onFocusField?.(path);
89
+ }
90
+
91
+ function cssEscape(value: string): string {
92
+ if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') return CSS.escape(value);
93
+ return value.replace(/(["\\])/g, '\\$1');
94
+ }
95
+
96
+ function handleClick(event: MouseEvent, path: string) {
97
+ event.preventDefault();
98
+ focusField(path);
99
+ }
100
+ </script>
101
+
102
+ {#if errors.length > 0}
103
+ <div
104
+ bind:this={rootEl}
105
+ role="alert"
106
+ tabindex="-1"
107
+ aria-live="assertive"
108
+ aria-atomic="true"
109
+ data-testid="form-error-summary"
110
+ class={cn(
111
+ 'border-destructive/40 bg-destructive-bg text-destructive flex flex-col gap-2 rounded-xl border p-4',
112
+ className
113
+ )}
114
+ >
115
+ <div class="flex items-start gap-2">
116
+ <AlertCircle class="mt-0.5 size-5 shrink-0" aria-hidden="true" />
117
+ <h2 class="flex-1 text-sm font-semibold">{resolvedTitle}</h2>
118
+ {#if onDismiss}
119
+ <button
120
+ type="button"
121
+ class="text-destructive/70 hover:text-destructive hover:bg-destructive/10 -my-1 -mr-1 shrink-0 rounded p-1 transition-colors"
122
+ aria-label={lang[interfaceLanguage.current].dismiss}
123
+ onclick={() => onDismiss?.()}
124
+ >
125
+ <XIcon class="size-4" aria-hidden="true" />
126
+ </button>
127
+ {/if}
128
+ </div>
129
+ <ul class="ml-7 flex list-disc flex-col gap-1 text-sm">
130
+ {#each errors as error, i (i)}
131
+ <li>
132
+ <a
133
+ href={`#${cssEscape(error.path)}`}
134
+ class="hover:text-destructive/80 underline underline-offset-2"
135
+ onclick={(e) => handleClick(e, error.path)}
136
+ >
137
+ <span class="font-medium">{error.label ?? error.path}</span>: {error.message}
138
+ </a>
139
+ </li>
140
+ {/each}
141
+ </ul>
142
+ </div>
143
+ {/if}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @internal
3
+ * Form error summary banner — renders above forms when validation fails.
4
+ * Aria-live alert announces errors to screen readers; click anchor scrolls
5
+ * to `[data-field-path="…"]` and focuses the input inside.
6
+ *
7
+ * Used by entry-page (S8), coupon-form, shipping-method-form, shop-order-detail.
8
+ */
9
+ export type FormErrorEntry = {
10
+ /** Dot/bracket path matching `data-field-path` in the form. */
11
+ path: string;
12
+ /** Localized error message. */
13
+ message: string;
14
+ /** Optional human-readable field label; falls back to `path`. */
15
+ label?: string;
16
+ };
17
+ type Props = {
18
+ errors?: FormErrorEntry[];
19
+ title?: string;
20
+ onFocusField?: (path: string) => void;
21
+ /** When provided, shows a close button that calls this. */
22
+ onDismiss?: () => void;
23
+ class?: string;
24
+ };
25
+ declare const FormErrorSummary: import("svelte").Component<Props, {}, "">;
26
+ type FormErrorSummary = ReturnType<typeof FormErrorSummary>;
27
+ export default FormErrorSummary;
@@ -9,7 +9,12 @@
9
9
  import NavShop from './nav-shop.svelte';
10
10
  import NavSearch from './nav-search.svelte';
11
11
  import { resolve } from '$app/paths';
12
+ import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
13
+ import { sidebarLang } from './lang.js';
14
+
12
15
  let { ...restProps }: ComponentProps<typeof Sidebar.Root> = $props();
16
+
17
+ const interfaceLanguage = useInterfaceLanguage();
13
18
  </script>
14
19
 
15
20
  <Sidebar.Root collapsible="icon" class="bg-sidebar" {...restProps}>
@@ -30,13 +35,13 @@
30
35
  </Sidebar.Header>
31
36
  <Sidebar.Content class="gap-0">
32
37
  <NavSearch />
33
- <div class="py-1">
38
+ <nav id="admin-nav" aria-label={sidebarLang[interfaceLanguage.current].nav.primary} class="py-1">
34
39
  <NavMain />
35
40
  <NavSingletons />
36
41
  <NavCollections />
37
42
  <NavForms />
38
43
  <NavShop />
39
- </div>
44
+ </nav>
40
45
  </Sidebar.Content>
41
46
  <Sidebar.Footer class="p-0">
42
47
  <NavFooter />