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
@@ -3,6 +3,9 @@ type Props = {
3
3
  orderId: string;
4
4
  currency: string;
5
5
  remainingRefundable: number;
6
+ /** When the order has a deposit policy with paid deposit + balance rows. */
7
+ hasPartial?: boolean;
8
+ balanceOwed?: boolean;
6
9
  onOpenChange: (open: boolean) => void;
7
10
  onRefunded: () => void;
8
11
  };
@@ -36,6 +36,37 @@
36
36
  const refundsQuery = $derived(remotes.getOrderRefundsAdmin(orderId));
37
37
  let refundDialogOpen = $state(false);
38
38
 
39
+ const invoiceQuery = $derived(remotes.getOrderInvoiceAdmin(orderId));
40
+ let issuingInvoice = $state(false);
41
+ let invoiceError = $state<string | null>(null);
42
+
43
+ async function issueInvoice(force: boolean) {
44
+ issuingInvoice = true;
45
+ invoiceError = null;
46
+ try {
47
+ const result = await remotes.issueInvoiceCmd({ orderId, force });
48
+ if (!result.success) invoiceError = result.error;
49
+ await invoiceQuery.refresh();
50
+ } finally {
51
+ issuingInvoice = false;
52
+ }
53
+ }
54
+
55
+ function invoiceStatusLabel(status: string): string {
56
+ switch (status) {
57
+ case 'pending':
58
+ return 'W trakcie';
59
+ case 'issued':
60
+ return 'Wystawiona';
61
+ case 'sent':
62
+ return 'Wysłana';
63
+ case 'failed':
64
+ return 'Błąd';
65
+ default:
66
+ return status;
67
+ }
68
+ }
69
+
39
70
  type OrderStatus =
40
71
  | 'new'
41
72
  | 'awaitingPayment'
@@ -66,6 +97,10 @@
66
97
  let shipping = $state(false);
67
98
  let errorMessage = $state<string | null>(null);
68
99
  let successMessage = $state<string | null>(null);
100
+ let balanceLinkUrl = $state<string | null>(null);
101
+ let balanceLinkBusy = $state(false);
102
+ let balanceLinkError = $state<string | null>(null);
103
+ let balanceLinkCopied = $state(false);
69
104
 
70
105
  type SaveStatus = 'idle' | 'saving' | 'saved' | 'unsaved' | 'error';
71
106
  let statusFormStatus = $state<SaveStatus>('idle');
@@ -166,6 +201,34 @@
166
201
  shipping = false;
167
202
  }
168
203
  }
204
+
205
+ async function generateBalanceLink() {
206
+ balanceLinkBusy = true;
207
+ balanceLinkError = null;
208
+ balanceLinkCopied = false;
209
+ try {
210
+ const result = await remotes.generateBalanceLinkForOrder(orderId);
211
+ if (!result.success) {
212
+ balanceLinkError = result.error;
213
+ balanceLinkUrl = null;
214
+ return;
215
+ }
216
+ balanceLinkUrl = result.url;
217
+ try {
218
+ const absolute = result.url.startsWith('http')
219
+ ? result.url
220
+ : `${window.location.origin}${result.url.startsWith('/') ? '' : '/'}${result.url}`;
221
+ await navigator.clipboard.writeText(absolute);
222
+ balanceLinkCopied = true;
223
+ } catch {
224
+ balanceLinkCopied = false;
225
+ }
226
+ } catch (err) {
227
+ balanceLinkError = err instanceof Error ? err.message : 'Nie udało się wygenerować linku';
228
+ } finally {
229
+ balanceLinkBusy = false;
230
+ }
231
+ }
169
232
  </script>
170
233
 
171
234
  {#if !query.ready}
@@ -351,6 +414,79 @@
351
414
  </Button>
352
415
  </section>
353
416
 
417
+ {#if order.partialPayment}
418
+ {@const pp = order.partialPayment as {
419
+ kind: string;
420
+ paidAmount: number;
421
+ balanceAmount: number;
422
+ paidAt: string | null;
423
+ }}
424
+ <section class="border-border bg-card space-y-3 rounded-xl border p-3 text-sm sm:p-5">
425
+ <div class="flex items-center justify-between gap-2">
426
+ <h2 class="text-base font-bold">Płatność dzielona</h2>
427
+ {#if order.balanceOwed}
428
+ <span
429
+ class="rounded-md bg-[#5B4A9E] px-2 py-1 text-xs font-semibold text-white"
430
+ aria-label="Czeka na dopłatę"
431
+ >
432
+ Czeka na dopłatę
433
+ </span>
434
+ {:else}
435
+ <span
436
+ class="bg-success-bg text-success rounded-md px-2 py-1 text-xs font-semibold"
437
+ >
438
+ Opłacone w całości
439
+ </span>
440
+ {/if}
441
+ </div>
442
+ <dl class="space-y-1">
443
+ <div class="text-muted-foreground flex justify-between text-xs">
444
+ <dt>Zapłacono</dt>
445
+ <dd class="tabular-nums">{formatCentsPrice(pp.paidAmount, order.currency)}</dd>
446
+ </div>
447
+ <div class="text-muted-foreground flex justify-between text-xs">
448
+ <dt>Pozostało do dopłaty</dt>
449
+ <dd class="tabular-nums">{formatCentsPrice(pp.balanceAmount, order.currency)}</dd>
450
+ </div>
451
+ <div class="text-muted-foreground flex justify-between text-xs">
452
+ <dt>Razem</dt>
453
+ <dd class="tabular-nums">{formatCentsPrice(order.totalGross, order.currency)}</dd>
454
+ </div>
455
+ </dl>
456
+ {#if order.balanceOwed}
457
+ <Button
458
+ onclick={generateBalanceLink}
459
+ variant="outline"
460
+ class="w-full"
461
+ disabled={balanceLinkBusy}
462
+ >
463
+ {balanceLinkBusy ? 'Generuję…' : 'Wyślij link do dopłaty'}
464
+ </Button>
465
+ {#if balanceLinkUrl}
466
+ <div class="space-y-1">
467
+ <p class="text-muted-foreground text-xs">
468
+ {#if balanceLinkCopied}
469
+ Link skopiowany do schowka. Wyślij go klientowi mailowo lub innym kanałem.
470
+ {:else}
471
+ Link wygenerowany — skopiuj go ręcznie:
472
+ {/if}
473
+ </p>
474
+ <code class="border-border block break-all rounded-md border bg-[#F4F2FA] p-2 text-xs"
475
+ >{balanceLinkUrl}</code
476
+ >
477
+ </div>
478
+ {/if}
479
+ {#if balanceLinkError}
480
+ <p class="text-destructive text-xs" role="alert">{balanceLinkError}</p>
481
+ {/if}
482
+ {:else if pp.paidAt}
483
+ <p class="text-muted-foreground text-xs">
484
+ Dopłata zaksięgowana: {formatDateTime(pp.paidAt, interfaceLanguage.current)}
485
+ </p>
486
+ {/if}
487
+ </section>
488
+ {/if}
489
+
354
490
  {#if refundsQuery.ready && refundsQuery.current}
355
491
  {@const r = refundsQuery.current}
356
492
  <section class="border-border bg-card space-y-3 rounded-xl border p-3 text-sm sm:p-5">
@@ -396,6 +532,60 @@
396
532
  </section>
397
533
  {/if}
398
534
 
535
+ {#if invoiceQuery.ready && invoiceQuery.current?.invoicingEnabled}
536
+ {@const inv = invoiceQuery.current.invoice}
537
+ {@const orderPaid =
538
+ order.status === 'paid' ||
539
+ order.status === 'preparing' ||
540
+ order.status === 'sent' ||
541
+ order.status === 'done'}
542
+ <section class="border-border bg-card space-y-3 rounded-xl border p-3 text-sm sm:p-5">
543
+ <h2 class="text-base font-bold">Faktura</h2>
544
+ {#if inv}
545
+ <div class="space-y-1">
546
+ <div class="flex items-center justify-between gap-2">
547
+ <span class="font-medium">{inv.number ?? '—'}</span>
548
+ <span class="text-muted-foreground text-xs">{invoiceStatusLabel(inv.status)}</span>
549
+ </div>
550
+ {#if inv.pdfUrl}
551
+ <a
552
+ href={inv.pdfUrl}
553
+ target="_blank"
554
+ rel="noopener noreferrer"
555
+ class="text-primary text-xs underline"
556
+ >
557
+ Otwórz fakturę
558
+ </a>
559
+ {/if}
560
+ {#if inv.status === 'failed' && inv.lastError}
561
+ <p class="text-destructive text-xs">{inv.lastError}</p>
562
+ {/if}
563
+ </div>
564
+ {:else}
565
+ <p class="text-muted-foreground text-xs">Brak faktury dla tego zamówienia.</p>
566
+ {/if}
567
+ {#if invoiceError}
568
+ <p class="text-destructive text-xs">{invoiceError}</p>
569
+ {/if}
570
+ {#if orderPaid}
571
+ <Button
572
+ onclick={() => issueInvoice(true)}
573
+ variant="outline"
574
+ class="w-full"
575
+ disabled={issuingInvoice}
576
+ >
577
+ {inv && (inv.status === 'issued' || inv.status === 'sent')
578
+ ? 'Wystaw ponownie'
579
+ : 'Wystaw fakturę'}
580
+ </Button>
581
+ {:else}
582
+ <p class="text-muted-foreground text-xs">
583
+ Fakturę można wystawić po opłaceniu zamówienia.
584
+ </p>
585
+ {/if}
586
+ </section>
587
+ {/if}
588
+
399
589
  {#if order.carrierType && order.carrierType !== 'none'}
400
590
  <section class="border-border bg-card space-y-3 rounded-xl border p-3 text-sm sm:p-5">
401
591
  <h2 class="text-base font-bold">
@@ -524,6 +714,8 @@
524
714
  {orderId}
525
715
  currency={refundsQuery.current.currency}
526
716
  remainingRefundable={refundsQuery.current.remainingRefundable}
717
+ hasPartial={query.current?.order.partialPayment != null}
718
+ balanceOwed={query.current?.order.balanceOwed === true}
527
719
  onOpenChange={(v) => (refundDialogOpen = v)}
528
720
  onRefunded={async () => {
529
721
  successMessage = 'Zwrot wykonany.';
@@ -161,6 +161,10 @@
161
161
  <DateTimeField {field} bind:value={$value} {...props} />
162
162
  {:else if field.type === 'select'}
163
163
  <SelectField {field} bind:value={$value} {...props} />
164
+ {:else if field.type === 'icon'}
165
+ {#await import('./icon-field.svelte') then { default: IconField }}
166
+ <IconField {field} bind:value={$value} {...props} />
167
+ {/await}
164
168
  {:else if field.type === 'custom'}
165
169
  {@const customDef = customFieldDefs.get(field.fieldType)}
166
170
  {#if customDef}
@@ -169,7 +173,8 @@
169
173
  <p>Nieznany custom field: {field.fieldType}</p>
170
174
  {/if}
171
175
  {:else}
172
- <p>Nieobsługiwany typ pola: {field.type}</p>
176
+ <!-- All Field types are handled above; fallback kept as defensive guard for future additions. -->
177
+ <p>Nieobsługiwany typ pola: {(field as Field).type}</p>
173
178
  {/if}
174
179
  </div>
175
180
  {/snippet}
@@ -0,0 +1,86 @@
1
+ <script lang="ts">
2
+ import type { IconField } from '../../../types/fields.js';
3
+ import { resolveIconSet } from '../../state/icon-sets.svelte.js';
4
+ import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
5
+ import { getLocalizedLabel } from '../../utils/collectionLabel.js';
6
+ import IconPickerDialog from './icon-picker-dialog.svelte';
7
+ import Plus from '@lucide/svelte/icons/plus';
8
+ import X from '@lucide/svelte/icons/x';
9
+ import HelpCircle from '@lucide/svelte/icons/help-circle';
10
+
11
+ type Props = {
12
+ field: IconField;
13
+ value: string | undefined;
14
+ };
15
+
16
+ let { field, value = $bindable(), ...props }: Props = $props();
17
+
18
+ const interfaceLanguage = useInterfaceLanguage();
19
+ const iconSet = $derived(resolveIconSet(field.set));
20
+ const iconDef = $derived(value && iconSet ? iconSet.icons[value] : undefined);
21
+ const isMissing = $derived(!!value && !!iconSet && !iconDef);
22
+ const label = $derived.by(() =>
23
+ iconDef ? getLocalizedLabel(iconDef.label, interfaceLanguage.current) : (value ?? '')
24
+ );
25
+
26
+ let dialogOpen = $state(false);
27
+
28
+ function clear() {
29
+ value = '';
30
+ }
31
+ function openDialog() {
32
+ dialogOpen = true;
33
+ }
34
+ function onConfirm(newValue: string) {
35
+ value = newValue;
36
+ dialogOpen = false;
37
+ }
38
+ </script>
39
+
40
+ {#if !iconSet}
41
+ <p class="text-sm text-destructive">
42
+ Brak zarejestrowanego zestawu ikon. Dodaj <code>IconSetPlugin</code> do
43
+ <code>plugins:</code> w cms.config i przekaż przez <code>buildIconSetMap(...)</code> do AdminLayout.
44
+ </p>
45
+ {:else}
46
+ <div class="relative inline-block">
47
+ <button
48
+ type="button"
49
+ onclick={openDialog}
50
+ class="flex h-24 w-24 flex-col items-center justify-center gap-1.5 rounded-lg border bg-muted/30 transition-colors hover:border-primary hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
51
+ aria-label={value ? `Zmień ikonę: ${label}` : 'Wybierz ikonę'}
52
+ {...props}
53
+ >
54
+ {#if !value}
55
+ <Plus class="h-7 w-7 text-muted-foreground" />
56
+ <span class="text-xs text-muted-foreground">Wybierz ikonę</span>
57
+ {:else if isMissing}
58
+ <HelpCircle class="h-7 w-7 text-warning" />
59
+ <span class="line-clamp-1 px-1 text-xs text-muted-foreground">{value}</span>
60
+ {:else if iconDef}
61
+ {@const Comp = iconDef.component}
62
+ <Comp size={32} />
63
+ <span class="line-clamp-1 px-1 text-xs">{label}</span>
64
+ {/if}
65
+ </button>
66
+
67
+ {#if value}
68
+ <button
69
+ type="button"
70
+ onclick={clear}
71
+ class="absolute -right-2 -top-2 rounded-full border bg-background p-1 text-muted-foreground shadow-sm transition-colors hover:text-destructive"
72
+ aria-label="Wyczyść ikonę"
73
+ >
74
+ <X class="h-3 w-3" />
75
+ </button>
76
+ {/if}
77
+ </div>
78
+
79
+ {#if isMissing}
80
+ <p class="mt-2 text-sm text-warning">
81
+ Ikona <code>{value}</code> nie jest już dostępna w bibliotece. Wybierz inną lub usuń.
82
+ </p>
83
+ {/if}
84
+
85
+ <IconPickerDialog bind:open={dialogOpen} {iconSet} initialValue={value} {onConfirm} />
86
+ {/if}
@@ -0,0 +1,8 @@
1
+ import type { IconField } from '../../../types/fields.js';
2
+ type Props = {
3
+ field: IconField;
4
+ value: string | undefined;
5
+ };
6
+ declare const IconField: import("svelte").Component<Props, {}, "value">;
7
+ type IconField = ReturnType<typeof IconField>;
8
+ export default IconField;
@@ -0,0 +1,174 @@
1
+ <script lang="ts">
2
+ import * as Dialog from '../../../components/ui/dialog/index.js';
3
+ import { Input } from '../../../components/ui/input/index.js';
4
+ import { Button } from '../../../components/ui/button/index.js';
5
+ import { LiveRegion } from '../../../components/ui/live-region/index.js';
6
+ import type { IconSetPlugin } from '../../../types/plugins.js';
7
+ import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
8
+ import { getLocalizedLabel } from '../../utils/collectionLabel.js';
9
+ import type { InterfaceLanguage } from '../../../types/languages.js';
10
+ import type { Component } from 'svelte';
11
+
12
+ type Props = {
13
+ open: boolean;
14
+ iconSet: IconSetPlugin;
15
+ initialValue: string | undefined;
16
+ onConfirm: (value: string) => void;
17
+ };
18
+
19
+ let { open = $bindable(), iconSet, initialValue, onConfirm }: Props = $props();
20
+
21
+ const lang: Record<
22
+ InterfaceLanguage,
23
+ {
24
+ title: string;
25
+ search: string;
26
+ cancel: string;
27
+ confirm: string;
28
+ noResults: string;
29
+ resultsCount: (n: number) => string;
30
+ close: string;
31
+ }
32
+ > = {
33
+ en: {
34
+ title: 'Select icon',
35
+ search: 'Search icons...',
36
+ cancel: 'Cancel',
37
+ confirm: 'OK',
38
+ noResults: 'No icons match',
39
+ resultsCount: (n) => `${n} icons`,
40
+ close: 'Close'
41
+ },
42
+ pl: {
43
+ title: 'Wybierz ikonę',
44
+ search: 'Szukaj ikon...',
45
+ cancel: 'Anuluj',
46
+ confirm: 'OK',
47
+ noResults: 'Brak pasujących ikon',
48
+ resultsCount: (n) => `Wyniki: ${n}`,
49
+ close: 'Zamknij'
50
+ }
51
+ };
52
+
53
+ const interfaceLanguage = useInterfaceLanguage();
54
+ const t = $derived(lang[interfaceLanguage.current]);
55
+
56
+ let searchValue = $state('');
57
+ let selected = $state<string>('');
58
+ let wasOpen = false;
59
+
60
+ // Re-sync gdy dialog się otwiera ze świeżą wartością początkową.
61
+ // Wzorzec "edge-trigger": resetujemy TYLKO na przejściu closed→open,
62
+ // żeby kliknięcia użytkownika w grid nie były nadpisywane przez re-run efektu.
63
+ $effect(() => {
64
+ if (open && !wasOpen) {
65
+ selected = initialValue ?? '';
66
+ searchValue = '';
67
+ }
68
+ wasOpen = open;
69
+ });
70
+
71
+ type IconEntry = {
72
+ key: string;
73
+ label: string;
74
+ keywords: string[];
75
+ component: Component;
76
+ };
77
+
78
+ const allEntries = $derived<IconEntry[]>(
79
+ Object.entries(iconSet.icons).map(([key, def]) => ({
80
+ key,
81
+ label: getLocalizedLabel(def.label, interfaceLanguage.current),
82
+ keywords: def.keywords ?? [],
83
+ component: def.component
84
+ }))
85
+ );
86
+
87
+ const filtered = $derived.by(() => {
88
+ const q = searchValue.trim().toLowerCase();
89
+ if (!q) return allEntries;
90
+ return allEntries.filter(
91
+ (e) =>
92
+ e.key.toLowerCase().includes(q) ||
93
+ e.label.toLowerCase().includes(q) ||
94
+ e.keywords.some((k) => k.toLowerCase().includes(q))
95
+ );
96
+ });
97
+
98
+ function confirm() {
99
+ if (selected) onConfirm(selected);
100
+ }
101
+ function cancel() {
102
+ open = false;
103
+ }
104
+ function select(key: string) {
105
+ selected = key;
106
+ }
107
+ function handleOpenChange(next: boolean) {
108
+ open = next;
109
+ }
110
+ </script>
111
+
112
+ <Dialog.Root bind:open onOpenChange={handleOpenChange}>
113
+ <Dialog.Content class="flex max-h-[85vh] flex-col gap-0 p-0 sm:max-w-2xl">
114
+ <Dialog.Header class="border-b px-4 py-3">
115
+ <Dialog.Title class="text-base font-semibold">{t.title}</Dialog.Title>
116
+ <Dialog.Description class="sr-only">{t.title}</Dialog.Description>
117
+ </Dialog.Header>
118
+
119
+ <div class="border-b px-4 py-2">
120
+ <Input
121
+ type="search"
122
+ placeholder={t.search}
123
+ bind:value={searchValue}
124
+ autofocus
125
+ class="h-9"
126
+ aria-label={t.search}
127
+ />
128
+ </div>
129
+
130
+ <div class="flex-1 overflow-y-auto p-4">
131
+ {#if filtered.length === 0}
132
+ <p class="text-center text-sm text-muted-foreground py-8">{t.noResults}</p>
133
+ {:else}
134
+ <div
135
+ role="listbox"
136
+ aria-label={t.title}
137
+ class="grid grid-cols-3 gap-2 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6"
138
+ >
139
+ {#each filtered as entry (entry.key)}
140
+ {@const isSelected = selected === entry.key}
141
+ {@const Comp = entry.component}
142
+ <button
143
+ type="button"
144
+ role="option"
145
+ aria-selected={isSelected}
146
+ onclick={() => select(entry.key)}
147
+ ondblclick={() => {
148
+ select(entry.key);
149
+ confirm();
150
+ }}
151
+ class={[
152
+ 'flex h-20 flex-col items-center justify-center gap-1 rounded-md border p-1.5 transition-colors',
153
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
154
+ isSelected
155
+ ? 'border-primary bg-primary/10'
156
+ : 'border-muted bg-background hover:bg-muted/50'
157
+ ].join(' ')}
158
+ >
159
+ <Comp size={24} />
160
+ <span class="line-clamp-1 w-full text-center text-[10px]">{entry.label}</span>
161
+ </button>
162
+ {/each}
163
+ </div>
164
+ {/if}
165
+ </div>
166
+
167
+ <Dialog.Footer class="border-t px-4 py-3 sm:justify-end">
168
+ <Button type="button" variant="outline" onclick={cancel}>{t.cancel}</Button>
169
+ <Button type="button" onclick={confirm} disabled={!selected}>{t.confirm}</Button>
170
+ </Dialog.Footer>
171
+
172
+ <LiveRegion message={t.resultsCount(filtered.length)} />
173
+ </Dialog.Content>
174
+ </Dialog.Root>
@@ -0,0 +1,11 @@
1
+ import type { IconSetPlugin } from '../../../types/plugins.js';
2
+ import type { Component } from 'svelte';
3
+ type Props = {
4
+ open: boolean;
5
+ iconSet: IconSetPlugin;
6
+ initialValue: string | undefined;
7
+ onConfirm: (value: string) => void;
8
+ };
9
+ declare const IconPickerDialog: Component<Props, {}, "open">;
10
+ type IconPickerDialog = ReturnType<typeof IconPickerDialog>;
11
+ export default IconPickerDialog;
@@ -9,7 +9,6 @@
9
9
  import type { ObjectField, ObjectFieldData } from '../../../types/fields.js';
10
10
  import { evaluateCondition } from '../../utils/fieldCondition.js';
11
11
  import { onMount } from 'svelte';
12
- import * as Item from '../../../components/ui/item/index.js';
13
12
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
14
13
  import { getLocalizedLabel } from '../../utils/collectionLabel.js';
15
14
  import { cn } from '../../../utils.js';
@@ -90,12 +89,33 @@
90
89
  {@render content()}
91
90
  </div>
92
91
  {:else}
93
- <!-- Top-level: with border -->
94
- <Item.Root variant="outline">
95
- <Item.Content>
96
- <Item.Title class="mb-4 text-lg">{getLocalizedLabel(field.label, interfaceLanguage.current)}</Item.Title>
92
+ <!-- Top-level: full-width card -->
93
+ <div class="object-card">
94
+ <div class="object-card-header">{getLocalizedLabel(field.label, interfaceLanguage.current)}</div>
95
+ <div class="object-card-body">
97
96
  {@render content()}
98
- </Item.Content>
99
- </Item.Root>
97
+ </div>
98
+ </div>
100
99
  {/if}
101
100
  {/if}
101
+
102
+ <style>
103
+ .object-card {
104
+ background: var(--card);
105
+ border: 1px solid var(--border);
106
+ border-radius: 12px;
107
+ box-shadow: 0 1px 2px rgba(43, 37, 88, 0.04);
108
+ overflow: hidden;
109
+ }
110
+
111
+ .object-card-header {
112
+ font-size: 13px;
113
+ font-weight: 700;
114
+ color: var(--foreground);
115
+ padding: 12px 16px 0;
116
+ }
117
+
118
+ .object-card-body {
119
+ padding: 10px 16px 16px;
120
+ }
121
+ </style>