@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.
Files changed (85) hide show
  1. package/dist/components/ChapterNav.svelte +1 -1
  2. package/dist/components/Checkbox.svelte +13 -15
  3. package/dist/components/Checkbox.svelte.d.ts +1 -0
  4. package/dist/components/ContentLogo.svelte +18 -13
  5. package/dist/components/ContentLogo.svelte.d.ts +6 -5
  6. package/dist/components/Grid.svelte +9 -5
  7. package/dist/components/Label.svelte +12 -1
  8. package/dist/components/Label.svelte.d.ts +8 -0
  9. package/dist/components/PageHero.svelte +6 -7
  10. package/dist/components/Pager.svelte +5 -2
  11. package/dist/components/PhotoTile.svelte +4 -2
  12. package/dist/components/ProductCard.svelte +66 -44
  13. package/dist/components/ProductCard.svelte.d.ts +17 -4
  14. package/dist/components/Radio.svelte +12 -8
  15. package/dist/components/Radio.svelte.d.ts +1 -0
  16. package/dist/components/Select.svelte +70 -12
  17. package/dist/components/Select.svelte.d.ts +11 -0
  18. package/dist/components/SiteFooter.svelte +19 -0
  19. package/dist/components/SiteFooter.svelte.d.ts +8 -0
  20. package/dist/components/SiteHeader.svelte +11 -9
  21. package/dist/components/Slider.svelte +6 -11
  22. package/dist/components/Switch.svelte +76 -0
  23. package/dist/components/Switch.svelte.d.ts +12 -0
  24. package/dist/components/TextField.svelte +61 -10
  25. package/dist/components/TextField.svelte.d.ts +13 -0
  26. package/dist/components/Textarea.svelte +92 -0
  27. package/dist/components/Textarea.svelte.d.ts +23 -0
  28. package/dist/components/Tile.svelte +12 -11
  29. package/dist/components/Tile.svelte.d.ts +8 -2
  30. package/dist/components/blocks/CmsButton.svelte +36 -0
  31. package/dist/components/blocks/CmsButton.svelte.d.ts +12 -0
  32. package/dist/components/blocks/CmsColumns.svelte +20 -0
  33. package/dist/components/blocks/CmsColumns.svelte.d.ts +9 -0
  34. package/dist/components/blocks/CmsCover.svelte +71 -0
  35. package/dist/components/blocks/CmsCover.svelte.d.ts +17 -0
  36. package/dist/components/blocks/CmsHeading.svelte +50 -0
  37. package/dist/components/blocks/CmsHeading.svelte.d.ts +14 -0
  38. package/dist/components/blocks/CmsImage.svelte +43 -0
  39. package/dist/components/blocks/CmsImage.svelte.d.ts +13 -0
  40. package/dist/components/blocks/CmsMediaText.svelte +64 -0
  41. package/dist/components/blocks/CmsMediaText.svelte.d.ts +16 -0
  42. package/dist/components/blocks/CmsText.svelte +53 -0
  43. package/dist/components/blocks/CmsText.svelte.d.ts +10 -0
  44. package/dist/components/commerce/Breadcrumbs.svelte +22 -5
  45. package/dist/components/commerce/CartDrawer.svelte +7 -4
  46. package/dist/components/commerce/CartLineItem.svelte +1 -1
  47. package/dist/components/commerce/CheckoutProgress.svelte +1 -1
  48. package/dist/components/commerce/ConfigSection.svelte +2 -2
  49. package/dist/components/commerce/DimensionSlider.svelte +1 -1
  50. package/dist/components/commerce/Drawer.svelte +38 -24
  51. package/dist/components/commerce/Drawer.svelte.d.ts +0 -2
  52. package/dist/components/commerce/EmptyState.svelte +37 -0
  53. package/dist/components/commerce/EmptyState.svelte.d.ts +13 -0
  54. package/dist/components/commerce/FilterChip.svelte +35 -0
  55. package/dist/components/commerce/FilterChip.svelte.d.ts +10 -0
  56. package/dist/components/commerce/FreeShippingBar.svelte +1 -1
  57. package/dist/components/commerce/Gallery.svelte +4 -2
  58. package/dist/components/commerce/ListingToolbar.svelte +62 -0
  59. package/dist/components/commerce/ListingToolbar.svelte.d.ts +19 -0
  60. package/dist/components/commerce/Modal.svelte +31 -21
  61. package/dist/components/commerce/Modal.svelte.d.ts +0 -2
  62. package/dist/components/commerce/OrderSummary.svelte +1 -1
  63. package/dist/components/commerce/Pagination.svelte +115 -0
  64. package/dist/components/commerce/Pagination.svelte.d.ts +14 -0
  65. package/dist/components/commerce/PriceDisplay.svelte +3 -3
  66. package/dist/components/commerce/ProductHeader.svelte +2 -2
  67. package/dist/components/commerce/QuantityStepper.svelte +1 -1
  68. package/dist/components/commerce/RatingStars.svelte +1 -1
  69. package/dist/components/commerce/SearchField.svelte +92 -0
  70. package/dist/components/commerce/SearchField.svelte.d.ts +15 -0
  71. package/dist/components/commerce/SideToggle.svelte +1 -1
  72. package/dist/components/commerce/Skeleton.svelte +4 -1
  73. package/dist/components/commerce/StickyBar.svelte +4 -4
  74. package/dist/components/commerce/StockBadge.svelte +2 -2
  75. package/dist/components/commerce/Toaster.svelte +9 -5
  76. package/dist/components/commerce/TrustBadges.svelte +1 -1
  77. package/dist/components/commerce/overlay.d.ts +14 -0
  78. package/dist/components/commerce/overlay.js +49 -0
  79. package/dist/index.d.ts +14 -0
  80. package/dist/index.js +14 -0
  81. package/dist/theme/base.css +22 -0
  82. package/dist/theme/extras.css +44 -0
  83. package/dist/theme/fonts.css +6 -42
  84. package/dist/theme/tokens.css +16 -9
  85. 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
- let {
18
- open,
19
- onclose,
20
- side = 'right',
21
- labelledby,
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
- <!-- z-scale contract: site header sits at z-1100; overlay primitives (this,
44
- Modal) go above it at z-[1200]; Toaster tops everything at z-[1500]. -->
45
- <div use:portal class="fixed inset-0 z-[1200]">
46
- <button
47
- type="button"
48
- aria-label={closeLabel}
49
- class="absolute inset-0 bg-black/40"
50
- transition:fade={{ duration: 150 }}
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
- ></button>
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'} bg-surface shadow-interactive flex w-full max-w-md flex-col outline-none {className}"
62
- transition:fly={{ x: side === 'right' ? 400 : -400, duration: 250 }}
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>
@@ -4,8 +4,6 @@ interface Props {
4
4
  onclose: () => void;
5
5
  side?: 'left' | 'right';
6
6
  labelledby?: string;
7
- /** Accessible label of the backdrop close button. */
8
- closeLabel?: string;
9
7
  class?: string;
10
8
  children: Snippet;
11
9
  }
@@ -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
- <Modal open={lightboxOpen} onclose={closeLightbox} {closeLabel}>
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
- let {
17
- open,
18
- onclose,
19
- labelledby,
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
- <!-- z-scale contract: site header sits at z-1100; overlay primitives (this,
42
- Drawer) go above it at z-[1200]; Toaster tops everything at z-[1500]. -->
43
- <div use:portal class="fixed inset-0 z-[1200]">
44
- <button
45
- type="button"
46
- aria-label={closeLabel}
47
- class="absolute inset-0 bg-black/40"
48
- transition:fade={{ duration: 150 }}
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
- ></button>
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-interactive absolute inset-0 m-auto h-fit max-h-[90dvh] w-fit max-w-[92vw] outline-none {className}"
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()}
@@ -3,8 +3,6 @@ interface Props {
3
3
  open: boolean;
4
4
  onclose: () => void;
5
5
  labelledby?: string;
6
- /** Accessible label of the backdrop close button. */
7
- closeLabel?: string;
8
6
  class?: string;
9
7
  children: Snippet;
10
8
  }
@@ -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-normal tracking-wide uppercase"
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-normal">{formatPrice(originalPrice, currency)}</s>
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:--step5) leading-tight font-bold">{name}</h1>
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>