@jelinek/ui 0.5.2 → 0.9.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 (62) hide show
  1. package/dist/components/OptionTile.svelte +5 -2
  2. package/dist/components/PageHero.svelte +37 -2
  3. package/dist/components/PageHero.svelte.d.ts +9 -0
  4. package/dist/components/commerce/Breadcrumbs.svelte +24 -0
  5. package/dist/components/commerce/Breadcrumbs.svelte.d.ts +11 -0
  6. package/dist/components/commerce/CartDrawer.svelte +128 -0
  7. package/dist/components/commerce/CartDrawer.svelte.d.ts +37 -0
  8. package/dist/components/commerce/CartLineItem.svelte +67 -0
  9. package/dist/components/commerce/CartLineItem.svelte.d.ts +14 -0
  10. package/dist/components/commerce/CheckoutProgress.svelte +49 -0
  11. package/dist/components/commerce/CheckoutProgress.svelte.d.ts +11 -0
  12. package/dist/components/commerce/ConfigSection.svelte +24 -0
  13. package/dist/components/commerce/ConfigSection.svelte.d.ts +11 -0
  14. package/dist/components/commerce/DimensionSlider.svelte +86 -0
  15. package/dist/components/commerce/DimensionSlider.svelte.d.ts +18 -0
  16. package/dist/components/commerce/Drawer.svelte +67 -0
  17. package/dist/components/commerce/Drawer.svelte.d.ts +14 -0
  18. package/dist/components/commerce/FreeShippingBar.svelte +63 -0
  19. package/dist/components/commerce/FreeShippingBar.svelte.d.ts +12 -0
  20. package/dist/components/commerce/Gallery.svelte +192 -0
  21. package/dist/components/commerce/Gallery.svelte.d.ts +27 -0
  22. package/dist/components/commerce/Modal.svelte +63 -0
  23. package/dist/components/commerce/Modal.svelte.d.ts +13 -0
  24. package/dist/components/commerce/OrderSummary.svelte +85 -0
  25. package/dist/components/commerce/OrderSummary.svelte.d.ts +25 -0
  26. package/dist/components/commerce/PriceDisplay.svelte +41 -0
  27. package/dist/components/commerce/PriceDisplay.svelte.d.ts +14 -0
  28. package/dist/components/commerce/ProductHeader.svelte +62 -0
  29. package/dist/components/commerce/ProductHeader.svelte.d.ts +21 -0
  30. package/dist/components/commerce/ProductStickyBar.svelte +42 -0
  31. package/dist/components/commerce/ProductStickyBar.svelte.d.ts +14 -0
  32. package/dist/components/commerce/ProductTabs.svelte +87 -0
  33. package/dist/components/commerce/ProductTabs.svelte.d.ts +18 -0
  34. package/dist/components/commerce/QuantityStepper.svelte +44 -0
  35. package/dist/components/commerce/QuantityStepper.svelte.d.ts +13 -0
  36. package/dist/components/commerce/RatingStars.svelte +39 -0
  37. package/dist/components/commerce/RatingStars.svelte.d.ts +9 -0
  38. package/dist/components/commerce/SideToggle.svelte +79 -0
  39. package/dist/components/commerce/SideToggle.svelte.d.ts +13 -0
  40. package/dist/components/commerce/Skeleton.svelte +10 -0
  41. package/dist/components/commerce/Skeleton.svelte.d.ts +7 -0
  42. package/dist/components/commerce/StickyBar.svelte +27 -0
  43. package/dist/components/commerce/StickyBar.svelte.d.ts +8 -0
  44. package/dist/components/commerce/StockBadge.svelte +45 -0
  45. package/dist/components/commerce/StockBadge.svelte.d.ts +9 -0
  46. package/dist/components/commerce/Toaster.svelte +34 -0
  47. package/dist/components/commerce/Toaster.svelte.d.ts +7 -0
  48. package/dist/components/commerce/TrustBadges.svelte +26 -0
  49. package/dist/components/commerce/TrustBadges.svelte.d.ts +16 -0
  50. package/dist/components/commerce/cart-types.d.ts +12 -0
  51. package/dist/components/commerce/cart-types.js +1 -0
  52. package/dist/components/commerce/money.d.ts +14 -0
  53. package/dist/components/commerce/money.js +26 -0
  54. package/dist/components/commerce/overlay.d.ts +12 -0
  55. package/dist/components/commerce/overlay.js +20 -0
  56. package/dist/components/commerce/toast.d.ts +26 -0
  57. package/dist/components/commerce/toast.js +44 -0
  58. package/dist/index.d.ts +27 -0
  59. package/dist/index.js +27 -0
  60. package/dist/theme/base.css +34 -7
  61. package/dist/theme/extras.css +19 -0
  62. package/package.json +6 -2
@@ -0,0 +1,87 @@
1
+ <script lang="ts">
2
+ // Description / parameters / benefits switcher for the PDP, on kit Tabs/Tab.
3
+ // Each tab renders only when its content exists — a product with no benefits
4
+ // never shows an empty „Benefity“ tab, and the whole component renders
5
+ // nothing when all three are empty.
6
+ import Tab from '../Tab.svelte';
7
+ import Tabs from '../Tabs.svelte';
8
+
9
+ interface Props {
10
+ /** Sanitized HTML (sanitize server-side!) — never author-controlled raw input. */
11
+ description?: string;
12
+ parameters?: { label: string; value: string }[];
13
+ benefits?: string[];
14
+ /** Localized tab captions; Czech defaults. */
15
+ labels?: { description?: string; parameters?: string; benefits?: string };
16
+ }
17
+
18
+ let { description, parameters, benefits, labels }: Props = $props();
19
+
20
+ type TabKey = 'description' | 'parameters' | 'benefits';
21
+
22
+ let availableTabs = $derived(
23
+ (
24
+ [
25
+ description ? 'description' : null,
26
+ parameters && parameters.length > 0 ? 'parameters' : null,
27
+ benefits && benefits.length > 0 ? 'benefits' : null
28
+ ] as (TabKey | null)[]
29
+ ).filter((tab): tab is TabKey => tab !== null)
30
+ );
31
+
32
+ // Tabs' `value` is a plain bindable string (not typed to our TabKey union),
33
+ // so the raw binding target stays a string and every read narrows it
34
+ // against availableTabs.
35
+ let active = $state('');
36
+
37
+ // Keep the active tab valid as content availability changes (e.g. the
38
+ // description loads after the initial render) — default to the first
39
+ // available tab whenever the current pick is gone.
40
+ $effect(() => {
41
+ if (!availableTabs.includes(active as TabKey)) {
42
+ active = availableTabs[0] ?? '';
43
+ }
44
+ });
45
+
46
+ const caption = $derived({
47
+ description: labels?.description ?? 'Popis',
48
+ parameters: labels?.parameters ?? 'Parametry',
49
+ benefits: labels?.benefits ?? 'Benefity'
50
+ });
51
+ </script>
52
+
53
+ {#if availableTabs.length > 0}
54
+ <div>
55
+ <Tabs bind:value={active}>
56
+ {#each availableTabs as tab (tab)}
57
+ <Tab value={tab}>{caption[tab]}</Tab>
58
+ {/each}
59
+ </Tabs>
60
+
61
+ <div class="mt-5">
62
+ {#if active === 'description' && description}
63
+ <!-- font-normal pinned: Archer has no 300 cut, and a font-light
64
+ ancestor (the guide's Demo panel) would otherwise substitute. -->
65
+ <div class="font-(family-name:--font-serif) font-normal text-(length:--step1) leading-relaxed">
66
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -- documented prop contract: sanitized server-side, never author-controlled raw HTML. -->
67
+ {@html description}
68
+ </div>
69
+ {:else if active === 'parameters' && parameters}
70
+ <dl class="divide-border-subtle divide-y">
71
+ {#each parameters as parameter, index (`${index}-${parameter.label}-${parameter.value}`)}
72
+ <div class="flex items-baseline justify-between gap-4 py-2.5">
73
+ <dt class="text-fg-muted1 text-(length:--step-down1)">{parameter.label}</dt>
74
+ <dd class="text-right font-medium">{parameter.value}</dd>
75
+ </div>
76
+ {/each}
77
+ </dl>
78
+ {:else if active === 'benefits' && benefits}
79
+ <ul class="space-y-3 text-(length:--step-down1) leading-6">
80
+ {#each benefits as benefit (benefit)}
81
+ <li>{benefit}</li>
82
+ {/each}
83
+ </ul>
84
+ {/if}
85
+ </div>
86
+ </div>
87
+ {/if}
@@ -0,0 +1,18 @@
1
+ interface Props {
2
+ /** Sanitized HTML (sanitize server-side!) — never author-controlled raw input. */
3
+ description?: string;
4
+ parameters?: {
5
+ label: string;
6
+ value: string;
7
+ }[];
8
+ benefits?: string[];
9
+ /** Localized tab captions; Czech defaults. */
10
+ labels?: {
11
+ description?: string;
12
+ parameters?: string;
13
+ benefits?: string;
14
+ };
15
+ }
16
+ declare const ProductTabs: import("svelte").Component<Props, {}, "">;
17
+ type ProductTabs = ReturnType<typeof ProductTabs>;
18
+ export default ProductTabs;
@@ -0,0 +1,44 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ value: number;
4
+ min?: number;
5
+ max?: number;
6
+ disabled?: boolean;
7
+ onchange: (value: number) => void;
8
+ /** Accessible labels of the − / + buttons. */
9
+ decreaseLabel?: string;
10
+ increaseLabel?: string;
11
+ }
12
+
13
+ let {
14
+ value,
15
+ min = 1,
16
+ max,
17
+ disabled = false,
18
+ onchange,
19
+ decreaseLabel = 'Snížit množství',
20
+ increaseLabel = 'Zvýšit množství'
21
+ }: Props = $props();
22
+ </script>
23
+
24
+ <div class="rounded-button border-border-subtle inline-flex items-center border">
25
+ <button
26
+ type="button"
27
+ aria-label={decreaseLabel}
28
+ disabled={disabled || value <= min}
29
+ class="text-fg-muted1 hover:text-fg grid size-9 place-items-center disabled:opacity-40"
30
+ onclick={() => onchange(value - 1)}
31
+ >
32
+ <svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M5 12h14" /></svg>
33
+ </button>
34
+ <span class="min-w-8 text-center font-medium tabular-nums">{value}</span>
35
+ <button
36
+ type="button"
37
+ aria-label={increaseLabel}
38
+ disabled={disabled || (max !== undefined && value >= max)}
39
+ class="text-fg-muted1 hover:text-fg grid size-9 place-items-center disabled:opacity-40"
40
+ onclick={() => onchange(value + 1)}
41
+ >
42
+ <svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>
43
+ </button>
44
+ </div>
@@ -0,0 +1,13 @@
1
+ interface Props {
2
+ value: number;
3
+ min?: number;
4
+ max?: number;
5
+ disabled?: boolean;
6
+ onchange: (value: number) => void;
7
+ /** Accessible labels of the − / + buttons. */
8
+ decreaseLabel?: string;
9
+ increaseLabel?: string;
10
+ }
11
+ declare const QuantityStepper: import("svelte").Component<Props, {}, "">;
12
+ type QuantityStepper = ReturnType<typeof QuantityStepper>;
13
+ export default QuantityStepper;
@@ -0,0 +1,39 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ /** 0–5, fractions paint a partial star. */
4
+ rating: number;
5
+ /** Review count printed after the stars. */
6
+ count?: number;
7
+ }
8
+
9
+ let { rating, count }: Props = $props();
10
+
11
+ // $props.id() instead of Math.random(): stable across SSR + hydration, so
12
+ // the gradient defs and their url(#…) references cannot mismatch.
13
+ const uid = $props.id();
14
+ const stars = $derived(
15
+ [1, 2, 3, 4, 5].map((i) => (rating >= i ? 1 : rating > i - 1 ? rating - (i - 1) : 0))
16
+ );
17
+ </script>
18
+
19
+ <span class="inline-flex items-center gap-1" role="img" aria-label="{rating.toFixed(1)} z 5">
20
+ {#each stars as fill, i (i)}
21
+ <svg viewBox="0 0 24 24" class="size-4">
22
+ <defs
23
+ ><linearGradient id="{uid}-{i}"
24
+ ><stop offset="{fill * 100}%" stop-color="var(--JELINEK-BROWN)" /><stop
25
+ offset="{fill * 100}%"
26
+ stop-color="var(--border-subtle)"
27
+ /></linearGradient
28
+ ></defs
29
+ >
30
+ <path
31
+ fill="url(#{uid}-{i})"
32
+ d="M12 2l2.9 6.26 6.6.56-5 4.44 1.5 6.5L12 16.3 5.99 19.76l1.5-6.5-5-4.44 6.6-.56z"
33
+ />
34
+ </svg>
35
+ {/each}
36
+ {#if count !== undefined}<span class="text-fg-muted1 text-(length:--step-down1) ml-1"
37
+ >({count})</span
38
+ >{/if}
39
+ </span>
@@ -0,0 +1,9 @@
1
+ interface Props {
2
+ /** 0–5, fractions paint a partial star. */
3
+ rating: number;
4
+ /** Review count printed after the stars. */
5
+ count?: number;
6
+ }
7
+ declare const RatingStars: import("svelte").Component<Props, {}, "">;
8
+ type RatingStars = ReturnType<typeof RatingStars>;
9
+ export default RatingStars;
@@ -0,0 +1,79 @@
1
+ <script lang="ts">
2
+ // Bed-side picker for dual-configuration products (matrace-fe's SideButton,
3
+ // genericized): the caller supplies the selection summary as children, the
4
+ // component owns the active/inactive chrome and the bed illustration.
5
+ import type { Snippet } from 'svelte';
6
+
7
+ interface Props {
8
+ side: 'left' | 'right';
9
+ /** Caption above the summary („Levá strana“). */
10
+ label: string;
11
+ active: boolean;
12
+ onclick: () => void;
13
+ /** Summary of that side's configuration (firmness, foam…). */
14
+ children?: Snippet;
15
+ }
16
+
17
+ let { side, label, active, onclick, children }: Props = $props();
18
+
19
+ // Illustration palette, not UI surface colors — the soft magenta highlight
20
+ // and wood headboard come from matrace-fe's artwork and deliberately stay
21
+ // literal (a division re-theme must not repaint a drawing of a bed).
22
+ const leftFill = $derived(side === 'left' && active ? '#fae6f1' : '#f2f4f3');
23
+ const leftStroke = $derived(side === 'left' && active ? '#f0b1d5' : '#cfd7d2');
24
+ const rightFill = $derived(side === 'right' && active ? '#fae6f1' : '#f2f4f3');
25
+ const rightStroke = $derived(side === 'right' && active ? '#f0b1d5' : '#cfd7d2');
26
+ </script>
27
+
28
+ <button
29
+ type="button"
30
+ class="rounded-card cursor-pointer border px-3 py-2.5 text-left transition-all duration-200 max-sm:px-2 max-sm:py-2 {active
31
+ ? 'border-(--JELINEK-BROWN) bg-surface-gray shadow-resting'
32
+ : 'border-border-subtle bg-surface hover:border-(--text-muted2)'}"
33
+ aria-pressed={active}
34
+ {onclick}
35
+ >
36
+ <div class="flex items-start justify-between gap-1 sm:gap-3">
37
+ <div class="flex min-w-0 flex-col">
38
+ <p
39
+ class="text-(--color-accent-text) truncate text-[0.65rem] font-bold tracking-[0.05em] uppercase sm:text-[0.7rem]"
40
+ >
41
+ {label}
42
+ </p>
43
+ {#if children}
44
+ <div class="mt-2 flex flex-col gap-2">
45
+ {@render children()}
46
+ </div>
47
+ {/if}
48
+ </div>
49
+ <svg
50
+ class="mt-0.5 h-20 w-15 shrink-0 max-sm:h-14 max-sm:w-10"
51
+ viewBox="0 0 100 135"
52
+ fill="none"
53
+ xmlns="http://www.w3.org/2000/svg"
54
+ aria-hidden="true"
55
+ >
56
+ <!-- Headboard -->
57
+ <rect x="0" y="10" width="100" height="10" rx="3" fill="#D8B589" />
58
+ <!-- Left mattress -->
59
+ <path d="M 49 22 H 9 A 4 4 0 0 0 5 26 V 116 A 4 4 0 0 0 9 120 H 49 Z" fill={leftFill} />
60
+ <path
61
+ d="M 49 22 H 9 A 4 4 0 0 0 5 26 V 116 A 4 4 0 0 0 9 120 H 49"
62
+ stroke={leftStroke}
63
+ stroke-width="2"
64
+ fill="none"
65
+ />
66
+ <!-- Right mattress -->
67
+ <path d="M 51 22 H 91 A 4 4 0 0 1 95 26 V 116 A 4 4 0 0 1 91 120 H 51 Z" fill={rightFill} />
68
+ <path
69
+ d="M 51 120 H 91 A 4 4 0 0 0 95 116 V 26 A 4 4 0 0 0 91 22 H 51"
70
+ stroke={rightStroke}
71
+ stroke-width="2"
72
+ fill="none"
73
+ />
74
+ <!-- Pillows -->
75
+ <rect x="12" y="29" width="30" height="16" rx="4" fill="white" stroke="#c8d3cd" stroke-width="1.5" />
76
+ <rect x="57" y="29" width="30" height="16" rx="4" fill="white" stroke="#c8d3cd" stroke-width="1.5" />
77
+ </svg>
78
+ </div>
79
+ </button>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ side: 'left' | 'right';
4
+ /** Caption above the summary („Levá strana“). */
5
+ label: string;
6
+ active: boolean;
7
+ onclick: () => void;
8
+ /** Summary of that side's configuration (firmness, foam…). */
9
+ children?: Snippet;
10
+ }
11
+ declare const SideToggle: import("svelte").Component<Props, {}, "">;
12
+ type SideToggle = ReturnType<typeof SideToggle>;
13
+ export default SideToggle;
@@ -0,0 +1,10 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ /** Rozměry placeholderu — sám je jen plocha. */
4
+ class?: string;
5
+ }
6
+
7
+ let { class: className = '' }: Props = $props();
8
+ </script>
9
+
10
+ <div class="animate-pulse rounded-card bg-surface-gray {className}" aria-hidden="true"></div>
@@ -0,0 +1,7 @@
1
+ interface Props {
2
+ /** Rozměry placeholderu — sám je jen plocha. */
3
+ class?: string;
4
+ }
5
+ declare const Skeleton: import("svelte").Component<Props, {}, "">;
6
+ type Skeleton = ReturnType<typeof Skeleton>;
7
+ export default Skeleton;
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ interface Props {
5
+ visible: boolean;
6
+ children: Snippet;
7
+ }
8
+
9
+ let { visible, children }: Props = $props();
10
+ </script>
11
+
12
+ <!-- Glass strip under the kit's floating header card. z-40 keeps it BELOW the
13
+ header (SiteHeader is z-50) — any higher and the strip renders over the
14
+ card and its backdrop blur smears the whole top of the page. Top offset:
15
+ var(--header-h) from 640px up so the strip hangs under the fixed card;
16
+ top-0 on a phone, where the kit bar is absolute and has already scrolled
17
+ away by the time this strip appears (it only shows once the product header
18
+ left the viewport). -->
19
+ <div
20
+ aria-hidden={!visible}
21
+ class="fixed inset-x-0 top-0 z-40 border-b border-(--glass-border) bg-(--glass-fill) backdrop-blur-[25px] backdrop-saturate-140 transition-transform duration-(--dur-base) min-[640px]:top-(--header-h)
22
+ {visible ? 'translate-y-0' : 'pointer-events-none -translate-y-[calc(100%+var(--header-h))]'}"
23
+ >
24
+ <div class="max-w-page mx-auto flex w-full items-center gap-4 px-6 py-3">
25
+ {@render children()}
26
+ </div>
27
+ </div>
@@ -0,0 +1,8 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ visible: boolean;
4
+ children: Snippet;
5
+ }
6
+ declare const StickyBar: import("svelte").Component<Props, {}, "">;
7
+ type StickyBar = ReturnType<typeof StickyBar>;
8
+ export default StickyBar;
@@ -0,0 +1,45 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ state: 'in-stock' | 'on-order' | 'sold-out' | 'lead';
4
+ /** Text override — a localized label or a concrete lead time („Do 3 týdnů“). */
5
+ label?: string;
6
+ compact?: boolean;
7
+ }
8
+
9
+ let { state, label, compact = false }: Props = $props();
10
+
11
+ const DEFAULTS: Record<Props['state'], string> = {
12
+ 'in-stock': 'Skladem',
13
+ 'on-order': 'Na objednávku',
14
+ 'sold-out': 'Vyprodáno',
15
+ // `lead` has no meaningful default — the whole point of the state is a
16
+ // concrete lead time the caller computed. Empty keeps the dot alone.
17
+ lead: ''
18
+ };
19
+
20
+ const config = $derived(
21
+ {
22
+ 'in-stock': {
23
+ cls: 'text-(--sem-success-strong)',
24
+ dot: 'bg-(--sem-success-strong)'
25
+ },
26
+ 'on-order': { cls: 'text-(--color-stock-order)', dot: 'bg-(--color-stock-order)' },
27
+ 'sold-out': { cls: 'text-fg-muted1', dot: 'bg-(--text-muted2)' },
28
+ lead: { cls: 'text-(--color-stock-order)', dot: 'bg-(--color-stock-order)' }
29
+ }[state]
30
+ );
31
+
32
+ const text = $derived(label ?? DEFAULTS[state]);
33
+ </script>
34
+
35
+ {#if compact}
36
+ <span class="inline-flex items-center gap-1.5 text-(length:--step-down1) font-medium {config.cls}">
37
+ <span class="size-2 rounded-pill {config.dot}"></span>{text}
38
+ </span>
39
+ {:else}
40
+ <span
41
+ class="rounded-pill bg-surface-gray inline-flex items-center gap-2 px-3 py-1 text-(length:--step-down1) font-medium {config.cls}"
42
+ >
43
+ <span class="size-2 rounded-pill {config.dot}"></span>{text}
44
+ </span>
45
+ {/if}
@@ -0,0 +1,9 @@
1
+ interface Props {
2
+ state: 'in-stock' | 'on-order' | 'sold-out' | 'lead';
3
+ /** Text override — a localized label or a concrete lead time („Do 3 týdnů“). */
4
+ label?: string;
5
+ compact?: boolean;
6
+ }
7
+ declare const StockBadge: import("svelte").Component<Props, {}, "">;
8
+ type StockBadge = ReturnType<typeof StockBadge>;
9
+ export default StockBadge;
@@ -0,0 +1,34 @@
1
+ <script lang="ts">
2
+ import { fly } from 'svelte/transition';
3
+ import { onMount } from 'svelte';
4
+ import { toasts, type ToastItem } from './toast.js';
5
+
6
+ interface Props {
7
+ /** Extra classes on the fixed wrapper (e.g. a different bottom offset). */
8
+ class?: string;
9
+ }
10
+
11
+ let { class: className = '' }: Props = $props();
12
+
13
+ // toast.ts is a plain (non-runes) class — see the comment at its top for why — so this
14
+ // component mirrors it into its own `$state` via subscribe() rather than reading it directly.
15
+ let items: readonly ToastItem[] = $state([]);
16
+
17
+ onMount(() => toasts.subscribe((next) => (items = next)));
18
+ </script>
19
+
20
+ <!-- z-scale contract: header 1100, Drawer/Modal 1200+, search overlays 1400.
21
+ Toasts must clear all of it so an add-to-cart confirmation is never hidden
22
+ under a drawer/overlay — hence z-[1500]. -->
23
+ <div
24
+ class="pointer-events-none fixed inset-x-0 bottom-6 z-[1500] flex flex-col items-center gap-2 {className}"
25
+ >
26
+ {#each items as t (t.id)}
27
+ <div
28
+ transition:fly={{ y: 16, duration: 200 }}
29
+ class="rounded-pill text-fg-inverse shadow-interactive pointer-events-auto bg-(--bg-black) px-5 py-2.5 text-(length:--step-down1) font-medium"
30
+ >
31
+ {t.message}
32
+ </div>
33
+ {/each}
34
+ </div>
@@ -0,0 +1,7 @@
1
+ interface Props {
2
+ /** Extra classes on the fixed wrapper (e.g. a different bottom offset). */
3
+ class?: string;
4
+ }
5
+ declare const Toaster: import("svelte").Component<Props, {}, "">;
6
+ type Toaster = ReturnType<typeof Toaster>;
7
+ export default Toaster;
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ import type { Component } from 'svelte';
3
+
4
+ interface Props {
5
+ /**
6
+ * Required on purpose — the kit ships no icon library, so the badges
7
+ * (icon component + label) come from the app. Icons render at 14 px.
8
+ */
9
+ items: { icon: Component; label: string }[];
10
+ /** Absolute glass strip over a photo's bottom edge instead of an inline row. */
11
+ overlay?: boolean;
12
+ }
13
+
14
+ let { items, overlay = false }: Props = $props();
15
+ </script>
16
+
17
+ <div class={overlay ? 'absolute inset-x-3 bottom-3 flex flex-wrap gap-2' : 'flex flex-wrap gap-2'}>
18
+ {#each items as item (item.label)}
19
+ <span
20
+ class="rounded-pill flex items-center gap-1.5 bg-(--glass-fill) px-3 py-1.5 text-(length:--step-down2) font-medium backdrop-blur-[25px]"
21
+ >
22
+ <item.icon class="size-3.5" />
23
+ {item.label}
24
+ </span>
25
+ {/each}
26
+ </div>
@@ -0,0 +1,16 @@
1
+ import type { Component } from 'svelte';
2
+ interface Props {
3
+ /**
4
+ * Required on purpose — the kit ships no icon library, so the badges
5
+ * (icon component + label) come from the app. Icons render at 14 px.
6
+ */
7
+ items: {
8
+ icon: Component;
9
+ label: string;
10
+ }[];
11
+ /** Absolute glass strip over a photo's bottom edge instead of an inline row. */
12
+ overlay?: boolean;
13
+ }
14
+ declare const TrustBadges: Component<Props, {}, "">;
15
+ type TrustBadges = ReturnType<typeof TrustBadges>;
16
+ export default TrustBadges;
@@ -0,0 +1,12 @@
1
+ /** One cart row as the kit's cart components see it — plain data, no PIM types. */
2
+ export type CartLine = {
3
+ id: number | string;
4
+ productName: string;
5
+ /** One-line configuration recap („180×200, dub, tvrdá“). Null and undefined
6
+ * both mean „none“ — API rows often carry explicit nulls. */
7
+ configSummary?: string | null;
8
+ imageUrl?: string | null;
9
+ quantity: number;
10
+ /** Row total (quantity already included). */
11
+ lineTotal: number;
12
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The kit's two money formatters, ported verbatim from the store so every
3
+ * consumer prints prices the same way.
4
+ *
5
+ * `formatPrice` — display surfaces (product cards, price tags, CTAs): whole
6
+ * CZK with non-breaking thousand separators, legibility first.
7
+ * `formatMoney` — cart/checkout amounts: EUR keeps 2-decimal precision, CZK
8
+ * rounds to whole crowns.
9
+ *
10
+ * @example formatPrice(18350) → "18 350 Kč"
11
+ * @example formatMoney(1234.5, "EUR") → "1 234,50 €"
12
+ */
13
+ export declare function formatPrice(amount: number, currency?: string): string;
14
+ export declare function formatMoney(amount: number, currency?: string): string;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The kit's two money formatters, ported verbatim from the store so every
3
+ * consumer prints prices the same way.
4
+ *
5
+ * `formatPrice` — display surfaces (product cards, price tags, CTAs): whole
6
+ * CZK with non-breaking thousand separators, legibility first.
7
+ * `formatMoney` — cart/checkout amounts: EUR keeps 2-decimal precision, CZK
8
+ * rounds to whole crowns.
9
+ *
10
+ * @example formatPrice(18350) → "18 350 Kč"
11
+ * @example formatMoney(1234.5, "EUR") → "1 234,50 €"
12
+ */
13
+ export function formatPrice(amount, currency = 'CZK') {
14
+ return new Intl.NumberFormat('cs-CZ', {
15
+ style: 'currency',
16
+ currency,
17
+ maximumFractionDigits: 0
18
+ }).format(amount);
19
+ }
20
+ export function formatMoney(amount, currency = 'CZK') {
21
+ const rounded = currency === 'EUR' ? Math.round(amount * 100) / 100 : Math.round(amount);
22
+ if (currency === 'EUR') {
23
+ return `${rounded.toLocaleString('sk-SK', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} €`;
24
+ }
25
+ return `${rounded.toLocaleString('cs-CZ')} Kč`;
26
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Svelte action: reparent the node to <body>. `fixed` + z-index is not enough
3
+ * when the overlay is mounted inside an ancestor that creates a stacking
4
+ * context (e.g. a PDP gallery's `lg:sticky` column) — the whole overlay then
5
+ * paints below later siblings regardless of z-index. Portaling out of the
6
+ * ancestor is the only robust fix.
7
+ */
8
+ export declare function portal(node: HTMLElement): {
9
+ destroy: () => void;
10
+ };
11
+ /** Lock body scroll; returns the unlock function (for use inside $effect). */
12
+ export declare function lockScroll(): () => void;
@@ -0,0 +1,20 @@
1
+ // Shared plumbing for overlay primitives (Modal, Drawer).
2
+ /**
3
+ * Svelte action: reparent the node to <body>. `fixed` + z-index is not enough
4
+ * when the overlay is mounted inside an ancestor that creates a stacking
5
+ * context (e.g. a PDP gallery's `lg:sticky` column) — the whole overlay then
6
+ * paints below later siblings regardless of z-index. Portaling out of the
7
+ * ancestor is the only robust fix.
8
+ */
9
+ export function portal(node) {
10
+ document.body.appendChild(node);
11
+ return { destroy: () => node.remove() };
12
+ }
13
+ /** Lock body scroll; returns the unlock function (for use inside $effect). */
14
+ export function lockScroll() {
15
+ const prev = document.body.style.overflow;
16
+ document.body.style.overflow = 'hidden';
17
+ return () => {
18
+ document.body.style.overflow = prev;
19
+ };
20
+ }
@@ -0,0 +1,26 @@
1
+ export type ToastItem = {
2
+ id: string;
3
+ message: string;
4
+ };
5
+ type Listener = (items: readonly ToastItem[]) => void;
6
+ declare class ToastStore {
7
+ #private;
8
+ /** Plain snapshot, not reactive — re-reading this getter after a mutation sees the update, but
9
+ * nothing re-renders automatically outside a `subscribe()` listener (see below). */
10
+ get items(): readonly ToastItem[];
11
+ /** Show a toast. Passing an `id` that's already showing replaces its message and resets its timer. */
12
+ show(message: string, opts?: {
13
+ id?: string;
14
+ duration?: number;
15
+ }): void;
16
+ dismiss(id: string): void;
17
+ /**
18
+ * For Toaster.svelte to mirror `items` into its own reactive state. Returns an unsubscribe fn.
19
+ * Note: `items` is a plain getter, not a Svelte rune — it is NOT reactive outside a subscriber.
20
+ * Reading `toasts.items` directly from a component template will not update on show/dismiss;
21
+ * either call `subscribe()` yourself or route the UI through `<Toaster />`.
22
+ */
23
+ subscribe(listener: Listener): () => void;
24
+ }
25
+ export declare const toasts: ToastStore;
26
+ export {};