@jelinek/ui 0.9.0 → 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 +6 -7
- 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 +22 -5
- package/dist/components/commerce/CartDrawer.svelte +7 -4
- package/dist/components/commerce/CartLineItem.svelte +1 -1
- package/dist/components/commerce/CheckoutProgress.svelte +1 -1
- package/dist/components/commerce/ConfigSection.svelte +2 -2
- package/dist/components/commerce/DimensionSlider.svelte +1 -1
- package/dist/components/commerce/Drawer.svelte +38 -24
- package/dist/components/commerce/Drawer.svelte.d.ts +0 -2
- 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 +1 -1
- package/dist/components/commerce/Gallery.svelte +4 -2
- package/dist/components/commerce/ListingToolbar.svelte +62 -0
- package/dist/components/commerce/ListingToolbar.svelte.d.ts +19 -0
- package/dist/components/commerce/Modal.svelte +31 -21
- package/dist/components/commerce/Modal.svelte.d.ts +0 -2
- package/dist/components/commerce/OrderSummary.svelte +1 -1
- package/dist/components/commerce/Pagination.svelte +115 -0
- package/dist/components/commerce/Pagination.svelte.d.ts +14 -0
- package/dist/components/commerce/PriceDisplay.svelte +3 -3
- package/dist/components/commerce/ProductHeader.svelte +2 -2
- package/dist/components/commerce/QuantityStepper.svelte +1 -1
- package/dist/components/commerce/RatingStars.svelte +1 -1
- package/dist/components/commerce/SearchField.svelte +92 -0
- package/dist/components/commerce/SearchField.svelte.d.ts +15 -0
- package/dist/components/commerce/SideToggle.svelte +1 -1
- package/dist/components/commerce/Skeleton.svelte +4 -1
- package/dist/components/commerce/StickyBar.svelte +4 -4
- package/dist/components/commerce/StockBadge.svelte +2 -2
- package/dist/components/commerce/Toaster.svelte +9 -5
- package/dist/components/commerce/TrustBadges.svelte +1 -1
- package/dist/components/commerce/overlay.d.ts +14 -0
- package/dist/components/commerce/overlay.js +49 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/theme/base.css +22 -0
- package/dist/theme/extras.css +44 -0
- package/dist/theme/fonts.css +6 -42
- package/dist/theme/tokens.css +16 -9
- package/package.json +1 -1
|
@@ -1,55 +1,68 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { fade, fly } from 'svelte/transition';
|
|
3
3
|
import type { Snippet } from 'svelte';
|
|
4
|
-
import { portal, lockScroll } from './overlay.js';
|
|
4
|
+
import { portal, lockScroll, trapFocus, rememberFocus } from './overlay.js';
|
|
5
5
|
|
|
6
6
|
interface Props {
|
|
7
7
|
open: boolean;
|
|
8
8
|
onclose: () => void;
|
|
9
9
|
side?: 'left' | 'right';
|
|
10
10
|
labelledby?: string;
|
|
11
|
-
/** Accessible label of the backdrop close button. */
|
|
12
|
-
closeLabel?: string;
|
|
13
11
|
class?: string;
|
|
14
12
|
children: Snippet;
|
|
15
13
|
}
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
closeLabel = 'Zavřít',
|
|
23
|
-
class: className = '',
|
|
24
|
-
children
|
|
25
|
-
}: Props = $props();
|
|
15
|
+
// closeLabel zanikl s přestavbou pozadí na nefokusovatelnou vrstvu
|
|
16
|
+
// (přístupnost EAA) — zavírá Escape, klik vedle, nebo viditelné tlačítko
|
|
17
|
+
// v obsahu (CartDrawer ho nese). Poslaný prop se neškodně ignoruje.
|
|
18
|
+
let { open, onclose, side = 'right', labelledby, class: className = '', children }: Props =
|
|
19
|
+
$props();
|
|
26
20
|
|
|
27
21
|
let panel: HTMLElement | undefined = $state();
|
|
28
22
|
|
|
29
23
|
$effect(() => {
|
|
30
24
|
if (!open) return;
|
|
25
|
+
// Přístupnost překryvu (EAA): fokus se po zavření vrací na prvek,
|
|
26
|
+
// který drawer otevřel, a Tab cyklí uvnitř panelu (trapFocus).
|
|
27
|
+
const restore = rememberFocus();
|
|
31
28
|
const unlock = lockScroll();
|
|
32
29
|
panel?.focus();
|
|
30
|
+
const untrap = panel ? trapFocus(panel) : () => {};
|
|
33
31
|
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && onclose();
|
|
34
32
|
window.addEventListener('keydown', onKey);
|
|
35
33
|
return () => {
|
|
36
34
|
unlock();
|
|
35
|
+
untrap();
|
|
37
36
|
window.removeEventListener('keydown', onKey);
|
|
37
|
+
restore();
|
|
38
38
|
};
|
|
39
39
|
});
|
|
40
40
|
</script>
|
|
41
41
|
|
|
42
42
|
{#if open}
|
|
43
|
-
<!--
|
|
44
|
-
Modal)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
<!-- Škála vrstev (jelinek.css :root): hlavička --z-header (50); překryvy
|
|
44
|
+
(tento, Modal) nad ní na --z-overlay (1200); toasty --z-toast (1500)
|
|
45
|
+
úplně nahoře. -->
|
|
46
|
+
<!-- CHOVÁNÍ SJEDNOCENÉ S MOBILNÍM MENU (majitel, 14. 8. 2026): vyjetí
|
|
47
|
+
zprava na tokenu --dur-slow (0,2 s), zavření Esc i klepnutím vedle,
|
|
48
|
+
zamčený scroll, šířka min(84vw, 448px) nechává vidět proužek stránky
|
|
49
|
+
i na telefonu. MATERIÁL je ale kvůli čitelnosti obsahu (ceny, řádky
|
|
50
|
+
košíku) neprůhledný panel — majitel 14. 8. 2026 sklo vrátil: bílá
|
|
51
|
+
plocha (--bg-white, v tmavém režimu tmavá), jemná hrana
|
|
52
|
+
border-subtle a silnější ztmavení stránky o 50 %. -->
|
|
53
|
+
<div use:portal class="fixed inset-0 z-(--z-overlay)">
|
|
54
|
+
<!-- Pozadí je čistě myší zkratka pro zavření — NENÍ v tab pořadí
|
|
55
|
+
(dřív to byl <button> a neviditelné celoobrazovkové tlačítko bylo
|
|
56
|
+
první zastávkou tabulátoru). Klávesnice zavírá Escapem, čtečky
|
|
57
|
+
mají viditelné zavírací tlačítko v obsahu (CartDrawer ho nese). -->
|
|
58
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
59
|
+
<div
|
|
60
|
+
data-overlay-backdrop
|
|
61
|
+
aria-hidden="true"
|
|
62
|
+
class="absolute inset-0 bg-[var(--overlay-scrim)] [-webkit-backdrop-filter:var(--overlay-filter)] [backdrop-filter:var(--overlay-filter)]"
|
|
63
|
+
transition:fade={{ duration: 200 }}
|
|
51
64
|
onclick={onclose}
|
|
52
|
-
></
|
|
65
|
+
></div>
|
|
53
66
|
<div
|
|
54
67
|
bind:this={panel}
|
|
55
68
|
tabindex="-1"
|
|
@@ -57,9 +70,10 @@
|
|
|
57
70
|
aria-modal="true"
|
|
58
71
|
aria-labelledby={labelledby}
|
|
59
72
|
class="absolute inset-y-0 {side === 'right'
|
|
60
|
-
? 'right-0'
|
|
61
|
-
: 'left-0'}
|
|
62
|
-
|
|
73
|
+
? 'right-0 border-l'
|
|
74
|
+
: 'left-0 border-r'} border-border-subtle bg-surface
|
|
75
|
+
shadow-resting flex w-[min(84vw,448px)] flex-col font-secondary font-light outline-none {className}"
|
|
76
|
+
transition:fly={{ x: side === 'right' ? 400 : -400, duration: 200 }}
|
|
63
77
|
>
|
|
64
78
|
{@render children()}
|
|
65
79
|
</div>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../../utils/cn.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** Titulek prázdného stavu. */
|
|
7
|
+
heading?: string;
|
|
8
|
+
/** Vysvětlení a rada, co zkusit jinak. */
|
|
9
|
+
children?: Snippet;
|
|
10
|
+
/** Akce pod textem — typicky tlačítko „Zrušit filtry“. */
|
|
11
|
+
action?: Snippet;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
heading = 'Nic jsme nenašli',
|
|
17
|
+
children,
|
|
18
|
+
action,
|
|
19
|
+
class: className
|
|
20
|
+
}: Props = $props();
|
|
21
|
+
|
|
22
|
+
// A quiet gray panel, no illustration (the kit ships no icon set) and no
|
|
23
|
+
// colour — an empty result is a normal state, not an error. Centered so it
|
|
24
|
+
// visibly replaces the product grid it stands in for.
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<div class={cn('rounded-card bg-surface-gray px-6 py-14 text-center', className)}>
|
|
28
|
+
<h3 class="font-primary text-step1 font-bold text-fg">{heading}</h3>
|
|
29
|
+
{#if children}
|
|
30
|
+
<p class="mx-auto mt-2 max-w-[480px] font-secondary font-light text-step-down1 text-fg-muted1">
|
|
31
|
+
{@render children()}
|
|
32
|
+
</p>
|
|
33
|
+
{/if}
|
|
34
|
+
{#if action}
|
|
35
|
+
<div class="mt-5 flex justify-center gap-3">{@render action()}</div>
|
|
36
|
+
{/if}
|
|
37
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Titulek prázdného stavu. */
|
|
4
|
+
heading?: string;
|
|
5
|
+
/** Vysvětlení a rada, co zkusit jinak. */
|
|
6
|
+
children?: Snippet;
|
|
7
|
+
/** Akce pod textem — typicky tlačítko „Zrušit filtry“. */
|
|
8
|
+
action?: Snippet;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
declare const EmptyState: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type EmptyState = ReturnType<typeof EmptyState>;
|
|
13
|
+
export default EmptyState;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../utils/cn.js';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
/** Text nasazeného filtru, např. „Výška: 22 cm“. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Zavolá se po kliknutí — odebrání filtru řeší aplikace. */
|
|
8
|
+
onremove?: () => void;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { label, onremove, class: className }: Props = $props();
|
|
13
|
+
|
|
14
|
+
// The whole chip is one button whose action is REMOVAL — the × is just the
|
|
15
|
+
// visual hint, the accessible name says what the click does. Neutral
|
|
16
|
+
// emphasis (forms rule): resting border-fg-muted2 like an input, black on
|
|
17
|
+
// hover like a tile; colour stays reserved for errors.
|
|
18
|
+
const CHIP =
|
|
19
|
+
'inline-flex cursor-pointer items-center gap-1.5 rounded-pill border border-fg-muted2 ' +
|
|
20
|
+
'bg-surface py-[5px] pr-2.5 pl-3 font-secondary text-step-down1 text-fg ' +
|
|
21
|
+
'transition-colors duration-[var(--dur-base)] ease-[var(--ease)] hover:border-jelinek-black';
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<button type="button" aria-label="Zrušit filtr: {label}" class={cn(CHIP, className)} onclick={onremove}>
|
|
25
|
+
{label}
|
|
26
|
+
<svg
|
|
27
|
+
viewBox="0 0 24 24"
|
|
28
|
+
class="size-3.5 text-fg-muted1"
|
|
29
|
+
fill="none"
|
|
30
|
+
stroke="currentColor"
|
|
31
|
+
stroke-width="2"
|
|
32
|
+
stroke-linecap="round"
|
|
33
|
+
aria-hidden="true"><path d="M18 6 6 18M6 6l12 12" /></svg
|
|
34
|
+
>
|
|
35
|
+
</button>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** Text nasazeného filtru, např. „Výška: 22 cm“. */
|
|
3
|
+
label: string;
|
|
4
|
+
/** Zavolá se po kliknutí — odebrání filtru řeší aplikace. */
|
|
5
|
+
onremove?: () => void;
|
|
6
|
+
class?: string;
|
|
7
|
+
}
|
|
8
|
+
declare const FilterChip: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type FilterChip = ReturnType<typeof FilterChip>;
|
|
10
|
+
export default FilterChip;
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
);
|
|
29
29
|
</script>
|
|
30
30
|
|
|
31
|
-
<div class="rounded-card border-border-subtle border p-3">
|
|
31
|
+
<div class="rounded-card border-border-subtle border p-3 font-secondary font-light">
|
|
32
32
|
<p class="flex items-center gap-1.5 text-(length:--step-down1) font-medium text-fg mb-2">
|
|
33
33
|
<svg
|
|
34
34
|
viewBox="0 0 24 24"
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
>
|
|
119
119
|
<img src={image.src} alt={image.alt} loading="lazy" class="h-full w-full object-cover" />
|
|
120
120
|
<div
|
|
121
|
-
class="text-step1 absolute inset-0 flex items-center justify-center bg-(--glass-fill) font-medium text-fg backdrop-blur-[25px]"
|
|
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
122
|
>
|
|
123
123
|
{moreLabel(hiddenCount)}
|
|
124
124
|
</div>
|
|
@@ -142,7 +142,9 @@
|
|
|
142
142
|
</div>
|
|
143
143
|
{/if}
|
|
144
144
|
|
|
145
|
-
|
|
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}>
|
|
146
148
|
<div
|
|
147
149
|
class="relative flex max-h-[calc(90dvh-2rem)] w-[86vw] max-w-3xl flex-col items-center gap-3 p-4"
|
|
148
150
|
role="group"
|
|
@@ -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;
|
|
@@ -1,60 +1,70 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { fade } from 'svelte/transition';
|
|
3
3
|
import type { Snippet } from 'svelte';
|
|
4
|
-
import { portal, lockScroll } from './overlay.js';
|
|
4
|
+
import { portal, lockScroll, trapFocus, rememberFocus } from './overlay.js';
|
|
5
5
|
|
|
6
6
|
interface Props {
|
|
7
7
|
open: boolean;
|
|
8
8
|
onclose: () => void;
|
|
9
9
|
labelledby?: string;
|
|
10
|
-
/** Accessible label of the backdrop close button. */
|
|
11
|
-
closeLabel?: string;
|
|
12
10
|
class?: string;
|
|
13
11
|
children: Snippet;
|
|
14
12
|
}
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
closeLabel = 'Zavřít',
|
|
21
|
-
class: className = '',
|
|
22
|
-
children
|
|
23
|
-
}: Props = $props();
|
|
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();
|
|
24
18
|
|
|
25
19
|
let panel: HTMLElement | undefined = $state();
|
|
26
20
|
|
|
27
21
|
$effect(() => {
|
|
28
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();
|
|
29
26
|
const unlock = lockScroll();
|
|
30
27
|
panel?.focus();
|
|
28
|
+
const untrap = panel ? trapFocus(panel) : () => {};
|
|
31
29
|
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && onclose();
|
|
32
30
|
window.addEventListener('keydown', onKey);
|
|
33
31
|
return () => {
|
|
34
32
|
unlock();
|
|
33
|
+
untrap();
|
|
35
34
|
window.removeEventListener('keydown', onKey);
|
|
35
|
+
restore();
|
|
36
36
|
};
|
|
37
37
|
});
|
|
38
38
|
</script>
|
|
39
39
|
|
|
40
40
|
{#if open}
|
|
41
|
-
<!--
|
|
42
|
-
Drawer)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 }}
|
|
49
59
|
onclick={onclose}
|
|
50
|
-
></
|
|
60
|
+
></div>
|
|
51
61
|
<div
|
|
52
62
|
bind:this={panel}
|
|
53
63
|
tabindex="-1"
|
|
54
64
|
role="dialog"
|
|
55
65
|
aria-modal="true"
|
|
56
66
|
aria-labelledby={labelledby}
|
|
57
|
-
class="rounded-card bg-surface shadow-
|
|
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}"
|
|
58
68
|
transition:fade={{ duration: 200 }}
|
|
59
69
|
>
|
|
60
70
|
{@render children()}
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
});
|
|
49
49
|
</script>
|
|
50
50
|
|
|
51
|
-
<div class="space-y-2 text-(length:--step-down1)">
|
|
51
|
+
<div class="space-y-2 font-secondary font-light text-(length:--step-down1)">
|
|
52
52
|
<div class="flex justify-between">
|
|
53
53
|
<span class="text-fg-muted1">{l.itemsTotal}</span>
|
|
54
54
|
<span class="text-fg">{formatMoney(itemsTotal, currency)}</span>
|
|
@@ -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;
|
|
@@ -29,13 +29,13 @@
|
|
|
29
29
|
};
|
|
30
30
|
</script>
|
|
31
31
|
|
|
32
|
-
<span class="inline-flex items-baseline gap-2 font-bold {sizes[size]}">
|
|
32
|
+
<span class="inline-flex items-baseline gap-2 font-secondary font-bold {sizes[size]}">
|
|
33
33
|
{#if from}<span
|
|
34
|
-
class="text-fg-muted1 text-(length:--step-down2) font-
|
|
34
|
+
class="text-fg-muted1 text-(length:--step-down2) font-light tracking-wide uppercase"
|
|
35
35
|
>{fromLabel}</span
|
|
36
36
|
>{/if}
|
|
37
37
|
<span>{formatPrice(price, currency)}</span>
|
|
38
38
|
{#if originalPrice && originalPrice > price}
|
|
39
|
-
<s class="text-fg-muted2 text-[0.7em] font-
|
|
39
|
+
<s class="text-fg-muted2 text-[0.7em] font-light">{formatPrice(originalPrice, currency)}</s>
|
|
40
40
|
{/if}
|
|
41
41
|
</span>
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
}: Props = $props();
|
|
43
43
|
</script>
|
|
44
44
|
|
|
45
|
-
<div class="flex flex-col gap-3">
|
|
46
|
-
<h1 class="text-(length:--
|
|
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
47
|
{#if subtitle}<p class="text-fg-muted1 text-(length:--step1)">{subtitle}</p>{/if}
|
|
48
48
|
{#if rating && rating.count > 0}
|
|
49
49
|
<RatingStars rating={rating.value} count={rating.count} />
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
}: Props = $props();
|
|
22
22
|
</script>
|
|
23
23
|
|
|
24
|
-
<div class="rounded-button border-border-subtle inline-flex items-center border">
|
|
24
|
+
<div class="rounded-button border-border-subtle inline-flex items-center border font-secondary">
|
|
25
25
|
<button
|
|
26
26
|
type="button"
|
|
27
27
|
aria-label={decreaseLabel}
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
/>
|
|
34
34
|
</svg>
|
|
35
35
|
{/each}
|
|
36
|
-
{#if count !== undefined}<span class="text-fg-muted1 text-(length:--step-down1) ml-1"
|
|
36
|
+
{#if count !== undefined}<span class="text-fg-muted1 font-secondary font-light text-(length:--step-down1) ml-1"
|
|
37
37
|
>({count})</span
|
|
38
38
|
>{/if}
|
|
39
39
|
</span>
|