includio-cms 0.26.0 → 0.27.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 (112) hide show
  1. package/API.md +42 -2
  2. package/CHANGELOG.md +65 -0
  3. package/DOCS.md +1 -1
  4. package/ROADMAP.md +8 -0
  5. package/dist/admin/auth-client.d.ts +42 -42
  6. package/dist/admin/client/admin/admin-layout.svelte +12 -2
  7. package/dist/admin/client/admin/admin-layout.svelte.d.ts +2 -1
  8. package/dist/admin/client/collection/data-table.svelte +0 -39
  9. package/dist/admin/client/collection/data-table.svelte.d.ts +0 -2
  10. package/dist/admin/client/shop/coupon-schema.d.ts +1 -1
  11. package/dist/admin/client/shop/refund-dialog.svelte +37 -1
  12. package/dist/admin/client/shop/refund-dialog.svelte.d.ts +3 -0
  13. package/dist/admin/client/shop/shop-order-detail-page.svelte +107 -0
  14. package/dist/admin/components/fields/field-renderer.svelte +6 -1
  15. package/dist/admin/components/fields/icon-field.svelte +86 -0
  16. package/dist/admin/components/fields/icon-field.svelte.d.ts +8 -0
  17. package/dist/admin/components/fields/icon-picker-dialog.svelte +174 -0
  18. package/dist/admin/components/fields/icon-picker-dialog.svelte.d.ts +11 -0
  19. package/dist/admin/components/fields/object-field.svelte +27 -7
  20. package/dist/admin/components/fields/shop-field.svelte +210 -20
  21. package/dist/admin/components/layout/layout-tabs.svelte +1 -0
  22. package/dist/admin/components/variant-form/VariantAttributeRenderer.svelte +109 -0
  23. package/dist/admin/components/variant-form/VariantAttributeRenderer.svelte.d.ts +9 -0
  24. package/dist/admin/helpers/build-icon-set-map.d.ts +8 -0
  25. package/dist/admin/helpers/build-icon-set-map.js +16 -0
  26. package/dist/admin/helpers/index.d.ts +2 -0
  27. package/dist/admin/helpers/index.js +2 -0
  28. package/dist/admin/remote/shop.remote.d.ts +58 -24
  29. package/dist/admin/remote/shop.remote.js +61 -6
  30. package/dist/admin/state/icon-sets.svelte.d.ts +9 -0
  31. package/dist/admin/state/icon-sets.svelte.js +20 -0
  32. package/dist/cli/scaffold/admin.js +2 -2
  33. package/dist/components/ui/checkbox/checkbox.svelte +3 -3
  34. package/dist/core/cms.d.ts +11 -2
  35. package/dist/core/cms.js +29 -0
  36. package/dist/core/fields/fieldSchemaToTs.js +7 -0
  37. package/dist/core/server/generator/fields.d.ts +2 -0
  38. package/dist/core/server/generator/fields.js +34 -1
  39. package/dist/core/server/generator/generator.js +2 -1
  40. package/dist/db-postgres/schema/shop/order.d.ts +37 -1
  41. package/dist/db-postgres/schema/shop/order.js +3 -1
  42. package/dist/db-postgres/schema/shop/payment.d.ts +20 -0
  43. package/dist/db-postgres/schema/shop/payment.js +4 -1
  44. package/dist/db-postgres/schema/shop/product.d.ts +20 -0
  45. package/dist/db-postgres/schema/shop/product.js +3 -1
  46. package/dist/db-postgres/schema/shop/productVariant.d.ts +12 -2
  47. package/dist/db-postgres/schema/shop/productVariant.js +22 -0
  48. package/dist/paraglide/messages/_index.d.ts +36 -3
  49. package/dist/paraglide/messages/_index.js +71 -3
  50. package/dist/paraglide/messages/en.d.ts +5 -0
  51. package/dist/paraglide/messages/en.js +14 -0
  52. package/dist/paraglide/messages/pl.d.ts +5 -0
  53. package/dist/paraglide/messages/pl.js +14 -0
  54. package/dist/shop/cart/types.d.ts +1 -0
  55. package/dist/shop/client/index.d.ts +54 -0
  56. package/dist/shop/client/index.js +5 -1
  57. package/dist/shop/expiry.d.ts +35 -0
  58. package/dist/shop/expiry.js +68 -0
  59. package/dist/shop/http/balance-handler.d.ts +20 -0
  60. package/dist/shop/http/balance-handler.js +91 -0
  61. package/dist/shop/http/cart-handler.js +19 -0
  62. package/dist/shop/http/checkout-handler.js +19 -1
  63. package/dist/shop/http/index.d.ts +2 -0
  64. package/dist/shop/http/index.js +2 -0
  65. package/dist/shop/http/upcoming-handler.d.ts +16 -0
  66. package/dist/shop/http/upcoming-handler.js +65 -0
  67. package/dist/shop/http/webhook-handler.js +46 -9
  68. package/dist/shop/index.d.ts +4 -1
  69. package/dist/shop/index.js +7 -1
  70. package/dist/shop/server/balance-payment.d.ts +40 -0
  71. package/dist/shop/server/balance-payment.js +140 -0
  72. package/dist/shop/server/cart-hydrate.js +2 -0
  73. package/dist/shop/server/init.d.ts +14 -0
  74. package/dist/shop/server/init.js +35 -0
  75. package/dist/shop/server/orders.d.ts +34 -0
  76. package/dist/shop/server/orders.js +141 -2
  77. package/dist/shop/server/payment-policy.d.ts +35 -0
  78. package/dist/shop/server/payment-policy.js +55 -0
  79. package/dist/shop/server/payments.d.ts +29 -0
  80. package/dist/shop/server/payments.js +64 -0
  81. package/dist/shop/server/populate.d.ts +1 -1
  82. package/dist/shop/server/refund.d.ts +17 -12
  83. package/dist/shop/server/refund.js +96 -13
  84. package/dist/shop/server/shop-data.d.ts +4 -1
  85. package/dist/shop/server/shop-data.js +24 -2
  86. package/dist/shop/template.d.ts +13 -0
  87. package/dist/shop/template.js +98 -0
  88. package/dist/shop/types.d.ts +142 -1
  89. package/dist/shop/variant-attributes.d.ts +28 -0
  90. package/dist/shop/variant-attributes.js +69 -0
  91. package/dist/sveltekit/server/index.d.ts +1 -0
  92. package/dist/sveltekit/server/index.js +2 -0
  93. package/dist/types/cms.d.ts +4 -3
  94. package/dist/types/cms.schema.d.ts +1 -1
  95. package/dist/types/cms.schema.js +9 -0
  96. package/dist/types/fields.d.ts +21 -2
  97. package/dist/types/index.d.ts +1 -1
  98. package/dist/types/index.js +1 -1
  99. package/dist/types/plugins.d.ts +40 -0
  100. package/dist/types/plugins.js +4 -1
  101. package/dist/updates/0.26.1/index.d.ts +2 -0
  102. package/dist/updates/0.26.1/index.js +19 -0
  103. package/dist/updates/0.27.0/index.d.ts +2 -0
  104. package/dist/updates/0.27.0/index.js +50 -0
  105. package/dist/updates/index.js +5 -1
  106. package/package.json +1 -1
  107. package/dist/paraglide/messages/hello_world.d.ts +0 -5
  108. package/dist/paraglide/messages/hello_world.js +0 -33
  109. package/dist/paraglide/messages/login_hello.d.ts +0 -16
  110. package/dist/paraglide/messages/login_hello.js +0 -34
  111. package/dist/paraglide/messages/login_please_login.d.ts +0 -16
  112. package/dist/paraglide/messages/login_please_login.js +0 -34
@@ -2,7 +2,10 @@ import { asc, eq, inArray } from 'drizzle-orm';
2
2
  import { shopProductsTable, shopProductVariantsTable } from '../../db-postgres/schema/shop/index.js';
3
3
  import { entriesTable } from '../../db-postgres/schema/entry.js';
4
4
  import { entryVersionsTable } from '../../db-postgres/schema/entryVersion.js';
5
+ import { getCMS } from '../../core/cms.js';
5
6
  import { getShopDb } from './db.js';
7
+ import { validateVariantAttributes } from '../variant-attributes.js';
8
+ import { validatePaymentPolicy } from './payment-policy.js';
6
9
  const MAX_PLN = 1e9;
7
10
  function validateShopData(input) {
8
11
  if (!Number.isFinite(input.basePrice) || input.basePrice < 0 || input.basePrice > MAX_PLN) {
@@ -11,6 +14,8 @@ function validateShopData(input) {
11
14
  if (!Number.isInteger(input.vatRate) || input.vatRate < 0 || input.vatRate > 100) {
12
15
  throw new Error('vatRate must be an integer between 0 and 100.');
13
16
  }
17
+ if (input.paymentPolicy != null)
18
+ validatePaymentPolicy(input.paymentPolicy);
14
19
  }
15
20
  function mapShopRow(r) {
16
21
  return { ...r, basePrice: Number(r.basePrice) };
@@ -40,13 +45,18 @@ export async function upsertShopData(entryId, input, variants) {
40
45
  let productId;
41
46
  const basePriceSql = String(input.basePrice);
42
47
  if (existing) {
48
+ // `paymentPolicy === undefined` (caller didn't include the key) preserves
49
+ // the existing policy — admin clients that don't surface the field
50
+ // must not silently wipe it. Explicit `null` clears.
51
+ const nextPolicy = input.paymentPolicy === undefined ? existing.paymentPolicy : input.paymentPolicy;
43
52
  const [updated] = await db
44
53
  .update(shopProductsTable)
45
54
  .set({
46
55
  basePrice: basePriceSql,
47
56
  vatRate: input.vatRate,
48
57
  isActive: input.isActive ?? existing.isActive,
49
- sortOrder: input.sortOrder ?? existing.sortOrder,
58
+ sortOrder: input.sortOrder ?? existing.sortOrder ?? null,
59
+ paymentPolicy: nextPolicy,
50
60
  updatedAt: new Date()
51
61
  })
52
62
  .where(eq(shopProductsTable.entryId, entryId))
@@ -61,12 +71,24 @@ export async function upsertShopData(entryId, input, variants) {
61
71
  basePrice: basePriceSql,
62
72
  vatRate: input.vatRate,
63
73
  isActive: input.isActive ?? true,
64
- sortOrder: input.sortOrder
74
+ sortOrder: input.sortOrder ?? null,
75
+ paymentPolicy: input.paymentPolicy ?? null
65
76
  })
66
77
  .returning();
67
78
  productId = created.id;
68
79
  }
69
80
  if (variants !== undefined) {
81
+ // Validate against the schema declared in defineShop({ variantAttributes }).
82
+ // Missing/extra keys, type mismatches → InvalidVariantAttributesError
83
+ // (code INVALID_VARIANT_ATTRIBUTES). Skipped if no shop config or empty
84
+ // schema (legacy untyped behavior preserved).
85
+ const shopConfig = getCMS().shopConfig;
86
+ const attrSchema = shopConfig?.variantAttributes ?? {};
87
+ if (Object.keys(attrSchema).length > 0) {
88
+ for (const v of variants) {
89
+ validateVariantAttributes(v.attributes, attrSchema);
90
+ }
91
+ }
70
92
  const submittedIds = new Set(variants.filter((v) => v.id).map((v) => v.id));
71
93
  const currentVariants = existing?.variants ?? [];
72
94
  for (const v of currentVariants) {
@@ -0,0 +1,13 @@
1
+ /**
2
+ * String interpolation engine used by `defineShop({ variantLabel.template })`
3
+ * to render variant names from typed `variantAttributes`.
4
+ *
5
+ * Syntax: `{key}` or `{key|filter}` or `{key|filter:arg}`.
6
+ * Supported filters: `date` (long|medium|short), `currency` (PLN by default),
7
+ * `uppercase`. Unknown filter → value passes through unchanged. Unknown key
8
+ * → empty string + `console.warn` in dev. Malformed template (unclosed brace)
9
+ * → raw template + warn.
10
+ *
11
+ * @public
12
+ */
13
+ export declare function interpolateTemplate(template: string, vars: Record<string, unknown>, locale: string): string;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * String interpolation engine used by `defineShop({ variantLabel.template })`
3
+ * to render variant names from typed `variantAttributes`.
4
+ *
5
+ * Syntax: `{key}` or `{key|filter}` or `{key|filter:arg}`.
6
+ * Supported filters: `date` (long|medium|short), `currency` (PLN by default),
7
+ * `uppercase`. Unknown filter → value passes through unchanged. Unknown key
8
+ * → empty string + `console.warn` in dev. Malformed template (unclosed brace)
9
+ * → raw template + warn.
10
+ *
11
+ * @public
12
+ */
13
+ export function interpolateTemplate(template, vars, locale) {
14
+ if (!hasBalancedBraces(template)) {
15
+ console.warn(`[interpolateTemplate] Malformed template (unclosed brace): ${template}`);
16
+ return template;
17
+ }
18
+ return template.replace(/\{([^}]+)\}/g, (_match, body) => {
19
+ const { key, filter, arg } = parsePlaceholder(body);
20
+ if (!(key in vars)) {
21
+ console.warn(`[interpolateTemplate] Unknown key "${key}" in template: ${template}`);
22
+ return '';
23
+ }
24
+ const raw = vars[key];
25
+ if (raw === null || raw === undefined)
26
+ return '';
27
+ if (!filter)
28
+ return String(raw);
29
+ return applyFilter(raw, filter, arg, locale);
30
+ });
31
+ }
32
+ /** @internal */
33
+ function parsePlaceholder(body) {
34
+ const [keyPart, filterPart] = body.split('|', 2);
35
+ const key = keyPart.trim();
36
+ if (!filterPart)
37
+ return { key, filter: null, arg: null };
38
+ const [filterName, ...argParts] = filterPart.split(':');
39
+ return {
40
+ key,
41
+ filter: filterName.trim(),
42
+ arg: argParts.length > 0 ? argParts.join(':').trim() : null
43
+ };
44
+ }
45
+ /** @internal */
46
+ function applyFilter(value, filter, arg, locale) {
47
+ switch (filter) {
48
+ case 'date':
49
+ return formatDate(value, arg ?? 'medium', locale);
50
+ case 'currency':
51
+ return formatCurrency(value, arg ?? 'PLN', locale);
52
+ case 'uppercase':
53
+ return String(value).toUpperCase();
54
+ default:
55
+ return String(value);
56
+ }
57
+ }
58
+ /** @internal */
59
+ function formatDate(value, style, locale) {
60
+ const date = new Date(String(value));
61
+ if (Number.isNaN(date.getTime()))
62
+ return String(value);
63
+ const dateStyle = ['long', 'medium', 'short'].includes(style)
64
+ ? style
65
+ : 'medium';
66
+ try {
67
+ return new Intl.DateTimeFormat(locale, { dateStyle }).format(date);
68
+ }
69
+ catch {
70
+ return String(value);
71
+ }
72
+ }
73
+ /** @internal */
74
+ function formatCurrency(value, currency, locale) {
75
+ const num = typeof value === 'number' ? value : Number(value);
76
+ if (!Number.isFinite(num))
77
+ return String(value);
78
+ try {
79
+ return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(num);
80
+ }
81
+ catch {
82
+ return String(value);
83
+ }
84
+ }
85
+ /** @internal */
86
+ function hasBalancedBraces(template) {
87
+ let depth = 0;
88
+ for (const ch of template) {
89
+ if (ch === '{')
90
+ depth++;
91
+ else if (ch === '}') {
92
+ depth--;
93
+ if (depth < 0)
94
+ return false;
95
+ }
96
+ }
97
+ return depth === 0;
98
+ }
@@ -147,6 +147,124 @@ export interface CouponRef {
147
147
  value: number;
148
148
  discountAmount: number;
149
149
  }
150
+ /**
151
+ * Schema descriptor for one product-variant attribute (city, startsAt, …).
152
+ * Drives Zod validation, ts-gen typings, GIN indexes, and admin renderers.
153
+ * @public
154
+ */
155
+ export type VariantAttribute = VariantAttributeText | VariantAttributeNumber | VariantAttributeDatetime | VariantAttributeSelect | VariantAttributeBoolean | VariantAttributeImage | VariantAttributeEntry | VariantAttributeSlug;
156
+ interface VariantAttributeBase {
157
+ label: I18nText;
158
+ required?: boolean;
159
+ /**
160
+ * Generate a `GIN ((attributes->'<key>'))` index on the variants table.
161
+ * Enable for keys filtered on storefront (`city`, `startsAt`, …).
162
+ */
163
+ indexable?: boolean;
164
+ }
165
+ /** @public */
166
+ export interface VariantAttributeText extends VariantAttributeBase {
167
+ type: 'text';
168
+ }
169
+ /** @public */
170
+ export interface VariantAttributeNumber extends VariantAttributeBase {
171
+ type: 'number';
172
+ }
173
+ /** @public — value stored as ISO-8601 string. */
174
+ export interface VariantAttributeDatetime extends VariantAttributeBase {
175
+ type: 'datetime';
176
+ }
177
+ /** @public */
178
+ export interface VariantAttributeSelect extends VariantAttributeBase {
179
+ type: 'select';
180
+ options: {
181
+ value: string;
182
+ label: I18nText;
183
+ }[];
184
+ }
185
+ /** @public */
186
+ export interface VariantAttributeBoolean extends VariantAttributeBase {
187
+ type: 'boolean';
188
+ }
189
+ /** @public — value = media file id (UUID). */
190
+ export interface VariantAttributeImage extends VariantAttributeBase {
191
+ type: 'image';
192
+ }
193
+ /** @public — value = entry id (UUID) from given collection. */
194
+ export interface VariantAttributeEntry extends VariantAttributeBase {
195
+ type: 'entry';
196
+ entryCollection: string;
197
+ }
198
+ /** @public — value matches slug regex `^[a-z0-9-]+$`. */
199
+ export interface VariantAttributeSlug extends VariantAttributeBase {
200
+ type: 'slug';
201
+ }
202
+ /**
203
+ * Template used to auto-generate `variant.name` from `variantAttributes`.
204
+ * Syntax: `{key|filter:arg}`. Filters: `date` (long|medium|short),
205
+ * `currency` (currency code), `uppercase`. Admin pre-fills the name input
206
+ * with the interpolated string; once the editor types into the name field,
207
+ * pre-fill stops for that variant (per-session dirty flag).
208
+ * @public
209
+ */
210
+ export interface VariantLabelConfig {
211
+ template: string;
212
+ }
213
+ /**
214
+ * Per-product payment policy. `full` (default) charges the full order total
215
+ * immediately; `deposit` charges only a deposit at checkout and leaves a
216
+ * remaining balance owed by the customer, redeemable via a signed balance
217
+ * link sent by the admin. Set on `ShopFieldData.paymentPolicy` per product
218
+ * entry — not globally on `defineShop`.
219
+ * @public
220
+ */
221
+ export type PaymentPolicy = {
222
+ type: 'full';
223
+ } | {
224
+ type: 'deposit';
225
+ depositAmount: DepositAmount;
226
+ };
227
+ /**
228
+ * Deposit amount specifier. `percent` charges `floor(base * value / 100)` of
229
+ * the line total; `amount` charges a fixed minor-unit value (clamped to the
230
+ * line total). Validation: `value > 0` always; `percent` ≤ 100; `amount` in
231
+ * the same minor-unit as `order.totalGross` (grosze for PLN).
232
+ * @public
233
+ */
234
+ export type DepositAmount = {
235
+ type: 'percent';
236
+ value: number;
237
+ } | {
238
+ type: 'amount';
239
+ value: number;
240
+ };
241
+ /**
242
+ * Persisted partial-payment summary on `order.partialPayment` when the order
243
+ * was placed under a deposit policy. Amounts are in minor currency units
244
+ * (grosze for PLN). `paidAt` is the ISO timestamp when the deposit cleared
245
+ * (or `null` while still pending). On balance payment, `paidAmount` is
246
+ * bumped to the full order total and `paidAt` is refreshed.
247
+ * @public
248
+ */
249
+ export interface PartialPayment {
250
+ kind: 'deposit';
251
+ paidAmount: number;
252
+ balanceAmount: number;
253
+ paidAt: string | null;
254
+ }
255
+ /**
256
+ * Opt-in storefront filter that hides variants whose datetime attribute
257
+ * (`source`, e.g. `startsAt`) has already passed. `offsetDays` shifts the
258
+ * cut-off: `0` = expire exactly when the source datetime hits now, `1` = one
259
+ * day after (grace period), `-1` = one day before. Admin still shows expired
260
+ * variants with a "Zakończony" badge; storefront `listUpcoming` filters them
261
+ * out and cart/checkout reject them with `VARIANT_EXPIRED`.
262
+ * @public
263
+ */
264
+ export interface VariantExpiryConfig {
265
+ source: string;
266
+ offsetDays: number;
267
+ }
150
268
  export interface ShopConfig {
151
269
  currency: Currency;
152
270
  vatRates: number[];
@@ -168,11 +286,34 @@ export interface ShopConfig {
168
286
  * automatic admin notifications. Omitted = those alerts are no-ops.
169
287
  */
170
288
  adminEmail?: string;
289
+ /**
290
+ * Typed variant attribute schema. Each key declares one attribute (city,
291
+ * startsAt, …) — drives Zod validation, ts-gen typings, GIN indexes, and
292
+ * admin variant form renderer. Omitted = legacy untyped string map.
293
+ * @public
294
+ */
295
+ variantAttributes?: Record<string, VariantAttribute>;
296
+ /**
297
+ * Template used by admin to auto-prefill `variant.name` from typed
298
+ * `variantAttributes`. Omitted = no pre-fill (editor enters name manually).
299
+ * @public
300
+ */
301
+ variantLabel?: VariantLabelConfig;
302
+ /**
303
+ * Opt-in storefront filter for time-bound variants (events, courses).
304
+ * Omitted = no filtering (legacy behavior — every variant always listed).
305
+ * @public
306
+ */
307
+ variantExpiry?: VariantExpiryConfig;
171
308
  }
172
- export interface ResolvedShopConfig extends ShopConfig {
309
+ export interface ResolvedShopConfig extends Omit<ShopConfig, 'variantLabel' | 'variantExpiry'> {
173
310
  features: Required<ShopFeatures>;
174
311
  rateLimit: Required<ShopRateLimit>;
175
312
  carriers: CarrierAdapter[];
176
313
  consents: ConsentConfig[];
177
314
  orderViewUrl: string;
315
+ variantAttributes: Record<string, VariantAttribute>;
316
+ variantLabel: VariantLabelConfig | null;
317
+ variantExpiry: VariantExpiryConfig | null;
178
318
  }
319
+ export {};
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+ import type { VariantAttribute } from './types.js';
3
+ /**
4
+ * Build a Zod object schema from a `defineShop({ variantAttributes })` map.
5
+ * Required attributes are mandatory keys; optional ones use `.optional()`.
6
+ * Empty map → `z.object({})`.
7
+ * @internal
8
+ */
9
+ export declare function buildVariantAttributesSchema(attrs: Record<string, VariantAttribute>): z.ZodObject<Record<string, z.ZodType>>;
10
+ /**
11
+ * @public
12
+ * Thrown by `upsertShopData` (and `validateVariantAttributes`) when a
13
+ * `variant.attributes` payload fails the schema derived from
14
+ * `defineShop({ variantAttributes })`. Carries the raw Zod issues.
15
+ */
16
+ export declare class InvalidVariantAttributesError extends Error {
17
+ readonly code = "INVALID_VARIANT_ATTRIBUTES";
18
+ readonly issues: z.ZodIssue[];
19
+ constructor(issues: z.ZodIssue[]);
20
+ }
21
+ /**
22
+ * Validate a variant attributes payload against a shop config.
23
+ * Throws `InvalidVariantAttributesError` (code `INVALID_VARIANT_ATTRIBUTES`) on
24
+ * failure. Returns the parsed (and coerced where applicable) value on success.
25
+ * `null` / `undefined` payloads are treated as `{}`.
26
+ * @internal
27
+ */
28
+ export declare function validateVariantAttributes(value: unknown, attrs: Record<string, VariantAttribute>): Record<string, unknown>;
@@ -0,0 +1,69 @@
1
+ import { z } from 'zod';
2
+ const SLUG_REGEX = /^[a-z0-9-]+$/;
3
+ function buildLeafSchema(attr) {
4
+ switch (attr.type) {
5
+ case 'text':
6
+ return attr.required ? z.string().min(1) : z.string();
7
+ case 'number':
8
+ return z.number().finite();
9
+ case 'datetime':
10
+ // Accept full ISO 8601: UTC (`...Z`) AND offset forms (`+02:00`).
11
+ // Admin DatetimeField produces `Z` via toISOString(); imports/seeds
12
+ // may use local offsets. Both are valid ISO 8601.
13
+ return z.string().datetime({ offset: true });
14
+ case 'select':
15
+ return z.enum(attr.options.map((o) => o.value));
16
+ case 'boolean':
17
+ return z.boolean();
18
+ case 'image':
19
+ case 'entry':
20
+ return z.string().uuid();
21
+ case 'slug':
22
+ return z.string().regex(SLUG_REGEX);
23
+ }
24
+ }
25
+ /**
26
+ * Build a Zod object schema from a `defineShop({ variantAttributes })` map.
27
+ * Required attributes are mandatory keys; optional ones use `.optional()`.
28
+ * Empty map → `z.object({})`.
29
+ * @internal
30
+ */
31
+ export function buildVariantAttributesSchema(attrs) {
32
+ const shape = {};
33
+ for (const [key, attr] of Object.entries(attrs)) {
34
+ const leaf = buildLeafSchema(attr);
35
+ shape[key] = attr.required ? leaf : leaf.optional();
36
+ }
37
+ return z.object(shape);
38
+ }
39
+ /**
40
+ * @public
41
+ * Thrown by `upsertShopData` (and `validateVariantAttributes`) when a
42
+ * `variant.attributes` payload fails the schema derived from
43
+ * `defineShop({ variantAttributes })`. Carries the raw Zod issues.
44
+ */
45
+ export class InvalidVariantAttributesError extends Error {
46
+ code = 'INVALID_VARIANT_ATTRIBUTES';
47
+ issues;
48
+ constructor(issues) {
49
+ super(`Invalid variant attributes: ${issues.map((i) => i.message).join('; ')}`);
50
+ this.name = 'InvalidVariantAttributesError';
51
+ this.issues = issues;
52
+ }
53
+ }
54
+ /**
55
+ * Validate a variant attributes payload against a shop config.
56
+ * Throws `InvalidVariantAttributesError` (code `INVALID_VARIANT_ATTRIBUTES`) on
57
+ * failure. Returns the parsed (and coerced where applicable) value on success.
58
+ * `null` / `undefined` payloads are treated as `{}`.
59
+ * @internal
60
+ */
61
+ export function validateVariantAttributes(value, attrs) {
62
+ const schema = buildVariantAttributesSchema(attrs);
63
+ const input = value ?? {};
64
+ const result = schema.safeParse(input);
65
+ if (!result.success) {
66
+ throw new InvalidVariantAttributesError(result.error.issues);
67
+ }
68
+ return result.data;
69
+ }
@@ -7,3 +7,4 @@ export { createConsentLog } from '../../core/server/consentLogs/operations/creat
7
7
  export { getPreviewEntry } from './preview.js';
8
8
  export { createRestApiHandler } from '../../admin/api/rest/handler.js';
9
9
  export { generateApiKey } from '../../admin/api/rest/middleware/generateApiKey.js';
10
+ export { createAdminApiHandler } from '../../admin/api/handler.js';
@@ -8,3 +8,5 @@ export { getPreviewEntry } from './preview.js';
8
8
  // Folded from `./admin/api/rest/handler` (dropped as separate export in 0.20.0)
9
9
  export { createRestApiHandler } from '../../admin/api/rest/handler.js';
10
10
  export { generateApiKey } from '../../admin/api/rest/middleware/generateApiKey.js';
11
+ // Folded from `./admin/api/handler` (dropped as separate export in 0.26.1)
12
+ export { createAdminApiHandler } from '../../admin/api/handler.js';
@@ -3,7 +3,7 @@ import type { FilesAdapter } from './adapters/files.js';
3
3
  import type { CollectionConfig, CollectionConfigWithType } from './collections.js';
4
4
  import type { Language } from './languages.js';
5
5
  import type { SingleConfig, SingleConfigWithType } from './singles.js';
6
- import type { CustomFieldDefinition, PluginConfig } from './plugins.js';
6
+ import type { CustomFieldDefinition, IconSetPlugin, Plugin } from './plugins.js';
7
7
  import type { FormConfig } from './forms.js';
8
8
  import type { AIAdapter } from './adapters/ai.js';
9
9
  import type { EmailAdapter } from './adapters/email.js';
@@ -77,7 +77,7 @@ export interface CMSConfig {
77
77
  files: FilesAdapter;
78
78
  email?: EmailAdapter;
79
79
  auth?: AuthConfig;
80
- plugins?: PluginConfig[];
80
+ plugins?: Plugin[];
81
81
  ai?: AIAdapter;
82
82
  media?: MediaConfig;
83
83
  apiKeys?: ApiKeyConfig[];
@@ -95,8 +95,9 @@ export interface ICMS {
95
95
  filesAdapter: FilesAdapter;
96
96
  emailAdapter: EmailAdapter | null;
97
97
  authConfig: AuthConfig | null;
98
- plugins: PluginConfig[];
98
+ plugins: Plugin[];
99
99
  customFields: Map<string, CustomFieldDefinition>;
100
+ iconSets: Map<string, IconSetPlugin>;
100
101
  aiAdapter: AIAdapter | null;
101
102
  mediaConfig: MediaConfig;
102
103
  apiKeys: ApiKeyConfig[];
@@ -445,7 +445,7 @@ export declare const _internal: {
445
445
  shop: z.ZodOptional<z.ZodUnknown>;
446
446
  cmp: z.ZodOptional<z.ZodUnknown>;
447
447
  }, z.core.$loose>;
448
- FIELD_TYPES: readonly ["text", "content", "number", "boolean", "date", "datetime", "file", "media", "select", "radio", "checkboxes", "relation", "object", "array", "blocks", "slug", "seo", "shop", "url", "custom"];
448
+ FIELD_TYPES: readonly ["text", "content", "number", "boolean", "date", "datetime", "file", "media", "select", "radio", "checkboxes", "relation", "object", "array", "blocks", "slug", "seo", "shop", "url", "icon", "custom"];
449
449
  FORM_FIELD_TYPES: readonly ["text", "email", "textarea", "checkbox", "select", "file"];
450
450
  getLangCode: typeof getLangCode;
451
451
  };
@@ -51,6 +51,7 @@ const FIELD_TYPES = [
51
51
  'seo',
52
52
  'shop',
53
53
  'url',
54
+ 'icon',
54
55
  'custom'
55
56
  ];
56
57
  const baseFieldShape = {
@@ -269,6 +270,14 @@ const fieldSchema = z.lazy(() => z.discriminatedUnion('type', [
269
270
  type: z.literal('custom'),
270
271
  fieldType: z.string().min(1, { message: 'custom.fieldType is required' }),
271
272
  config: z.record(z.string(), z.unknown()).optional()
273
+ })
274
+ .passthrough(),
275
+ // icon — set provided by IconSetPlugin (registered in plugins[])
276
+ z
277
+ .object({
278
+ ...baseFieldShape,
279
+ type: z.literal('icon'),
280
+ set: z.string().optional()
272
281
  })
273
282
  .passthrough()
274
283
  ]));
@@ -2,7 +2,7 @@ import type { FormatEnum } from 'sharp';
2
2
  import type { ImageStyle, MediaFile, VideoStyle } from './media.js';
3
3
  import type { Localized } from './languages.js';
4
4
  import type { StructuredContentDoc } from './structured-content.js';
5
- export type FieldType = 'text' | 'content' | 'number' | 'boolean' | 'date' | 'datetime' | 'file' | 'media' | 'select' | 'radio' | 'checkboxes' | 'relation' | 'object' | 'array' | 'blocks' | 'slug' | 'seo' | 'shop' | 'url' | 'custom';
5
+ export type FieldType = 'text' | 'content' | 'number' | 'boolean' | 'date' | 'datetime' | 'file' | 'media' | 'select' | 'radio' | 'checkboxes' | 'relation' | 'object' | 'array' | 'blocks' | 'slug' | 'seo' | 'shop' | 'url' | 'icon' | 'custom';
6
6
  export interface FieldCondition {
7
7
  field: string;
8
8
  equals?: string | string[];
@@ -234,6 +234,11 @@ export interface ShopFieldData {
234
234
  basePrice: number;
235
235
  vatRate: number;
236
236
  isActive: boolean;
237
+ /**
238
+ * Per-product payment policy. Omitted = `{ type: 'full' }` (legacy behavior).
239
+ * @public
240
+ */
241
+ paymentPolicy?: import('../shop/types.js').PaymentPolicy;
237
242
  variants?: Array<{
238
243
  id?: string;
239
244
  sku?: string | null;
@@ -264,4 +269,18 @@ export interface CustomField extends BaseField {
264
269
  /** Plugin-specific configuration, opaque to core */
265
270
  config?: Record<string, unknown>;
266
271
  }
267
- export type Field = TextField | ContentField | NumberField | BooleanField | DateField | DateTimeField | FileField | MediaField | SelectField | RadioField | CheckboxesField | RelationField | ObjectField | ArrayField | BlocksField | SlugField | SeoField | ShopField | UrlField | CustomField;
272
+ /**
273
+ * @public
274
+ * Icon picker field. Stores a string key referencing an icon registered by an
275
+ * {@link import('./plugins.js').IconSetPlugin}. Admin UI renders a kafelek
276
+ * (collapsed) and opens a searchable grid dialog. The set of icons (Svelte
277
+ * components + labels) is provided by a plugin at admin bootstrap — NOT inline
278
+ * in `cms.config.ts` (which is server-only / Zod-validated).
279
+ */
280
+ export interface IconField extends BaseField {
281
+ type: 'icon';
282
+ /** Plugin slug to source icons from when multiple icon-set plugins registered. Defaults to the first one. */
283
+ set?: string;
284
+ defaultValue?: string;
285
+ }
286
+ export type Field = TextField | ContentField | NumberField | BooleanField | DateField | DateTimeField | FileField | MediaField | SelectField | RadioField | CheckboxesField | RelationField | ObjectField | ArrayField | BlocksField | SlugField | SeoField | ShopField | UrlField | IconField | CustomField;
@@ -10,7 +10,7 @@ export { type SingleConfig } from './singles.js';
10
10
  export { type FormConfig, type FormSubmission } from './forms.js';
11
11
  export { type FormField, type FormFieldType, type FormBaseField, type FormTextField, type FormEmailField, type FormTextareaField, type FormCheckboxField, type FormSelectField } from './formFields.js';
12
12
  export { type CMSConfig, type ApiKeyConfig, type AuthConfig, type TypographyConfig } from './cms.js';
13
- export { type PluginConfig, type CustomFieldDefinition } from './plugins.js';
13
+ export { type PluginConfig, type CustomFieldDefinition, type IconSetPlugin, type IconDefinition, type Plugin, isIconSetPlugin } from './plugins.js';
14
14
  export { type Language, type Localized } from './languages.js';
15
15
  export { type Layout, type LayoutNode, type LayoutPreset, type LayoutNodeType, type ColumnRatio, type SectionNode, type ColumnsNode, type CardNode, type AccordionNode, type StackNode, type TabsNode, type TabNode } from './layout.js';
16
16
  export { type CmsContext } from './cms-context.js';
@@ -10,7 +10,7 @@ export {} from './singles.js';
10
10
  export {} from './forms.js';
11
11
  export {} from './formFields.js';
12
12
  export {} from './cms.js';
13
- export {} from './plugins.js';
13
+ export { isIconSetPlugin } from './plugins.js';
14
14
  export {} from './languages.js';
15
15
  export {} from './layout.js';
16
16
  export {} from './cms-context.js';
@@ -2,6 +2,7 @@ import type { Component } from 'svelte';
2
2
  import type { z } from 'zod';
3
3
  import type { RawEntry } from './entries.js';
4
4
  import type { CustomField } from './fields.js';
5
+ import type { Localized } from './languages.js';
5
6
  import type { PopulateCtx } from '../core/server/entries/operations/resolveEntry.js';
6
7
  /**
7
8
  * Defines a custom field type contributed by a plugin.
@@ -40,3 +41,42 @@ export interface PluginConfig {
40
41
  afterDelete?: (id: string) => Promise<void>;
41
42
  };
42
43
  }
44
+ /**
45
+ * @public
46
+ * Single icon entry contributed by an {@link IconSetPlugin}. Carries a Svelte
47
+ * component, a localized label and optional search keywords. Components are
48
+ * eagerly imported (tree-shakable) — for small sets (10–50 icons) lazy import
49
+ * adds overhead without benefit.
50
+ */
51
+ export interface IconDefinition {
52
+ /** Svelte icon component (e.g. `Brain` from `@lucide/svelte`). */
53
+ component: Component;
54
+ /** Localized display label shown in collapsed state and in the picker grid. */
55
+ label: Localized;
56
+ /** Extra search keywords (non-localized). Search matches key + label + keywords. */
57
+ keywords?: string[];
58
+ }
59
+ /**
60
+ * @public
61
+ * Icon set plugin — registers a named set of icons consumable by the `icon`
62
+ * field type. Registered alongside other plugins in `cms.config.ts`'s
63
+ * `plugins:` array. Because Svelte components do NOT serialize to the client,
64
+ * the application's admin layout must wire the same plugin instance to
65
+ * `AdminLayout` via `buildIconSetMap(...)` — see admin/helpers.
66
+ */
67
+ export interface IconSetPlugin {
68
+ /** Unique slug — referenced by `IconField.set` when multiple sets exist. */
69
+ slug: string;
70
+ type: 'icon-set';
71
+ icons: Record<string, IconDefinition>;
72
+ }
73
+ /**
74
+ * @public
75
+ * Union of all plugin kinds accepted in `cms.config.ts → plugins:`. New
76
+ * plugin kinds must be added here. Type discriminated by:
77
+ * - {@link IconSetPlugin} → `type === 'icon-set'`
78
+ * - {@link PluginConfig} → everything else (no `type` field).
79
+ */
80
+ export type Plugin = PluginConfig | IconSetPlugin;
81
+ /** Type guard for {@link IconSetPlugin}. */
82
+ export declare function isIconSetPlugin(p: Plugin): p is IconSetPlugin;
@@ -1 +1,4 @@
1
- export {};
1
+ /** Type guard for {@link IconSetPlugin}. */
2
+ export function isIconSetPlugin(p) {
3
+ return p.type === 'icon-set';
4
+ }
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,19 @@
1
+ export const update = {
2
+ version: '0.26.1',
3
+ date: '2026-05-28',
4
+ description: 'Nowy typ pola `icon` + IconSetPlugin (zestaw ikon dostarczany przez aplikację, dialog z gridem + search). Scaffolder fix + DataTable polish: scaffold admin używa publicznych entry points; pierwsza kolumna tabeli bez sticky-first; checkbox visual wraca do 16 px z transparent hit-area 24×24 (WCAG 2.5.5).',
5
+ features: [
6
+ 'Nowy typ pola `icon` (`IconField` @public, `src/lib/types/fields.ts`) — kompaktowy kafelek 96×96 w formularzu (ikona + nazwa, X do wyczyszczenia) otwiera dialog z gridem responsive 3–6 kolumn, wyszukiwarką (filtruje po `key`, lokalizowanym `label`, opcjonalnych `keywords`) i przyciskami Anuluj / OK. Klik na kafelku w dialogu zaznacza, OK zatwierdza; dwuklik = szybki zapis. Stan "missing" (wartość w DB nieobecna w bibliotece) pokazuje placeholder + przyjazne ostrzeżenie zamiast resetować wartość — zgodne z ToV "informuj, nie strasz". i18n PL/EN, `role="listbox"`/`role="option"`/`aria-selected`, LiveRegion z liczbą wyników.',
7
+ '`IconSetPlugin` / `IconDefinition` (`@public`, `src/lib/types/plugins.ts`) — nowy rodzaj pluginu rejestrujący nazwany zestaw ikon: `{ slug, type: \'icon-set\', icons: Record<key, { component, label: Localized, keywords?: string[] }> }`. Komponenty Svelte (eager import, tree-shakable — np. `Brain` z `@lucide/svelte`) ŻYJĄ w pluginie, bo `cms.config.ts` jest server-only i Zod-validated. `Plugin = PluginConfig | IconSetPlugin` + `isIconSetPlugin` type guard.',
8
+ '`buildIconSetMap(...plugins)` (`includio-cms/admin`) + nowy prop `iconSets?: Map<string, IconSetPlugin>` na `AdminLayout` — konsumencki `+layout.svelte` admina przekazuje plugin instancje na klienta (funkcji komponentów nie da się serializować przez SvelteKit data). `getIconSets()` / `resolveIconSet(slug?)` w `includio-cms/admin` — runtime lookup używany przez `icon-field.svelte` przez Svelte context.',
9
+ '`CMS.iconSets: Map<slug, IconSetPlugin>` agregowane przy starcie z `cms.config.plugins` przez `isIconSetPlugin` guard; duplikat slug → throw. Generator TS mapuje `icon` → `string` w wygenerowanym `types.ts`; Zod runtime → `z.string()` (analogicznie do `slug` field).'
10
+ ],
11
+ fixes: [
12
+ '`scaffold admin` generuje `src/routes/admin/api/[...path]/+server.ts` z `import { createAdminApiHandler } from \'includio-cms/sveltekit/server\'`. Wcześniej generowany import `includio-cms/admin/api/handler` rzucał Vite `Missing "./admin/api/handler" specifier in "includio-cms" package` przy starcie dev/build — `admin/api/handler` nie ma w `exports` (v1.0 frozen 18 ścieżek).',
13
+ '`scaffold admin` generuje `src/routes/admin/api/rest/[...restPath]/+server.ts` z `import { createRestApiHandler } from \'includio-cms/sveltekit/server\'` (analogicznie — był broken w 0.20.0+ ale scaffolder nie zaktualizowany).',
14
+ '`includio-cms/sveltekit/server` re-eksportuje teraz `createAdminApiHandler` (obok istniejącego `createRestApiHandler`).',
15
+ '`DataTable`: revert sticky-first column (z 0.26.0 / S10c) — pierwsza kolumna scrolluje razem z resztą, bez wyróżnienia kolorystycznego. Usunięty prop `stickyFirstColumn` i powiązany CSS.',
16
+ '`Checkbox` primitive: visual wraca do 16×16 px (z 24×24 w 0.26.0 / S10b), klikalny obszar utrzymany na ≥24×24 przez transparent `::after` pseudo-element (`after:-inset-1`) — WCAG 2.5.5 target-size AA dalej spełniony, lepszy wygląd zgodny z proporcjami pól tekstowych.'
17
+ ],
18
+ breakingChanges: []
19
+ };