@jelinek/ui 0.9.0 → 0.9.2

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 (98) hide show
  1. package/README.md +33 -10
  2. package/dist/components/ChapterNav.svelte +1 -1
  3. package/dist/components/Checkbox.svelte +13 -15
  4. package/dist/components/Checkbox.svelte.d.ts +1 -0
  5. package/dist/components/ContentLogo.svelte +18 -13
  6. package/dist/components/ContentLogo.svelte.d.ts +6 -5
  7. package/dist/components/Grid.svelte +9 -5
  8. package/dist/components/Icon.svelte +52 -0
  9. package/dist/components/Icon.svelte.d.ts +30 -0
  10. package/dist/components/InfoBar.svelte +254 -0
  11. package/dist/components/InfoBar.svelte.d.ts +46 -0
  12. package/dist/components/Label.svelte +12 -1
  13. package/dist/components/Label.svelte.d.ts +8 -0
  14. package/dist/components/PageHero.svelte +12 -9
  15. package/dist/components/Pager.svelte +5 -2
  16. package/dist/components/PhotoTile.svelte +4 -2
  17. package/dist/components/ProductCard.svelte +66 -44
  18. package/dist/components/ProductCard.svelte.d.ts +17 -4
  19. package/dist/components/Radio.svelte +12 -8
  20. package/dist/components/Radio.svelte.d.ts +1 -0
  21. package/dist/components/Select.svelte +70 -12
  22. package/dist/components/Select.svelte.d.ts +11 -0
  23. package/dist/components/SiteFooter.svelte +19 -0
  24. package/dist/components/SiteFooter.svelte.d.ts +8 -0
  25. package/dist/components/SiteHeader.svelte +70 -9
  26. package/dist/components/SiteHeader.svelte.d.ts +16 -0
  27. package/dist/components/Slider.svelte +6 -11
  28. package/dist/components/Switch.svelte +76 -0
  29. package/dist/components/Switch.svelte.d.ts +12 -0
  30. package/dist/components/Tab.svelte +29 -1
  31. package/dist/components/TextField.svelte +61 -10
  32. package/dist/components/TextField.svelte.d.ts +13 -0
  33. package/dist/components/Textarea.svelte +92 -0
  34. package/dist/components/Textarea.svelte.d.ts +23 -0
  35. package/dist/components/Tile.svelte +12 -11
  36. package/dist/components/Tile.svelte.d.ts +8 -2
  37. package/dist/components/blocks/CmsButton.svelte +36 -0
  38. package/dist/components/blocks/CmsButton.svelte.d.ts +12 -0
  39. package/dist/components/blocks/CmsColumns.svelte +20 -0
  40. package/dist/components/blocks/CmsColumns.svelte.d.ts +9 -0
  41. package/dist/components/blocks/CmsCover.svelte +71 -0
  42. package/dist/components/blocks/CmsCover.svelte.d.ts +17 -0
  43. package/dist/components/blocks/CmsHeading.svelte +50 -0
  44. package/dist/components/blocks/CmsHeading.svelte.d.ts +14 -0
  45. package/dist/components/blocks/CmsImage.svelte +43 -0
  46. package/dist/components/blocks/CmsImage.svelte.d.ts +13 -0
  47. package/dist/components/blocks/CmsMediaText.svelte +64 -0
  48. package/dist/components/blocks/CmsMediaText.svelte.d.ts +16 -0
  49. package/dist/components/blocks/CmsText.svelte +53 -0
  50. package/dist/components/blocks/CmsText.svelte.d.ts +10 -0
  51. package/dist/components/commerce/Breadcrumbs.svelte +22 -5
  52. package/dist/components/commerce/CartDrawer.svelte +7 -4
  53. package/dist/components/commerce/CartLineItem.svelte +1 -1
  54. package/dist/components/commerce/CheckoutProgress.svelte +1 -1
  55. package/dist/components/commerce/ConfigSection.svelte +2 -2
  56. package/dist/components/commerce/DimensionSlider.svelte +1 -1
  57. package/dist/components/commerce/Drawer.svelte +38 -24
  58. package/dist/components/commerce/Drawer.svelte.d.ts +0 -2
  59. package/dist/components/commerce/EmptyState.svelte +37 -0
  60. package/dist/components/commerce/EmptyState.svelte.d.ts +13 -0
  61. package/dist/components/commerce/FilterChip.svelte +35 -0
  62. package/dist/components/commerce/FilterChip.svelte.d.ts +10 -0
  63. package/dist/components/commerce/FreeShippingBar.svelte +1 -1
  64. package/dist/components/commerce/Gallery.svelte +4 -2
  65. package/dist/components/commerce/ListingToolbar.svelte +62 -0
  66. package/dist/components/commerce/ListingToolbar.svelte.d.ts +19 -0
  67. package/dist/components/commerce/Modal.svelte +31 -21
  68. package/dist/components/commerce/Modal.svelte.d.ts +0 -2
  69. package/dist/components/commerce/OrderSummary.svelte +1 -1
  70. package/dist/components/commerce/Pagination.svelte +115 -0
  71. package/dist/components/commerce/Pagination.svelte.d.ts +14 -0
  72. package/dist/components/commerce/PriceDisplay.svelte +3 -3
  73. package/dist/components/commerce/ProductHeader.svelte +2 -2
  74. package/dist/components/commerce/QuantityStepper.svelte +1 -1
  75. package/dist/components/commerce/RatingStars.svelte +1 -1
  76. package/dist/components/commerce/SearchField.svelte +92 -0
  77. package/dist/components/commerce/SearchField.svelte.d.ts +15 -0
  78. package/dist/components/commerce/SideToggle.svelte +1 -1
  79. package/dist/components/commerce/Skeleton.svelte +4 -1
  80. package/dist/components/commerce/StickyBar.svelte +4 -4
  81. package/dist/components/commerce/StockBadge.svelte +2 -2
  82. package/dist/components/commerce/Toaster.svelte +9 -5
  83. package/dist/components/commerce/TrustBadges.svelte +1 -1
  84. package/dist/components/commerce/overlay.d.ts +14 -0
  85. package/dist/components/commerce/overlay.js +49 -0
  86. package/dist/components/info-bar.d.ts +26 -0
  87. package/dist/components/info-bar.js +23 -0
  88. package/dist/icons/material-icons.generated.d.ts +4 -0
  89. package/dist/icons/material-icons.generated.js +118 -0
  90. package/dist/icons/material.d.ts +17 -0
  91. package/dist/icons/material.js +52 -0
  92. package/dist/index.d.ts +17 -0
  93. package/dist/index.js +17 -0
  94. package/dist/theme/base.css +22 -0
  95. package/dist/theme/extras.css +62 -0
  96. package/dist/theme/fonts.css +9 -42
  97. package/dist/theme/tokens.css +23 -9
  98. package/package.json +4 -2
package/README.md CHANGED
@@ -26,7 +26,7 @@ Packages for a package that is no longer published there:
26
26
  ```json
27
27
  {
28
28
  "dependencies": {
29
- "@jelinek/ui": "^0.4.0"
29
+ "@jelinek/ui": "^0.9.2"
30
30
  }
31
31
  }
32
32
  ```
@@ -365,10 +365,7 @@ as before; the DOM-seeding only applies when it's left unbound.
365
365
 
366
366
  ## Components
367
367
 
368
- Alert, Button, Card, ChapterNav, Checkbox, CodeCopy, Container, ContentLogo,
369
- Demo, Eyebrow, Glass, Grid, Label, PageHero, Pager, Panel, ProductCard, Radio,
370
- RadioGroup, Select, SiteFooter, SiteHeader, Slider, Swatch, Tab, Tabs, Tag,
371
- TextField, ThemeToggle, Tile, TileScroller.
368
+ Alert, Breadcrumbs, Button, Card, CartDrawer, CartLineItem, ChapterNav, Checkbox, CheckoutProgress, CmsButton, CmsColumns, CmsCover, CmsHeading, CmsImage, CmsMediaText, CmsText, CodeCopy, ConfigSection, Container, ContentLogo, Demo, DimensionSlider, Drawer, EmptyState, Eyebrow, FilterChip, FreeShippingBar, Gallery, Glass, Grid, Icon, Label, ListingToolbar, Modal, OptionTile, OrderSummary, PageHero, Pager, Pagination, Panel, PhotoTile, PriceDisplay, ProductCard, ProductHeader, ProductStickyBar, ProductTabs, QuantityStepper, Radio, RadioGroup, RatingStars, SearchField, Select, SideToggle, SiteFooter, SiteHeader, Skeleton, Slider, StickyBar, StockBadge, Swatch, Switch, Tab, Tabs, Tag, TextField, Textarea, ThemeToggle, Tile, TileScroller, Toaster, TrustBadges.
372
369
 
373
370
  See `src/lib/index.ts` for the exact export list and `src/lib/components/`
374
371
  for source. Per-component contracts, including the two below, are printed at
@@ -395,6 +392,22 @@ showcase's `+layout.svelte` both model it. And below 640px `ChapterNav` renders
395
392
  nothing at all: pass the same chapter list to `SiteHeader`'s `chapterItems` so
396
393
  a phone reader can still reach the chapters from the menu panel.
397
394
 
395
+ **`SiteHeader` — the way back to the company portal is `homeHref`, not a
396
+ button in `actions`.** Company apps built from the templates link back to the
397
+ rozcestník. Pass its URL as `homeHref` and the header renders a `home` icon
398
+ (from the kit's icon set, no text) as the first item of the bar, at the far
399
+ left before the logo, at every width including phones. It is a 44px square
400
+ — the burger's size — rendered through the kit's own ghost/black `Button`,
401
+ so hover, transition and the global focus-visible ring match the nav links.
402
+ `homeLabel` (default `'Zpět na rozcestník'`) is its `aria-label` and `title`.
403
+ Without `homeHref` the header is unchanged. Do not put a "Zpět na rozcestník"
404
+ `Button` into `actions` any more: it sat on the right next to the nav links at
405
+ a different size (owner's decision, 15. 9. 2026).
406
+
407
+ ```svelte
408
+ <SiteHeader subtitle="Objednávky" items={menu} homeHref="https://jlnk.cz/" />
409
+ ```
410
+
398
411
  ## Sync contract — and its limits
399
412
 
400
413
  Two automated checks keep this kit from silently drifting away from the
@@ -413,6 +426,18 @@ never hand-edit it**; run `pnpm gen:tokens` and commit the result.
413
426
  Hand-written theme additions belong in `src/lib/theme/extras.css`, which
414
427
  the generator never touches.
415
428
 
429
+ **`pnpm check:icons`** (`scripts/gen-icons.js --check`) does the same for
430
+ the interface icon set: `src/lib/icons/material-icons.generated.ts` is
431
+ generated from `assets/icons/material/` (Google icons as per-icon SVGs plus
432
+ our own drawings in `vlastni/`) and fails if it is stale. The generator also
433
+ refuses any SVG without `fill="currentColor"` on its root or carrying its
434
+ own colours — an icon that would not re-theme. The module is **generated —
435
+ never hand-edit it**; add or remove SVG files, run `pnpm gen:icons`, commit.
436
+ It exists so that `<Icon name="…" />` works from the installed package:
437
+ the earlier `import.meta.glob` reached outside `dist/` and only worked
438
+ inside this repo (review, 1. 9. 2026). The whole set is ~18 kB gzipped and
439
+ ships with the package.
440
+
416
441
  **`pnpm check:css-lock`** (`scripts/check-css-lock.js`) hashes the guide's
417
442
  CSS rule groups for two fixed, hardcoded allow-lists in that file:
418
443
  `GUARDED` — class names (`btn`, `tag`, `alert`, `tile`, `panel`, `input`,
@@ -525,11 +550,9 @@ code.
525
550
 
526
551
  The guide does not yet define everything this kit's consumers need. See:
527
552
 
528
- - `docs/chybejici-komponenty.md` — missing interface components (icons,
529
- form states, modals/toasts, tables, e-shop pieces, navigation), with
530
- priority.
531
- - `docs/chybejici-bloky-cms.md` — the 16 CMS content blocks that exist in
532
- code but were never designed against the brand guide.
553
+ - `/audit.html#plan-prace` — the single work plan (formerly the two backlog
554
+ documents): missing interface components, the CMS content blocks still to
555
+ be designed, admin components, with priority and deadline.
533
556
 
534
557
  Five components in this kit have **no corresponding CSS in the guide** and
535
558
  were necessarily invented rather than reimplemented: `Checkbox`, `Radio`,
@@ -61,7 +61,7 @@
61
61
  // while `display: none` holds; from 640px up the bar is `absolute`, which is
62
62
  // what makes it the containing block for the scroll-affordance fades below.
63
63
  const OUTER =
64
- 'hidden z-[49] px-6 ' +
64
+ 'hidden z-(--z-chapter-nav) px-6 ' +
65
65
  'min-[640px]:block min-[640px]:absolute min-[640px]:inset-x-0 ' +
66
66
  'min-[640px]:top-[calc(var(--header-h)-11px)]';
67
67
 
@@ -6,6 +6,7 @@
6
6
  checked?: boolean;
7
7
  label: string;
8
8
  id?: string;
9
+ disabled?: boolean;
9
10
  class?: string;
10
11
  }
11
12
 
@@ -13,6 +14,7 @@
13
14
  checked = $bindable(false),
14
15
  label,
15
16
  id,
17
+ disabled = false,
16
18
  class: className,
17
19
  ...rest
18
20
  }: Props & Omit<HTMLInputAttributes, 'checked' | 'type'> = $props();
@@ -31,22 +33,18 @@
31
33
  // without depending on an ambient .demo wrapper the consumer may not
32
34
  // have. gap-2.5 matches the 10px inline gap.
33
35
  //
34
- // ACCENT COLOUR — no CSS source of truth exists, so this is a judgement
35
- // call, flagged for adjudication (see task-7-report.md). The checkbox
36
- // square itself uses accent-jelinek-brown, NOT accent-jelinek-magenta:
37
- // magenta is reserved for the Zdravý spánek (mattress) division and the
38
- // guide is explicit brown/magenta never mix in one interface, so baking
39
- // magenta into every checkbox would leak the mattress accent into
40
- // furniture-division UIs. jelinek-brown is also what TextField/Select's
41
- // own focus ring already uses (jelinek.css:661), and the token layer
42
- // already re-themes --JELINEK-BROWN to magenta under
43
- // [data-division="mattress"] (tokens.css:161-166) — the same mechanism
44
- // Button relies on for its brand colours — so this accent still turns
45
- // magenta automatically on mattress-themed pages, without hardcoding it.
46
- const LABEL = 'flex cursor-pointer items-center gap-2.5 font-secondary text-step0 text-fg';
36
+ // ACCENT COLOUR — adjudicated by the owner (14. 8. 2026): forms are
37
+ // colour-neutral. Every emphasis in a form is BLACK (--JELINEK-BLACK,
38
+ // which flips to near-white in dark mode so it stays visible); the only
39
+ // colour a form carries is the error state. No division re-theme here —
40
+ // forms look the same in Nábytek and Zdravý spánek.
41
+ const LABEL = 'flex cursor-pointer items-center gap-2.5 font-secondary font-light text-step0 text-fg';
42
+ // Zakázaný stav mluví jako zakázané tlačítko (Button.svelte:117):
43
+ // utlumený text a cursor-not-allowed; políčko samotné tlumí prohlížeč.
44
+ const DISABLED = 'cursor-not-allowed text-fg-muted2';
47
45
  </script>
48
46
 
49
- <label for={inputId} class={cn(LABEL, className)}>
50
- <input {...rest} id={inputId} type="checkbox" bind:checked class="accent-jelinek-brown" />
47
+ <label for={inputId} class={cn(LABEL, disabled && DISABLED, className)}>
48
+ <input {...rest} id={inputId} type="checkbox" {disabled} bind:checked class="accent-jelinek-black" />
51
49
  {label}
52
50
  </label>
@@ -3,6 +3,7 @@ interface Props {
3
3
  checked?: boolean;
4
4
  label: string;
5
5
  id?: string;
6
+ disabled?: boolean;
6
7
  class?: string;
7
8
  }
8
9
  type $$ComponentProps = Props & Omit<HTMLInputAttributes, 'checked' | 'type'>;
@@ -3,13 +3,14 @@
3
3
  import { cn } from '../utils/cn.js';
4
4
  import { DEER_MARK_PATH } from './deer-mark-path.js';
5
5
 
6
- type Variant = 'brown' | 'magenta';
6
+ type Variant = 'brown';
7
7
 
8
8
  interface Props {
9
- /** jelinek.css:744-745. Left unset for the bare/default rule (mark
10
- * colour --text-white, jelinek.css:743) — NOT itself a "neutral"
11
- * variant name, since there is no third `.content-logo--*` modifier
12
- * in the guide to give it one. */
9
+ /** Jelen je jen HNĚDÝ, nebo BÍLÝ (brand → Znak, Užití s produktovými
10
+ * barvami; majitel, 21. 8. 2026): `brown` pro divizi Nábytek, bez
11
+ * varianty BÍLÝ — divize Zdravý spánek a neutrální užití (interní
12
+ * aplikace). Purpurová varianta NEEXISTUJE — dřívější `magenta` byla
13
+ * chyba proti manuálu a je odstraněná. */
13
14
  variant?: Variant;
14
15
  /**
15
16
  * Sets `--logo-mark` (default matches jelinek.css:743's own inline
@@ -76,14 +77,13 @@
76
77
  // actually invisible once the colour scoping below is right.
77
78
  const BASE = 'inline-flex items-center gap-[calc(var(--logo-mark)*107/400)] text-fg-inverse';
78
79
 
79
- // jelinek.css:744 (--brown, --JELINEK-BROWN, division-tracking) and :745
80
- // (--magenta, --JELINEK-MAGENTA, fixed regardless of division) — same
81
- // token pair and same division-tracking-vs-fixed distinction Eyebrow's
82
- // own VARIANT already documents for the identical reason (tokens.css's
83
- // mattress re-theme only ever reassigns --JELINEK-BROWN).
80
+ // jelinek.css .content-logo--brown: the brown deer. The division re-theme
81
+ // remaps --JELINEK-BROWN to magenta — for the deer that is a FORBIDDEN
82
+ // state, so under [data-division="mattress"] every lockup flips to WHITE
83
+ // instead; the rule lives in extras.css and hooks on the data-content-logo
84
+ // attribute below, mirroring jelinek.css's own division override.
84
85
  const VARIANT: Record<Variant, string> = {
85
- brown: 'text-jelinek-brown',
86
- magenta: 'text-jelinek-magenta'
86
+ brown: 'text-jelinek-brown'
87
87
  };
88
88
 
89
89
  // jelinek.css:746 (.content-logo__svg): width/height both var(--logo-mark),
@@ -142,7 +142,12 @@
142
142
  );
143
143
  </script>
144
144
 
145
- <div {...rest} style={mergedStyle} class={cn(BASE, variant && VARIANT[variant], className)}>
145
+ <div
146
+ {...rest}
147
+ style={mergedStyle}
148
+ data-content-logo=""
149
+ class={cn(BASE, variant && VARIANT[variant], className)}
150
+ >
146
151
  <svg class={SVG} viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
147
152
  <rect width="400" height="400" rx="25" ry="25" fill="#000" />
148
153
  <path d={DEER_MARK_PATH} fill="currentColor" />
@@ -1,10 +1,11 @@
1
1
  import type { HTMLAttributes } from 'svelte/elements';
2
- type Variant = 'brown' | 'magenta';
2
+ type Variant = 'brown';
3
3
  interface Props {
4
- /** jelinek.css:744-745. Left unset for the bare/default rule (mark
5
- * colour --text-white, jelinek.css:743) — NOT itself a "neutral"
6
- * variant name, since there is no third `.content-logo--*` modifier
7
- * in the guide to give it one. */
4
+ /** Jelen je jen HNĚDÝ, nebo BÍLÝ (brand → Znak, Užití s produktovými
5
+ * barvami; majitel, 21. 8. 2026): `brown` pro divizi Nábytek, bez
6
+ * varianty BÍLÝ — divize Zdravý spánek a neutrální užití (interní
7
+ * aplikace). Purpurová varianta NEEXISTUJE — dřívější `magenta` byla
8
+ * chyba proti manuálu a je odstraněná. */
8
9
  variant?: Variant;
9
10
  /**
10
11
  * Sets `--logo-mark` (default matches jelinek.css:743's own inline
@@ -49,10 +49,11 @@
49
49
  // Behaviour by width (the guide's, verbatim):
50
50
  // w < 640 -> 1 column, every cols value
51
51
  // 640 <= w < 1024 -> 2 columns, every cols value
52
- // w >= 1024 -> the cols value itself (2/3/4)
53
- // (.grid--3 and .grid--4 both collapse to 2 columns between 640 and 1024,
54
- // so cols=4 goes 4 -> 2 -> 1 and never passes through 3; cols=2 has no
55
- // 1024px rule at all, it is already 2-up from 640px.)
52
+ // w >= 1024 -> the cols value itself for 2/3; cols=4 steps
53
+ // 3 (>=1024) -> 4 (>=1280) -> 5 (>=1600), so the
54
+ // four-column grid climbs 1/2/3/4/5 with width
55
+ // (owner's request) instead of jumping 2 -> 4.
56
+ // (cols=2 has no 1024px rule at all, it is already 2-up from 640px.)
56
57
  //
57
58
  // Why Tailwind's native `min-*` variant family and NOT arbitrary *media*
58
59
  // variants (`[@media(min-width:1024px)]:...`), even though those can express
@@ -75,10 +76,13 @@
75
76
  // `min-[1024px]:grid-cols-${n}` template, so Tailwind's static scanner can
76
77
  // see each one literally — a template string is invisible to it and the
77
78
  // class would never be generated.
79
+ // cols={4} přidává od 1600 px pátý sloupec (jelinek.css .grid--4, majitel:
80
+ // širší web = víc obsahu do řady; trojka a dvojka zůstávají — jsou volené
81
+ // kvůli obsahu, ne kvůli místu).
78
82
  const COLS: Record<2 | 3 | 4, string> = {
79
83
  2: 'grid-cols-1 min-[640px]:grid-cols-2',
80
84
  3: 'grid-cols-1 min-[640px]:grid-cols-2 min-[1024px]:grid-cols-3',
81
- 4: 'grid-cols-1 min-[640px]:grid-cols-2 min-[1024px]:grid-cols-4'
85
+ 4: 'grid-cols-1 min-[640px]:grid-cols-2 min-[1024px]:grid-cols-3 min-[1280px]:grid-cols-4 min-[1600px]:grid-cols-5'
82
86
  };
83
87
  </script>
84
88
 
@@ -0,0 +1,52 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { cn } from '../utils/cn.js';
4
+ import { materialIcon } from '../icons/material.js';
5
+
6
+ /**
7
+ * Ikona rozhraní — Google icons (Material Symbols Rounded) jako inline SVG
8
+ * (rozhodnutí majitele 28. 8. 2026: soubory, ne ikonové písmo; pravidla
9
+ * v kapitole /ui/ikony).
10
+ *
11
+ * `name` je přesné jméno ikony z fonts.google.com/icons (shopping_cart,
12
+ * search, favorite…) = jméno souboru v assets/icons/material/. Soubor se
13
+ * vloží inline, takže `fill="currentColor"` dědí barvu okolního textu;
14
+ * neexistující jméno spadne hned při vykreslení (loader hlásí překlep
15
+ * i chybějící stažení). Obal .icon (extras.css) dává rozměr 20 px
16
+ * a posazení na účaří; `size` ho přepíše. Plná varianta (`fill`) sahá po
17
+ * souboru `<name>_fill.svg` — existuje jen tam, kde ji stavy potřebují
18
+ * (star, favorite…), jinak loader spadne a fill variantu je potřeba
19
+ * nejdřív stáhnout. Ikona je čtečkám skrytá, ledaže dostane `label`.
20
+ */
21
+ interface Props {
22
+ name: string;
23
+ /** Plná varianta pro aktivní/vybraný stav — soubor `<name>_fill.svg`. */
24
+ fill?: boolean;
25
+ /** Popisek pro čtečky; bez něj je ikona dekorace (aria-hidden). */
26
+ label?: string;
27
+ /** Velikost v px (šířka i výška). Výchozích 20 px dává utilita .icon. */
28
+ size?: number;
29
+ class?: string;
30
+ }
31
+
32
+ let {
33
+ name,
34
+ fill = false,
35
+ label,
36
+ size,
37
+ class: className,
38
+ ...rest
39
+ }: Props & HTMLAttributes<HTMLSpanElement> = $props();
40
+
41
+ const svg = $derived(materialIcon(fill ? `${name}_fill` : name));
42
+ </script>
43
+
44
+ <span
45
+ {...rest}
46
+ class={cn('icon', className)}
47
+ style:width={size ? `${size}px` : undefined}
48
+ style:height={size ? `${size}px` : undefined}
49
+ role={label ? 'img' : undefined}
50
+ aria-label={label}
51
+ aria-hidden={label ? undefined : 'true'}>{@html svg}</span
52
+ >
@@ -0,0 +1,30 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ /**
3
+ * Ikona rozhraní — Google icons (Material Symbols Rounded) jako inline SVG
4
+ * (rozhodnutí majitele 28. 8. 2026: soubory, ne ikonové písmo; pravidla
5
+ * v kapitole /ui/ikony).
6
+ *
7
+ * `name` je přesné jméno ikony z fonts.google.com/icons (shopping_cart,
8
+ * search, favorite…) = jméno souboru v assets/icons/material/. Soubor se
9
+ * vloží inline, takže `fill="currentColor"` dědí barvu okolního textu;
10
+ * neexistující jméno spadne hned při vykreslení (loader hlásí překlep
11
+ * i chybějící stažení). Obal .icon (extras.css) dává rozměr 20 px
12
+ * a posazení na účaří; `size` ho přepíše. Plná varianta (`fill`) sahá po
13
+ * souboru `<name>_fill.svg` — existuje jen tam, kde ji stavy potřebují
14
+ * (star, favorite…), jinak loader spadne a fill variantu je potřeba
15
+ * nejdřív stáhnout. Ikona je čtečkám skrytá, ledaže dostane `label`.
16
+ */
17
+ interface Props {
18
+ name: string;
19
+ /** Plná varianta pro aktivní/vybraný stav — soubor `<name>_fill.svg`. */
20
+ fill?: boolean;
21
+ /** Popisek pro čtečky; bez něj je ikona dekorace (aria-hidden). */
22
+ label?: string;
23
+ /** Velikost v px (šířka i výška). Výchozích 20 px dává utilita .icon. */
24
+ size?: number;
25
+ class?: string;
26
+ }
27
+ type $$ComponentProps = Props & HTMLAttributes<HTMLSpanElement>;
28
+ declare const Icon: import("svelte").Component<$$ComponentProps, {}, "">;
29
+ type Icon = ReturnType<typeof Icon>;
30
+ export default Icon;
@@ -0,0 +1,254 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { cn } from '../utils/cn.js';
5
+ import Button from './Button.svelte';
6
+ import Icon from './Icon.svelte';
7
+ import { INFO_BAR_COLORS, isActive, type InfoBarItem } from './info-bar.js';
8
+
9
+ /**
10
+ * INFORMAČNÍ LIŠTA (majitel, 4. 9. 2026) — celoplošná sdělení nad menu:
11
+ * celozávodní dovolená, doprava zdarma, otevírací doba o svátcích.
12
+ *
13
+ * - Leží NAD hlavičkou a PŘES hero (absolute, jako hlavička): pozadí
14
+ * úvodního pásu běží pod ní i pod menu, žádný bílý pruh. Při rolování
15
+ * odjede, plovoucí hlavička se přilepí k hornímu okraji jako bez lišty.
16
+ * Lišta hlásí na <html> dvě proměnné: `--info-bar-h` (svou výšku — hero ji
17
+ * přičítá k rezervě pro hlavičku) a `--header-offset` (aktuální `top`
18
+ * hlavičky, od výšky lišty k nule podle rolování).
19
+ * - Tvar jako hlavička: plovoucí karta stejné šířky a zaoblení, na
20
+ * telefonu pruh od kraje ke kraji.
21
+ * - Výška podle obsahu — ale podle NEJVYŠŠÍHO sdělení: všechna jsou vykreslená
22
+ * přes sebe v jedné buňce mřížky a střídá se jen průhlednost, takže obsah pod
23
+ * lištou při přepnutí neposkočí (majitel, 4. 9. 2026). Křížek je u
24
+ * jednořádkového textu uprostřed výšky, u víceřádkového v pravém horním rohu.
25
+ * - Víc sdělení se střídá samo (výchozí 8 s), prolínáním; při najetí myší
26
+ * nebo zaostření se střídání zastaví. Kdo má v systému omezený pohyb,
27
+ * dostane výměnu bez prolínání.
28
+ * - Křížek zavře jen aktuální sdělení; web si to pamatuje po dobu návštěvy
29
+ * (sessionStorage). Lišta zmizí, až když nezbývá co ukázat.
30
+ * - Každé sdělení může mít časové okno od–do včetně času; kontroluje se
31
+ * každou minutu, takže lišta zmizí i bez obnovení stránky.
32
+ * - Odkaz vpravo je TLAČÍTKO v neutrální barvě (majitel, 4. 9. 2026): černé
33
+ * z kapitoly Tlačítka. Na černé liště se neutrální tokeny přemapují jako
34
+ * v tmavém režimu, takže je bílé s černým textem a hover má podle pravidel.
35
+ * Značkový akcent v liště nepoužíváme, sdělení nese barva samotné lišty.
36
+ */
37
+ interface Props {
38
+ items: InfoBarItem[];
39
+ /** Střídání sdělení v ms. */
40
+ interval?: number;
41
+ /** Klíč v sessionStorage se zavřenými sděleními. */
42
+ storageKey?: string;
43
+ /** Posouvat plovoucí hlavičku pod lištu (vypnout v ukázkách uvnitř stránky). */
44
+ offsetHeader?: boolean;
45
+ /** „Teď“ pro časová okna — přepisuje se jen v testech a ukázkách. */
46
+ now?: Date;
47
+ class?: string;
48
+ }
49
+
50
+ let {
51
+ items,
52
+ interval = 8000,
53
+ storageKey = 'jelinek-info-bar',
54
+ offsetHeader = true,
55
+ now: nowProp,
56
+ class: className,
57
+ ...rest
58
+ }: Props & HTMLAttributes<HTMLDivElement> = $props();
59
+
60
+ let closed = $state<string[]>([]);
61
+ // svelte-ignore state_referenced_locally — výchozí hodnota, dál se řídí hodinami v onMount
62
+ let now = $state(nowProp ?? new Date());
63
+ let index = $state(0);
64
+ let paused = $state(false);
65
+ let single = $state<Record<string, boolean>>({});
66
+
67
+ const visible = $derived(items.filter((i) => !closed.includes(i.id) && isActive(i, now)));
68
+ const current = $derived(visible[Math.min(index, Math.max(visible.length - 1, 0))]);
69
+
70
+ function readClosed(): string[] {
71
+ try {
72
+ return JSON.parse(sessionStorage.getItem(storageKey) ?? '[]');
73
+ } catch {
74
+ return [];
75
+ }
76
+ }
77
+ function close(id: string) {
78
+ closed = [...closed, id];
79
+ try {
80
+ sessionStorage.setItem(storageKey, JSON.stringify(closed));
81
+ } catch {
82
+ /* soukromý režim bez úložiště — lišta se zavře jen pro tuto stránku */
83
+ }
84
+ index = 0;
85
+ }
86
+
87
+ function advance() {
88
+ if (paused || visible.length < 2) return;
89
+ index = (index + 1) % visible.length;
90
+ }
91
+
92
+ // ODHAD VÝŠKY PŘED HYDRATACÍ (majitel, 4. 9. 2026: „menu nejdřív zčerná“).
93
+ // Než doběhne JS a lišta se změří, seděla hlavička na top:0 — tedy PŘES
94
+ // černou lištu, a skleněná karta nad černou vypadá černě. Proto se do <head>
95
+ // vykreslí odhad --header-offset / --info-bar-h už při prerenderu: řádek
96
+ // textu ≈ 20 px, svislé okraje 24 px, na desktopu horní mezera 12 px; na
97
+ // telefonu se odhadne zalomení (~34 znaků na řádek). JS pak hodnotu opraví
98
+ // přesným měřením, případný rozdíl je pár pixelů, žádný černý záblesk.
99
+ const LINE = 20;
100
+ function estimate(phone: boolean): number {
101
+ let lines = 1;
102
+ for (const i of visible) {
103
+ const width = phone ? 34 : 90;
104
+ const titleLines = i.title ? Math.max(1, Math.ceil(i.title.length / (phone ? 30 : 80))) : 0;
105
+ const n = titleLines + Math.max(1, Math.ceil(i.text.length / width));
106
+ lines = Math.max(lines, n);
107
+ }
108
+ return (phone ? 0 : 12) + 24 + LINE * lines;
109
+ }
110
+ const ssrCss = $derived(
111
+ offsetHeader && visible.length
112
+ ? `:root{--header-offset:${estimate(false)}px;--info-bar-h:${estimate(false)}px}@media (max-width:639px){:root{--header-offset:${estimate(true)}px;--info-bar-h:${estimate(true)}px}}`
113
+ : ''
114
+ );
115
+
116
+ let wrap = $state<HTMLDivElement | null>(null);
117
+ let textEls = $state<Record<string, HTMLDivElement | null>>({});
118
+
119
+ // Výška lišty → posun hlavičky. Od 640 px je hlavička fixed: posun se
120
+ // zmenšuje s rolováním až na 0 (lišta odjela, menu se přilepilo nahoru).
121
+ // Na telefonu je hlavička absolute a roluje s obsahem: posun = výška lišty.
122
+ function updateOffset() {
123
+ if (!offsetHeader || typeof document === 'undefined') return;
124
+ const root = document.documentElement;
125
+ const h = wrap?.offsetHeight ?? 0;
126
+ if (!h) {
127
+ root.style.removeProperty('--header-offset');
128
+ root.style.removeProperty('--info-bar-h');
129
+ return;
130
+ }
131
+ root.style.setProperty('--info-bar-h', `${h}px`);
132
+ const fixed = matchMedia('(min-width: 640px)').matches;
133
+ root.style.setProperty('--header-offset', `${fixed ? Math.max(0, h - window.scrollY) : h}px`);
134
+ }
135
+
136
+ function updateLines() {
137
+ const next: Record<string, boolean> = {};
138
+ for (const [id, el] of Object.entries(textEls)) {
139
+ if (!el) continue;
140
+ const lh = parseFloat(getComputedStyle(el).lineHeight) || 20;
141
+ next[id] = el.offsetHeight <= lh * 1.5;
142
+ }
143
+ single = next;
144
+ }
145
+
146
+ onMount(() => {
147
+ closed = readClosed();
148
+ if (!nowProp) now = new Date();
149
+ const tick = setInterval(advance, interval);
150
+ const clock = nowProp ? null : setInterval(() => (now = new Date()), 60_000);
151
+ const onScroll = () => updateOffset();
152
+ window.addEventListener('scroll', onScroll, { passive: true });
153
+ window.addEventListener('resize', onScroll);
154
+ const ro = typeof ResizeObserver === 'function' ? new ResizeObserver(() => { updateOffset(); updateLines(); }) : null;
155
+ if (ro && wrap) ro.observe(wrap);
156
+ return () => {
157
+ clearInterval(tick);
158
+ if (clock) clearInterval(clock);
159
+ window.removeEventListener('scroll', onScroll);
160
+ window.removeEventListener('resize', onScroll);
161
+ ro?.disconnect();
162
+ if (offsetHeader && typeof document !== 'undefined') {
163
+ document.documentElement.style.removeProperty('--header-offset');
164
+ document.documentElement.style.removeProperty('--info-bar-h');
165
+ }
166
+ };
167
+ });
168
+
169
+ $effect(() => {
170
+ // Nová lišta / jiná sada sdělení → přeměřit.
171
+ wrap; visible; textEls;
172
+ updateOffset();
173
+ updateLines();
174
+ });
175
+
176
+ // Stejná geometrie jako SiteHeader (OUTER/CARD): na telefonu pruh, od 640 px
177
+ // karta se stejným bočním okrajem, stropem šířky a zaoblením.
178
+ // Nad menu, přes hero: absolute jako hlavička (roluje s obsahem). V ukázkách
179
+ // uvnitř stránky (offsetHeader=false) zůstává v toku.
180
+ const WRAP = $derived(
181
+ (offsetHeader ? 'absolute inset-x-0 top-0 ' : 'relative ') + 'z-(--z-bar) min-[640px]:px-6 min-[640px]:pt-3'
182
+ );
183
+ // Všechna sdělení v jedné buňce mřížky (grid-area 1/1): mřížka má výšku
184
+ // nejvyššího z nich, viditelné je jen aktuální (ostatní průhledná a skrytá
185
+ // pro čtečky i klávesnici). Prolínání = přechod průhlednosti; komu systém
186
+ // omezuje pohyb, tomu ho `motion-reduce` vypne.
187
+ // grid-cols minmax(0,1fr) + min-w-0: bez toho by mřížka rostla podle
188
+ // nejdelšího slova a lišta na 320 px přetekla za okraj okna.
189
+ const STACK = 'mx-auto grid w-full max-w-page grid-cols-[minmax(0,1fr)]';
190
+ // flex-wrap + pravý vnitřní okraj pro křížek: na úzkém displeji tlačítko
191
+ // spadne pod text, křížek zůstává v rohu (je absolute).
192
+ const BAR =
193
+ '[grid-area:1/1] relative flex min-w-0 w-full flex-wrap gap-x-3 gap-y-2 py-3 pl-6 pr-14 font-secondary text-[0.9rem] leading-snug ' +
194
+ 'shadow-resting transition-opacity duration-[var(--dur-base)] ease-[var(--ease)] motion-reduce:transition-none min-[640px]:rounded-card';
195
+ // Tlačítko v liště: vždy neutrální černé (Komponenty → Tlačítka), jen
196
+ // kompaktnější kvůli výšce lišty. Na černé liště ho čitelným dělá lokální
197
+ // přemapování neutrálních tokenů v INFO_BAR_COLORS.black (stejně jako tmavý
198
+ // režim a .demo--black): černé tlačítko je tam bílé s černým textem a hover
199
+ // jde na 60 % šedou — přesně stav „Černé“ z kapitoly Tlačítka v tmavém režimu.
200
+ const BTN = 'shrink-0 self-center px-4 py-2 text-[0.9rem]';
201
+ const CLOSE =
202
+ 'absolute right-2 inline-flex h-11 w-11 cursor-pointer items-center justify-center rounded-button border-0 bg-transparent p-0 ' +
203
+ 'text-current opacity-80 hover:opacity-100 focus-visible:outline-2 focus-visible:outline-current';
204
+ </script>
205
+
206
+ <svelte:head>
207
+ <!-- {@html}: literální <style> by Svelte vzal jako styl komponenty, ne jako element. -->
208
+ {#if ssrCss}{@html `<style>${ssrCss}</style>`}{/if}
209
+ </svelte:head>
210
+
211
+ {#if current}
212
+ <div
213
+ {...rest}
214
+ bind:this={wrap}
215
+ class={cn(WRAP, className)}
216
+ data-info-bar
217
+ onmouseenter={() => (paused = true)}
218
+ onmouseleave={() => (paused = false)}
219
+ onfocusin={() => (paused = true)}
220
+ onfocusout={() => (paused = false)}
221
+ >
222
+ <div class={STACK}>
223
+ {#each visible as item, i (item.id)}
224
+ {@const isCurrent = item === current}
225
+ {@const oneLine = single[item.id] ?? true}
226
+ <div
227
+ role="region"
228
+ aria-label="Informační lišta"
229
+ aria-hidden={!isCurrent}
230
+ inert={!isCurrent}
231
+ class={cn(BAR, INFO_BAR_COLORS[item.color ?? 'black'], oneLine ? 'items-center' : 'items-start', !isCurrent && 'pointer-events-none opacity-0')}
232
+ data-color={item.color ?? 'black'}
233
+ data-current={isCurrent || undefined}
234
+ >
235
+ {#if item.icon}
236
+ <span class="shrink-0 {oneLine ? '' : 'mt-0.5'} [&_svg]:h-5 [&_svg]:w-5"><Icon name={item.icon} size={20} /></span>
237
+ {/if}
238
+ <div bind:this={textEls[item.id]} class="min-w-[10rem] flex-1 self-center">
239
+ {#if item.title}<strong class="block font-bold">{item.title}</strong>{/if}
240
+ <span class="font-light">{item.text}</span>
241
+ </div>
242
+ {#if item.href}
243
+ <Button href={item.href} color="black" class={BTN}>
244
+ {item.linkLabel ?? 'Více'}
245
+ </Button>
246
+ {/if}
247
+ <button type="button" class={cn(CLOSE, oneLine ? 'top-1/2 -translate-y-1/2' : 'top-1.5')} aria-label="Zavřít sdělení" title="Zavřít sdělení" onclick={() => close(item.id)}>
248
+ <Icon name="close" size={18} />
249
+ </button>
250
+ </div>
251
+ {/each}
252
+ </div>
253
+ </div>
254
+ {/if}
@@ -0,0 +1,46 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import { type InfoBarItem } from './info-bar.js';
3
+ /**
4
+ * INFORMAČNÍ LIŠTA (majitel, 4. 9. 2026) — celoplošná sdělení nad menu:
5
+ * celozávodní dovolená, doprava zdarma, otevírací doba o svátcích.
6
+ *
7
+ * - Leží NAD hlavičkou a PŘES hero (absolute, jako hlavička): pozadí
8
+ * úvodního pásu běží pod ní i pod menu, žádný bílý pruh. Při rolování
9
+ * odjede, plovoucí hlavička se přilepí k hornímu okraji jako bez lišty.
10
+ * Lišta hlásí na <html> dvě proměnné: `--info-bar-h` (svou výšku — hero ji
11
+ * přičítá k rezervě pro hlavičku) a `--header-offset` (aktuální `top`
12
+ * hlavičky, od výšky lišty k nule podle rolování).
13
+ * - Tvar jako hlavička: plovoucí karta stejné šířky a zaoblení, na
14
+ * telefonu pruh od kraje ke kraji.
15
+ * - Výška podle obsahu — ale podle NEJVYŠŠÍHO sdělení: všechna jsou vykreslená
16
+ * přes sebe v jedné buňce mřížky a střídá se jen průhlednost, takže obsah pod
17
+ * lištou při přepnutí neposkočí (majitel, 4. 9. 2026). Křížek je u
18
+ * jednořádkového textu uprostřed výšky, u víceřádkového v pravém horním rohu.
19
+ * - Víc sdělení se střídá samo (výchozí 8 s), prolínáním; při najetí myší
20
+ * nebo zaostření se střídání zastaví. Kdo má v systému omezený pohyb,
21
+ * dostane výměnu bez prolínání.
22
+ * - Křížek zavře jen aktuální sdělení; web si to pamatuje po dobu návštěvy
23
+ * (sessionStorage). Lišta zmizí, až když nezbývá co ukázat.
24
+ * - Každé sdělení může mít časové okno od–do včetně času; kontroluje se
25
+ * každou minutu, takže lišta zmizí i bez obnovení stránky.
26
+ * - Odkaz vpravo je TLAČÍTKO v neutrální barvě (majitel, 4. 9. 2026): černé
27
+ * z kapitoly Tlačítka. Na černé liště se neutrální tokeny přemapují jako
28
+ * v tmavém režimu, takže je bílé s černým textem a hover má podle pravidel.
29
+ * Značkový akcent v liště nepoužíváme, sdělení nese barva samotné lišty.
30
+ */
31
+ interface Props {
32
+ items: InfoBarItem[];
33
+ /** Střídání sdělení v ms. */
34
+ interval?: number;
35
+ /** Klíč v sessionStorage se zavřenými sděleními. */
36
+ storageKey?: string;
37
+ /** Posouvat plovoucí hlavičku pod lištu (vypnout v ukázkách uvnitř stránky). */
38
+ offsetHeader?: boolean;
39
+ /** „Teď“ pro časová okna — přepisuje se jen v testech a ukázkách. */
40
+ now?: Date;
41
+ class?: string;
42
+ }
43
+ type $$ComponentProps = Props & HTMLAttributes<HTMLDivElement>;
44
+ declare const InfoBar: import("svelte").Component<$$ComponentProps, {}, "">;
45
+ type InfoBar = ReturnType<typeof InfoBar>;
46
+ export default InfoBar;