includio-cms 0.26.0 → 0.28.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 (128) hide show
  1. package/API.md +58 -2
  2. package/CHANGELOG.md +105 -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 +192 -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 +116 -24
  29. package/dist/admin/remote/shop.remote.js +79 -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/index.d.ts +1 -0
  41. package/dist/db-postgres/schema/shop/index.js +1 -0
  42. package/dist/db-postgres/schema/shop/invoice.d.ts +254 -0
  43. package/dist/db-postgres/schema/shop/invoice.js +27 -0
  44. package/dist/db-postgres/schema/shop/order.d.ts +107 -1
  45. package/dist/db-postgres/schema/shop/order.js +7 -1
  46. package/dist/db-postgres/schema/shop/payment.d.ts +20 -0
  47. package/dist/db-postgres/schema/shop/payment.js +4 -1
  48. package/dist/db-postgres/schema/shop/product.d.ts +20 -0
  49. package/dist/db-postgres/schema/shop/product.js +3 -1
  50. package/dist/db-postgres/schema/shop/productVariant.d.ts +12 -2
  51. package/dist/db-postgres/schema/shop/productVariant.js +22 -0
  52. package/dist/paraglide/messages/_index.d.ts +36 -3
  53. package/dist/paraglide/messages/_index.js +71 -3
  54. package/dist/paraglide/messages/en.d.ts +5 -0
  55. package/dist/paraglide/messages/en.js +14 -0
  56. package/dist/paraglide/messages/pl.d.ts +5 -0
  57. package/dist/paraglide/messages/pl.js +14 -0
  58. package/dist/shop/adapters/fakturownia/client.d.ts +28 -0
  59. package/dist/shop/adapters/fakturownia/client.js +67 -0
  60. package/dist/shop/adapters/fakturownia/index.d.ts +27 -0
  61. package/dist/shop/adapters/fakturownia/index.js +36 -0
  62. package/dist/shop/adapters/fakturownia/payload.d.ts +35 -0
  63. package/dist/shop/adapters/fakturownia/payload.js +45 -0
  64. package/dist/shop/cart/types.d.ts +1 -0
  65. package/dist/shop/client/index.d.ts +61 -0
  66. package/dist/shop/client/index.js +5 -1
  67. package/dist/shop/expiry.d.ts +35 -0
  68. package/dist/shop/expiry.js +68 -0
  69. package/dist/shop/http/balance-handler.d.ts +20 -0
  70. package/dist/shop/http/balance-handler.js +91 -0
  71. package/dist/shop/http/cart-handler.js +19 -0
  72. package/dist/shop/http/checkout-handler.js +30 -1
  73. package/dist/shop/http/index.d.ts +2 -0
  74. package/dist/shop/http/index.js +2 -0
  75. package/dist/shop/http/upcoming-handler.d.ts +16 -0
  76. package/dist/shop/http/upcoming-handler.js +65 -0
  77. package/dist/shop/http/webhook-handler.js +46 -9
  78. package/dist/shop/index.d.ts +7 -1
  79. package/dist/shop/index.js +10 -1
  80. package/dist/shop/nip.d.ts +12 -0
  81. package/dist/shop/nip.js +23 -0
  82. package/dist/shop/server/balance-payment.d.ts +40 -0
  83. package/dist/shop/server/balance-payment.js +140 -0
  84. package/dist/shop/server/cart-hydrate.js +2 -0
  85. package/dist/shop/server/init.d.ts +14 -0
  86. package/dist/shop/server/init.js +35 -0
  87. package/dist/shop/server/invoices.d.ts +64 -0
  88. package/dist/shop/server/invoices.js +237 -0
  89. package/dist/shop/server/orders.d.ts +38 -0
  90. package/dist/shop/server/orders.js +152 -2
  91. package/dist/shop/server/payment-policy.d.ts +35 -0
  92. package/dist/shop/server/payment-policy.js +55 -0
  93. package/dist/shop/server/payments.d.ts +29 -0
  94. package/dist/shop/server/payments.js +64 -0
  95. package/dist/shop/server/populate.d.ts +1 -1
  96. package/dist/shop/server/refund.d.ts +17 -12
  97. package/dist/shop/server/refund.js +96 -13
  98. package/dist/shop/server/shop-data.d.ts +4 -1
  99. package/dist/shop/server/shop-data.js +24 -2
  100. package/dist/shop/template.d.ts +13 -0
  101. package/dist/shop/template.js +98 -0
  102. package/dist/shop/types.d.ts +208 -1
  103. package/dist/shop/variant-attributes.d.ts +28 -0
  104. package/dist/shop/variant-attributes.js +69 -0
  105. package/dist/sveltekit/server/index.d.ts +1 -0
  106. package/dist/sveltekit/server/index.js +2 -0
  107. package/dist/types/cms.d.ts +4 -3
  108. package/dist/types/cms.schema.d.ts +1 -1
  109. package/dist/types/cms.schema.js +9 -0
  110. package/dist/types/fields.d.ts +21 -2
  111. package/dist/types/index.d.ts +1 -1
  112. package/dist/types/index.js +1 -1
  113. package/dist/types/plugins.d.ts +40 -0
  114. package/dist/types/plugins.js +4 -1
  115. package/dist/updates/0.26.1/index.d.ts +2 -0
  116. package/dist/updates/0.26.1/index.js +19 -0
  117. package/dist/updates/0.27.0/index.d.ts +2 -0
  118. package/dist/updates/0.27.0/index.js +50 -0
  119. package/dist/updates/0.28.0/index.d.ts +2 -0
  120. package/dist/updates/0.28.0/index.js +38 -0
  121. package/dist/updates/index.js +7 -1
  122. package/package.json +1 -1
  123. package/dist/paraglide/messages/hello_world.d.ts +0 -5
  124. package/dist/paraglide/messages/hello_world.js +0 -33
  125. package/dist/paraglide/messages/login_hello.d.ts +0 -16
  126. package/dist/paraglide/messages/login_hello.js +0 -34
  127. package/dist/paraglide/messages/login_please_login.d.ts +0 -16
  128. package/dist/paraglide/messages/login_please_login.js +0 -34
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { getContext } from 'svelte';
2
+ import { getContext, untrack } from 'svelte';
3
3
  import { Button } from '../../../components/ui/button/index.js';
4
4
  import { Switch } from '../../../components/ui/switch/index.js';
5
5
  import TrashIcon from '@tabler/icons-svelte/icons/trash';
@@ -8,6 +8,9 @@
8
8
  import { getRemotes } from '../../../sveltekit/index.js';
9
9
  import { errorMessages } from '../../i18n/errors.js';
10
10
  import type { ShopField } from '../../../types/fields.js';
11
+ import VariantAttributeRenderer from '../variant-form/VariantAttributeRenderer.svelte';
12
+ import { interpolateTemplate } from '../../../shop/template.js';
13
+ import { isVariantExpired } from '../../../shop/expiry.js';
11
14
 
12
15
  type Props = { field: ShopField };
13
16
  const { field }: Props = $props();
@@ -22,14 +25,21 @@
22
25
  let inputPrice = $state('0.00');
23
26
  let vatRate = $state<number | string>(23);
24
27
  let isActive = $state(true);
28
+ type PolicyType = 'full' | 'deposit';
29
+ type DepositKind = 'percent' | 'amount';
30
+ let policyType = $state<PolicyType>('full');
31
+ let depositKind = $state<DepositKind>('percent');
32
+ let depositValue = $state<string>('30');
25
33
  let variants = $state<
26
34
  Array<{
27
35
  id?: string;
28
36
  sku: string;
29
37
  name: string;
38
+ nameDirty: boolean;
30
39
  priceDelta: string;
31
40
  priceDeltaMode: InputMode;
32
41
  stock: string;
42
+ attributes: Record<string, unknown>;
33
43
  }>
34
44
  >([]);
35
45
 
@@ -37,6 +47,7 @@
37
47
  let saving = $state(false);
38
48
  let errorMessage = $state<string | null>(null);
39
49
  let successMessage = $state<string | null>(null);
50
+ let showExpired = $state(false);
40
51
 
41
52
  // PLN-precise math — storage w numeric(20,6). Round do groszy tylko przy prezentacji.
42
53
  const inputPln = $derived(parseFloat(inputPrice || '0') || 0);
@@ -68,20 +79,35 @@
68
79
  inputPrice = formatPln(grossInitial);
69
80
  vatRate = shop.vatRate;
70
81
  isActive = shop.isActive;
71
- variants = shop.variants.map((v) => ({
72
- id: v.id,
73
- sku: v.sku ?? '',
74
- name:
82
+ const policy = shop.paymentPolicy ?? null;
83
+ if (policy && policy.type === 'deposit') {
84
+ policyType = 'deposit';
85
+ depositKind = policy.depositAmount.type;
86
+ depositValue = String(policy.depositAmount.value);
87
+ } else {
88
+ policyType = 'full';
89
+ }
90
+ variants = shop.variants.map((v) => {
91
+ const name =
75
92
  typeof v.name === 'object' && v.name !== null
76
93
  ? String(
77
94
  Object.values(v.name as Record<string, string>)[0] ?? ''
78
95
  )
79
- : '',
80
- // Delta hydratowana jako netto (kanoniczna), mode default 'net' dla istniejących.
81
- priceDelta: formatPln(Number(v.priceDelta)),
82
- priceDeltaMode: 'net' as InputMode,
83
- stock: v.stock == null ? '' : String(v.stock)
84
- }));
96
+ : '';
97
+ return {
98
+ id: v.id,
99
+ sku: v.sku ?? '',
100
+ name,
101
+ // Saved non-empty name = editor's choice; lock pre-fill for this session.
102
+ nameDirty: name !== '',
103
+ // Delta hydratowana jako netto (kanoniczna), mode default 'net' dla istniejących.
104
+ priceDelta: formatPln(Number(v.priceDelta)),
105
+ priceDeltaMode: 'net' as InputMode,
106
+ stock: v.stock == null ? '' : String(v.stock),
107
+ attributes:
108
+ (v.attributes as Record<string, unknown> | null | undefined) ?? {}
109
+ };
110
+ });
85
111
  } else {
86
112
  const defaultVat = configQuery.current?.vatRates[0] ?? 23;
87
113
  vatRate = defaultVat;
@@ -101,12 +127,31 @@
101
127
  id: undefined,
102
128
  sku: '',
103
129
  name: '',
130
+ nameDirty: false,
104
131
  priceDelta: '0.00',
105
132
  priceDeltaMode: 'gross',
106
- stock: ''
133
+ stock: '',
134
+ attributes: {}
107
135
  });
108
136
  }
109
137
 
138
+ // Auto-prefill `name` from variantLabel.template whenever attributes change,
139
+ // unless the user has edited the name manually (dirty flag, per session).
140
+ $effect(() => {
141
+ const template = configQuery.current?.variantLabel?.template;
142
+ if (!template) return;
143
+ for (let i = 0; i < variants.length; i++) {
144
+ // Tracked: re-run when this variant's attributes change.
145
+ const attrs = variants[i].attributes;
146
+ const next = interpolateTemplate(template, attrs, 'pl');
147
+ untrack(() => {
148
+ const v = variants[i];
149
+ if (v.nameDirty) return;
150
+ if (v.name !== next) v.name = next;
151
+ });
152
+ }
153
+ });
154
+
110
155
  function switchVariantMode(i: number, newMode: InputMode) {
111
156
  const v = variants[i];
112
157
  if (v.priceDeltaMode === newMode) return;
@@ -119,6 +164,38 @@
119
164
  variants.splice(i, 1);
120
165
  }
121
166
 
167
+ const expiryConfig = $derived(configQuery.current?.variantExpiry ?? null);
168
+
169
+ const displayVariants = $derived.by(() => {
170
+ const enriched = variants.map((v, originalIndex) => ({
171
+ v,
172
+ originalIndex,
173
+ expired: expiryConfig
174
+ ? isVariantExpired({ attributes: v.attributes }, expiryConfig)
175
+ : false
176
+ }));
177
+ const visible = showExpired ? enriched : enriched.filter((x) => !x.expired);
178
+ const source = expiryConfig?.source;
179
+ if (!source) return visible;
180
+ return [...visible].sort((a, b) => {
181
+ const aVal = a.v.attributes?.[source];
182
+ const bVal = b.v.attributes?.[source];
183
+ const aTs = typeof aVal === 'string' ? Date.parse(aVal) : NaN;
184
+ const bTs = typeof bVal === 'string' ? Date.parse(bVal) : NaN;
185
+ if (Number.isNaN(aTs) && Number.isNaN(bTs)) return 0;
186
+ if (Number.isNaN(aTs)) return 1;
187
+ if (Number.isNaN(bTs)) return -1;
188
+ return aTs - bTs;
189
+ });
190
+ });
191
+
192
+ const expiredCount = $derived(
193
+ expiryConfig
194
+ ? variants.filter((v) => isVariantExpired({ attributes: v.attributes }, expiryConfig))
195
+ .length
196
+ : 0
197
+ );
198
+
122
199
  async function save() {
123
200
  if (!entryId) {
124
201
  errorMessage = 'Brak ID wpisu — zapisz najpierw wpis, potem dane sklepu.';
@@ -130,12 +207,26 @@
130
207
  try {
131
208
  const stockEnabled = configQuery.current?.features.stock ?? false;
132
209
  const variantsEnabled = configQuery.current?.features.variants ?? false;
210
+ const policyValue = Number(depositValue);
211
+ const paymentPolicy =
212
+ policyType === 'full'
213
+ ? ({ type: 'full' as const })
214
+ : Number.isFinite(policyValue) && policyValue > 0
215
+ ? ({
216
+ type: 'deposit' as const,
217
+ depositAmount:
218
+ depositKind === 'percent'
219
+ ? { type: 'percent' as const, value: policyValue }
220
+ : { type: 'amount' as const, value: Math.round(policyValue) }
221
+ })
222
+ : ({ type: 'full' as const });
133
223
  await remotes.upsertShopDataForEntry({
134
224
  entryId,
135
225
  data: {
136
226
  basePrice: netPln,
137
227
  vatRate: Number(vatRate),
138
- isActive
228
+ isActive,
229
+ paymentPolicy
139
230
  },
140
231
  variants: variantsEnabled
141
232
  ? variants.map((v) => ({
@@ -144,7 +235,7 @@
144
235
  name: v.name ? { pl: v.name } : null,
145
236
  priceDelta: variantDeltaNetPln(v),
146
237
  stock: stockEnabled && v.stock !== '' ? parseInt(v.stock, 10) : null,
147
- attributes: null
238
+ attributes: Object.keys(v.attributes).length > 0 ? v.attributes : null
148
239
  }))
149
240
  : []
150
241
  });
@@ -243,24 +334,122 @@
243
334
  </div>
244
335
  </div>
245
336
 
337
+ <div class="border-border space-y-3 rounded-lg border-t pt-4">
338
+ <h4 class="text-sm font-bold">Zasady płatności</h4>
339
+ <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
340
+ <label class="flex items-center gap-2 text-sm">
341
+ <input
342
+ type="radio"
343
+ name="policy-type"
344
+ value="full"
345
+ checked={policyType === 'full'}
346
+ onchange={() => (policyType = 'full')}
347
+ />
348
+ <span>Płatność z góry (pełna kwota)</span>
349
+ </label>
350
+ <label class="flex items-center gap-2 text-sm">
351
+ <input
352
+ type="radio"
353
+ name="policy-type"
354
+ value="deposit"
355
+ checked={policyType === 'deposit'}
356
+ onchange={() => (policyType = 'deposit')}
357
+ />
358
+ <span>Zaliczka + dopłata</span>
359
+ </label>
360
+ </div>
361
+ {#if policyType === 'deposit'}
362
+ <div class="bg-muted/30 grid grid-cols-1 gap-3 rounded-md p-3 sm:grid-cols-[auto_1fr]">
363
+ <label class="flex flex-col gap-1 text-xs">
364
+ <span class="text-muted-foreground font-semibold">Typ zaliczki</span>
365
+ <select
366
+ class="border-border rounded-md border px-2 py-1.5 text-sm"
367
+ bind:value={depositKind}
368
+ >
369
+ <option value="percent">Procent ceny</option>
370
+ <option value="amount">Kwota stała (grosze)</option>
371
+ </select>
372
+ </label>
373
+ <label class="flex flex-col gap-1 text-xs">
374
+ <span class="text-muted-foreground font-semibold">
375
+ {depositKind === 'percent' ? 'Procent (1–100)' : 'Kwota w groszach'}
376
+ </span>
377
+ <input
378
+ type="number"
379
+ inputmode="numeric"
380
+ min="1"
381
+ max={depositKind === 'percent' ? '100' : undefined}
382
+ step={depositKind === 'percent' ? '1' : '1'}
383
+ class="border-border rounded-md border px-2 py-1.5 text-sm tabular-nums"
384
+ bind:value={depositValue}
385
+ />
386
+ </label>
387
+ <p class="text-muted-foreground col-span-full text-xs">
388
+ {#if depositKind === 'percent'}
389
+ Klient płaci {depositValue || '0'}% przy zamówieniu, resztę po wygenerowaniu linka do dopłaty w panelu admina.
390
+ {:else}
391
+ Klient płaci {(Number(depositValue) || 0) / 100} zł przy zamówieniu, resztę po wygenerowaniu linka do dopłaty.
392
+ {/if}
393
+ </p>
394
+ </div>
395
+ {/if}
396
+ </div>
397
+
246
398
  {#if config.features.variants}
247
399
  <div class="border-border space-y-3 rounded-lg border-t pt-4">
248
- <div class="flex items-center justify-between">
400
+ <div class="flex items-center justify-between gap-2">
249
401
  <h4 class="text-sm font-bold">Warianty</h4>
250
- <Button type="button" variant="outline" size="sm" onclick={addVariant}>
251
- <PlusIcon class="mr-1 size-4" /> Dodaj wariant
252
- </Button>
402
+ <div class="flex items-center gap-3">
403
+ {#if expiryConfig && expiredCount > 0}
404
+ <label class="text-muted-foreground flex items-center gap-1.5 text-xs">
405
+ <input type="checkbox" bind:checked={showExpired} class="size-3.5" />
406
+ Pokaż zakończone ({expiredCount})
407
+ </label>
408
+ {/if}
409
+ <Button type="button" variant="outline" size="sm" onclick={addVariant}>
410
+ <PlusIcon class="mr-1 size-4" /> Dodaj wariant
411
+ </Button>
412
+ </div>
253
413
  </div>
254
414
  {#if variants.length === 0}
255
415
  <p class="text-muted-foreground text-xs">Brak wariantów — zostanie użyta cena bazowa.</p>
416
+ {:else if displayVariants.length === 0}
417
+ <p class="text-muted-foreground text-xs">
418
+ Wszystkie warianty są zakończone. Zaznacz „Pokaż zakończone", aby je zobaczyć.
419
+ </p>
256
420
  {/if}
257
- {#each variants as v, i (i)}
258
- <div class="border-border grid grid-cols-2 gap-2 rounded-lg border p-3">
421
+ {#each displayVariants as { v, originalIndex, expired } (originalIndex)}
422
+ {@const i = originalIndex}
423
+ <div
424
+ class="border-border space-y-3 rounded-lg border p-3 {expired ? 'opacity-60' : ''}"
425
+ >
426
+ {#if expired}
427
+ <span
428
+ class="bg-muted text-muted-foreground inline-flex items-center rounded-md border border-border px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide"
429
+ aria-label="Wariant zakończony"
430
+ >
431
+ Zakończony
432
+ </span>
433
+ {/if}
434
+ {#if config.variantAttributes && Object.keys(config.variantAttributes).length > 0}
435
+ <div class="grid grid-cols-1 gap-3 md:grid-cols-2">
436
+ {#each Object.entries(config.variantAttributes) as [attrKey, attrConfig] (attrKey)}
437
+ <VariantAttributeRenderer
438
+ {attrKey}
439
+ {attrConfig}
440
+ bind:value={v.attributes[attrKey]}
441
+ />
442
+ {/each}
443
+ </div>
444
+ <div class="border-border border-t pt-3" aria-hidden="true"></div>
445
+ {/if}
446
+ <div class="grid grid-cols-2 gap-2">
259
447
  <label class="block">
260
448
  <span class="text-muted-foreground mb-0.5 block text-xs">Nazwa</span>
261
449
  <input
262
450
  type="text"
263
451
  bind:value={v.name}
452
+ oninput={() => (v.nameDirty = true)}
264
453
  class="border-border w-full rounded border px-2 py-1 text-sm"
265
454
  />
266
455
  </label>
@@ -323,6 +512,7 @@
323
512
  <TrashIcon class="size-4" />
324
513
  </Button>
325
514
  </div>
515
+ </div>
326
516
  </div>
327
517
  {/each}
328
518
  </div>
@@ -128,6 +128,7 @@
128
128
 
129
129
  :global(.layout-tab-panel.bleed) {
130
130
  box-sizing: border-box;
131
+ width: 100%;
131
132
  max-width: 1100px;
132
133
  margin: 0 auto;
133
134
  padding: 24px;
@@ -0,0 +1,109 @@
1
+ <script lang="ts">
2
+ import type { VariantAttribute } from '../../../shop/types.js';
3
+ import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
4
+ import { getLocalizedLabel } from '../../utils/collectionLabel.js';
5
+ import TextField from '../fields/text-field.svelte';
6
+ import NumberField from '../fields/number-field.svelte';
7
+ import DatetimeField from '../fields/datetime-field.svelte';
8
+ import SelectField from '../fields/select-field.svelte';
9
+ import BooleanField from '../fields/boolean-field.svelte';
10
+ import Input from '../../../components/ui/input/input.svelte';
11
+ import type {
12
+ TextField as TextFieldType,
13
+ NumberField as NumberFieldType,
14
+ DateTimeField as DateTimeFieldType,
15
+ SelectField as SelectFieldType,
16
+ BooleanField as BooleanFieldType
17
+ } from '../../../types/fields.js';
18
+
19
+ type Props = {
20
+ attrKey: string;
21
+ attrConfig: VariantAttribute;
22
+ value: unknown;
23
+ };
24
+
25
+ let { attrKey, attrConfig, value = $bindable() }: Props = $props();
26
+
27
+ const interfaceLanguage = useInterfaceLanguage();
28
+ const label = $derived(
29
+ getLocalizedLabel(attrConfig.label, interfaceLanguage.current) || attrKey
30
+ );
31
+
32
+ // Adapter — VariantAttribute → CMS Field shape that each field component expects.
33
+ // Booleans render their own label inline; everything else uses our outer <label>.
34
+ const textField = $derived<TextFieldType>({
35
+ type: 'text',
36
+ slug: attrKey,
37
+ label,
38
+ required: attrConfig.required,
39
+ multiline: false
40
+ });
41
+ const numberField = $derived<NumberFieldType>({
42
+ type: 'number',
43
+ slug: attrKey,
44
+ label,
45
+ required: attrConfig.required
46
+ });
47
+ const datetimeField = $derived<DateTimeFieldType>({
48
+ type: 'datetime',
49
+ slug: attrKey,
50
+ label,
51
+ required: attrConfig.required
52
+ });
53
+ const selectField = $derived<SelectFieldType>(
54
+ attrConfig.type === 'select'
55
+ ? {
56
+ type: 'select',
57
+ slug: attrKey,
58
+ label,
59
+ required: attrConfig.required,
60
+ options: attrConfig.options
61
+ }
62
+ : ({} as SelectFieldType)
63
+ );
64
+ const booleanField = $derived<BooleanFieldType>({
65
+ type: 'boolean',
66
+ slug: attrKey,
67
+ label,
68
+ required: attrConfig.required,
69
+ defaultValue: false
70
+ });
71
+ </script>
72
+
73
+ {#if attrConfig.type === 'boolean'}
74
+ <!-- boolean-field renders its own label, asterisk, and switch row -->
75
+ <BooleanField field={booleanField} bind:value={value as boolean | undefined} />
76
+ {:else}
77
+ <label class="block">
78
+ <span class="text-muted-foreground mb-1 block text-xs font-semibold">
79
+ {label}
80
+ {#if attrConfig.required}<span class="text-destructive ml-0.5" aria-hidden="true"
81
+ >*</span
82
+ >{/if}
83
+ </span>
84
+
85
+ {#if attrConfig.type === 'text'}
86
+ <TextField field={textField} bind:value={value as string | undefined} />
87
+ {:else if attrConfig.type === 'number'}
88
+ <NumberField field={numberField} bind:value={value as number | undefined} />
89
+ {:else if attrConfig.type === 'datetime'}
90
+ <DatetimeField field={datetimeField} bind:value={value as string | undefined} />
91
+ {:else if attrConfig.type === 'select'}
92
+ <SelectField field={selectField} bind:value={value as string | undefined} />
93
+ {:else if attrConfig.type === 'slug'}
94
+ <Input
95
+ bind:value={value as string | undefined}
96
+ type="text"
97
+ pattern="[a-z0-9-]+"
98
+ placeholder="moj-slug"
99
+ />
100
+ {:else if attrConfig.type === 'image' || attrConfig.type === 'entry'}
101
+ <!-- Pełny picker UI defer post-1.0; tymczasowo: wklej UUID -->
102
+ <Input
103
+ bind:value={value as string | undefined}
104
+ type="text"
105
+ placeholder="UUID ({attrConfig.type === 'image' ? 'media' : 'wpis'})"
106
+ />
107
+ {/if}
108
+ </label>
109
+ {/if}
@@ -0,0 +1,9 @@
1
+ import type { VariantAttribute } from '../../../shop/types.js';
2
+ type Props = {
3
+ attrKey: string;
4
+ attrConfig: VariantAttribute;
5
+ value: unknown;
6
+ };
7
+ declare const VariantAttributeRenderer: import("svelte").Component<Props, {}, "value">;
8
+ type VariantAttributeRenderer = ReturnType<typeof VariantAttributeRenderer>;
9
+ export default VariantAttributeRenderer;
@@ -0,0 +1,8 @@
1
+ import type { IconSetPlugin } from '../../types/plugins.js';
2
+ /**
3
+ * Build a Map<slug, IconSetPlugin> from icon-set plugin instances.
4
+ * Use in consumer's admin `+layout.svelte` to pass `iconSets` to AdminLayout —
5
+ * Svelte icon components cannot serialize through SvelteKit data, so the same
6
+ * plugin instances must be imported on the client and forwarded explicitly.
7
+ */
8
+ export declare function buildIconSetMap(...plugins: IconSetPlugin[]): Map<string, IconSetPlugin>;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Build a Map<slug, IconSetPlugin> from icon-set plugin instances.
3
+ * Use in consumer's admin `+layout.svelte` to pass `iconSets` to AdminLayout —
4
+ * Svelte icon components cannot serialize through SvelteKit data, so the same
5
+ * plugin instances must be imported on the client and forwarded explicitly.
6
+ */
7
+ export function buildIconSetMap(...plugins) {
8
+ const map = new Map();
9
+ for (const p of plugins) {
10
+ if (map.has(p.slug)) {
11
+ throw new Error(`Duplicate icon-set plugin slug: "${p.slug}"`);
12
+ }
13
+ map.set(p.slug, p);
14
+ }
15
+ return map;
16
+ }
@@ -1,7 +1,9 @@
1
1
  export { useField } from './use-field.js';
2
2
  export { buildCustomFieldsMap } from './build-custom-fields-map.js';
3
+ export { buildIconSetMap } from './build-icon-set-map.js';
3
4
  export { useInterfaceLanguage } from '../state/interface-language.svelte.js';
4
5
  export { getContentLanguage } from '../state/content-language.svelte.js';
5
6
  export { getRemotes } from '../context/remotes.js';
6
7
  export { getCustomFields } from '../state/custom-fields.svelte.js';
8
+ export { getIconSets, resolveIconSet } from '../state/icon-sets.svelte.js';
7
9
  export { getLocalizedLabel } from '../utils/collectionLabel.js';
@@ -1,7 +1,9 @@
1
1
  export { useField } from './use-field.js';
2
2
  export { buildCustomFieldsMap } from './build-custom-fields-map.js';
3
+ export { buildIconSetMap } from './build-icon-set-map.js';
3
4
  export { useInterfaceLanguage } from '../state/interface-language.svelte.js';
4
5
  export { getContentLanguage } from '../state/content-language.svelte.js';
5
6
  export { getRemotes } from '../context/remotes.js';
6
7
  export { getCustomFields } from '../state/custom-fields.svelte.js';
8
+ export { getIconSets, resolveIconSet } from '../state/icon-sets.svelte.js';
7
9
  export { getLocalizedLabel } from '../utils/collectionLabel.js';