includio-cms 0.36.9 → 0.37.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 (243) hide show
  1. package/API.md +178 -3
  2. package/CHANGELOG.md +54 -0
  3. package/DOCS.md +28 -3
  4. package/ROADMAP.md +18 -0
  5. package/dist/admin/client/booking/booking-dashboard.svelte +40 -0
  6. package/dist/admin/client/booking/booking-dashboard.svelte.d.ts +3 -0
  7. package/dist/admin/client/booking/customer-panel.svelte +102 -0
  8. package/dist/admin/client/booking/customer-panel.svelte.d.ts +11 -0
  9. package/dist/admin/client/booking/customer-view.d.ts +52 -0
  10. package/dist/admin/client/booking/customer-view.js +134 -0
  11. package/dist/admin/client/booking/participant-data-panel.svelte +330 -0
  12. package/dist/admin/client/booking/participant-data-panel.svelte.d.ts +22 -0
  13. package/dist/admin/client/booking/reservation-detail-page.svelte +637 -0
  14. package/dist/admin/client/booking/reservation-detail-page.svelte.d.ts +3 -0
  15. package/dist/admin/client/booking/reservations-page.svelte +221 -0
  16. package/dist/admin/client/booking/reservations-page.svelte.d.ts +3 -0
  17. package/dist/admin/client/collection/collection-entries.svelte +1 -1
  18. package/dist/admin/client/collection/entry-link.svelte +6 -4
  19. package/dist/admin/client/collection/entry-link.svelte.d.ts +3 -1
  20. package/dist/admin/client/collection/status-badge.svelte +39 -3
  21. package/dist/admin/client/collection/status-badge.svelte.d.ts +5 -3
  22. package/dist/admin/client/entry/entry.svelte +26 -9
  23. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +73 -10
  24. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte.d.ts +6 -5
  25. package/dist/admin/client/entry/preview-views-context.svelte.d.ts +22 -0
  26. package/dist/admin/client/entry/preview-views-context.svelte.js +58 -0
  27. package/dist/admin/client/entry/preview-views.d.ts +27 -0
  28. package/dist/admin/client/entry/preview-views.js +33 -0
  29. package/dist/admin/client/index.d.ts +3 -0
  30. package/dist/admin/client/index.js +3 -0
  31. package/dist/admin/client/modules/dashboard-registry.d.ts +7 -0
  32. package/dist/admin/client/modules/dashboard-registry.js +11 -0
  33. package/dist/admin/client/modules/module-dashboard-page.svelte +26 -0
  34. package/dist/admin/client/modules/module-dashboard-page.svelte.d.ts +6 -0
  35. package/dist/admin/client/modules/stat-card.svelte +22 -0
  36. package/dist/admin/client/modules/stat-card.svelte.d.ts +10 -0
  37. package/dist/admin/client/shop/shop-dashboard.svelte +29 -0
  38. package/dist/admin/client/shop/shop-dashboard.svelte.d.ts +3 -0
  39. package/dist/admin/components/fields/relation-field.svelte +11 -2
  40. package/dist/admin/components/icons/icon-map.js +8 -0
  41. package/dist/admin/components/layout/app-sidebar.svelte +45 -14
  42. package/dist/admin/components/layout/context-switcher.svelte +90 -0
  43. package/dist/admin/components/layout/context-switcher.svelte.d.ts +3 -0
  44. package/dist/admin/components/layout/lang.d.ts +4 -0
  45. package/dist/admin/components/layout/lang.js +8 -0
  46. package/dist/admin/components/layout/layout-renderer.svelte +2 -1
  47. package/dist/admin/components/layout/layout-tabs.svelte +14 -0
  48. package/dist/admin/components/layout/layout-tabs.svelte.d.ts +2 -0
  49. package/dist/admin/components/layout/nav-collections.svelte +16 -9
  50. package/dist/admin/components/layout/nav-collections.svelte.d.ts +5 -1
  51. package/dist/admin/components/layout/nav-global.svelte +45 -0
  52. package/dist/admin/components/layout/nav-global.svelte.d.ts +3 -0
  53. package/dist/admin/components/layout/nav-main.svelte +8 -32
  54. package/dist/admin/components/layout/nav-main.svelte.d.ts +6 -1
  55. package/dist/admin/components/layout/nav-module.svelte +57 -0
  56. package/dist/admin/components/layout/nav-module.svelte.d.ts +7 -0
  57. package/dist/admin/components/layout/nav-section.svelte +1 -1
  58. package/dist/admin/components/layout/nav-section.svelte.d.ts +1 -1
  59. package/dist/admin/components/layout/nav-singletons.svelte +14 -7
  60. package/dist/admin/components/layout/nav-singletons.svelte.d.ts +5 -1
  61. package/dist/admin/remote/booking.remote.d.ts +244 -0
  62. package/dist/admin/remote/booking.remote.js +283 -0
  63. package/dist/admin/remote/index.d.ts +2 -0
  64. package/dist/admin/remote/index.js +2 -0
  65. package/dist/admin/remote/modules.remote.d.ts +10 -0
  66. package/dist/admin/remote/modules.remote.js +14 -0
  67. package/dist/admin/state/active-context-resolve.d.ts +37 -0
  68. package/dist/admin/state/active-context-resolve.js +66 -0
  69. package/dist/admin/state/active-context.svelte.d.ts +8 -0
  70. package/dist/admin/state/active-context.svelte.js +27 -0
  71. package/dist/booking/adjustments.d.ts +33 -0
  72. package/dist/booking/adjustments.js +26 -0
  73. package/dist/booking/capacity.d.ts +12 -0
  74. package/dist/booking/capacity.js +10 -0
  75. package/dist/booking/client/index.d.ts +39 -0
  76. package/dist/booking/client/index.js +44 -0
  77. package/dist/booking/client/use-booking.svelte.d.ts +25 -0
  78. package/dist/booking/client/use-booking.svelte.js +56 -0
  79. package/dist/booking/config.d.ts +156 -0
  80. package/dist/booking/config.js +36 -0
  81. package/dist/booking/draft.d.ts +77 -0
  82. package/dist/booking/draft.js +67 -0
  83. package/dist/booking/expiry-logic.d.ts +30 -0
  84. package/dist/booking/expiry-logic.js +33 -0
  85. package/dist/booking/index.d.ts +18 -0
  86. package/dist/booking/index.js +52 -0
  87. package/dist/booking/ledger.d.ts +46 -0
  88. package/dist/booking/ledger.js +60 -0
  89. package/dist/booking/number.d.ts +2 -0
  90. package/dist/booking/number.js +11 -0
  91. package/dist/booking/participant-fields/access.d.ts +10 -0
  92. package/dist/booking/participant-fields/access.js +14 -0
  93. package/dist/booking/participant-fields/age.d.ts +12 -0
  94. package/dist/booking/participant-fields/age.js +28 -0
  95. package/dist/booking/participant-fields/completeness.d.ts +8 -0
  96. package/dist/booking/participant-fields/completeness.js +13 -0
  97. package/dist/booking/participant-fields/group.d.ts +6 -0
  98. package/dist/booking/participant-fields/group.js +15 -0
  99. package/dist/booking/participant-fields/index.d.ts +6 -0
  100. package/dist/booking/participant-fields/index.js +6 -0
  101. package/dist/booking/participant-fields/types.d.ts +50 -0
  102. package/dist/booking/participant-fields/types.js +5 -0
  103. package/dist/booking/participant-fields/validate.d.ts +7 -0
  104. package/dist/booking/participant-fields/validate.js +44 -0
  105. package/dist/booking/participant-pricing.d.ts +93 -0
  106. package/dist/booking/participant-pricing.js +91 -0
  107. package/dist/booking/participants-view.d.ts +32 -0
  108. package/dist/booking/participants-view.js +22 -0
  109. package/dist/booking/payment-context.d.ts +19 -0
  110. package/dist/booking/payment-context.js +29 -0
  111. package/dist/booking/pricing-inputs.d.ts +28 -0
  112. package/dist/booking/pricing-inputs.js +35 -0
  113. package/dist/booking/pricing.d.ts +86 -0
  114. package/dist/booking/pricing.js +125 -0
  115. package/dist/booking/recompute.d.ts +22 -0
  116. package/dist/booking/recompute.js +31 -0
  117. package/dist/booking/reminders-schedule.d.ts +11 -0
  118. package/dist/booking/reminders-schedule.js +28 -0
  119. package/dist/booking/server/adjustments.d.ts +27 -0
  120. package/dist/booking/server/adjustments.js +136 -0
  121. package/dist/booking/server/bookings.d.ts +45 -0
  122. package/dist/booking/server/bookings.js +288 -0
  123. package/dist/booking/server/catalog.d.ts +9 -0
  124. package/dist/booking/server/catalog.js +20 -0
  125. package/dist/booking/server/db.d.ts +7 -0
  126. package/dist/booking/server/db.js +18 -0
  127. package/dist/booking/server/email.d.ts +23 -0
  128. package/dist/booking/server/email.js +59 -0
  129. package/dist/booking/server/emailTemplateRegistry.d.ts +29 -0
  130. package/dist/booking/server/emailTemplateRegistry.js +125 -0
  131. package/dist/booking/server/http/create-handler.d.ts +5 -0
  132. package/dist/booking/server/http/create-handler.js +68 -0
  133. package/dist/booking/server/http/index.d.ts +4 -0
  134. package/dist/booking/server/http/index.js +4 -0
  135. package/dist/booking/server/http/maintenance-handler.d.ts +9 -0
  136. package/dist/booking/server/http/maintenance-handler.js +36 -0
  137. package/dist/booking/server/http/portal-handler.d.ts +13 -0
  138. package/dist/booking/server/http/portal-handler.js +99 -0
  139. package/dist/booking/server/http/webhook-handler.d.ts +9 -0
  140. package/dist/booking/server/http/webhook-handler.js +39 -0
  141. package/dist/booking/server/index.d.ts +12 -0
  142. package/dist/booking/server/index.js +14 -0
  143. package/dist/booking/server/maintenance.d.ts +28 -0
  144. package/dist/booking/server/maintenance.js +134 -0
  145. package/dist/booking/server/participants.d.ts +26 -0
  146. package/dist/booking/server/participants.js +92 -0
  147. package/dist/booking/server/payments.d.ts +34 -0
  148. package/dist/booking/server/payments.js +207 -0
  149. package/dist/booking/server/portal-session.d.ts +16 -0
  150. package/dist/booking/server/portal-session.js +42 -0
  151. package/dist/booking/server/portal.d.ts +207 -0
  152. package/dist/booking/server/portal.js +234 -0
  153. package/dist/booking/server/refunds.d.ts +7 -0
  154. package/dist/booking/server/refunds.js +58 -0
  155. package/dist/booking/server/reminders.d.ts +9 -0
  156. package/dist/booking/server/reminders.js +24 -0
  157. package/dist/booking/server/validation.d.ts +17 -0
  158. package/dist/booking/server/validation.js +55 -0
  159. package/dist/booking/state-machine.d.ts +41 -0
  160. package/dist/booking/state-machine.js +73 -0
  161. package/dist/booking/templates/_partials/footer.en.html +4 -0
  162. package/dist/booking/templates/_partials/footer.pl.html +4 -0
  163. package/dist/booking/templates/_partials/header.en.html +4 -0
  164. package/dist/booking/templates/_partials/header.pl.html +4 -0
  165. package/dist/booking/templates/balance-due.en.html +10 -0
  166. package/dist/booking/templates/balance-due.pl.html +10 -0
  167. package/dist/booking/templates/balance-overdue-cancelled.en.html +5 -0
  168. package/dist/booking/templates/balance-overdue-cancelled.pl.html +5 -0
  169. package/dist/booking/templates/booking-received.en.html +9 -0
  170. package/dist/booking/templates/booking-received.pl.html +9 -0
  171. package/dist/booking/templates/deposit-paid.en.html +6 -0
  172. package/dist/booking/templates/deposit-paid.pl.html +6 -0
  173. package/dist/booking/templates/payment-link.en.html +5 -0
  174. package/dist/booking/templates/payment-link.pl.html +5 -0
  175. package/dist/booking/templates/payment-received.en.html +10 -0
  176. package/dist/booking/templates/payment-received.pl.html +10 -0
  177. package/dist/booking/totals.d.ts +38 -0
  178. package/dist/booking/totals.js +61 -0
  179. package/dist/booking/webhook-logic.d.ts +28 -0
  180. package/dist/booking/webhook-logic.js +50 -0
  181. package/dist/cli/create-user.d.ts +7 -1
  182. package/dist/cli/create-user.js +23 -15
  183. package/dist/cli/index.js +55 -7
  184. package/dist/cli/scaffold/admin.d.ts +6 -0
  185. package/dist/cli/scaffold/admin.js +5 -3
  186. package/dist/cli/scaffold/booking.d.ts +12 -0
  187. package/dist/cli/scaffold/booking.js +103 -0
  188. package/dist/core/cms.d.ts +24 -9
  189. package/dist/core/cms.js +72 -34
  190. package/dist/core/config/validatePreviews.d.ts +12 -0
  191. package/dist/core/config/validatePreviews.js +90 -0
  192. package/dist/core/fields/slugPath.d.ts +9 -1
  193. package/dist/core/fields/slugPath.js +16 -2
  194. package/dist/core/server/fields/resolveRelationFields.d.ts +11 -2
  195. package/dist/core/server/fields/resolveRelationFields.js +12 -3
  196. package/dist/core/server/generator/generator.js +7 -2
  197. package/dist/db-postgres/schema/booking/bookingAdjustments.d.ts +186 -0
  198. package/dist/db-postgres/schema/booking/bookingAdjustments.js +22 -0
  199. package/dist/db-postgres/schema/booking/bookingItemPersons.d.ts +117 -0
  200. package/dist/db-postgres/schema/booking/bookingItemPersons.js +22 -0
  201. package/dist/db-postgres/schema/booking/bookingItems.d.ts +181 -0
  202. package/dist/db-postgres/schema/booking/bookingItems.js +22 -0
  203. package/dist/db-postgres/schema/booking/bookingPayments.d.ts +204 -0
  204. package/dist/db-postgres/schema/booking/bookingPayments.js +25 -0
  205. package/dist/db-postgres/schema/booking/bookingPersons.d.ts +99 -0
  206. package/dist/db-postgres/schema/booking/bookingPersons.js +17 -0
  207. package/dist/db-postgres/schema/booking/bookingReminders.d.ts +131 -0
  208. package/dist/db-postgres/schema/booking/bookingReminders.js +18 -0
  209. package/dist/db-postgres/schema/booking/bookings.d.ts +307 -0
  210. package/dist/db-postgres/schema/booking/bookings.js +40 -0
  211. package/dist/db-postgres/schema/booking/index.d.ts +7 -0
  212. package/dist/db-postgres/schema/booking/index.js +7 -0
  213. package/dist/db-postgres/schema/index.d.ts +1 -0
  214. package/dist/db-postgres/schema/index.js +1 -0
  215. package/dist/db-postgres/schema-booking.d.ts +1 -0
  216. package/dist/db-postgres/schema-booking.js +1 -0
  217. package/dist/server/security/rate-limit-handler.d.ts +19 -0
  218. package/dist/server/security/rate-limit-handler.js +34 -0
  219. package/dist/shop/adapters/payu/client.js +3 -1
  220. package/dist/shop/adapters/payu/index.js +37 -17
  221. package/dist/shop/adapters/payu/payload.d.ts +6 -0
  222. package/dist/shop/adapters/payu/payload.js +1 -1
  223. package/dist/shop/adapters/stripe/index.js +36 -21
  224. package/dist/shop/index.d.ts +11 -2
  225. package/dist/shop/index.js +43 -1
  226. package/dist/shop/rate-limit.d.ts +1 -9
  227. package/dist/shop/rate-limit.js +3 -28
  228. package/dist/shop/types.d.ts +19 -0
  229. package/dist/sveltekit/config.d.ts +4 -0
  230. package/dist/sveltekit/config.js +6 -0
  231. package/dist/types/cms.d.ts +15 -1
  232. package/dist/types/cms.schema.d.ts +96 -2
  233. package/dist/types/cms.schema.js +84 -1
  234. package/dist/types/config.d.ts +33 -0
  235. package/dist/types/layout.d.ts +6 -0
  236. package/dist/types/modules.d.ts +133 -0
  237. package/dist/types/modules.js +27 -0
  238. package/dist/updates/0.37.0/index.d.ts +2 -0
  239. package/dist/updates/0.37.0/index.js +52 -0
  240. package/dist/updates/index.js +3 -1
  241. package/package.json +19 -1
  242. package/dist/admin/components/layout/nav-shop.svelte +0 -33
  243. package/dist/admin/components/layout/nav-shop.svelte.d.ts +0 -3
@@ -0,0 +1,58 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ import { defaultView, findView, resolveActiveView } from './preview-views.js';
3
+ const PREVIEW_VIEWS_KEY = Symbol('preview-views');
4
+ /**
5
+ * Holds which preview view is showing, plus the per-tab memory of manual picks:
6
+ * a view the user selected by hand on a tab wins over that tab's `preview`
7
+ * binding when they come back to it. Lives as long as the open entry — nothing
8
+ * is persisted to the URL or the database.
9
+ */
10
+ export function createPreviewViewsContext(views) {
11
+ let activeName = $state(defaultView(views)?.name ?? '');
12
+ let activeTab = $state('');
13
+ const overrides = new Map();
14
+ const ctx = {
15
+ get views() {
16
+ return views;
17
+ },
18
+ get activeName() {
19
+ return activeName;
20
+ },
21
+ get activeView() {
22
+ return findView(views, activeName) ?? defaultView(views);
23
+ },
24
+ /** Manual pick — remember it for the tab the user is on. */
25
+ select(name) {
26
+ if (!findView(views, name))
27
+ return;
28
+ activeName = name;
29
+ if (activeTab)
30
+ overrides.set(activeTab, name);
31
+ },
32
+ /** A tab became active (on mount too, so a `?tab=` deep-link lands right). */
33
+ onTabActivate(tabValue, tabPreview) {
34
+ activeTab = tabValue;
35
+ activeName = resolveActiveView({
36
+ views,
37
+ current: activeName,
38
+ tabValue,
39
+ tabPreview,
40
+ overrides
41
+ });
42
+ }
43
+ };
44
+ setContext(PREVIEW_VIEWS_KEY, ctx);
45
+ return ctx;
46
+ }
47
+ /**
48
+ * Optional — layout components also render outside the entry editor (Storybook,
49
+ * forms), where no preview context exists.
50
+ */
51
+ export function getPreviewViewsContext() {
52
+ try {
53
+ return getContext(PREVIEW_VIEWS_KEY);
54
+ }
55
+ catch {
56
+ return undefined;
57
+ }
58
+ }
@@ -0,0 +1,27 @@
1
+ import type { PreviewView } from '../../../types/config.js';
2
+ /** The subset of a collection/single config this module cares about. */
3
+ export type PreviewSource = {
4
+ previewUrl?: string;
5
+ previews?: PreviewView[];
6
+ };
7
+ /**
8
+ * Collapse the two config shapes into one: `previewUrl` becomes a single view
9
+ * named `default`, so no caller has to know about the shorthand.
10
+ */
11
+ export declare function normalizePreviews(config: PreviewSource): PreviewView[];
12
+ /** The view marked `default: true`, else the first one. */
13
+ export declare function defaultView(views: PreviewView[]): PreviewView | undefined;
14
+ export declare function findView(views: PreviewView[], name: string): PreviewView | undefined;
15
+ /** `{url}?preview={entryId}&view={name}` — respects a query string already present in `url`. */
16
+ export declare function buildPreviewUrl(view: PreviewView, entryId: string): string;
17
+ /**
18
+ * Rule (c) — per-tab memory. A view the user picked by hand on this tab beats
19
+ * the tab's own binding; a tab without a binding leaves the current view alone.
20
+ */
21
+ export declare function resolveActiveView(params: {
22
+ views: PreviewView[];
23
+ current: string;
24
+ tabValue: string;
25
+ tabPreview?: string;
26
+ overrides: Map<string, string>;
27
+ }): string;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Collapse the two config shapes into one: `previewUrl` becomes a single view
3
+ * named `default`, so no caller has to know about the shorthand.
4
+ */
5
+ export function normalizePreviews(config) {
6
+ if (config.previews?.length)
7
+ return config.previews;
8
+ if (config.previewUrl)
9
+ return [{ name: 'default', url: config.previewUrl }];
10
+ return [];
11
+ }
12
+ /** The view marked `default: true`, else the first one. */
13
+ export function defaultView(views) {
14
+ return views.find((v) => v.default) ?? views[0];
15
+ }
16
+ export function findView(views, name) {
17
+ return views.find((v) => v.name === name);
18
+ }
19
+ /** `{url}?preview={entryId}&view={name}` — respects a query string already present in `url`. */
20
+ export function buildPreviewUrl(view, entryId) {
21
+ const sep = view.url.includes('?') ? '&' : '?';
22
+ return `${view.url}${sep}preview=${encodeURIComponent(entryId)}&view=${encodeURIComponent(view.name)}`;
23
+ }
24
+ /**
25
+ * Rule (c) — per-tab memory. A view the user picked by hand on this tab beats
26
+ * the tab's own binding; a tab without a binding leaves the current view alone.
27
+ */
28
+ export function resolveActiveView(params) {
29
+ const next = params.overrides.get(params.tabValue) ?? params.tabPreview;
30
+ if (next && findView(params.views, next))
31
+ return next;
32
+ return params.current;
33
+ }
@@ -22,6 +22,9 @@ export { default as ShopOrderDetailPage } from './shop/shop-order-detail-page.sv
22
22
  export { default as ShopCouponsListPage } from './shop/coupons-list-page.svelte';
23
23
  export { default as ShopCouponNewPage } from './shop/coupon-new-page.svelte';
24
24
  export { default as ShopCouponEditPage } from './shop/coupon-edit-page.svelte';
25
+ export { default as BookingReservationsPage } from './booking/reservations-page.svelte';
26
+ export { default as BookingReservationDetailPage } from './booking/reservation-detail-page.svelte';
27
+ export { default as ModuleDashboardPage } from './modules/module-dashboard-page.svelte';
25
28
  export * from '../helpers/index.js';
26
29
  export * from '../ui/index.js';
27
30
  export { default as DataTable } from './collection/data-table.svelte';
@@ -22,6 +22,9 @@ export { default as ShopOrderDetailPage } from './shop/shop-order-detail-page.sv
22
22
  export { default as ShopCouponsListPage } from './shop/coupons-list-page.svelte';
23
23
  export { default as ShopCouponNewPage } from './shop/coupon-new-page.svelte';
24
24
  export { default as ShopCouponEditPage } from './shop/coupon-edit-page.svelte';
25
+ export { default as BookingReservationsPage } from './booking/reservations-page.svelte';
26
+ export { default as BookingReservationDetailPage } from './booking/reservation-detail-page.svelte';
27
+ export { default as ModuleDashboardPage } from './modules/module-dashboard-page.svelte';
25
28
  // Folded from `./admin/helpers` (dropped as separate export in 0.20.0)
26
29
  export * from '../helpers/index.js';
27
30
  // Folded from `./admin/ui` (dropped as separate export in 0.20.0)
@@ -0,0 +1,7 @@
1
+ import type { Component } from 'svelte';
2
+ /**
3
+ * Client-side registry of per-module dashboard components, keyed by module id.
4
+ * Dashboards live here (not in the serializable {@link import('../../../types/modules.js').CMSModule}
5
+ * descriptor) because Svelte components cannot cross the config/remote boundary.
6
+ */
7
+ export declare const moduleDashboards: Record<string, Component>;
@@ -0,0 +1,11 @@
1
+ import ShopDashboard from '../shop/shop-dashboard.svelte';
2
+ import BookingDashboard from '../booking/booking-dashboard.svelte';
3
+ /**
4
+ * Client-side registry of per-module dashboard components, keyed by module id.
5
+ * Dashboards live here (not in the serializable {@link import('../../../types/modules.js').CMSModule}
6
+ * descriptor) because Svelte components cannot cross the config/remote boundary.
7
+ */
8
+ export const moduleDashboards = {
9
+ shop: ShopDashboard,
10
+ booking: BookingDashboard
11
+ };
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ import { getRemotes } from '../../../sveltekit/index.js';
3
+ import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
4
+ import { getLocalizedLabel } from '../../utils/collectionLabel.js';
5
+ import PageHeader from '../../components/layout/page-header.svelte';
6
+ import { moduleDashboards } from './dashboard-registry.js';
7
+ import type { Localized } from '../../../types/languages.js';
8
+
9
+ let { moduleId }: { moduleId: string } = $props();
10
+
11
+ const remotes = getRemotes();
12
+ const interfaceLanguage = useInterfaceLanguage();
13
+
14
+ const modulesQuery = $derived(remotes.getModules());
15
+ const mod = $derived((modulesQuery.current ?? []).find((m) => m.id === moduleId) ?? null);
16
+ const title = $derived(
17
+ mod ? getLocalizedLabel(mod.label as Localized, interfaceLanguage.current) || moduleId : moduleId
18
+ );
19
+ const Dashboard = $derived(moduleDashboards[moduleId]);
20
+ </script>
21
+
22
+ <PageHeader {title} />
23
+
24
+ {#if Dashboard}
25
+ <Dashboard />
26
+ {/if}
@@ -0,0 +1,6 @@
1
+ type $$ComponentProps = {
2
+ moduleId: string;
3
+ };
4
+ declare const ModuleDashboardPage: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type ModuleDashboardPage = ReturnType<typeof ModuleDashboardPage>;
6
+ export default ModuleDashboardPage;
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ let {
5
+ label,
6
+ value,
7
+ hint,
8
+ children
9
+ }: {
10
+ label: string;
11
+ value: string | number;
12
+ hint?: string;
13
+ children?: Snippet;
14
+ } = $props();
15
+ </script>
16
+
17
+ <div class="border-border bg-surface/40 flex flex-col gap-1 rounded-xl border p-4">
18
+ <span class="text-muted-foreground text-xs font-medium tracking-wide uppercase">{label}</span>
19
+ <span class="text-foreground text-2xl font-extrabold tabular-nums">{value}</span>
20
+ {#if hint}<span class="text-muted-foreground text-xs">{hint}</span>{/if}
21
+ {#if children}{@render children()}{/if}
22
+ </div>
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ label: string;
4
+ value: string | number;
5
+ hint?: string;
6
+ children?: Snippet;
7
+ };
8
+ declare const StatCard: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ type StatCard = ReturnType<typeof StatCard>;
10
+ export default StatCard;
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ import { getRemotes } from '../../../sveltekit/index.js';
3
+ import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
4
+ import StatCard from '../modules/stat-card.svelte';
5
+ import { Button } from '../../../components/ui/button/index.js';
6
+
7
+ const remotes = getRemotes();
8
+ const interfaceLanguage = useInterfaceLanguage();
9
+
10
+ const ordersQuery = $derived(remotes.listOrdersAdmin({ limit: 1 }));
11
+ const totalOrders = $derived(ordersQuery.current?.total ?? 0);
12
+
13
+ const t = $derived(
14
+ interfaceLanguage.current === 'pl'
15
+ ? { orders: 'Zamówienia', manageProducts: 'Produkty', manageOrders: 'Zamówienia', coupons: 'Kody rabatowe' }
16
+ : { orders: 'Orders', manageProducts: 'Products', manageOrders: 'Orders', coupons: 'Coupons' }
17
+ );
18
+ </script>
19
+
20
+ <div class="flex flex-col gap-4 px-4 pb-6 sm:px-6">
21
+ <div class="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
22
+ <StatCard label={t.orders} value={ordersQuery.ready ? totalOrders : '—'} />
23
+ </div>
24
+ <div class="flex flex-wrap gap-2">
25
+ <Button href="/admin/shop/products" variant="outline" size="sm">{t.manageProducts}</Button>
26
+ <Button href="/admin/shop/orders" variant="outline" size="sm">{t.manageOrders}</Button>
27
+ <Button href="/admin/shop/coupons" variant="outline" size="sm">{t.coupons}</Button>
28
+ </div>
29
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const ShopDashboard: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ShopDashboard = ReturnType<typeof ShopDashboard>;
3
+ export default ShopDashboard;
@@ -422,8 +422,15 @@
422
422
  <Command.Empty>{t.noResults}</Command.Empty>
423
423
  <Command.Group>
424
424
  {#each pickerOptions as option (option.id)}
425
+ <!--
426
+ `value` to tożsamość pozycji w Command (filtrowanie, nawigacja
427
+ klawiaturą, podświetlenie) — musi być unikalna, inaczej dwa wpisy
428
+ o tej samej nazwie zlewają się w jeden. Szukanie po nazwie zapewnia
429
+ `keywords` (domyślny scorer skleja value + keywords).
430
+ -->
425
431
  <Command.Item
426
- value={option.label}
432
+ value={option.id}
433
+ keywords={[option.label]}
427
434
  onSelect={() => toggleItem(option.id)}
428
435
  class="flex items-center gap-2"
429
436
  >
@@ -523,8 +530,10 @@
523
530
  <Command.Empty>{t.noResults}</Command.Empty>
524
531
  <Command.Group>
525
532
  {#each pickerOptions as option (option.id)}
533
+ <!-- Tożsamość po id, szukanie po nazwie — patrz komentarz w wariancie multi. -->
526
534
  <Command.Item
527
- value={option.label}
535
+ value={option.id}
536
+ keywords={[option.label]}
528
537
  onSelect={() => {
529
538
  value = option.id;
530
539
  closePopoverAndFocus();
@@ -24,6 +24,10 @@ import IconTag from '@tabler/icons-svelte/icons/tag';
24
24
  import IconTags from '@tabler/icons-svelte/icons/tags';
25
25
  import IconReceipt from '@tabler/icons-svelte/icons/receipt';
26
26
  import IconCreditCard from '@tabler/icons-svelte/icons/credit-card';
27
+ import IconBuildingStore from '@tabler/icons-svelte/icons/building-store';
28
+ import IconPackage from '@tabler/icons-svelte/icons/package';
29
+ import IconTruck from '@tabler/icons-svelte/icons/truck';
30
+ import IconTicket from '@tabler/icons-svelte/icons/ticket';
27
31
  // Media
28
32
  import IconPhoto from '@tabler/icons-svelte/icons/photo';
29
33
  import IconCamera from '@tabler/icons-svelte/icons/camera';
@@ -89,6 +93,10 @@ export const iconMap = {
89
93
  // E-commerce
90
94
  'shopping-cart': IconShoppingCart,
91
95
  'shopping-bag': IconShoppingBag,
96
+ 'building-store': IconBuildingStore,
97
+ package: IconPackage,
98
+ truck: IconTruck,
99
+ ticket: IconTicket,
92
100
  tag: IconTag,
93
101
  tags: IconTags,
94
102
  receipt: IconReceipt,
@@ -6,43 +6,74 @@
6
6
  import NavCollections from './nav-collections.svelte';
7
7
  import NavFooter from './nav-footer.svelte';
8
8
  import NavForms from './nav-forms.svelte';
9
- import NavShop from './nav-shop.svelte';
9
+ import NavModule from './nav-module.svelte';
10
10
  import NavCustom from './nav-custom.svelte';
11
11
  import NavSearch from './nav-search.svelte';
12
+ import NavGlobal from './nav-global.svelte';
13
+ import ContextSwitcher from './context-switcher.svelte';
14
+ import { getIcon } from '../icons/icon-map.js';
15
+ import BoxIcon from '@tabler/icons-svelte/icons/box';
12
16
  import { resolve } from '$app/paths';
13
17
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
18
+ import { getRemotes } from '../../../sveltekit/index.js';
19
+ import { getActiveContext, CONTENT_CONTEXT } from '../../state/active-context.svelte.js';
14
20
  import { sidebarLang } from './lang.js';
15
21
 
16
22
  let { ...restProps }: ComponentProps<typeof Sidebar.Root> = $props();
17
23
 
18
24
  const interfaceLanguage = useInterfaceLanguage();
25
+ const remotes = getRemotes();
26
+
27
+ const modulesQuery = $derived(remotes.getModules());
28
+ const modules = $derived(modulesQuery.current ?? []);
29
+ const moduleIds = $derived(modules.map((m) => m.id));
30
+ const activeContext = $derived(getActiveContext(moduleIds));
31
+ const activeModule = $derived(modules.find((m) => m.id === activeContext) ?? null);
19
32
  </script>
20
33
 
21
- <Sidebar.Root collapsible="icon" class="bg-sidebar" {...restProps}>
34
+ <Sidebar.Root collapsible="icon" class="bg-sidebar" data-context={activeContext} {...restProps}>
22
35
  <Sidebar.Header
23
36
  class="border-sidebar-border h-[52px] shrink-0 flex-row items-center border-b px-3 group-data-[collapsible=icon]:px-2"
24
37
  >
25
38
  <a
26
39
  href={resolve('/admin')}
27
- class="flex items-baseline gap-px"
40
+ class="flex items-baseline gap-px group-data-[collapsible=icon]:hidden"
28
41
  aria-label="AriaCMS — strona główna"
29
42
  >
30
43
  <span class="text-primary text-base font-extrabold">Aria</span>
31
- <span
32
- class="text-primary text-[11px] font-light tracking-[0.15em] uppercase group-data-[collapsible=icon]:hidden"
33
- >CMS</span
34
- >
44
+ <span class="text-primary text-[11px] font-light tracking-[0.15em] uppercase">CMS</span>
35
45
  </a>
46
+ <ContextSwitcher />
36
47
  </Sidebar.Header>
37
48
  <Sidebar.Content class="gap-0">
38
49
  <NavSearch />
39
- <nav id="admin-nav" aria-label={sidebarLang[interfaceLanguage.current].nav.primary} class="py-1">
40
- <NavMain />
41
- <NavSingletons />
42
- <NavCollections />
43
- <NavForms />
44
- <NavShop />
45
- <NavCustom />
50
+ <nav
51
+ id="admin-nav"
52
+ aria-label={sidebarLang[interfaceLanguage.current].nav.primary}
53
+ class="flex flex-1 flex-col py-1"
54
+ >
55
+ {#if activeContext === CONTENT_CONTEXT || !activeModule}
56
+ <NavMain />
57
+ <NavSingletons />
58
+ <NavCollections />
59
+ <NavForms />
60
+ <NavCustom />
61
+ {:else}
62
+ <NavMain
63
+ url={activeModule.context.landingUrl}
64
+ icon={getIcon(activeModule.icon) ?? BoxIcon}
65
+ />
66
+ <NavModule module={activeModule} />
67
+ <NavCollections
68
+ filterSlugs={activeModule.context.surfacedCollections ?? []}
69
+ ctx={activeContext}
70
+ />
71
+ <NavSingletons
72
+ filterSlugs={activeModule.context.surfacedSingles ?? []}
73
+ ctx={activeContext}
74
+ />
75
+ {/if}
76
+ <NavGlobal />
46
77
  </nav>
47
78
  </Sidebar.Content>
48
79
  <Sidebar.Footer class="p-0">
@@ -0,0 +1,90 @@
1
+ <script lang="ts">
2
+ import * as DropdownMenu from '../../../components/ui/dropdown-menu/index.js';
3
+ import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
4
+ import { getLocalizedLabel } from '../../utils/collectionLabel.js';
5
+ import { getIcon } from '../icons/icon-map.js';
6
+ import { getRemotes } from '../../../sveltekit/index.js';
7
+ import {
8
+ getActiveContext,
9
+ setActiveContext,
10
+ CONTENT_CONTEXT
11
+ } from '../../state/active-context.svelte.js';
12
+ import { sidebarLang } from './lang.js';
13
+ import { goto } from '$app/navigation';
14
+ import ChevronDownIcon from '@tabler/icons-svelte/icons/chevron-down';
15
+ import FileTextIcon from '@tabler/icons-svelte/icons/file-text';
16
+ import CheckIcon from '@tabler/icons-svelte/icons/check';
17
+ import type { Localized } from '../../../types/languages.js';
18
+
19
+ const interfaceLanguage = useInterfaceLanguage();
20
+ const remotes = getRemotes();
21
+
22
+ const modulesQuery = $derived(remotes.getModules());
23
+ const modules = $derived(modulesQuery.current ?? []);
24
+ const moduleIds = $derived(modules.map((m) => m.id));
25
+ const active = $derived(getActiveContext(moduleIds));
26
+ const lang = $derived(sidebarLang[interfaceLanguage.current].contexts);
27
+
28
+ function labelFor(id: string): string {
29
+ if (id === CONTENT_CONTEXT) return lang.content;
30
+ const m = modules.find((x) => x.id === id);
31
+ if (!m) return id;
32
+ return (
33
+ getLocalizedLabel(
34
+ (m.context.contextLabel ?? m.label) as Localized,
35
+ interfaceLanguage.current
36
+ ) || m.id
37
+ );
38
+ }
39
+
40
+ function iconFor(id: string) {
41
+ if (id === CONTENT_CONTEXT) return FileTextIcon;
42
+ const m = modules.find((x) => x.id === id);
43
+ return (m && getIcon(m.icon)) || FileTextIcon;
44
+ }
45
+
46
+ function choose(id: string) {
47
+ setActiveContext(id);
48
+ if (id === CONTENT_CONTEXT) {
49
+ goto('/admin');
50
+ return;
51
+ }
52
+ const m = modules.find((x) => x.id === id);
53
+ goto(m?.context.landingUrl ?? '/admin');
54
+ }
55
+ </script>
56
+
57
+ {#if modulesQuery.ready && modules.length >= 1}
58
+ {@const ActiveIcon = iconFor(active)}
59
+ <DropdownMenu.Root>
60
+ <DropdownMenu.Trigger
61
+ aria-label={lang.switch}
62
+ class="border-input hover:bg-lavender-lighter hover:border-lavender focus-visible:ring-ring ml-auto inline-flex cursor-pointer items-center gap-1.5 rounded-md border bg-transparent px-2 py-1 text-[13px] font-medium outline-none focus-visible:ring-2 focus-visible:ring-offset-1 group-data-[collapsible=icon]:ml-0 group-data-[collapsible=icon]:px-1.5"
63
+ >
64
+ <ActiveIcon class="text-primary size-4 shrink-0" />
65
+ <span class="text-primary group-data-[collapsible=icon]:hidden">{labelFor(active)}</span>
66
+ <ChevronDownIcon class="size-3.5 shrink-0 opacity-60 group-data-[collapsible=icon]:hidden" />
67
+ </DropdownMenu.Trigger>
68
+ <DropdownMenu.Content align="start" class="w-52">
69
+ {@const ContentIcon = iconFor(CONTENT_CONTEXT)}
70
+ <DropdownMenu.Item onSelect={() => choose(CONTENT_CONTEXT)}>
71
+ <ContentIcon class="mr-2 size-4" />
72
+ <span class="flex-1">{labelFor(CONTENT_CONTEXT)}</span>
73
+ {#if active === CONTENT_CONTEXT}
74
+ <CheckIcon class="text-primary size-4" />
75
+ {/if}
76
+ </DropdownMenu.Item>
77
+ <DropdownMenu.Separator />
78
+ {#each modules as m (m.id)}
79
+ {@const MIcon = iconFor(m.id)}
80
+ <DropdownMenu.Item onSelect={() => choose(m.id)}>
81
+ <MIcon class="mr-2 size-4" />
82
+ <span class="flex-1">{labelFor(m.id)}</span>
83
+ {#if active === m.id}
84
+ <CheckIcon class="text-primary size-4" />
85
+ {/if}
86
+ </DropdownMenu.Item>
87
+ {/each}
88
+ </DropdownMenu.Content>
89
+ </DropdownMenu.Root>
90
+ {/if}
@@ -0,0 +1,3 @@
1
+ declare const ContextSwitcher: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ContextSwitcher = ReturnType<typeof ContextSwitcher>;
3
+ export default ContextSwitcher;
@@ -42,4 +42,8 @@ export declare const sidebarLang: Record<string, {
42
42
  breadcrumb: string;
43
43
  skipToMain: string;
44
44
  };
45
+ contexts: {
46
+ switch: string;
47
+ content: string;
48
+ };
45
49
  }>;
@@ -42,6 +42,10 @@ export const sidebarLang = {
42
42
  primary: 'Główna nawigacja',
43
43
  breadcrumb: 'Ścieżka okruszków',
44
44
  skipToMain: 'Przejdź do głównej treści'
45
+ },
46
+ contexts: {
47
+ switch: 'Przełącz widok',
48
+ content: 'Treści'
45
49
  }
46
50
  },
47
51
  en: {
@@ -87,6 +91,10 @@ export const sidebarLang = {
87
91
  primary: 'Main navigation',
88
92
  breadcrumb: 'Breadcrumb',
89
93
  skipToMain: 'Skip to main content'
94
+ },
95
+ contexts: {
96
+ switch: 'Switch view',
97
+ content: 'Content'
90
98
  }
91
99
  }
92
100
  };
@@ -109,7 +109,8 @@
109
109
  const hasError =
110
110
  !!errorPaths &&
111
111
  collectLeafPathsForNodes([child], fields).some((p) => errorPaths!.has(p));
112
- return { value, label, child, hasError };
112
+ const preview = child.type === 'tab' ? child.preview : undefined;
113
+ return { value, label, child, hasError, preview };
113
114
  });
114
115
  }
115
116
 
@@ -11,6 +11,8 @@
11
11
  child: LayoutNode;
12
12
  /** True when a leaf field inside this tab is currently invalid. */
13
13
  hasError: boolean;
14
+ /** Preview view this tab binds to (`TabNode.preview`), if any. */
15
+ preview?: string;
14
16
  };
15
17
  </script>
16
18
 
@@ -22,6 +24,7 @@
22
24
  import { browser } from '$app/environment';
23
25
  import { page } from '$app/state';
24
26
  import { replaceState } from '$app/navigation';
27
+ import { getPreviewViewsContext } from '../../client/entry/preview-views-context.svelte.js';
25
28
 
26
29
  type Props = {
27
30
  tabs: TabInfo[];
@@ -44,6 +47,17 @@
44
47
 
45
48
  let active = $state(initialValue());
46
49
 
50
+ const previewViews = getPreviewViewsContext();
51
+
52
+ // Tell the preview which view the active tab wants. Only the URL-synced
53
+ // (outermost) group drives it, so two tab groups never fight over the preview.
54
+ // Runs on mount too, so a `?tab=` deep-link lands on the right view.
55
+ $effect(() => {
56
+ if (!syncUrl || !previewViews) return;
57
+ const tab = tabs.find((t) => t.value === active);
58
+ if (tab) previewViews.onTabActivate(tab.value, tab.preview);
59
+ });
60
+
47
61
  // Write the active tab back to the URL (outermost group only). Guard against
48
62
  // the no-op case so we never trigger an infinite replaceState loop.
49
63
  $effect(() => {
@@ -9,6 +9,8 @@ export type TabInfo = {
9
9
  child: LayoutNode;
10
10
  /** True when a leaf field inside this tab is currently invalid. */
11
11
  hasError: boolean;
12
+ /** Preview view this tab binds to (`TabNode.preview`), if any. */
13
+ preview?: string;
12
14
  };
13
15
  import type { Snippet } from 'svelte';
14
16
  type Props = {
@@ -11,16 +11,19 @@
11
11
  import { getIcon } from '../icons/icon-map.js';
12
12
  import NavSection, { type NavSectionItem } from './nav-section.svelte';
13
13
 
14
+ let { filterSlugs, ctx }: { filterSlugs?: string[]; ctx?: string } = $props();
15
+
14
16
  const interfaceLanguage = useInterfaceLanguage();
15
17
  const remotes = getRemotes();
16
18
  const breadcrumbs = getBreadcrumbs();
17
19
 
18
20
  function isActive(url: string) {
21
+ const base = url.split('?')[0];
19
22
  const pathname = page.url.pathname;
20
- if (pathname === url || pathname.startsWith(url + '/')) return true;
23
+ if (pathname === base || pathname.startsWith(base + '/')) return true;
21
24
  // Also highlight a collection while editing one of its entries — the entry
22
25
  // page sets a breadcrumb linking back to the collection list.
23
- return breadcrumbs.state.some((b) => b.href === url);
26
+ return breadcrumbs.state.some((b) => b.href === base);
24
27
  }
25
28
 
26
29
  const collectionsQuery = $derived(remotes.getCollections());
@@ -41,16 +44,20 @@
41
44
  });
42
45
 
43
46
  const items: NavSectionItem[] = $derived(
44
- collectionsQuery.current?.map((item) => ({
45
- title: getLocalizedLabel(item.labels?.plural, interfaceLanguage.current) ?? item.slug,
46
- url: `/admin/collections/${item.slug}`,
47
- icon: getIcon(item.sidebarIcon) ?? FolderIcon,
48
- count: countsMap.get(item.slug)
49
- })) ?? []
47
+ (collectionsQuery.current ?? [])
48
+ .filter((item) => !filterSlugs || filterSlugs.includes(item.slug))
49
+ .map((item) => ({
50
+ title: getLocalizedLabel(item.labels?.plural, interfaceLanguage.current) ?? item.slug,
51
+ url: `/admin/collections/${item.slug}${ctx ? `?ctx=${ctx}` : ''}`,
52
+ icon: getIcon(item.sidebarIcon) ?? FolderIcon,
53
+ count: countsMap.get(item.slug)
54
+ }))
50
55
  );
51
56
  </script>
52
57
 
53
- {#if !collectionsQuery.ready}
58
+ {#if filterSlugs && filterSlugs.length === 0}
59
+ <!-- no surfaced collections in this context -->
60
+ {:else if !collectionsQuery.ready}
54
61
  <Sidebar.Group class="border-sidebar-border mt-1 border-t px-0 pt-2 pb-1">
55
62
  <Sidebar.GroupLabel
56
63
  >{sidebarLang[interfaceLanguage.current].collections.title}</Sidebar.GroupLabel
@@ -1,3 +1,7 @@
1
- declare const NavCollections: import("svelte").Component<Record<string, never>, {}, "">;
1
+ type $$ComponentProps = {
2
+ filterSlugs?: string[];
3
+ ctx?: string;
4
+ };
5
+ declare const NavCollections: import("svelte").Component<$$ComponentProps, {}, "">;
2
6
  type NavCollections = ReturnType<typeof NavCollections>;
3
7
  export default NavCollections;