@jelinek/ui 0.9.1 → 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.
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`,
@@ -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;
@@ -198,10 +198,14 @@
198
198
  // different reason: the chapter bar is `display: none` below 640px (its
199
199
  // entries move into the header's burger panel), so there is no strip to
200
200
  // reserve until the bar exists.
201
+ //
202
+ // --info-bar-h (majitel, 4. 9. 2026): informační lišta nad menu leží PŘES hero
203
+ // jako hlavička a hlásí svou výšku na <html>; bez lišty je proměnná prázdná → 0.
204
+ // Pozadí hero tak běží pod lištou i pod menu, žádný bílý pruh nad hlavičkou.
201
205
  const paddingTop = $derived(
202
206
  hasChapterNav
203
- ? 'pt-[calc(var(--header-h)_+_28px)] min-[640px]:pt-[calc(var(--header-h)_+_var(--subnav-h)_+_20px)]'
204
- : 'pt-[calc(var(--header-h)_+_28px)]'
207
+ ? 'pt-[calc(var(--header-h)_+_var(--info-bar-h,0px)_+_28px)] min-[640px]:pt-[calc(var(--header-h)_+_var(--info-bar-h,0px)_+_var(--subnav-h)_+_20px)]'
208
+ : 'pt-[calc(var(--header-h)_+_var(--info-bar-h,0px)_+_28px)]'
205
209
  );
206
210
 
207
211
  // The heading-underline mechanism. jelinek.css draws this via
@@ -3,11 +3,14 @@
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
4
  import { cn } from '../utils/cn.js';
5
5
  import Button from './Button.svelte';
6
+ import Icon from './Icon.svelte';
6
7
  import { createChapterNavDock } from '../utils/chapter-nav-dock.svelte.js';
7
8
  import { DEER_MARK_PATH } from './deer-mark-path.js';
8
9
  import { isActive, type NavGroup, type NavItem } from './nav.js';
9
10
 
10
11
  interface Props {
12
+ /** Cíl odkazu „Přeskočit na obsah“ — id hlavního obsahu stránky. */
13
+ skipHref?: string;
11
14
  /**
12
15
  * Escape hatch: a snippet rendering custom mark content in place of the
13
16
  * bundled, re-theming deer mark (e.g. a division-specific logo). Most
@@ -21,6 +24,20 @@
21
24
  * app is not rooted at `/`, e.g. mounted under a base path.
22
25
  */
23
26
  logoHref?: string;
27
+ /**
28
+ * Cesta zpět na firemní rozcestník. Když je zadaná, vykreslí se jako
29
+ * PRVNÍ položka lišty, vlevo před znakem, ikona domečku (`home` ze sady
30
+ * ikon) bez textu — na každé šířce, telefon včetně. Rozhodnutí majitele
31
+ * 15. 9. 2026: firemní aplikace ze šablon tak nepotřebují tlačítko
32
+ * „Zpět na rozcestník“ v `actions`, které vedle odkazů menu vypadalo
33
+ * jinak velké. Bez této vlastnosti se hlavička nemění.
34
+ */
35
+ homeHref?: string;
36
+ /**
37
+ * Přístupný název ikony domečku (`aria-label` i `title`). Ikona nemá
38
+ * viditelný text, takže tohle je jediné, co přečte čtečka.
39
+ */
40
+ homeLabel?: string;
24
41
  subtitle?: string;
25
42
  items?: NavItem[];
26
43
  active?: string;
@@ -98,9 +115,12 @@
98
115
  }
99
116
 
100
117
  let {
118
+ skipHref = '#hlavni-obsah',
101
119
  logo,
102
120
  logoAlt = 'JELÍNEK',
103
121
  logoHref = '/',
122
+ homeHref,
123
+ homeLabel = 'Zpět na rozcestník',
104
124
  subtitle,
105
125
  items = [],
106
126
  active,
@@ -339,8 +359,14 @@
339
359
  // (`--header-h`/`--subnav-h` are exported passthrough tokens precisely so a
340
360
  // consumer's own layout can do that). PageHero and the showcase's
341
361
  // `+layout.svelte` model exactly that.
362
+ // .skip-link z jelinek.css: mimo obraz, dokud nemá fokus z klávesnice.
363
+ const SKIP =
364
+ 'absolute top-3 left-3 z-[60] rounded-button bg-jelinek-black px-4 py-2.5 font-primary font-bold text-on-jelinek-black no-underline ' +
365
+ '-translate-y-[200%] opacity-0 focus-visible:translate-y-0 focus-visible:opacity-100';
342
366
  const OUTER =
343
- 'absolute inset-x-0 top-0 z-(--z-header) p-0 [--logo-mark:46px] ' +
367
+ // top = --header-offset: InfoBar (lišta nad menu) ji nastaví na svou výšku
368
+ // a při rolování stahuje k nule; bez lišty je proměnná prázdná → 0.
369
+ 'absolute inset-x-0 top-[var(--header-offset,0px)] z-(--z-header) p-0 [--logo-mark:46px] ' +
344
370
  'min-[640px]:fixed min-[640px]:px-6 min-[640px]:py-3';
345
371
  // Task 43 — the glass card no longer IS the flex row: the row (logo,
346
372
  // lockup, burger, nav) moved into its own .site-header__bar-equivalent
@@ -377,6 +403,23 @@
377
403
  // (leading-none), border-radius:radius-button, overflow hidden — the
378
404
  // mark itself is a square var(--logo-mark) box.
379
405
  const LOGO_LINK = 'inline-flex flex-none leading-none rounded-button overflow-hidden';
406
+
407
+ // HOME LINK (`homeHref`). Not guide markup — partials/header.html has no
408
+ // way back to a portal, because the guide IS the rozcestník; this is a
409
+ // consumer seam for company apps built from the templates. Rendered through
410
+ // this kit's own <Button variant="ghost" color="black">, like NAV_LINK, so
411
+ // the hover colour, transition and flat shadow are the button system's and
412
+ // cannot drift. The override only reshapes it into the BURGER's 44px square
413
+ // (`h-11 w-11 p-0`, cn() merges it last so it beats Button's padding); the
414
+ // focus ring is the global :focus-visible rule every link gets. The icon is
415
+ // 28px (owner, 15. 9. 2026: at 24px the weight-300 glyph read too light
416
+ // next to the burger); the hit target stays 44px. The 28px box goes through
417
+ // Icon's `size` (an inline style), not `w-7 h-7`: the `.icon` rule in
418
+ // extras.css is unlayered and would beat a layered utility. The SVG itself
419
+ // is sized here too, because `.icon > svg` filling its box also lives in
420
+ // extras.css, which is optional — without it the SVG would keep its 20px
421
+ // attributes inside the 28px span.
422
+ const HOME_LINK = 'h-11 w-11 flex-none gap-0 p-0 [&_svg]:block [&_svg]:h-7 [&_svg]:w-7';
380
423
  const MARK = 'block h-[var(--logo-mark)] w-[var(--logo-mark)]';
381
424
 
382
425
  // jelinek.css:447 (.site-header__title): font-primary (GT Walsheim),
@@ -700,8 +743,24 @@
700
743
  {/snippet}
701
744
 
702
745
  <header {...rest} bind:this={headerEl} class={cn(OUTER, className)}>
746
+ <!-- Přeskočit na obsah (/ui/pristupnost): první prvek v pořadí Tabu, vidět jen
747
+ se zaostřením z klávesnice. Cíl `#hlavni-obsah` = <main id="hlavni-obsah"> layoutu; stejný
748
+ odkaz má partials/header.html (.skip-link v jelinek.css). -->
749
+ <a href={skipHref} class={SKIP}>Přeskočit na obsah</a>
703
750
  <div class={inner}>
704
751
  <div class={BAR_ROW}>
752
+ {#if homeHref}
753
+ <Button
754
+ href={homeHref}
755
+ variant="ghost"
756
+ color="black"
757
+ aria-label={homeLabel}
758
+ title={homeLabel}
759
+ class={HOME_LINK}
760
+ >
761
+ <Icon name="home" size={28} />
762
+ </Button>
763
+ {/if}
705
764
  <a href={logoHref} aria-label={logoAlt} class={LOGO_LINK}>
706
765
  {#if logo}
707
766
  {@render logo()}
@@ -2,6 +2,8 @@ import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
3
  import { type NavItem } from './nav.js';
4
4
  interface Props {
5
+ /** Cíl odkazu „Přeskočit na obsah“ — id hlavního obsahu stránky. */
6
+ skipHref?: string;
5
7
  /**
6
8
  * Escape hatch: a snippet rendering custom mark content in place of the
7
9
  * bundled, re-theming deer mark (e.g. a division-specific logo). Most
@@ -15,6 +17,20 @@ interface Props {
15
17
  * app is not rooted at `/`, e.g. mounted under a base path.
16
18
  */
17
19
  logoHref?: string;
20
+ /**
21
+ * Cesta zpět na firemní rozcestník. Když je zadaná, vykreslí se jako
22
+ * PRVNÍ položka lišty, vlevo před znakem, ikona domečku (`home` ze sady
23
+ * ikon) bez textu — na každé šířce, telefon včetně. Rozhodnutí majitele
24
+ * 15. 9. 2026: firemní aplikace ze šablon tak nepotřebují tlačítko
25
+ * „Zpět na rozcestník“ v `actions`, které vedle odkazů menu vypadalo
26
+ * jinak velké. Bez této vlastnosti se hlavička nemění.
27
+ */
28
+ homeHref?: string;
29
+ /**
30
+ * Přístupný název ikony domečku (`aria-label` i `title`). Ikona nemá
31
+ * viditelný text, takže tohle je jediné, co přečte čtečka.
32
+ */
33
+ homeLabel?: string;
18
34
  subtitle?: string;
19
35
  items?: NavItem[];
20
36
  active?: string;