@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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { formatMoney } from './money.js';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
threshold: number;
|
|
6
|
+
remaining: number;
|
|
7
|
+
qualified: boolean;
|
|
8
|
+
currency?: string;
|
|
9
|
+
qualifiedLabel?: string;
|
|
10
|
+
/** Builds the „zbývá X do dopravy zdarma“ line from the formatted amount. */
|
|
11
|
+
remainingLabel?: (amount: string) => string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
threshold,
|
|
16
|
+
remaining,
|
|
17
|
+
qualified,
|
|
18
|
+
currency,
|
|
19
|
+
qualifiedLabel = 'Máte dopravu zdarma',
|
|
20
|
+
remainingLabel = (amount) => `Do dopravy zdarma zbývá ${amount}`
|
|
21
|
+
}: Props = $props();
|
|
22
|
+
|
|
23
|
+
// Guards the division below: a misconfigured (or not-yet-loaded) threshold
|
|
24
|
+
// of 0 would otherwise divide by zero. Treated as "already qualified" —
|
|
25
|
+
// nothing more to progress toward.
|
|
26
|
+
let pct = $derived(
|
|
27
|
+
threshold > 0 ? Math.min(100, Math.round(((threshold - remaining) / threshold) * 100)) : 100
|
|
28
|
+
);
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<div class="rounded-card border-border-subtle border p-3">
|
|
32
|
+
<p class="flex items-center gap-1.5 text-(length:--step-down1) font-medium text-fg mb-2">
|
|
33
|
+
<svg
|
|
34
|
+
viewBox="0 0 24 24"
|
|
35
|
+
class="size-4 shrink-0 {qualified ? 'text-(--sem-success-strong)' : 'text-fg-muted1'}"
|
|
36
|
+
fill="none"
|
|
37
|
+
stroke="currentColor"
|
|
38
|
+
stroke-width="2"
|
|
39
|
+
stroke-linecap="round"
|
|
40
|
+
stroke-linejoin="round"
|
|
41
|
+
aria-hidden="true"
|
|
42
|
+
><path d="M14 17h-9v-11h9zM14 9h4l3 3v5h-7" /><circle cx="7.5" cy="17.5" r="1.5" /><circle
|
|
43
|
+
cx="16.5"
|
|
44
|
+
cy="17.5"
|
|
45
|
+
r="1.5"
|
|
46
|
+
/></svg
|
|
47
|
+
>
|
|
48
|
+
{#if qualified}
|
|
49
|
+
<span>{qualifiedLabel}</span>
|
|
50
|
+
{:else}
|
|
51
|
+
<span>{remainingLabel(formatMoney(remaining, currency))}</span>
|
|
52
|
+
{/if}
|
|
53
|
+
</p>
|
|
54
|
+
<div class="rounded-pill bg-surface-gray h-2 w-full overflow-hidden">
|
|
55
|
+
<!-- bg-(--JELINEK-BROWN) rather than an accent alias utility: this token
|
|
56
|
+
is the one a division re-theme (the mattress/magenta swap) actually
|
|
57
|
+
overrides at :root, so the fill tracks the division. -->
|
|
58
|
+
<div
|
|
59
|
+
class="rounded-pill h-full bg-(--JELINEK-BROWN) transition-all duration-500"
|
|
60
|
+
style="width: {qualified ? 100 : pct}%"
|
|
61
|
+
></div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
threshold: number;
|
|
3
|
+
remaining: number;
|
|
4
|
+
qualified: boolean;
|
|
5
|
+
currency?: string;
|
|
6
|
+
qualifiedLabel?: string;
|
|
7
|
+
/** Builds the „zbývá X do dopravy zdarma“ line from the formatted amount. */
|
|
8
|
+
remainingLabel?: (amount: string) => string;
|
|
9
|
+
}
|
|
10
|
+
declare const FreeShippingBar: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type FreeShippingBar = ReturnType<typeof FreeShippingBar>;
|
|
12
|
+
export default FreeShippingBar;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// PDP image mosaic + lightbox. Mechanics (keyboard nav, touch swipe, counter)
|
|
3
|
+
// originate in matrace-fe's Product/Gallery.svelte, rebuilt on the kit's own
|
|
4
|
+
// Modal — Modal already owns Esc/backdrop-click/scroll-lock/focus, so this
|
|
5
|
+
// component only adds prev/next + swipe + counter.
|
|
6
|
+
import type { Snippet } from 'svelte';
|
|
7
|
+
import Modal from './Modal.svelte';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
images: { src: string; full: string; alt: string }[];
|
|
11
|
+
/**
|
|
12
|
+
* Rendered over the hero tile's bottom edge from lg up — the store puts
|
|
13
|
+
* its TrustBadges strip here. A snippet, not a baked-in child: the kit
|
|
14
|
+
* doesn't decide what earns a place on the photo.
|
|
15
|
+
*/
|
|
16
|
+
badge?: Snippet;
|
|
17
|
+
/** Localized texts; Czech defaults. */
|
|
18
|
+
labels?: {
|
|
19
|
+
close?: string;
|
|
20
|
+
previous?: string;
|
|
21
|
+
next?: string;
|
|
22
|
+
/** Lightbox position, e.g. „3 / 8“. */
|
|
23
|
+
counter?: (index: number, total: number) => string;
|
|
24
|
+
/** Overflow tile, e.g. „+4 fotky“. */
|
|
25
|
+
more?: (count: number) => string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let { images, badge, labels }: Props = $props();
|
|
30
|
+
|
|
31
|
+
const closeLabel = $derived(labels?.close ?? 'Zavřít');
|
|
32
|
+
const prevLabel = $derived(labels?.previous ?? 'Předchozí fotka');
|
|
33
|
+
const nextLabel = $derived(labels?.next ?? 'Další fotka');
|
|
34
|
+
const counterLabel = $derived(labels?.counter ?? ((i: number, t: number) => `${i} / ${t}`));
|
|
35
|
+
const moreLabel = $derived(labels?.more ?? ((n: number) => `+${n}`));
|
|
36
|
+
|
|
37
|
+
const VISIBLE_TILES = 4;
|
|
38
|
+
let visibleImages = $derived(images.slice(0, VISIBLE_TILES));
|
|
39
|
+
let hiddenCount = $derived(Math.max(0, images.length - VISIBLE_TILES));
|
|
40
|
+
|
|
41
|
+
let lightboxOpen = $state(false);
|
|
42
|
+
let lightboxIndex = $state(0);
|
|
43
|
+
|
|
44
|
+
function openLightbox(index: number) {
|
|
45
|
+
lightboxIndex = index;
|
|
46
|
+
lightboxOpen = true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function closeLightbox() {
|
|
50
|
+
lightboxOpen = false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function prev() {
|
|
54
|
+
lightboxIndex = (lightboxIndex - 1 + images.length) % images.length;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function next() {
|
|
58
|
+
lightboxIndex = (lightboxIndex + 1) % images.length;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
62
|
+
if (!lightboxOpen) return;
|
|
63
|
+
if (e.key === 'ArrowLeft') prev();
|
|
64
|
+
if (e.key === 'ArrowRight') next();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Swipe support — 50px threshold, same as matrace-fe.
|
|
68
|
+
let touchStartX = 0;
|
|
69
|
+
|
|
70
|
+
function handleTouchStart(e: TouchEvent) {
|
|
71
|
+
touchStartX = e.changedTouches[0].screenX;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function handleTouchEnd(e: TouchEvent) {
|
|
75
|
+
const delta = e.changedTouches[0].screenX - touchStartX;
|
|
76
|
+
const threshold = 50;
|
|
77
|
+
if (delta < -threshold) next();
|
|
78
|
+
if (delta > threshold) prev();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Display utility (grid / hidden md:grid) is applied per-usage below, never
|
|
82
|
+
// here — keeping it out of the shared string avoids two display utilities
|
|
83
|
+
// landing on the same class list.
|
|
84
|
+
const navButtonClass =
|
|
85
|
+
'size-10 place-items-center rounded-pill bg-(--glass-fill) text-fg backdrop-blur-[25px] transition-opacity hover:opacity-80';
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<svelte:window onkeydown={handleKeydown} />
|
|
89
|
+
|
|
90
|
+
{#if images.length > 0}
|
|
91
|
+
<div class="grid grid-cols-2 gap-2.5">
|
|
92
|
+
{#each visibleImages as image, i (image.src)}
|
|
93
|
+
{#if i === 0}
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
class="group rounded-image bg-surface-gray relative col-span-2 aspect-video overflow-hidden"
|
|
97
|
+
onclick={() => openLightbox(0)}
|
|
98
|
+
aria-label={image.alt}
|
|
99
|
+
>
|
|
100
|
+
<img
|
|
101
|
+
src={image.src}
|
|
102
|
+
alt={image.alt}
|
|
103
|
+
loading="eager"
|
|
104
|
+
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
|
|
105
|
+
/>
|
|
106
|
+
{#if badge}
|
|
107
|
+
<div class="pointer-events-none absolute inset-0 hidden lg:block" aria-hidden="true">
|
|
108
|
+
{@render badge()}
|
|
109
|
+
</div>
|
|
110
|
+
{/if}
|
|
111
|
+
</button>
|
|
112
|
+
{:else if i === visibleImages.length - 1 && hiddenCount > 0}
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
class="group rounded-image bg-surface-gray relative aspect-[5/3] overflow-hidden"
|
|
116
|
+
onclick={() => openLightbox(i)}
|
|
117
|
+
aria-label={`${image.alt} — ${moreLabel(hiddenCount)}`}
|
|
118
|
+
>
|
|
119
|
+
<img src={image.src} alt={image.alt} loading="lazy" class="h-full w-full object-cover" />
|
|
120
|
+
<div
|
|
121
|
+
class="text-step1 absolute inset-0 flex items-center justify-center bg-(--glass-fill) font-medium text-fg backdrop-blur-[25px]"
|
|
122
|
+
>
|
|
123
|
+
{moreLabel(hiddenCount)}
|
|
124
|
+
</div>
|
|
125
|
+
</button>
|
|
126
|
+
{:else}
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
class="group rounded-image bg-surface-gray relative aspect-[5/3] overflow-hidden"
|
|
130
|
+
onclick={() => openLightbox(i)}
|
|
131
|
+
aria-label={image.alt}
|
|
132
|
+
>
|
|
133
|
+
<img
|
|
134
|
+
src={image.src}
|
|
135
|
+
alt={image.alt}
|
|
136
|
+
loading="lazy"
|
|
137
|
+
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
|
|
138
|
+
/>
|
|
139
|
+
</button>
|
|
140
|
+
{/if}
|
|
141
|
+
{/each}
|
|
142
|
+
</div>
|
|
143
|
+
{/if}
|
|
144
|
+
|
|
145
|
+
<Modal open={lightboxOpen} onclose={closeLightbox} {closeLabel}>
|
|
146
|
+
<div
|
|
147
|
+
class="relative flex max-h-[calc(90dvh-2rem)] w-[86vw] max-w-3xl flex-col items-center gap-3 p-4"
|
|
148
|
+
role="group"
|
|
149
|
+
aria-label={counterLabel(lightboxIndex + 1, images.length)}
|
|
150
|
+
ontouchstart={handleTouchStart}
|
|
151
|
+
ontouchend={handleTouchEnd}
|
|
152
|
+
>
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
class="grid {navButtonClass} absolute top-2 right-2 z-10"
|
|
156
|
+
onclick={closeLightbox}
|
|
157
|
+
aria-label={closeLabel}
|
|
158
|
+
>
|
|
159
|
+
<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>
|
|
160
|
+
</button>
|
|
161
|
+
|
|
162
|
+
{#if images[lightboxIndex]}
|
|
163
|
+
<img
|
|
164
|
+
src={images[lightboxIndex].full}
|
|
165
|
+
alt={images[lightboxIndex].alt}
|
|
166
|
+
class="rounded-image max-h-[70dvh] max-w-full object-contain"
|
|
167
|
+
/>
|
|
168
|
+
{/if}
|
|
169
|
+
|
|
170
|
+
<div class="flex items-center gap-4">
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
class="hidden md:grid {navButtonClass}"
|
|
174
|
+
onclick={prev}
|
|
175
|
+
aria-label={prevLabel}
|
|
176
|
+
>
|
|
177
|
+
<svg viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6" /></svg>
|
|
178
|
+
</button>
|
|
179
|
+
<p aria-live="polite" class="text-fg-muted1 min-w-16 text-center tabular-nums">
|
|
180
|
+
{counterLabel(lightboxIndex + 1, images.length)}
|
|
181
|
+
</p>
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
class="hidden md:grid {navButtonClass}"
|
|
185
|
+
onclick={next}
|
|
186
|
+
aria-label={nextLabel}
|
|
187
|
+
>
|
|
188
|
+
<svg viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6" /></svg>
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</Modal>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
images: {
|
|
4
|
+
src: string;
|
|
5
|
+
full: string;
|
|
6
|
+
alt: string;
|
|
7
|
+
}[];
|
|
8
|
+
/**
|
|
9
|
+
* Rendered over the hero tile's bottom edge from lg up — the store puts
|
|
10
|
+
* its TrustBadges strip here. A snippet, not a baked-in child: the kit
|
|
11
|
+
* doesn't decide what earns a place on the photo.
|
|
12
|
+
*/
|
|
13
|
+
badge?: Snippet;
|
|
14
|
+
/** Localized texts; Czech defaults. */
|
|
15
|
+
labels?: {
|
|
16
|
+
close?: string;
|
|
17
|
+
previous?: string;
|
|
18
|
+
next?: string;
|
|
19
|
+
/** Lightbox position, e.g. „3 / 8“. */
|
|
20
|
+
counter?: (index: number, total: number) => string;
|
|
21
|
+
/** Overflow tile, e.g. „+4 fotky“. */
|
|
22
|
+
more?: (count: number) => string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
declare const Gallery: import("svelte").Component<Props, {}, "">;
|
|
26
|
+
type Gallery = ReturnType<typeof Gallery>;
|
|
27
|
+
export default Gallery;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fade } 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
|
+
labelledby?: string;
|
|
10
|
+
/** Accessible label of the backdrop close button. */
|
|
11
|
+
closeLabel?: string;
|
|
12
|
+
class?: string;
|
|
13
|
+
children: Snippet;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
open,
|
|
18
|
+
onclose,
|
|
19
|
+
labelledby,
|
|
20
|
+
closeLabel = 'Zavřít',
|
|
21
|
+
class: className = '',
|
|
22
|
+
children
|
|
23
|
+
}: Props = $props();
|
|
24
|
+
|
|
25
|
+
let panel: HTMLElement | undefined = $state();
|
|
26
|
+
|
|
27
|
+
$effect(() => {
|
|
28
|
+
if (!open) return;
|
|
29
|
+
const unlock = lockScroll();
|
|
30
|
+
panel?.focus();
|
|
31
|
+
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && onclose();
|
|
32
|
+
window.addEventListener('keydown', onKey);
|
|
33
|
+
return () => {
|
|
34
|
+
unlock();
|
|
35
|
+
window.removeEventListener('keydown', onKey);
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
{#if open}
|
|
41
|
+
<!-- z-scale contract: site header sits at z-1100; overlay primitives (this,
|
|
42
|
+
Drawer) go above it at z-[1200]; Toaster tops everything at z-[1500]. -->
|
|
43
|
+
<div use:portal class="fixed inset-0 z-[1200]">
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
aria-label={closeLabel}
|
|
47
|
+
class="absolute inset-0 bg-black/40"
|
|
48
|
+
transition:fade={{ duration: 150 }}
|
|
49
|
+
onclick={onclose}
|
|
50
|
+
></button>
|
|
51
|
+
<div
|
|
52
|
+
bind:this={panel}
|
|
53
|
+
tabindex="-1"
|
|
54
|
+
role="dialog"
|
|
55
|
+
aria-modal="true"
|
|
56
|
+
aria-labelledby={labelledby}
|
|
57
|
+
class="rounded-card bg-surface shadow-interactive absolute inset-0 m-auto h-fit max-h-[90dvh] w-fit max-w-[92vw] outline-none {className}"
|
|
58
|
+
transition:fade={{ duration: 200 }}
|
|
59
|
+
>
|
|
60
|
+
{@render children()}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
{/if}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onclose: () => void;
|
|
5
|
+
labelledby?: string;
|
|
6
|
+
/** Accessible label of the backdrop close button. */
|
|
7
|
+
closeLabel?: string;
|
|
8
|
+
class?: string;
|
|
9
|
+
children: Snippet;
|
|
10
|
+
}
|
|
11
|
+
declare const Modal: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type Modal = ReturnType<typeof Modal>;
|
|
13
|
+
export default Modal;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { formatMoney } from './money.js';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
currency?: string;
|
|
6
|
+
itemsTotal: number;
|
|
7
|
+
discount?: number;
|
|
8
|
+
shipping?: number;
|
|
9
|
+
/** Extra fees total (dobírka, výnos…); 0 hides the row. */
|
|
10
|
+
surcharges?: number;
|
|
11
|
+
grand: number;
|
|
12
|
+
/** Deposit due now; 0 hides the highlighted row. */
|
|
13
|
+
deposit?: number;
|
|
14
|
+
shippingFree?: boolean;
|
|
15
|
+
labels?: {
|
|
16
|
+
itemsTotal?: string;
|
|
17
|
+
discount?: string;
|
|
18
|
+
shipping?: string;
|
|
19
|
+
free?: string;
|
|
20
|
+
surcharges?: string;
|
|
21
|
+
grandTotal?: string;
|
|
22
|
+
depositDue?: string;
|
|
23
|
+
vatIncluded?: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let {
|
|
28
|
+
currency,
|
|
29
|
+
itemsTotal,
|
|
30
|
+
discount = 0,
|
|
31
|
+
shipping = 0,
|
|
32
|
+
surcharges = 0,
|
|
33
|
+
grand,
|
|
34
|
+
deposit = 0,
|
|
35
|
+
shippingFree = false,
|
|
36
|
+
labels
|
|
37
|
+
}: Props = $props();
|
|
38
|
+
|
|
39
|
+
const l = $derived({
|
|
40
|
+
itemsTotal: labels?.itemsTotal ?? 'Zboží celkem',
|
|
41
|
+
discount: labels?.discount ?? 'Sleva',
|
|
42
|
+
shipping: labels?.shipping ?? 'Doprava',
|
|
43
|
+
free: labels?.free ?? 'Zdarma',
|
|
44
|
+
surcharges: labels?.surcharges ?? 'Příplatky',
|
|
45
|
+
grandTotal: labels?.grandTotal ?? 'Celkem',
|
|
46
|
+
depositDue: labels?.depositDue ?? 'Záloha nyní',
|
|
47
|
+
vatIncluded: labels?.vatIncluded ?? 'Ceny včetně DPH'
|
|
48
|
+
});
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<div class="space-y-2 text-(length:--step-down1)">
|
|
52
|
+
<div class="flex justify-between">
|
|
53
|
+
<span class="text-fg-muted1">{l.itemsTotal}</span>
|
|
54
|
+
<span class="text-fg">{formatMoney(itemsTotal, currency)}</span>
|
|
55
|
+
</div>
|
|
56
|
+
{#if discount > 0}
|
|
57
|
+
<div class="text-(--color-accent-text) flex justify-between">
|
|
58
|
+
<span>{l.discount}</span>
|
|
59
|
+
<span>−{formatMoney(discount, currency)}</span>
|
|
60
|
+
</div>
|
|
61
|
+
{/if}
|
|
62
|
+
<div class="flex justify-between">
|
|
63
|
+
<span class="text-fg-muted1">{l.shipping}</span>
|
|
64
|
+
<span class="text-fg">
|
|
65
|
+
{shippingFree || shipping === 0 ? l.free : formatMoney(shipping, currency)}
|
|
66
|
+
</span>
|
|
67
|
+
</div>
|
|
68
|
+
{#if surcharges > 0}
|
|
69
|
+
<div class="flex justify-between">
|
|
70
|
+
<span class="text-fg-muted1">{l.surcharges}</span>
|
|
71
|
+
<span class="text-fg">{formatMoney(surcharges, currency)}</span>
|
|
72
|
+
</div>
|
|
73
|
+
{/if}
|
|
74
|
+
<div class="border-border-subtle flex items-baseline justify-between border-t pt-2">
|
|
75
|
+
<span class="text-(length:--step0) font-bold text-fg">{l.grandTotal}</span>
|
|
76
|
+
<span class="text-(length:--step4) font-bold text-fg">{formatMoney(grand, currency)}</span>
|
|
77
|
+
</div>
|
|
78
|
+
{#if deposit > 0}
|
|
79
|
+
<div class="rounded-card bg-sem-warning flex justify-between px-2.5 py-1.5 text-fg">
|
|
80
|
+
<span>{l.depositDue}</span>
|
|
81
|
+
<span class="font-bold">{formatMoney(deposit, currency)}</span>
|
|
82
|
+
</div>
|
|
83
|
+
{/if}
|
|
84
|
+
<p class="text-fg-muted2 pt-1 text-(length:--step-down2)">{l.vatIncluded}</p>
|
|
85
|
+
</div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
currency?: string;
|
|
3
|
+
itemsTotal: number;
|
|
4
|
+
discount?: number;
|
|
5
|
+
shipping?: number;
|
|
6
|
+
/** Extra fees total (dobírka, výnos…); 0 hides the row. */
|
|
7
|
+
surcharges?: number;
|
|
8
|
+
grand: number;
|
|
9
|
+
/** Deposit due now; 0 hides the highlighted row. */
|
|
10
|
+
deposit?: number;
|
|
11
|
+
shippingFree?: boolean;
|
|
12
|
+
labels?: {
|
|
13
|
+
itemsTotal?: string;
|
|
14
|
+
discount?: string;
|
|
15
|
+
shipping?: string;
|
|
16
|
+
free?: string;
|
|
17
|
+
surcharges?: string;
|
|
18
|
+
grandTotal?: string;
|
|
19
|
+
depositDue?: string;
|
|
20
|
+
vatIncluded?: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
declare const OrderSummary: import("svelte").Component<Props, {}, "">;
|
|
24
|
+
type OrderSummary = ReturnType<typeof OrderSummary>;
|
|
25
|
+
export default OrderSummary;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { formatPrice } from './money.js';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
price: number;
|
|
6
|
+
/** Struck-through original — renders only when higher than `price`. */
|
|
7
|
+
originalPrice?: number;
|
|
8
|
+
/** Prefix the price with the „od“ qualifier (configurable products). */
|
|
9
|
+
from?: boolean;
|
|
10
|
+
size?: 'sm' | 'md' | 'lg';
|
|
11
|
+
currency?: string;
|
|
12
|
+
/** Text of the `from` qualifier. */
|
|
13
|
+
fromLabel?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
price,
|
|
18
|
+
originalPrice,
|
|
19
|
+
from = false,
|
|
20
|
+
size = 'md',
|
|
21
|
+
currency,
|
|
22
|
+
fromLabel = 'od'
|
|
23
|
+
}: Props = $props();
|
|
24
|
+
|
|
25
|
+
const sizes = {
|
|
26
|
+
sm: 'text-(length:--step0)',
|
|
27
|
+
md: 'text-(length:--step2)',
|
|
28
|
+
lg: 'text-(length:--step4)'
|
|
29
|
+
};
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<span class="inline-flex items-baseline gap-2 font-bold {sizes[size]}">
|
|
33
|
+
{#if from}<span
|
|
34
|
+
class="text-fg-muted1 text-(length:--step-down2) font-normal tracking-wide uppercase"
|
|
35
|
+
>{fromLabel}</span
|
|
36
|
+
>{/if}
|
|
37
|
+
<span>{formatPrice(price, currency)}</span>
|
|
38
|
+
{#if originalPrice && originalPrice > price}
|
|
39
|
+
<s class="text-fg-muted2 text-[0.7em] font-normal">{formatPrice(originalPrice, currency)}</s>
|
|
40
|
+
{/if}
|
|
41
|
+
</span>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
price: number;
|
|
3
|
+
/** Struck-through original — renders only when higher than `price`. */
|
|
4
|
+
originalPrice?: number;
|
|
5
|
+
/** Prefix the price with the „od“ qualifier (configurable products). */
|
|
6
|
+
from?: boolean;
|
|
7
|
+
size?: 'sm' | 'md' | 'lg';
|
|
8
|
+
currency?: string;
|
|
9
|
+
/** Text of the `from` qualifier. */
|
|
10
|
+
fromLabel?: string;
|
|
11
|
+
}
|
|
12
|
+
declare const PriceDisplay: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type PriceDisplay = ReturnType<typeof PriceDisplay>;
|
|
14
|
+
export default PriceDisplay;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Name/price/stock/CTA block for the PDP. Two modes, mirroring matrace-fe's
|
|
3
|
+
// own Product/ProductHeader.svelte precedent: `compact` (rendered once,
|
|
4
|
+
// mobile-only, above the sticky gallery) shows identity + price/stock at a
|
|
5
|
+
// smaller size, but no CTA — the buy button stays desktop-only (the full
|
|
6
|
+
// header further down the page) and mobile's own sticky bar, both reachable
|
|
7
|
+
// without duplicating the primary action here.
|
|
8
|
+
import Button from '../Button.svelte';
|
|
9
|
+
import PriceDisplay from './PriceDisplay.svelte';
|
|
10
|
+
import RatingStars from './RatingStars.svelte';
|
|
11
|
+
import StockBadge from './StockBadge.svelte';
|
|
12
|
+
import { formatPrice } from './money.js';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
name: string;
|
|
16
|
+
subtitle?: string;
|
|
17
|
+
price: number;
|
|
18
|
+
stock: 'in-stock' | 'on-order' | 'sold-out' | 'lead';
|
|
19
|
+
/** Localized/lead-time stock text — see StockBadge. */
|
|
20
|
+
stockLabel?: string;
|
|
21
|
+
rating?: { value: number; count: number };
|
|
22
|
+
compact?: boolean;
|
|
23
|
+
onaddtocart: () => void;
|
|
24
|
+
busy?: boolean;
|
|
25
|
+
/** CTA text; the formatted price is appended after an em-dash. */
|
|
26
|
+
addLabel?: string;
|
|
27
|
+
currency?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
name,
|
|
32
|
+
subtitle,
|
|
33
|
+
price,
|
|
34
|
+
stock,
|
|
35
|
+
stockLabel,
|
|
36
|
+
rating,
|
|
37
|
+
compact = false,
|
|
38
|
+
onaddtocart,
|
|
39
|
+
busy = false,
|
|
40
|
+
addLabel = 'Přidat do košíku',
|
|
41
|
+
currency
|
|
42
|
+
}: Props = $props();
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<div class="flex flex-col gap-3">
|
|
46
|
+
<h1 class="text-(length:--step5) leading-tight font-bold">{name}</h1>
|
|
47
|
+
{#if subtitle}<p class="text-fg-muted1 text-(length:--step1)">{subtitle}</p>{/if}
|
|
48
|
+
{#if rating && rating.count > 0}
|
|
49
|
+
<RatingStars rating={rating.value} count={rating.count} />
|
|
50
|
+
{/if}
|
|
51
|
+
|
|
52
|
+
<div class="mt-1 flex flex-wrap items-center gap-3">
|
|
53
|
+
<PriceDisplay {price} {currency} size={compact ? 'md' : 'lg'} />
|
|
54
|
+
<StockBadge state={stock} label={stockLabel} {compact} />
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
{#if !compact}
|
|
58
|
+
<Button color="brown" fullWidth onclick={onaddtocart} disabled={busy} class="mt-1">
|
|
59
|
+
{addLabel} — {formatPrice(price, currency)}
|
|
60
|
+
</Button>
|
|
61
|
+
{/if}
|
|
62
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
name: string;
|
|
3
|
+
subtitle?: string;
|
|
4
|
+
price: number;
|
|
5
|
+
stock: 'in-stock' | 'on-order' | 'sold-out' | 'lead';
|
|
6
|
+
/** Localized/lead-time stock text — see StockBadge. */
|
|
7
|
+
stockLabel?: string;
|
|
8
|
+
rating?: {
|
|
9
|
+
value: number;
|
|
10
|
+
count: number;
|
|
11
|
+
};
|
|
12
|
+
compact?: boolean;
|
|
13
|
+
onaddtocart: () => void;
|
|
14
|
+
busy?: boolean;
|
|
15
|
+
/** CTA text; the formatted price is appended after an em-dash. */
|
|
16
|
+
addLabel?: string;
|
|
17
|
+
currency?: string;
|
|
18
|
+
}
|
|
19
|
+
declare const ProductHeader: import("svelte").Component<Props, {}, "">;
|
|
20
|
+
type ProductHeader = ReturnType<typeof ProductHeader>;
|
|
21
|
+
export default ProductHeader;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Condensed name/price/stock/CTA bar, shown once the full ProductHeader
|
|
3
|
+
// scrolls out of view (the page wires the IntersectionObserver and passes
|
|
4
|
+
// `visible`). Purely presentational: sits on top of the shared StickyBar
|
|
5
|
+
// primitive, which owns fixed positioning under the kit header and the
|
|
6
|
+
// glass chrome.
|
|
7
|
+
import Button from '../Button.svelte';
|
|
8
|
+
import PriceDisplay from './PriceDisplay.svelte';
|
|
9
|
+
import StickyBar from './StickyBar.svelte';
|
|
10
|
+
import StockBadge from './StockBadge.svelte';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
name: string;
|
|
14
|
+
price: number;
|
|
15
|
+
stock: 'in-stock' | 'on-order' | 'sold-out' | 'lead';
|
|
16
|
+
stockLabel?: string;
|
|
17
|
+
visible: boolean;
|
|
18
|
+
onaddtocart: () => void;
|
|
19
|
+
busy?: boolean;
|
|
20
|
+
addLabel?: string;
|
|
21
|
+
currency?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
name,
|
|
26
|
+
price,
|
|
27
|
+
stock,
|
|
28
|
+
stockLabel,
|
|
29
|
+
visible,
|
|
30
|
+
onaddtocart,
|
|
31
|
+
busy = false,
|
|
32
|
+
addLabel = 'Přidat do košíku',
|
|
33
|
+
currency
|
|
34
|
+
}: Props = $props();
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<StickyBar {visible}>
|
|
38
|
+
<p class="min-w-0 flex-1 truncate font-bold">{name}</p>
|
|
39
|
+
<StockBadge state={stock} label={stockLabel} compact />
|
|
40
|
+
<PriceDisplay {price} {currency} size="md" />
|
|
41
|
+
<Button color="brown" onclick={onaddtocart} disabled={busy}>{addLabel}</Button>
|
|
42
|
+
</StickyBar>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
name: string;
|
|
3
|
+
price: number;
|
|
4
|
+
stock: 'in-stock' | 'on-order' | 'sold-out' | 'lead';
|
|
5
|
+
stockLabel?: string;
|
|
6
|
+
visible: boolean;
|
|
7
|
+
onaddtocart: () => void;
|
|
8
|
+
busy?: boolean;
|
|
9
|
+
addLabel?: string;
|
|
10
|
+
currency?: string;
|
|
11
|
+
}
|
|
12
|
+
declare const ProductStickyBar: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type ProductStickyBar = ReturnType<typeof ProductStickyBar>;
|
|
14
|
+
export default ProductStickyBar;
|