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.
- package/API.md +58 -2
- package/CHANGELOG.md +105 -0
- package/DOCS.md +1 -1
- package/ROADMAP.md +8 -0
- package/dist/admin/auth-client.d.ts +42 -42
- package/dist/admin/client/admin/admin-layout.svelte +12 -2
- package/dist/admin/client/admin/admin-layout.svelte.d.ts +2 -1
- package/dist/admin/client/collection/data-table.svelte +0 -39
- package/dist/admin/client/collection/data-table.svelte.d.ts +0 -2
- package/dist/admin/client/shop/coupon-schema.d.ts +1 -1
- package/dist/admin/client/shop/refund-dialog.svelte +37 -1
- package/dist/admin/client/shop/refund-dialog.svelte.d.ts +3 -0
- package/dist/admin/client/shop/shop-order-detail-page.svelte +192 -0
- package/dist/admin/components/fields/field-renderer.svelte +6 -1
- package/dist/admin/components/fields/icon-field.svelte +86 -0
- package/dist/admin/components/fields/icon-field.svelte.d.ts +8 -0
- package/dist/admin/components/fields/icon-picker-dialog.svelte +174 -0
- package/dist/admin/components/fields/icon-picker-dialog.svelte.d.ts +11 -0
- package/dist/admin/components/fields/object-field.svelte +27 -7
- package/dist/admin/components/fields/shop-field.svelte +210 -20
- package/dist/admin/components/layout/layout-tabs.svelte +1 -0
- package/dist/admin/components/variant-form/VariantAttributeRenderer.svelte +109 -0
- package/dist/admin/components/variant-form/VariantAttributeRenderer.svelte.d.ts +9 -0
- package/dist/admin/helpers/build-icon-set-map.d.ts +8 -0
- package/dist/admin/helpers/build-icon-set-map.js +16 -0
- package/dist/admin/helpers/index.d.ts +2 -0
- package/dist/admin/helpers/index.js +2 -0
- package/dist/admin/remote/shop.remote.d.ts +116 -24
- package/dist/admin/remote/shop.remote.js +79 -6
- package/dist/admin/state/icon-sets.svelte.d.ts +9 -0
- package/dist/admin/state/icon-sets.svelte.js +20 -0
- package/dist/cli/scaffold/admin.js +2 -2
- package/dist/components/ui/checkbox/checkbox.svelte +3 -3
- package/dist/core/cms.d.ts +11 -2
- package/dist/core/cms.js +29 -0
- package/dist/core/fields/fieldSchemaToTs.js +7 -0
- package/dist/core/server/generator/fields.d.ts +2 -0
- package/dist/core/server/generator/fields.js +34 -1
- package/dist/core/server/generator/generator.js +2 -1
- package/dist/db-postgres/schema/shop/index.d.ts +1 -0
- package/dist/db-postgres/schema/shop/index.js +1 -0
- package/dist/db-postgres/schema/shop/invoice.d.ts +254 -0
- package/dist/db-postgres/schema/shop/invoice.js +27 -0
- package/dist/db-postgres/schema/shop/order.d.ts +107 -1
- package/dist/db-postgres/schema/shop/order.js +7 -1
- package/dist/db-postgres/schema/shop/payment.d.ts +20 -0
- package/dist/db-postgres/schema/shop/payment.js +4 -1
- package/dist/db-postgres/schema/shop/product.d.ts +20 -0
- package/dist/db-postgres/schema/shop/product.js +3 -1
- package/dist/db-postgres/schema/shop/productVariant.d.ts +12 -2
- package/dist/db-postgres/schema/shop/productVariant.js +22 -0
- package/dist/paraglide/messages/_index.d.ts +36 -3
- package/dist/paraglide/messages/_index.js +71 -3
- package/dist/paraglide/messages/en.d.ts +5 -0
- package/dist/paraglide/messages/en.js +14 -0
- package/dist/paraglide/messages/pl.d.ts +5 -0
- package/dist/paraglide/messages/pl.js +14 -0
- package/dist/shop/adapters/fakturownia/client.d.ts +28 -0
- package/dist/shop/adapters/fakturownia/client.js +67 -0
- package/dist/shop/adapters/fakturownia/index.d.ts +27 -0
- package/dist/shop/adapters/fakturownia/index.js +36 -0
- package/dist/shop/adapters/fakturownia/payload.d.ts +35 -0
- package/dist/shop/adapters/fakturownia/payload.js +45 -0
- package/dist/shop/cart/types.d.ts +1 -0
- package/dist/shop/client/index.d.ts +61 -0
- package/dist/shop/client/index.js +5 -1
- package/dist/shop/expiry.d.ts +35 -0
- package/dist/shop/expiry.js +68 -0
- package/dist/shop/http/balance-handler.d.ts +20 -0
- package/dist/shop/http/balance-handler.js +91 -0
- package/dist/shop/http/cart-handler.js +19 -0
- package/dist/shop/http/checkout-handler.js +30 -1
- package/dist/shop/http/index.d.ts +2 -0
- package/dist/shop/http/index.js +2 -0
- package/dist/shop/http/upcoming-handler.d.ts +16 -0
- package/dist/shop/http/upcoming-handler.js +65 -0
- package/dist/shop/http/webhook-handler.js +46 -9
- package/dist/shop/index.d.ts +7 -1
- package/dist/shop/index.js +10 -1
- package/dist/shop/nip.d.ts +12 -0
- package/dist/shop/nip.js +23 -0
- package/dist/shop/server/balance-payment.d.ts +40 -0
- package/dist/shop/server/balance-payment.js +140 -0
- package/dist/shop/server/cart-hydrate.js +2 -0
- package/dist/shop/server/init.d.ts +14 -0
- package/dist/shop/server/init.js +35 -0
- package/dist/shop/server/invoices.d.ts +64 -0
- package/dist/shop/server/invoices.js +237 -0
- package/dist/shop/server/orders.d.ts +38 -0
- package/dist/shop/server/orders.js +152 -2
- package/dist/shop/server/payment-policy.d.ts +35 -0
- package/dist/shop/server/payment-policy.js +55 -0
- package/dist/shop/server/payments.d.ts +29 -0
- package/dist/shop/server/payments.js +64 -0
- package/dist/shop/server/populate.d.ts +1 -1
- package/dist/shop/server/refund.d.ts +17 -12
- package/dist/shop/server/refund.js +96 -13
- package/dist/shop/server/shop-data.d.ts +4 -1
- package/dist/shop/server/shop-data.js +24 -2
- package/dist/shop/template.d.ts +13 -0
- package/dist/shop/template.js +98 -0
- package/dist/shop/types.d.ts +208 -1
- package/dist/shop/variant-attributes.d.ts +28 -0
- package/dist/shop/variant-attributes.js +69 -0
- package/dist/sveltekit/server/index.d.ts +1 -0
- package/dist/sveltekit/server/index.js +2 -0
- package/dist/types/cms.d.ts +4 -3
- package/dist/types/cms.schema.d.ts +1 -1
- package/dist/types/cms.schema.js +9 -0
- package/dist/types/fields.d.ts +21 -2
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/plugins.d.ts +40 -0
- package/dist/types/plugins.js +4 -1
- package/dist/updates/0.26.1/index.d.ts +2 -0
- package/dist/updates/0.26.1/index.js +19 -0
- package/dist/updates/0.27.0/index.d.ts +2 -0
- package/dist/updates/0.27.0/index.js +50 -0
- package/dist/updates/0.28.0/index.d.ts +2 -0
- package/dist/updates/0.28.0/index.js +38 -0
- package/dist/updates/index.js +7 -1
- package/package.json +1 -1
- package/dist/paraglide/messages/hello_world.d.ts +0 -5
- package/dist/paraglide/messages/hello_world.js +0 -33
- package/dist/paraglide/messages/login_hello.d.ts +0 -16
- package/dist/paraglide/messages/login_hello.js +0 -34
- package/dist/paraglide/messages/login_please_login.d.ts +0 -16
- 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
|
-
|
|
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:
|
|
94
|
-
<
|
|
95
|
-
<
|
|
96
|
-
|
|
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
|
-
</
|
|
99
|
-
</
|
|
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>
|