@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,115 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../utils/cn.js';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
/** Aktuální stránka (od 1). */
|
|
6
|
+
page: number;
|
|
7
|
+
/** Celkový počet stránek. */
|
|
8
|
+
count: number;
|
|
9
|
+
/** Zavolá se s číslem cílové stránky. */
|
|
10
|
+
onchange?: (page: number) => void;
|
|
11
|
+
/** Když je zadané, čísla se vykreslí jako odkazy (lepší pro SEO výpisů). */
|
|
12
|
+
href?: (page: number) => string;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { page, count, onchange, href, class: className }: Props = $props();
|
|
17
|
+
|
|
18
|
+
// Window: always page 1 and the last page, the current page with one
|
|
19
|
+
// neighbour each side, ellipsis for the gaps. Seven slots at most, so the
|
|
20
|
+
// row never wraps awkwardly on a phone.
|
|
21
|
+
const items = $derived.by((): Array<number | '…'> => {
|
|
22
|
+
if (count <= 7) return Array.from({ length: count }, (_, i) => i + 1);
|
|
23
|
+
const middle = [page - 1, page, page + 1].filter((n) => n > 1 && n < count);
|
|
24
|
+
const out: Array<number | '…'> = [1];
|
|
25
|
+
if ((middle[0] ?? count) > 2) out.push('…');
|
|
26
|
+
out.push(...middle);
|
|
27
|
+
if ((middle[middle.length - 1] ?? 1) < count - 1) out.push('…');
|
|
28
|
+
out.push(count);
|
|
29
|
+
return out;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// 44px targets like the demo's gallery arrows; resting border-fg-muted2 /
|
|
33
|
+
// hover black follows the neutral-emphasis rule (colour is for errors).
|
|
34
|
+
// The current page is a filled JELINEK-BLACK square — same emphasis
|
|
35
|
+
// language as the active form frame, flips to light in dark mode for free.
|
|
36
|
+
const CELL =
|
|
37
|
+
'inline-flex h-11 min-w-11 items-center justify-center rounded-button border px-1 ' +
|
|
38
|
+
'font-secondary text-[0.98rem] text-fg [font-variant-numeric:tabular-nums] ' +
|
|
39
|
+
'transition-colors duration-[var(--dur-base)] ease-[var(--ease)]';
|
|
40
|
+
const IDLE = 'cursor-pointer border-fg-muted2 bg-surface hover:border-jelinek-black';
|
|
41
|
+
const CURRENT = 'border-jelinek-black bg-jelinek-black font-bold text-fg-inverse';
|
|
42
|
+
const ARROW_DISABLED = 'cursor-not-allowed border-border-subtle text-fg-muted2';
|
|
43
|
+
|
|
44
|
+
const go = (n: number) => {
|
|
45
|
+
if (n >= 1 && n <= count && n !== page) onchange?.(n);
|
|
46
|
+
};
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
{#snippet chevron(dir: 'prev' | 'next')}
|
|
50
|
+
<svg
|
|
51
|
+
viewBox="0 0 24 24"
|
|
52
|
+
class="size-4"
|
|
53
|
+
fill="none"
|
|
54
|
+
stroke="currentColor"
|
|
55
|
+
stroke-width="2"
|
|
56
|
+
stroke-linecap="round"
|
|
57
|
+
stroke-linejoin="round"
|
|
58
|
+
aria-hidden="true"
|
|
59
|
+
>
|
|
60
|
+
<path d={dir === 'prev' ? 'm15 18-6-6 6-6' : 'm9 18 6-6-6-6'} />
|
|
61
|
+
</svg>
|
|
62
|
+
{/snippet}
|
|
63
|
+
|
|
64
|
+
<nav aria-label="Stránkování" class={cn('flex flex-wrap items-center gap-2', className)}>
|
|
65
|
+
{#if href && page > 1}
|
|
66
|
+
<a href={href(page - 1)} aria-label="Předchozí stránka" class="{CELL} {IDLE}">
|
|
67
|
+
{@render chevron('prev')}
|
|
68
|
+
</a>
|
|
69
|
+
{:else}
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
aria-label="Předchozí stránka"
|
|
73
|
+
disabled={page <= 1}
|
|
74
|
+
class={cn(CELL, page <= 1 ? ARROW_DISABLED : IDLE)}
|
|
75
|
+
onclick={() => go(page - 1)}
|
|
76
|
+
>
|
|
77
|
+
{@render chevron('prev')}
|
|
78
|
+
</button>
|
|
79
|
+
{/if}
|
|
80
|
+
|
|
81
|
+
{#each items as item, i (typeof item === 'number' ? item : `gap-${i}`)}
|
|
82
|
+
{#if item === '…'}
|
|
83
|
+
<span aria-hidden="true" class="px-1 font-secondary text-fg-muted1">…</span>
|
|
84
|
+
{:else if item === page}
|
|
85
|
+
<!-- The current page is not a control — nothing to click, just where
|
|
86
|
+
you are. aria-current still names it for assistive tech. -->
|
|
87
|
+
<span aria-current="page" class="{CELL} {CURRENT}">{item}</span>
|
|
88
|
+
{:else if href}
|
|
89
|
+
<a href={href(item)} aria-label="Stránka {item}" class="{CELL} {IDLE}">{item}</a>
|
|
90
|
+
{:else}
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
aria-label="Stránka {item}"
|
|
94
|
+
class="{CELL} {IDLE}"
|
|
95
|
+
onclick={() => go(item)}>{item}</button
|
|
96
|
+
>
|
|
97
|
+
{/if}
|
|
98
|
+
{/each}
|
|
99
|
+
|
|
100
|
+
{#if href && page < count}
|
|
101
|
+
<a href={href(page + 1)} aria-label="Další stránka" class="{CELL} {IDLE}">
|
|
102
|
+
{@render chevron('next')}
|
|
103
|
+
</a>
|
|
104
|
+
{:else}
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
aria-label="Další stránka"
|
|
108
|
+
disabled={page >= count}
|
|
109
|
+
class={cn(CELL, page >= count ? ARROW_DISABLED : IDLE)}
|
|
110
|
+
onclick={() => go(page + 1)}
|
|
111
|
+
>
|
|
112
|
+
{@render chevron('next')}
|
|
113
|
+
</button>
|
|
114
|
+
{/if}
|
|
115
|
+
</nav>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** Aktuální stránka (od 1). */
|
|
3
|
+
page: number;
|
|
4
|
+
/** Celkový počet stránek. */
|
|
5
|
+
count: number;
|
|
6
|
+
/** Zavolá se s číslem cílové stránky. */
|
|
7
|
+
onchange?: (page: number) => void;
|
|
8
|
+
/** Když je zadané, čísla se vykreslí jako odkazy (lepší pro SEO výpisů). */
|
|
9
|
+
href?: (page: number) => string;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
declare const Pagination: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type Pagination = ReturnType<typeof Pagination>;
|
|
14
|
+
export default Pagination;
|
|
@@ -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-secondary font-bold {sizes[size]}">
|
|
33
|
+
{#if from}<span
|
|
34
|
+
class="text-fg-muted1 text-(length:--step-down2) font-light 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-light">{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 font-secondary font-light">
|
|
46
|
+
<h1 class="text-(length:--step6) 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;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Description / parameters / benefits switcher for the PDP, on kit Tabs/Tab.
|
|
3
|
+
// Each tab renders only when its content exists — a product with no benefits
|
|
4
|
+
// never shows an empty „Benefity“ tab, and the whole component renders
|
|
5
|
+
// nothing when all three are empty.
|
|
6
|
+
import Tab from '../Tab.svelte';
|
|
7
|
+
import Tabs from '../Tabs.svelte';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
/** Sanitized HTML (sanitize server-side!) — never author-controlled raw input. */
|
|
11
|
+
description?: string;
|
|
12
|
+
parameters?: { label: string; value: string }[];
|
|
13
|
+
benefits?: string[];
|
|
14
|
+
/** Localized tab captions; Czech defaults. */
|
|
15
|
+
labels?: { description?: string; parameters?: string; benefits?: string };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let { description, parameters, benefits, labels }: Props = $props();
|
|
19
|
+
|
|
20
|
+
type TabKey = 'description' | 'parameters' | 'benefits';
|
|
21
|
+
|
|
22
|
+
let availableTabs = $derived(
|
|
23
|
+
(
|
|
24
|
+
[
|
|
25
|
+
description ? 'description' : null,
|
|
26
|
+
parameters && parameters.length > 0 ? 'parameters' : null,
|
|
27
|
+
benefits && benefits.length > 0 ? 'benefits' : null
|
|
28
|
+
] as (TabKey | null)[]
|
|
29
|
+
).filter((tab): tab is TabKey => tab !== null)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Tabs' `value` is a plain bindable string (not typed to our TabKey union),
|
|
33
|
+
// so the raw binding target stays a string and every read narrows it
|
|
34
|
+
// against availableTabs.
|
|
35
|
+
let active = $state('');
|
|
36
|
+
|
|
37
|
+
// Keep the active tab valid as content availability changes (e.g. the
|
|
38
|
+
// description loads after the initial render) — default to the first
|
|
39
|
+
// available tab whenever the current pick is gone.
|
|
40
|
+
$effect(() => {
|
|
41
|
+
if (!availableTabs.includes(active as TabKey)) {
|
|
42
|
+
active = availableTabs[0] ?? '';
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const caption = $derived({
|
|
47
|
+
description: labels?.description ?? 'Popis',
|
|
48
|
+
parameters: labels?.parameters ?? 'Parametry',
|
|
49
|
+
benefits: labels?.benefits ?? 'Benefity'
|
|
50
|
+
});
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
{#if availableTabs.length > 0}
|
|
54
|
+
<div>
|
|
55
|
+
<Tabs bind:value={active}>
|
|
56
|
+
{#each availableTabs as tab (tab)}
|
|
57
|
+
<Tab value={tab}>{caption[tab]}</Tab>
|
|
58
|
+
{/each}
|
|
59
|
+
</Tabs>
|
|
60
|
+
|
|
61
|
+
<div class="mt-5">
|
|
62
|
+
{#if active === 'description' && description}
|
|
63
|
+
<!-- font-normal pinned: Archer has no 300 cut, and a font-light
|
|
64
|
+
ancestor (the guide's Demo panel) would otherwise substitute. -->
|
|
65
|
+
<div class="font-(family-name:--font-serif) font-normal text-(length:--step1) leading-relaxed">
|
|
66
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- documented prop contract: sanitized server-side, never author-controlled raw HTML. -->
|
|
67
|
+
{@html description}
|
|
68
|
+
</div>
|
|
69
|
+
{:else if active === 'parameters' && parameters}
|
|
70
|
+
<dl class="divide-border-subtle divide-y">
|
|
71
|
+
{#each parameters as parameter, index (`${index}-${parameter.label}-${parameter.value}`)}
|
|
72
|
+
<div class="flex items-baseline justify-between gap-4 py-2.5">
|
|
73
|
+
<dt class="text-fg-muted1 text-(length:--step-down1)">{parameter.label}</dt>
|
|
74
|
+
<dd class="text-right font-medium">{parameter.value}</dd>
|
|
75
|
+
</div>
|
|
76
|
+
{/each}
|
|
77
|
+
</dl>
|
|
78
|
+
{:else if active === 'benefits' && benefits}
|
|
79
|
+
<ul class="space-y-3 text-(length:--step-down1) leading-6">
|
|
80
|
+
{#each benefits as benefit (benefit)}
|
|
81
|
+
<li>{benefit}</li>
|
|
82
|
+
{/each}
|
|
83
|
+
</ul>
|
|
84
|
+
{/if}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
{/if}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** Sanitized HTML (sanitize server-side!) — never author-controlled raw input. */
|
|
3
|
+
description?: string;
|
|
4
|
+
parameters?: {
|
|
5
|
+
label: string;
|
|
6
|
+
value: string;
|
|
7
|
+
}[];
|
|
8
|
+
benefits?: string[];
|
|
9
|
+
/** Localized tab captions; Czech defaults. */
|
|
10
|
+
labels?: {
|
|
11
|
+
description?: string;
|
|
12
|
+
parameters?: string;
|
|
13
|
+
benefits?: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
declare const ProductTabs: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type ProductTabs = ReturnType<typeof ProductTabs>;
|
|
18
|
+
export default ProductTabs;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
value: number;
|
|
4
|
+
min?: number;
|
|
5
|
+
max?: number;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
onchange: (value: number) => void;
|
|
8
|
+
/** Accessible labels of the − / + buttons. */
|
|
9
|
+
decreaseLabel?: string;
|
|
10
|
+
increaseLabel?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
value,
|
|
15
|
+
min = 1,
|
|
16
|
+
max,
|
|
17
|
+
disabled = false,
|
|
18
|
+
onchange,
|
|
19
|
+
decreaseLabel = 'Snížit množství',
|
|
20
|
+
increaseLabel = 'Zvýšit množství'
|
|
21
|
+
}: Props = $props();
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<div class="rounded-button border-border-subtle inline-flex items-center border font-secondary">
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
aria-label={decreaseLabel}
|
|
28
|
+
disabled={disabled || value <= min}
|
|
29
|
+
class="text-fg-muted1 hover:text-fg grid size-9 place-items-center disabled:opacity-40"
|
|
30
|
+
onclick={() => onchange(value - 1)}
|
|
31
|
+
>
|
|
32
|
+
<svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M5 12h14" /></svg>
|
|
33
|
+
</button>
|
|
34
|
+
<span class="min-w-8 text-center font-medium tabular-nums">{value}</span>
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
aria-label={increaseLabel}
|
|
38
|
+
disabled={disabled || (max !== undefined && value >= max)}
|
|
39
|
+
class="text-fg-muted1 hover:text-fg grid size-9 place-items-center disabled:opacity-40"
|
|
40
|
+
onclick={() => onchange(value + 1)}
|
|
41
|
+
>
|
|
42
|
+
<svg viewBox="0 0 24 24" class="size-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
value: number;
|
|
3
|
+
min?: number;
|
|
4
|
+
max?: number;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
onchange: (value: number) => void;
|
|
7
|
+
/** Accessible labels of the − / + buttons. */
|
|
8
|
+
decreaseLabel?: string;
|
|
9
|
+
increaseLabel?: string;
|
|
10
|
+
}
|
|
11
|
+
declare const QuantityStepper: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type QuantityStepper = ReturnType<typeof QuantityStepper>;
|
|
13
|
+
export default QuantityStepper;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
/** 0–5, fractions paint a partial star. */
|
|
4
|
+
rating: number;
|
|
5
|
+
/** Review count printed after the stars. */
|
|
6
|
+
count?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let { rating, count }: Props = $props();
|
|
10
|
+
|
|
11
|
+
// $props.id() instead of Math.random(): stable across SSR + hydration, so
|
|
12
|
+
// the gradient defs and their url(#…) references cannot mismatch.
|
|
13
|
+
const uid = $props.id();
|
|
14
|
+
const stars = $derived(
|
|
15
|
+
[1, 2, 3, 4, 5].map((i) => (rating >= i ? 1 : rating > i - 1 ? rating - (i - 1) : 0))
|
|
16
|
+
);
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<span class="inline-flex items-center gap-1" role="img" aria-label="{rating.toFixed(1)} z 5">
|
|
20
|
+
{#each stars as fill, i (i)}
|
|
21
|
+
<svg viewBox="0 0 24 24" class="size-4">
|
|
22
|
+
<defs
|
|
23
|
+
><linearGradient id="{uid}-{i}"
|
|
24
|
+
><stop offset="{fill * 100}%" stop-color="var(--JELINEK-BROWN)" /><stop
|
|
25
|
+
offset="{fill * 100}%"
|
|
26
|
+
stop-color="var(--border-subtle)"
|
|
27
|
+
/></linearGradient
|
|
28
|
+
></defs
|
|
29
|
+
>
|
|
30
|
+
<path
|
|
31
|
+
fill="url(#{uid}-{i})"
|
|
32
|
+
d="M12 2l2.9 6.26 6.6.56-5 4.44 1.5 6.5L12 16.3 5.99 19.76l1.5-6.5-5-4.44 6.6-.56z"
|
|
33
|
+
/>
|
|
34
|
+
</svg>
|
|
35
|
+
{/each}
|
|
36
|
+
{#if count !== undefined}<span class="text-fg-muted1 font-secondary font-light text-(length:--step-down1) ml-1"
|
|
37
|
+
>({count})</span
|
|
38
|
+
>{/if}
|
|
39
|
+
</span>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** 0–5, fractions paint a partial star. */
|
|
3
|
+
rating: number;
|
|
4
|
+
/** Review count printed after the stars. */
|
|
5
|
+
count?: number;
|
|
6
|
+
}
|
|
7
|
+
declare const RatingStars: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type RatingStars = ReturnType<typeof RatingStars>;
|
|
9
|
+
export default RatingStars;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
+
import { cn } from '../../utils/cn.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
value?: string;
|
|
7
|
+
/** Popisek pro čtečky (pole nemá viditelný label). */
|
|
8
|
+
label?: string;
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
/** Zavolá se při odeslání (Enter) s aktuálním dotazem. */
|
|
11
|
+
onsearch?: (query: string) => void;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
value = $bindable(''),
|
|
18
|
+
label = 'Hledat',
|
|
19
|
+
placeholder = 'Hledat…',
|
|
20
|
+
onsearch,
|
|
21
|
+
disabled = false,
|
|
22
|
+
class: className,
|
|
23
|
+
...rest
|
|
24
|
+
}: Props & Omit<HTMLInputAttributes, 'value'> = $props();
|
|
25
|
+
|
|
26
|
+
// Field chrome is TextField's BASE verbatim (border-fg-muted2, 11px/14px
|
|
27
|
+
// padding, 0.98rem, tokenised focus ring — see TextField.svelte for the
|
|
28
|
+
// full provenance notes), with left padding widened for the magnifier and
|
|
29
|
+
// right padding for the clear button. type="search" is NOT used: WebKit
|
|
30
|
+
// draws its own clear control there, which would double up with ours.
|
|
31
|
+
const FIELD =
|
|
32
|
+
'w-full rounded-button border border-fg-muted2 bg-surface py-[11px] pr-11 pl-11 ' +
|
|
33
|
+
'font-secondary font-light text-[0.98rem] text-fg placeholder:text-fg-muted2 ' +
|
|
34
|
+
'focus:outline-[length:var(--focus-width)] focus:outline-offset-1 ' +
|
|
35
|
+
'focus:outline-[var(--focus-color)] focus:border-[var(--focus-color)]';
|
|
36
|
+
const DISABLED =
|
|
37
|
+
'cursor-not-allowed border-border-subtle bg-surface-gray text-fg-muted2 ' +
|
|
38
|
+
'placeholder:text-fg-muted2';
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<!-- role="search" comes with the <search>/form pattern: submit = Enter, which
|
|
42
|
+
is how people actually search. The magnifier is decorative (the field's
|
|
43
|
+
accessible name carries the meaning), the clear button is a real button. -->
|
|
44
|
+
<form
|
|
45
|
+
role="search"
|
|
46
|
+
class={cn('relative', className)}
|
|
47
|
+
onsubmit={(e) => {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
onsearch?.(value);
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
<svg
|
|
53
|
+
viewBox="0 0 24 24"
|
|
54
|
+
class="pointer-events-none absolute top-1/2 left-3.5 size-5 -translate-y-1/2 text-fg-muted1"
|
|
55
|
+
fill="none"
|
|
56
|
+
stroke="currentColor"
|
|
57
|
+
stroke-width="2"
|
|
58
|
+
stroke-linecap="round"
|
|
59
|
+
aria-hidden="true"
|
|
60
|
+
>
|
|
61
|
+
<circle cx="11" cy="11" r="7" /><path d="m21 21-4.3-4.3" />
|
|
62
|
+
</svg>
|
|
63
|
+
<input
|
|
64
|
+
{...rest}
|
|
65
|
+
type="text"
|
|
66
|
+
aria-label={label}
|
|
67
|
+
{placeholder}
|
|
68
|
+
{disabled}
|
|
69
|
+
bind:value
|
|
70
|
+
class={cn(FIELD, disabled && DISABLED)}
|
|
71
|
+
/>
|
|
72
|
+
{#if value && !disabled}
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
aria-label="Vymazat hledání"
|
|
76
|
+
class="absolute top-1/2 right-2 inline-flex size-8 -translate-y-1/2 cursor-pointer
|
|
77
|
+
items-center justify-center rounded-button text-fg-muted1 transition-colors
|
|
78
|
+
duration-[var(--dur-base)] ease-[var(--ease)] hover:text-fg"
|
|
79
|
+
onclick={() => (value = '')}
|
|
80
|
+
>
|
|
81
|
+
<svg
|
|
82
|
+
viewBox="0 0 24 24"
|
|
83
|
+
class="size-4"
|
|
84
|
+
fill="none"
|
|
85
|
+
stroke="currentColor"
|
|
86
|
+
stroke-width="2"
|
|
87
|
+
stroke-linecap="round"
|
|
88
|
+
aria-hidden="true"><path d="M18 6 6 18M6 6l12 12" /></svg
|
|
89
|
+
>
|
|
90
|
+
</button>
|
|
91
|
+
{/if}
|
|
92
|
+
</form>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
+
interface Props {
|
|
3
|
+
value?: string;
|
|
4
|
+
/** Popisek pro čtečky (pole nemá viditelný label). */
|
|
5
|
+
label?: string;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
/** Zavolá se při odeslání (Enter) s aktuálním dotazem. */
|
|
8
|
+
onsearch?: (query: string) => void;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
type $$ComponentProps = Props & Omit<HTMLInputAttributes, 'value'>;
|
|
13
|
+
declare const SearchField: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
14
|
+
type SearchField = ReturnType<typeof SearchField>;
|
|
15
|
+
export default SearchField;
|