@jelinek/ui 0.5.3 → 0.9.1
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/ChapterNav.svelte +1 -1
- package/dist/components/Checkbox.svelte +13 -15
- package/dist/components/Checkbox.svelte.d.ts +1 -0
- package/dist/components/ContentLogo.svelte +18 -13
- package/dist/components/ContentLogo.svelte.d.ts +6 -5
- package/dist/components/Grid.svelte +9 -5
- package/dist/components/Label.svelte +12 -1
- package/dist/components/Label.svelte.d.ts +8 -0
- package/dist/components/PageHero.svelte +36 -2
- package/dist/components/PageHero.svelte.d.ts +9 -0
- package/dist/components/Pager.svelte +5 -2
- package/dist/components/PhotoTile.svelte +4 -2
- package/dist/components/ProductCard.svelte +66 -44
- package/dist/components/ProductCard.svelte.d.ts +17 -4
- package/dist/components/Radio.svelte +12 -8
- package/dist/components/Radio.svelte.d.ts +1 -0
- package/dist/components/Select.svelte +70 -12
- package/dist/components/Select.svelte.d.ts +11 -0
- package/dist/components/SiteFooter.svelte +19 -0
- package/dist/components/SiteFooter.svelte.d.ts +8 -0
- package/dist/components/SiteHeader.svelte +11 -9
- package/dist/components/Slider.svelte +6 -11
- package/dist/components/Switch.svelte +76 -0
- package/dist/components/Switch.svelte.d.ts +12 -0
- package/dist/components/TextField.svelte +61 -10
- package/dist/components/TextField.svelte.d.ts +13 -0
- package/dist/components/Textarea.svelte +92 -0
- package/dist/components/Textarea.svelte.d.ts +23 -0
- package/dist/components/Tile.svelte +12 -11
- package/dist/components/Tile.svelte.d.ts +8 -2
- package/dist/components/blocks/CmsButton.svelte +36 -0
- package/dist/components/blocks/CmsButton.svelte.d.ts +12 -0
- package/dist/components/blocks/CmsColumns.svelte +20 -0
- package/dist/components/blocks/CmsColumns.svelte.d.ts +9 -0
- package/dist/components/blocks/CmsCover.svelte +71 -0
- package/dist/components/blocks/CmsCover.svelte.d.ts +17 -0
- package/dist/components/blocks/CmsHeading.svelte +50 -0
- package/dist/components/blocks/CmsHeading.svelte.d.ts +14 -0
- package/dist/components/blocks/CmsImage.svelte +43 -0
- package/dist/components/blocks/CmsImage.svelte.d.ts +13 -0
- package/dist/components/blocks/CmsMediaText.svelte +64 -0
- package/dist/components/blocks/CmsMediaText.svelte.d.ts +16 -0
- package/dist/components/blocks/CmsText.svelte +53 -0
- package/dist/components/blocks/CmsText.svelte.d.ts +10 -0
- package/dist/components/commerce/Breadcrumbs.svelte +41 -0
- package/dist/components/commerce/Breadcrumbs.svelte.d.ts +11 -0
- package/dist/components/commerce/CartDrawer.svelte +131 -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 +81 -0
- package/dist/components/commerce/Drawer.svelte.d.ts +12 -0
- package/dist/components/commerce/EmptyState.svelte +37 -0
- package/dist/components/commerce/EmptyState.svelte.d.ts +13 -0
- package/dist/components/commerce/FilterChip.svelte +35 -0
- package/dist/components/commerce/FilterChip.svelte.d.ts +10 -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 +194 -0
- package/dist/components/commerce/Gallery.svelte.d.ts +27 -0
- package/dist/components/commerce/ListingToolbar.svelte +62 -0
- package/dist/components/commerce/ListingToolbar.svelte.d.ts +19 -0
- package/dist/components/commerce/Modal.svelte +73 -0
- package/dist/components/commerce/Modal.svelte.d.ts +11 -0
- package/dist/components/commerce/OrderSummary.svelte +85 -0
- package/dist/components/commerce/OrderSummary.svelte.d.ts +25 -0
- package/dist/components/commerce/Pagination.svelte +115 -0
- package/dist/components/commerce/Pagination.svelte.d.ts +14 -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/SearchField.svelte +92 -0
- package/dist/components/commerce/SearchField.svelte.d.ts +15 -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 +13 -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 +38 -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 +26 -0
- package/dist/components/commerce/overlay.js +69 -0
- package/dist/components/commerce/toast.d.ts +26 -0
- package/dist/components/commerce/toast.js +44 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +41 -0
- package/dist/theme/base.css +56 -7
- package/dist/theme/extras.css +63 -0
- package/dist/theme/fonts.css +6 -42
- package/dist/theme/tokens.css +16 -9
- 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 font-secondary font-light">
|
|
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,194 @@
|
|
|
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-secondary 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
|
+
<!-- closeLabel Modal opustil (EAA přestavba pozadí) — lightbox má vlastní
|
|
146
|
+
viditelné zavírací tlačítko, které label používá dál. -->
|
|
147
|
+
<Modal open={lightboxOpen} onclose={closeLightbox}>
|
|
148
|
+
<div
|
|
149
|
+
class="relative flex max-h-[calc(90dvh-2rem)] w-[86vw] max-w-3xl flex-col items-center gap-3 p-4"
|
|
150
|
+
role="group"
|
|
151
|
+
aria-label={counterLabel(lightboxIndex + 1, images.length)}
|
|
152
|
+
ontouchstart={handleTouchStart}
|
|
153
|
+
ontouchend={handleTouchEnd}
|
|
154
|
+
>
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
class="grid {navButtonClass} absolute top-2 right-2 z-10"
|
|
158
|
+
onclick={closeLightbox}
|
|
159
|
+
aria-label={closeLabel}
|
|
160
|
+
>
|
|
161
|
+
<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>
|
|
162
|
+
</button>
|
|
163
|
+
|
|
164
|
+
{#if images[lightboxIndex]}
|
|
165
|
+
<img
|
|
166
|
+
src={images[lightboxIndex].full}
|
|
167
|
+
alt={images[lightboxIndex].alt}
|
|
168
|
+
class="rounded-image max-h-[70dvh] max-w-full object-contain"
|
|
169
|
+
/>
|
|
170
|
+
{/if}
|
|
171
|
+
|
|
172
|
+
<div class="flex items-center gap-4">
|
|
173
|
+
<button
|
|
174
|
+
type="button"
|
|
175
|
+
class="hidden md:grid {navButtonClass}"
|
|
176
|
+
onclick={prev}
|
|
177
|
+
aria-label={prevLabel}
|
|
178
|
+
>
|
|
179
|
+
<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>
|
|
180
|
+
</button>
|
|
181
|
+
<p aria-live="polite" class="text-fg-muted1 min-w-16 text-center tabular-nums">
|
|
182
|
+
{counterLabel(lightboxIndex + 1, images.length)}
|
|
183
|
+
</p>
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
class="hidden md:grid {navButtonClass}"
|
|
187
|
+
onclick={next}
|
|
188
|
+
aria-label={nextLabel}
|
|
189
|
+
>
|
|
190
|
+
<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>
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</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,62 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../../utils/cn.js';
|
|
4
|
+
import Button from '../Button.svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
/** Počet nalezených produktů. Bez hodnoty se počet nevypíše. */
|
|
8
|
+
count?: number;
|
|
9
|
+
/** Tvary slova pro 1 / 2–4 / 5+ — výchozí „produkt/produkty/produktů“. */
|
|
10
|
+
unit?: [string, string, string];
|
|
11
|
+
/** Zavolá se po kliknutí na tlačítko Filtrování (otevření modalu/draweru). */
|
|
12
|
+
onfilter?: () => void;
|
|
13
|
+
/** Počet nasazených filtrů — ukáže se v závorce na tlačítku. */
|
|
14
|
+
filterCount?: number;
|
|
15
|
+
/** Nasazené filtry (FilterChip) — vykreslí se v řádku pod lištou. */
|
|
16
|
+
chips?: Snippet;
|
|
17
|
+
/** Pravá strana lišty, typicky Select s řazením. */
|
|
18
|
+
children?: Snippet;
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
count,
|
|
24
|
+
unit = ['produkt', 'produkty', 'produktů'],
|
|
25
|
+
onfilter,
|
|
26
|
+
filterCount = 0,
|
|
27
|
+
chips,
|
|
28
|
+
children,
|
|
29
|
+
class: className
|
|
30
|
+
}: Props = $props();
|
|
31
|
+
|
|
32
|
+
// Czech plural: 1 / 2–4 / everything else (0, 5+, fractions don't occur
|
|
33
|
+
// for a result count).
|
|
34
|
+
const plural = (n: number) => (n === 1 ? unit[0] : n >= 2 && n <= 4 ? unit[1] : unit[2]);
|
|
35
|
+
|
|
36
|
+
// Layout follows the worked listing in the demo category page
|
|
37
|
+
// (demo/ortopedicke-lamelove-matrace): filter button left, sort right,
|
|
38
|
+
// wrapping on narrow screens. The count is aria-live so a screen-reader
|
|
39
|
+
// user hears the new total when filters change the listing under them.
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<div class={cn('flex flex-wrap items-end justify-between gap-4', className)}>
|
|
43
|
+
<div class="flex flex-wrap items-center gap-4">
|
|
44
|
+
{#if onfilter}
|
|
45
|
+
<Button variant="secondary" color="black" onclick={onfilter}>
|
|
46
|
+
Filtrování{filterCount > 0 ? ` (${filterCount})` : ''}
|
|
47
|
+
</Button>
|
|
48
|
+
{/if}
|
|
49
|
+
{#if count !== undefined}
|
|
50
|
+
<p aria-live="polite" class="font-secondary font-light text-step-down1 text-fg-muted1">
|
|
51
|
+
<strong class="font-medium text-fg [font-variant-numeric:tabular-nums]">{count}</strong>
|
|
52
|
+
{plural(count)}
|
|
53
|
+
</p>
|
|
54
|
+
{/if}
|
|
55
|
+
</div>
|
|
56
|
+
{#if children}
|
|
57
|
+
<div class="flex items-end gap-3">{@render children()}</div>
|
|
58
|
+
{/if}
|
|
59
|
+
</div>
|
|
60
|
+
{#if chips}
|
|
61
|
+
<div class="mt-3 flex flex-wrap gap-2">{@render chips()}</div>
|
|
62
|
+
{/if}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Počet nalezených produktů. Bez hodnoty se počet nevypíše. */
|
|
4
|
+
count?: number;
|
|
5
|
+
/** Tvary slova pro 1 / 2–4 / 5+ — výchozí „produkt/produkty/produktů“. */
|
|
6
|
+
unit?: [string, string, string];
|
|
7
|
+
/** Zavolá se po kliknutí na tlačítko Filtrování (otevření modalu/draweru). */
|
|
8
|
+
onfilter?: () => void;
|
|
9
|
+
/** Počet nasazených filtrů — ukáže se v závorce na tlačítku. */
|
|
10
|
+
filterCount?: number;
|
|
11
|
+
/** Nasazené filtry (FilterChip) — vykreslí se v řádku pod lištou. */
|
|
12
|
+
chips?: Snippet;
|
|
13
|
+
/** Pravá strana lišty, typicky Select s řazením. */
|
|
14
|
+
children?: Snippet;
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
declare const ListingToolbar: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type ListingToolbar = ReturnType<typeof ListingToolbar>;
|
|
19
|
+
export default ListingToolbar;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fade } from 'svelte/transition';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
import { portal, lockScroll, trapFocus, rememberFocus } from './overlay.js';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
open: boolean;
|
|
8
|
+
onclose: () => void;
|
|
9
|
+
labelledby?: string;
|
|
10
|
+
class?: string;
|
|
11
|
+
children: Snippet;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// closeLabel zanikl s přestavbou pozadí na nefokusovatelnou vrstvu
|
|
15
|
+
// (přístupnost EAA) — zavírá Escape, klik vedle, nebo viditelné tlačítko
|
|
16
|
+
// v obsahu. Konzument, který prop dál posílá, nic nerozbije (ignoruje se).
|
|
17
|
+
let { open, onclose, labelledby, class: className = '', children }: Props = $props();
|
|
18
|
+
|
|
19
|
+
let panel: HTMLElement | undefined = $state();
|
|
20
|
+
|
|
21
|
+
$effect(() => {
|
|
22
|
+
if (!open) return;
|
|
23
|
+
// Přístupnost překryvu (EAA): fokus se po zavření vrací na prvek,
|
|
24
|
+
// který overlay otevřel, a Tab cyklí uvnitř panelu (trapFocus).
|
|
25
|
+
const restore = rememberFocus();
|
|
26
|
+
const unlock = lockScroll();
|
|
27
|
+
panel?.focus();
|
|
28
|
+
const untrap = panel ? trapFocus(panel) : () => {};
|
|
29
|
+
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && onclose();
|
|
30
|
+
window.addEventListener('keydown', onKey);
|
|
31
|
+
return () => {
|
|
32
|
+
unlock();
|
|
33
|
+
untrap();
|
|
34
|
+
window.removeEventListener('keydown', onKey);
|
|
35
|
+
restore();
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
{#if open}
|
|
41
|
+
<!-- Škála vrstev (jelinek.css :root): hlavička --z-header (50); překryvy
|
|
42
|
+
(tento, Drawer) nad ní na --z-overlay (1200); toasty --z-toast (1500)
|
|
43
|
+
úplně nahoře. -->
|
|
44
|
+
<!-- SJEDNOCENO S DRAWEREM (majitel, 14. 8. 2026): stejné ztmavení stránky
|
|
45
|
+
o 50 %, stejný neprůhledný bílý panel (--bg-white, v tmavém tmavý)
|
|
46
|
+
s jemnou hranou border-subtle, stejný pohyb na 0,2 s a stejné chování
|
|
47
|
+
— Esc, klik na pozadí, zamčený scroll. Jedna vrstva, dva tvary. -->
|
|
48
|
+
<div use:portal class="fixed inset-0 z-(--z-overlay)">
|
|
49
|
+
<!-- Pozadí je čistě myší zkratka pro zavření — NENÍ v tab pořadí
|
|
50
|
+
(dřív to byl <button> a neviditelné celoobrazovkové tlačítko bylo
|
|
51
|
+
první zastávkou tabulátoru). Klávesnice zavírá Escapem, čtečky
|
|
52
|
+
mají viditelné zavírací tlačítko v obsahu. -->
|
|
53
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
54
|
+
<div
|
|
55
|
+
data-overlay-backdrop
|
|
56
|
+
aria-hidden="true"
|
|
57
|
+
class="absolute inset-0 bg-[var(--overlay-scrim)] [-webkit-backdrop-filter:var(--overlay-filter)] [backdrop-filter:var(--overlay-filter)]"
|
|
58
|
+
transition:fade={{ duration: 200 }}
|
|
59
|
+
onclick={onclose}
|
|
60
|
+
></div>
|
|
61
|
+
<div
|
|
62
|
+
bind:this={panel}
|
|
63
|
+
tabindex="-1"
|
|
64
|
+
role="dialog"
|
|
65
|
+
aria-modal="true"
|
|
66
|
+
aria-labelledby={labelledby}
|
|
67
|
+
class="rounded-card border border-border-subtle bg-surface font-secondary font-light shadow-resting absolute inset-0 m-auto h-fit max-h-[90dvh] w-fit max-w-[92vw] outline-none {className}"
|
|
68
|
+
transition:fade={{ duration: 200 }}
|
|
69
|
+
>
|
|
70
|
+
{@render children()}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
{/if}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onclose: () => void;
|
|
5
|
+
labelledby?: string;
|
|
6
|
+
class?: string;
|
|
7
|
+
children: Snippet;
|
|
8
|
+
}
|
|
9
|
+
declare const Modal: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type Modal = ReturnType<typeof Modal>;
|
|
11
|
+
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 font-secondary font-light 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;
|