@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.
- package/dist/components/OptionTile.svelte +5 -2
- package/dist/components/PageHero.svelte +37 -2
- package/dist/components/PageHero.svelte.d.ts +9 -0
- package/dist/components/commerce/Breadcrumbs.svelte +24 -0
- package/dist/components/commerce/Breadcrumbs.svelte.d.ts +11 -0
- package/dist/components/commerce/CartDrawer.svelte +128 -0
- package/dist/components/commerce/CartDrawer.svelte.d.ts +37 -0
- package/dist/components/commerce/CartLineItem.svelte +67 -0
- package/dist/components/commerce/CartLineItem.svelte.d.ts +14 -0
- package/dist/components/commerce/CheckoutProgress.svelte +49 -0
- package/dist/components/commerce/CheckoutProgress.svelte.d.ts +11 -0
- package/dist/components/commerce/ConfigSection.svelte +24 -0
- package/dist/components/commerce/ConfigSection.svelte.d.ts +11 -0
- package/dist/components/commerce/DimensionSlider.svelte +86 -0
- package/dist/components/commerce/DimensionSlider.svelte.d.ts +18 -0
- package/dist/components/commerce/Drawer.svelte +67 -0
- package/dist/components/commerce/Drawer.svelte.d.ts +14 -0
- package/dist/components/commerce/FreeShippingBar.svelte +63 -0
- package/dist/components/commerce/FreeShippingBar.svelte.d.ts +12 -0
- package/dist/components/commerce/Gallery.svelte +192 -0
- package/dist/components/commerce/Gallery.svelte.d.ts +27 -0
- package/dist/components/commerce/Modal.svelte +63 -0
- package/dist/components/commerce/Modal.svelte.d.ts +13 -0
- package/dist/components/commerce/OrderSummary.svelte +85 -0
- package/dist/components/commerce/OrderSummary.svelte.d.ts +25 -0
- package/dist/components/commerce/PriceDisplay.svelte +41 -0
- package/dist/components/commerce/PriceDisplay.svelte.d.ts +14 -0
- package/dist/components/commerce/ProductHeader.svelte +62 -0
- package/dist/components/commerce/ProductHeader.svelte.d.ts +21 -0
- package/dist/components/commerce/ProductStickyBar.svelte +42 -0
- package/dist/components/commerce/ProductStickyBar.svelte.d.ts +14 -0
- package/dist/components/commerce/ProductTabs.svelte +87 -0
- package/dist/components/commerce/ProductTabs.svelte.d.ts +18 -0
- package/dist/components/commerce/QuantityStepper.svelte +44 -0
- package/dist/components/commerce/QuantityStepper.svelte.d.ts +13 -0
- package/dist/components/commerce/RatingStars.svelte +39 -0
- package/dist/components/commerce/RatingStars.svelte.d.ts +9 -0
- package/dist/components/commerce/SideToggle.svelte +79 -0
- package/dist/components/commerce/SideToggle.svelte.d.ts +13 -0
- package/dist/components/commerce/Skeleton.svelte +10 -0
- package/dist/components/commerce/Skeleton.svelte.d.ts +7 -0
- package/dist/components/commerce/StickyBar.svelte +27 -0
- package/dist/components/commerce/StickyBar.svelte.d.ts +8 -0
- package/dist/components/commerce/StockBadge.svelte +45 -0
- package/dist/components/commerce/StockBadge.svelte.d.ts +9 -0
- package/dist/components/commerce/Toaster.svelte +34 -0
- package/dist/components/commerce/Toaster.svelte.d.ts +7 -0
- package/dist/components/commerce/TrustBadges.svelte +26 -0
- package/dist/components/commerce/TrustBadges.svelte.d.ts +16 -0
- package/dist/components/commerce/cart-types.d.ts +12 -0
- package/dist/components/commerce/cart-types.js +1 -0
- package/dist/components/commerce/money.d.ts +14 -0
- package/dist/components/commerce/money.js +26 -0
- package/dist/components/commerce/overlay.d.ts +12 -0
- package/dist/components/commerce/overlay.js +20 -0
- package/dist/components/commerce/toast.d.ts +26 -0
- package/dist/components/commerce/toast.js +44 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +27 -0
- package/dist/theme/base.css +34 -7
- package/dist/theme/extras.css +19 -0
- 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
|
-
|
|
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;
|