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
@@ -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
  };
@@ -66,6 +66,10 @@
66
66
  let shipping = $state(false);
67
67
  let errorMessage = $state<string | null>(null);
68
68
  let successMessage = $state<string | null>(null);
69
+ let balanceLinkUrl = $state<string | null>(null);
70
+ let balanceLinkBusy = $state(false);
71
+ let balanceLinkError = $state<string | null>(null);
72
+ let balanceLinkCopied = $state(false);
69
73
 
70
74
  type SaveStatus = 'idle' | 'saving' | 'saved' | 'unsaved' | 'error';
71
75
  let statusFormStatus = $state<SaveStatus>('idle');
@@ -166,6 +170,34 @@
166
170
  shipping = false;
167
171
  }
168
172
  }
173
+
174
+ async function generateBalanceLink() {
175
+ balanceLinkBusy = true;
176
+ balanceLinkError = null;
177
+ balanceLinkCopied = false;
178
+ try {
179
+ const result = await remotes.generateBalanceLinkForOrder(orderId);
180
+ if (!result.success) {
181
+ balanceLinkError = result.error;
182
+ balanceLinkUrl = null;
183
+ return;
184
+ }
185
+ balanceLinkUrl = result.url;
186
+ try {
187
+ const absolute = result.url.startsWith('http')
188
+ ? result.url
189
+ : `${window.location.origin}${result.url.startsWith('/') ? '' : '/'}${result.url}`;
190
+ await navigator.clipboard.writeText(absolute);
191
+ balanceLinkCopied = true;
192
+ } catch {
193
+ balanceLinkCopied = false;
194
+ }
195
+ } catch (err) {
196
+ balanceLinkError = err instanceof Error ? err.message : 'Nie udało się wygenerować linku';
197
+ } finally {
198
+ balanceLinkBusy = false;
199
+ }
200
+ }
169
201
  </script>
170
202
 
171
203
  {#if !query.ready}
@@ -351,6 +383,79 @@
351
383
  </Button>
352
384
  </section>
353
385
 
386
+ {#if order.partialPayment}
387
+ {@const pp = order.partialPayment as {
388
+ kind: string;
389
+ paidAmount: number;
390
+ balanceAmount: number;
391
+ paidAt: string | null;
392
+ }}
393
+ <section class="border-border bg-card space-y-3 rounded-xl border p-3 text-sm sm:p-5">
394
+ <div class="flex items-center justify-between gap-2">
395
+ <h2 class="text-base font-bold">Płatność dzielona</h2>
396
+ {#if order.balanceOwed}
397
+ <span
398
+ class="rounded-md bg-[#5B4A9E] px-2 py-1 text-xs font-semibold text-white"
399
+ aria-label="Czeka na dopłatę"
400
+ >
401
+ Czeka na dopłatę
402
+ </span>
403
+ {:else}
404
+ <span
405
+ class="bg-success-bg text-success rounded-md px-2 py-1 text-xs font-semibold"
406
+ >
407
+ Opłacone w całości
408
+ </span>
409
+ {/if}
410
+ </div>
411
+ <dl class="space-y-1">
412
+ <div class="text-muted-foreground flex justify-between text-xs">
413
+ <dt>Zapłacono</dt>
414
+ <dd class="tabular-nums">{formatCentsPrice(pp.paidAmount, order.currency)}</dd>
415
+ </div>
416
+ <div class="text-muted-foreground flex justify-between text-xs">
417
+ <dt>Pozostało do dopłaty</dt>
418
+ <dd class="tabular-nums">{formatCentsPrice(pp.balanceAmount, order.currency)}</dd>
419
+ </div>
420
+ <div class="text-muted-foreground flex justify-between text-xs">
421
+ <dt>Razem</dt>
422
+ <dd class="tabular-nums">{formatCentsPrice(order.totalGross, order.currency)}</dd>
423
+ </div>
424
+ </dl>
425
+ {#if order.balanceOwed}
426
+ <Button
427
+ onclick={generateBalanceLink}
428
+ variant="outline"
429
+ class="w-full"
430
+ disabled={balanceLinkBusy}
431
+ >
432
+ {balanceLinkBusy ? 'Generuję…' : 'Wyślij link do dopłaty'}
433
+ </Button>
434
+ {#if balanceLinkUrl}
435
+ <div class="space-y-1">
436
+ <p class="text-muted-foreground text-xs">
437
+ {#if balanceLinkCopied}
438
+ Link skopiowany do schowka. Wyślij go klientowi mailowo lub innym kanałem.
439
+ {:else}
440
+ Link wygenerowany — skopiuj go ręcznie:
441
+ {/if}
442
+ </p>
443
+ <code class="border-border block break-all rounded-md border bg-[#F4F2FA] p-2 text-xs"
444
+ >{balanceLinkUrl}</code
445
+ >
446
+ </div>
447
+ {/if}
448
+ {#if balanceLinkError}
449
+ <p class="text-destructive text-xs" role="alert">{balanceLinkError}</p>
450
+ {/if}
451
+ {:else if pp.paidAt}
452
+ <p class="text-muted-foreground text-xs">
453
+ Dopłata zaksięgowana: {formatDateTime(pp.paidAt, interfaceLanguage.current)}
454
+ </p>
455
+ {/if}
456
+ </section>
457
+ {/if}
458
+
354
459
  {#if refundsQuery.ready && refundsQuery.current}
355
460
  {@const r = refundsQuery.current}
356
461
  <section class="border-border bg-card space-y-3 rounded-xl border p-3 text-sm sm:p-5">
@@ -524,6 +629,8 @@
524
629
  {orderId}
525
630
  currency={refundsQuery.current.currency}
526
631
  remainingRefundable={refundsQuery.current.remainingRefundable}
632
+ hasPartial={query.current?.order.partialPayment != null}
633
+ balanceOwed={query.current?.order.balanceOwed === true}
527
634
  onOpenChange={(v) => (refundDialogOpen = v)}
528
635
  onRefunded={async () => {
529
636
  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>