@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
@@ -109,9 +109,12 @@
109
109
 
110
110
  // h-full so tiles in a CSS-grid row equalise: the grid stretches the
111
111
  // wrapper, and the button must follow it — otherwise a two-line label
112
- // makes one tile taller and its row neighbours look ragged.
112
+ // makes one tile taller and its row neighbours look ragged. flex-col
113
+ // because a stretched <button> vertically centres its content by
114
+ // default — the label of a short tile would float mid-cell instead of
115
+ // starting at the same height as its neighbours'.
113
116
  const BASE =
114
- 'h-full w-full cursor-pointer rounded-card border-2 bg-surface text-left ' +
117
+ 'flex h-full w-full cursor-pointer flex-col rounded-card border-2 bg-surface text-left ' +
115
118
  'font-secondary font-light text-fg ' +
116
119
  'transition-[border-color,box-shadow] duration-[var(--dur-base)] ease-[var(--ease)]';
117
120
 
@@ -59,6 +59,12 @@
59
59
  * colour) and crops it with object-fit: cover. Mirrors the guide's
60
60
  * `.page-hero__media`. The text content is lifted above it. */
61
61
  media?: Snippet;
62
+ /** Task 46 — breadcrumb trail, the hero's first line ("under the menu"):
63
+ * `Sekce / Kapitola`. Entries WITH `href` render as links, the last
64
+ * (current page) is plain text with aria-current. Mirrors the guide's
65
+ * `.breadcrumbs` row (jelinek.css) — no "home" entry, the logo is the
66
+ * way home. */
67
+ crumbs?: { label: string; href?: string }[];
62
68
  class?: string;
63
69
  }
64
70
 
@@ -71,10 +77,21 @@
71
77
  heading,
72
78
  children,
73
79
  media,
80
+ crumbs = [],
74
81
  class: className,
75
82
  ...rest
76
83
  }: Props & HTMLAttributes<HTMLElement> = $props();
77
84
 
85
+ // jelinek.css .breadcrumbs — a small Avenir UI row: muted, 600 weight,
86
+ // step-down2, links darken on hover, current chapter reads in full text
87
+ // colour. SUBTITLE_STYLE (below) carries the Avenir stack inline, same as
88
+ // the header's subtitle does.
89
+ const CRUMBS =
90
+ 'mb-4 flex flex-wrap items-center gap-2 text-step-down2 font-semibold tracking-[0.02em] text-fg-muted1';
91
+ const CRUMB_LINK =
92
+ 'text-fg-muted1 no-underline transition-[color] duration-[var(--dur-base)] ease-[var(--ease)] hover:text-fg hover:no-underline';
93
+ const CRUMBS_STYLE = "font-family: 'Avenir Next LT Pro', 'Avenir Next', Arial, sans-serif;";
94
+
78
95
  // `hasContext` must run synchronously during this component's own
79
96
  // initialization (Svelte's lifecycle rule for context APIs) — captured
80
97
  // into a plain const here, NOT inside `$derived`'s callback, since that
@@ -255,8 +272,14 @@
255
272
  "w-fit max-w-full mb-0 after:content-[''] " +
256
273
  'after:block after:h-[3px] after:w-[75%] after:rounded-[2px] after:mt-4 after:mb-6';
257
274
 
258
- // Which levels the guide's `:is(h1, h2, h3):has(+ .rule)` covers.
259
- const hasRule = $derived(level !== 'h4');
275
+ // Which levels the guide's `:is(h1, h2, h3):has(+ .rule + p)` covers —
276
+ // and, Task 46, the owner's "underline only above text" rule: the bar
277
+ // draws only when something follows the heading (the `children` lede).
278
+ // A component cannot inspect what a snippet renders, so "children exist"
279
+ // stands in for the guide CSS's "a <p> follows the marker" — every real
280
+ // hero lede in this showcase is a paragraph, so the two agree; a bare
281
+ // eyebrow+heading hero (brand/znacka.html's shape) loses the bar in both.
282
+ const hasRule = $derived(level !== 'h4' && !!children);
260
283
 
261
284
  // jelinek.css:356-359 (h1-h4; h5/h6 have no counterpart here since `Level`
262
285
  // stops at h4). Note this list is NOT the same set as the underline's
@@ -305,6 +328,18 @@
305
328
  contain itself" branches below render the identical markup instead of
306
329
  two copies drifting apart. -->
307
330
  {#snippet content()}
331
+ {#if crumbs.length > 0}
332
+ <nav class={CRUMBS} style={CRUMBS_STYLE} aria-label="Drobečková navigace">
333
+ {#each crumbs as crumb, i (crumb.label)}
334
+ {#if i > 0}<span class="text-fg-muted2" aria-hidden="true">/</span>{/if}
335
+ {#if crumb.href}
336
+ <a href={crumb.href} class={CRUMB_LINK}>{crumb.label}</a>
337
+ {:else}
338
+ <span class="text-fg" aria-current="page">{crumb.label}</span>
339
+ {/if}
340
+ {/each}
341
+ </nav>
342
+ {/if}
308
343
  {#if eyebrow}
309
344
  <Eyebrow {variant}>{@render eyebrow()}</Eyebrow>
310
345
  {/if}
@@ -53,6 +53,15 @@ interface Props {
53
53
  * colour) and crops it with object-fit: cover. Mirrors the guide's
54
54
  * `.page-hero__media`. The text content is lifted above it. */
55
55
  media?: Snippet;
56
+ /** Task 46 — breadcrumb trail, the hero's first line ("under the menu"):
57
+ * `Sekce / Kapitola`. Entries WITH `href` render as links, the last
58
+ * (current page) is plain text with aria-current. Mirrors the guide's
59
+ * `.breadcrumbs` row (jelinek.css) — no "home" entry, the logo is the
60
+ * way home. */
61
+ crumbs?: {
62
+ label: string;
63
+ href?: string;
64
+ }[];
56
65
  class?: string;
57
66
  }
58
67
  type $$ComponentProps = Props & HTMLAttributes<HTMLElement>;
@@ -0,0 +1,24 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ items: { href?: string; label: string }[];
4
+ /** aria-label of the <nav> landmark. */
5
+ label?: string;
6
+ }
7
+
8
+ let { items, label = 'Drobečková navigace' }: Props = $props();
9
+ </script>
10
+
11
+ <nav aria-label={label} class="overflow-x-auto whitespace-nowrap py-3 [scrollbar-width:none]">
12
+ <ol class="flex items-center gap-2 text-(length:--step-down1)">
13
+ {#each items as item, i (i)}
14
+ <li class="flex items-center gap-2">
15
+ {#if i > 0}<span aria-hidden="true" class="text-fg-muted2">›</span>{/if}
16
+ {#if item.href && i < items.length - 1}
17
+ <a href={item.href} class="text-fg-muted1 hover:text-fg">{item.label}</a>
18
+ {:else}
19
+ <span class="text-(--color-accent-text) font-medium" aria-current="page">{item.label}</span>
20
+ {/if}
21
+ </li>
22
+ {/each}
23
+ </ol>
24
+ </nav>
@@ -0,0 +1,11 @@
1
+ interface Props {
2
+ items: {
3
+ href?: string;
4
+ label: string;
5
+ }[];
6
+ /** aria-label of the <nav> landmark. */
7
+ label?: string;
8
+ }
9
+ declare const Breadcrumbs: import("svelte").Component<Props, {}, "">;
10
+ type Breadcrumbs = ReturnType<typeof Breadcrumbs>;
11
+ export default Breadcrumbs;
@@ -0,0 +1,128 @@
1
+ <script lang="ts">
2
+ // The whole cart drawer, composed from kit parts (Drawer + CartLineItem +
3
+ // FreeShippingBar). Purely presentational: the app owns the cart store and
4
+ // maps it onto these props; navigation targets come in as hrefs.
5
+ import Button from '../Button.svelte';
6
+ import CartLineItem from './CartLineItem.svelte';
7
+ import Drawer from './Drawer.svelte';
8
+ import FreeShippingBar from './FreeShippingBar.svelte';
9
+ import { formatMoney } from './money.js';
10
+ import type { CartLine } from './cart-types.js';
11
+
12
+ interface Props {
13
+ open: boolean;
14
+ onclose: () => void;
15
+ items: CartLine[];
16
+ /** Item count shown next to the title. */
17
+ count: number;
18
+ currency?: string;
19
+ /** Items total minus discounts — the number above the CTA. */
20
+ subtotal: number;
21
+ checkoutHref: string;
22
+ viewCartHref?: string;
23
+ /** Disables steppers/remove while a cart mutation is in flight. */
24
+ disabled?: boolean;
25
+ onQuantity: (id: CartLine['id'], quantity: number) => void;
26
+ onRemove: (id: CartLine['id']) => void;
27
+ /** Free-shipping progress; omit to hide the bar. */
28
+ shipping?: { threshold: number; remaining: number; qualified: boolean };
29
+ labels?: {
30
+ title?: string;
31
+ close?: string;
32
+ empty?: string;
33
+ subtotal?: string;
34
+ checkout?: string;
35
+ viewCart?: string;
36
+ remove?: string;
37
+ decrease?: string;
38
+ increase?: string;
39
+ };
40
+ }
41
+
42
+ let {
43
+ open,
44
+ onclose,
45
+ items,
46
+ count,
47
+ currency,
48
+ subtotal,
49
+ checkoutHref,
50
+ viewCartHref,
51
+ disabled = false,
52
+ onQuantity,
53
+ onRemove,
54
+ shipping,
55
+ labels
56
+ }: Props = $props();
57
+
58
+ const title = $derived(labels?.title ?? 'Košík');
59
+ const closeLabel = $derived(labels?.close ?? 'Zavřít');
60
+ const emptyLabel = $derived(labels?.empty ?? 'Košík je prázdný');
61
+ const subtotalLabel = $derived(labels?.subtotal ?? 'Mezisoučet');
62
+ const checkoutLabel = $derived(labels?.checkout ?? 'K pokladně');
63
+ const viewCartLabel = $derived(labels?.viewCart ?? 'Zobrazit košík');
64
+ </script>
65
+
66
+ <Drawer {open} {onclose} side="right" labelledby="cart-drawer-title" {closeLabel}>
67
+ <header class="border-border-subtle flex items-center justify-between border-b px-5 py-4">
68
+ <h2 id="cart-drawer-title" class="font-primary text-(length:--step1) font-bold text-fg">
69
+ {title} ({count})
70
+ </h2>
71
+ <button
72
+ type="button"
73
+ class="rounded-button hover:bg-surface-gray flex items-center justify-center p-2 text-fg transition-colors"
74
+ aria-label={closeLabel}
75
+ onclick={onclose}
76
+ >
77
+ <svg viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12" /></svg>
78
+ </button>
79
+ </header>
80
+
81
+ <div class="flex-1 overflow-y-auto px-5">
82
+ {#if items.length === 0}
83
+ <p class="text-fg-muted1 py-10 text-center">{emptyLabel}</p>
84
+ {:else}
85
+ {#if shipping}
86
+ <div class="py-4">
87
+ <FreeShippingBar
88
+ threshold={shipping.threshold}
89
+ remaining={shipping.remaining}
90
+ qualified={shipping.qualified}
91
+ {currency}
92
+ />
93
+ </div>
94
+ {/if}
95
+ <div class="divide-border-subtle divide-y">
96
+ {#each items as item (item.id)}
97
+ <CartLineItem
98
+ {item}
99
+ {currency}
100
+ {disabled}
101
+ {onQuantity}
102
+ {onRemove}
103
+ removeLabel={labels?.remove}
104
+ decreaseLabel={labels?.decrease}
105
+ increaseLabel={labels?.increase}
106
+ />
107
+ {/each}
108
+ </div>
109
+ {/if}
110
+ </div>
111
+
112
+ {#if items.length > 0}
113
+ <footer class="border-border-subtle border-t px-5 py-4">
114
+ <div class="mb-3 flex items-center justify-between text-(length:--step0) font-bold text-fg">
115
+ <span>{subtotalLabel}</span>
116
+ <span>{formatMoney(subtotal, currency)}</span>
117
+ </div>
118
+ <Button color="brown" fullWidth href={checkoutHref} onclick={onclose} class="mb-2">
119
+ {checkoutLabel}
120
+ </Button>
121
+ {#if viewCartHref}
122
+ <Button variant="ghost" color="brown" fullWidth href={viewCartHref} onclick={onclose}>
123
+ {viewCartLabel}
124
+ </Button>
125
+ {/if}
126
+ </footer>
127
+ {/if}
128
+ </Drawer>
@@ -0,0 +1,37 @@
1
+ import type { CartLine } from './cart-types.js';
2
+ interface Props {
3
+ open: boolean;
4
+ onclose: () => void;
5
+ items: CartLine[];
6
+ /** Item count shown next to the title. */
7
+ count: number;
8
+ currency?: string;
9
+ /** Items total minus discounts — the number above the CTA. */
10
+ subtotal: number;
11
+ checkoutHref: string;
12
+ viewCartHref?: string;
13
+ /** Disables steppers/remove while a cart mutation is in flight. */
14
+ disabled?: boolean;
15
+ onQuantity: (id: CartLine['id'], quantity: number) => void;
16
+ onRemove: (id: CartLine['id']) => void;
17
+ /** Free-shipping progress; omit to hide the bar. */
18
+ shipping?: {
19
+ threshold: number;
20
+ remaining: number;
21
+ qualified: boolean;
22
+ };
23
+ labels?: {
24
+ title?: string;
25
+ close?: string;
26
+ empty?: string;
27
+ subtotal?: string;
28
+ checkout?: string;
29
+ viewCart?: string;
30
+ remove?: string;
31
+ decrease?: string;
32
+ increase?: string;
33
+ };
34
+ }
35
+ declare const CartDrawer: import("svelte").Component<Props, {}, "">;
36
+ type CartDrawer = ReturnType<typeof CartDrawer>;
37
+ export default CartDrawer;
@@ -0,0 +1,67 @@
1
+ <script lang="ts">
2
+ import QuantityStepper from './QuantityStepper.svelte';
3
+ import { formatMoney } from './money.js';
4
+ import type { CartLine } from './cart-types.js';
5
+
6
+ interface Props {
7
+ item: CartLine;
8
+ currency?: string;
9
+ disabled?: boolean;
10
+ onQuantity: (id: CartLine['id'], quantity: number) => void;
11
+ onRemove: (id: CartLine['id']) => void;
12
+ removeLabel?: string;
13
+ decreaseLabel?: string;
14
+ increaseLabel?: string;
15
+ }
16
+
17
+ let {
18
+ item,
19
+ currency,
20
+ disabled = false,
21
+ onQuantity,
22
+ onRemove,
23
+ removeLabel = 'Odebrat z košíku',
24
+ decreaseLabel,
25
+ increaseLabel
26
+ }: Props = $props();
27
+ </script>
28
+
29
+ <div class="flex gap-3 py-4">
30
+ <div class="rounded-image bg-surface-gray h-20 w-20 shrink-0 overflow-hidden">
31
+ {#if item.imageUrl}
32
+ <img src={item.imageUrl} alt={item.productName} class="h-full w-full object-cover" />
33
+ {/if}
34
+ </div>
35
+
36
+ <div class="flex min-w-0 flex-1 flex-col">
37
+ <div class="flex items-start justify-between gap-2">
38
+ <p class="font-medium text-fg">{item.productName}</p>
39
+ <p class="shrink-0 font-bold text-fg">
40
+ {formatMoney(item.lineTotal, currency)}
41
+ </p>
42
+ </div>
43
+ {#if item.configSummary}
44
+ <p class="text-fg-muted1 mt-0.5 text-(length:--step-down2)">{item.configSummary}</p>
45
+ {/if}
46
+
47
+ <div class="mt-auto flex items-center justify-between pt-2">
48
+ <QuantityStepper
49
+ value={item.quantity}
50
+ min={1}
51
+ {disabled}
52
+ onchange={(quantity) => onQuantity(item.id, quantity)}
53
+ {decreaseLabel}
54
+ {increaseLabel}
55
+ />
56
+ <button
57
+ type="button"
58
+ class="rounded-button text-fg-muted1 hover:text-(--sem-error-strong) flex items-center justify-center p-1.5 transition-colors disabled:opacity-40"
59
+ aria-label={removeLabel}
60
+ {disabled}
61
+ onclick={() => onRemove(item.id)}
62
+ >
63
+ <svg viewBox="0 0 24 24" class="size-[17px]" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14ZM10 11v6M14 11v6" /></svg>
64
+ </button>
65
+ </div>
66
+ </div>
67
+ </div>
@@ -0,0 +1,14 @@
1
+ import type { CartLine } from './cart-types.js';
2
+ interface Props {
3
+ item: CartLine;
4
+ currency?: string;
5
+ disabled?: boolean;
6
+ onQuantity: (id: CartLine['id'], quantity: number) => void;
7
+ onRemove: (id: CartLine['id']) => void;
8
+ removeLabel?: string;
9
+ decreaseLabel?: string;
10
+ increaseLabel?: string;
11
+ }
12
+ declare const CartLineItem: import("svelte").Component<Props, {}, "">;
13
+ type CartLineItem = ReturnType<typeof CartLineItem>;
14
+ export default CartLineItem;
@@ -0,0 +1,49 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ /** 1-based current step. */
4
+ step: number;
5
+ /** Step captions in order; length defines the step count. */
6
+ steps?: string[];
7
+ /** Called with the step number when a completed step is clicked. */
8
+ onStep?: (step: number) => void;
9
+ }
10
+
11
+ let { step, steps = ['Rekapitulace', 'Doprava', 'Platba'], onStep }: Props = $props();
12
+ </script>
13
+
14
+ <ol class="flex items-center justify-center gap-2 sm:gap-4">
15
+ {#each steps as label, i (i)}
16
+ {@const n = i + 1}
17
+ <li class="flex items-center gap-2">
18
+ <button
19
+ type="button"
20
+ disabled={n >= step}
21
+ class="flex items-center gap-2 {n > step ? 'opacity-50' : ''}"
22
+ onclick={() => n < step && onStep?.(n)}
23
+ >
24
+ <span
25
+ class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-(length:--step-down1) font-bold {n <
26
+ step
27
+ ? 'bg-(--JELINEK-BROWN) text-fg-inverse'
28
+ : n === step
29
+ ? 'border-2 border-(--JELINEK-BROWN) text-(--color-accent-text)'
30
+ : 'bg-surface-gray text-fg-muted2'}"
31
+ >
32
+ {#if n < step}
33
+ <svg viewBox="0 0 24 24" class="size-3.5" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 13 4 4L19 7" /></svg>
34
+ {:else}
35
+ {n}
36
+ {/if}
37
+ </span>
38
+ <span
39
+ class="text-(length:--step-down1) font-medium max-sm:hidden {n === step
40
+ ? 'text-fg'
41
+ : 'text-fg-muted1'}">{label}</span
42
+ >
43
+ </button>
44
+ {#if n < steps.length}
45
+ <span class="bg-border-subtle h-px w-6 sm:w-10"></span>
46
+ {/if}
47
+ </li>
48
+ {/each}
49
+ </ol>
@@ -0,0 +1,11 @@
1
+ interface Props {
2
+ /** 1-based current step. */
3
+ step: number;
4
+ /** Step captions in order; length defines the step count. */
5
+ steps?: string[];
6
+ /** Called with the step number when a completed step is clicked. */
7
+ onStep?: (step: number) => void;
8
+ }
9
+ declare const CheckoutProgress: import("svelte").Component<Props, {}, "">;
10
+ type CheckoutProgress = ReturnType<typeof CheckoutProgress>;
11
+ export default CheckoutProgress;
@@ -0,0 +1,24 @@
1
+ <script lang="ts">
2
+ // Numbered configurator section („1. Šířka“, „2. Tuhost“…) — the numbering
3
+ // pattern matrace-fe's configurator established for guiding the shopper
4
+ // through a long decision list.
5
+ import type { Snippet } from 'svelte';
6
+
7
+ interface Props {
8
+ number: number;
9
+ title: string;
10
+ /** One-line helper under the heading. */
11
+ hint?: string;
12
+ children: Snippet;
13
+ }
14
+
15
+ let { number, title, hint, children }: Props = $props();
16
+ </script>
17
+
18
+ <section class="py-6">
19
+ <h3 class="flex items-baseline gap-3 text-(length:--step2) font-bold">
20
+ <span class="text-(--color-accent-text)">{number}.</span>{title}
21
+ </h3>
22
+ {#if hint}<p class="text-fg-muted1 mt-1 text-(length:--step-down1)">{hint}</p>{/if}
23
+ <div class="mt-4">{@render children()}</div>
24
+ </section>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ number: number;
4
+ title: string;
5
+ /** One-line helper under the heading. */
6
+ hint?: string;
7
+ children: Snippet;
8
+ }
9
+ declare const ConfigSection: import("svelte").Component<Props, {}, "">;
10
+ type ConfigSection = ReturnType<typeof ConfigSection>;
11
+ export default ConfigSection;
@@ -0,0 +1,86 @@
1
+ <script lang="ts">
2
+ // Custom-dimension picker (matrace-fe's DimensionSlider, restyled to kit
3
+ // tokens): a range slider synced with a stepper number input, plus the
4
+ // price delta the chosen dimension costs.
5
+ import { formatPrice } from './money.js';
6
+
7
+ interface Props {
8
+ value: number;
9
+ min: number;
10
+ max: number;
11
+ step?: number;
12
+ /** Dimension name shown next to the input („Šířka“). */
13
+ label: string;
14
+ /** Price delta of the current value in CZK; 0 hides the tag. */
15
+ surcharge?: number;
16
+ /** One-line note above the slider („Atypický rozměr vyrábíme na míru“). */
17
+ note?: string | null;
18
+ /** Unit shown after the number input. */
19
+ unit?: string;
20
+ currency?: string;
21
+ }
22
+
23
+ let {
24
+ value = $bindable(),
25
+ min,
26
+ max,
27
+ step = 1,
28
+ label,
29
+ surcharge = 0,
30
+ note = null,
31
+ unit = 'cm',
32
+ currency
33
+ }: Props = $props();
34
+
35
+ function update(val: number) {
36
+ value = Math.max(min, Math.min(max, val));
37
+ }
38
+
39
+ const stepButton =
40
+ 'text-fg hover:bg-surface-gray flex cursor-pointer items-center justify-center rounded border-none bg-transparent px-1.5 text-xl leading-none font-medium transition-colors duration-200 select-none';
41
+ </script>
42
+
43
+ <div
44
+ class="bg-surface border-border-subtle rounded-card mt-2 flex flex-col gap-2 border pt-5 pr-4 pb-4 pl-4"
45
+ >
46
+ {#if note}
47
+ <div class="text-fg-muted1 mb-1 text-(length:--step-down2) font-medium">{note}</div>
48
+ {/if}
49
+ <input
50
+ type="range"
51
+ {min}
52
+ {max}
53
+ {step}
54
+ bind:value
55
+ oninput={() => update(value)}
56
+ aria-label={label}
57
+ class="w-full cursor-pointer border-none bg-transparent outline-none accent-(--JELINEK-BROWN)"
58
+ />
59
+ <div
60
+ class="flex items-center justify-between text-[0.95rem] max-sm:flex-col max-sm:items-start max-sm:gap-1"
61
+ >
62
+ <div class="flex items-center gap-2">
63
+ <span>{label}:</span>
64
+ <div
65
+ class="bg-surface border-border-subtle rounded-button flex items-center gap-1 border px-2 py-1 transition-colors focus-within:border-(--JELINEK-BROWN)"
66
+ >
67
+ <button type="button" class={stepButton} onclick={() => update(value - step)}>−</button>
68
+ <input
69
+ type="number"
70
+ bind:value
71
+ {min}
72
+ {max}
73
+ oninput={() => update(value)}
74
+ aria-label={label}
75
+ class="w-[50px] border-none p-0 font-bold text-fg outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
76
+ />
77
+ <span class="text-fg-muted1 text-(length:--step-down1) font-medium">{unit}</span>
78
+ </div>
79
+ </div>
80
+ {#if surcharge !== 0}
81
+ <span class="text-(--color-accent-text) text-(length:--step-down1) font-bold">
82
+ {surcharge > 0 ? '+' : '−'}{formatPrice(Math.abs(surcharge), currency)}
83
+ </span>
84
+ {/if}
85
+ </div>
86
+ </div>
@@ -0,0 +1,18 @@
1
+ interface Props {
2
+ value: number;
3
+ min: number;
4
+ max: number;
5
+ step?: number;
6
+ /** Dimension name shown next to the input („Šířka“). */
7
+ label: string;
8
+ /** Price delta of the current value in CZK; 0 hides the tag. */
9
+ surcharge?: number;
10
+ /** One-line note above the slider („Atypický rozměr vyrábíme na míru“). */
11
+ note?: string | null;
12
+ /** Unit shown after the number input. */
13
+ unit?: string;
14
+ currency?: string;
15
+ }
16
+ declare const DimensionSlider: import("svelte").Component<Props, {}, "value">;
17
+ type DimensionSlider = ReturnType<typeof DimensionSlider>;
18
+ export default DimensionSlider;
@@ -0,0 +1,67 @@
1
+ <script lang="ts">
2
+ import { fade, fly } from 'svelte/transition';
3
+ import type { Snippet } from 'svelte';
4
+ import { portal, lockScroll } from './overlay.js';
5
+
6
+ interface Props {
7
+ open: boolean;
8
+ onclose: () => void;
9
+ side?: 'left' | 'right';
10
+ labelledby?: string;
11
+ /** Accessible label of the backdrop close button. */
12
+ closeLabel?: string;
13
+ class?: string;
14
+ children: Snippet;
15
+ }
16
+
17
+ let {
18
+ open,
19
+ onclose,
20
+ side = 'right',
21
+ labelledby,
22
+ closeLabel = 'Zavřít',
23
+ class: className = '',
24
+ children
25
+ }: Props = $props();
26
+
27
+ let panel: HTMLElement | undefined = $state();
28
+
29
+ $effect(() => {
30
+ if (!open) return;
31
+ const unlock = lockScroll();
32
+ panel?.focus();
33
+ const onKey = (e: KeyboardEvent) => e.key === 'Escape' && onclose();
34
+ window.addEventListener('keydown', onKey);
35
+ return () => {
36
+ unlock();
37
+ window.removeEventListener('keydown', onKey);
38
+ };
39
+ });
40
+ </script>
41
+
42
+ {#if open}
43
+ <!-- z-scale contract: site header sits at z-1100; overlay primitives (this,
44
+ Modal) go above it at z-[1200]; Toaster tops everything at z-[1500]. -->
45
+ <div use:portal class="fixed inset-0 z-[1200]">
46
+ <button
47
+ type="button"
48
+ aria-label={closeLabel}
49
+ class="absolute inset-0 bg-black/40"
50
+ transition:fade={{ duration: 150 }}
51
+ onclick={onclose}
52
+ ></button>
53
+ <div
54
+ bind:this={panel}
55
+ tabindex="-1"
56
+ role="dialog"
57
+ aria-modal="true"
58
+ aria-labelledby={labelledby}
59
+ class="absolute inset-y-0 {side === 'right'
60
+ ? 'right-0'
61
+ : 'left-0'} bg-surface shadow-interactive flex w-full max-w-md flex-col outline-none {className}"
62
+ transition:fly={{ x: side === 'right' ? 400 : -400, duration: 250 }}
63
+ >
64
+ {@render children()}
65
+ </div>
66
+ </div>
67
+ {/if}
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ open: boolean;
4
+ onclose: () => void;
5
+ side?: 'left' | 'right';
6
+ labelledby?: string;
7
+ /** Accessible label of the backdrop close button. */
8
+ closeLabel?: string;
9
+ class?: string;
10
+ children: Snippet;
11
+ }
12
+ declare const Drawer: import("svelte").Component<Props, {}, "">;
13
+ type Drawer = ReturnType<typeof Drawer>;
14
+ export default Drawer;