@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 +33 -10
- package/dist/components/Icon.svelte +52 -0
- package/dist/components/Icon.svelte.d.ts +30 -0
- package/dist/components/InfoBar.svelte +254 -0
- package/dist/components/InfoBar.svelte.d.ts +46 -0
- package/dist/components/PageHero.svelte +6 -2
- package/dist/components/SiteHeader.svelte +60 -1
- package/dist/components/SiteHeader.svelte.d.ts +16 -0
- package/dist/components/Tab.svelte +29 -1
- package/dist/components/blocks/CmsHeading.svelte +1 -1
- package/dist/components/info-bar.d.ts +26 -0
- package/dist/components/info-bar.js +23 -0
- package/dist/icons/material-icons.generated.d.ts +4 -0
- package/dist/icons/material-icons.generated.js +118 -0
- package/dist/icons/material.d.ts +17 -0
- package/dist/icons/material.js +52 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/theme/extras.css +18 -0
- package/dist/theme/fonts.css +3 -0
- package/dist/theme/tokens.css +7 -0
- 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.
|
|
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
|
-
-
|
|
529
|
-
|
|
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
|
-
|
|
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;
|