@jelinek/ui 0.3.0
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 +492 -0
- package/dist/components/Alert.svelte +59 -0
- package/dist/components/Alert.svelte.d.ts +13 -0
- package/dist/components/Button.svelte +178 -0
- package/dist/components/Button.svelte.d.ts +19 -0
- package/dist/components/Card.svelte +68 -0
- package/dist/components/Card.svelte.d.ts +11 -0
- package/dist/components/ChapterNav.svelte +290 -0
- package/dist/components/ChapterNav.svelte.d.ts +18 -0
- package/dist/components/Checkbox.svelte +52 -0
- package/dist/components/Checkbox.svelte.d.ts +11 -0
- package/dist/components/CodeCopy.svelte +294 -0
- package/dist/components/CodeCopy.svelte.d.ts +51 -0
- package/dist/components/Container.svelte +34 -0
- package/dist/components/Container.svelte.d.ts +10 -0
- package/dist/components/ContentLogo.svelte +151 -0
- package/dist/components/ContentLogo.svelte.d.ts +37 -0
- package/dist/components/Demo.svelte +111 -0
- package/dist/components/Demo.svelte.d.ts +12 -0
- package/dist/components/Eyebrow.svelte +70 -0
- package/dist/components/Eyebrow.svelte.d.ts +12 -0
- package/dist/components/Glass.svelte +75 -0
- package/dist/components/Glass.svelte.d.ts +10 -0
- package/dist/components/Grid.svelte +87 -0
- package/dist/components/Grid.svelte.d.ts +11 -0
- package/dist/components/Label.svelte +37 -0
- package/dist/components/Label.svelte.d.ts +11 -0
- package/dist/components/PageHero.svelte +312 -0
- package/dist/components/PageHero.svelte.d.ts +55 -0
- package/dist/components/Pager.svelte +98 -0
- package/dist/components/Pager.svelte.d.ts +13 -0
- package/dist/components/Panel.svelte +89 -0
- package/dist/components/Panel.svelte.d.ts +12 -0
- package/dist/components/ProductCard.svelte +81 -0
- package/dist/components/ProductCard.svelte.d.ts +18 -0
- package/dist/components/Radio.svelte +54 -0
- package/dist/components/Radio.svelte.d.ts +11 -0
- package/dist/components/RadioGroup.svelte +42 -0
- package/dist/components/RadioGroup.svelte.d.ts +12 -0
- package/dist/components/Select.svelte +65 -0
- package/dist/components/Select.svelte.d.ts +20 -0
- package/dist/components/SiteFooter.svelte +121 -0
- package/dist/components/SiteFooter.svelte.d.ts +13 -0
- package/dist/components/SiteHeader.svelte +613 -0
- package/dist/components/SiteHeader.svelte.d.ts +80 -0
- package/dist/components/Slider.svelte +55 -0
- package/dist/components/Slider.svelte.d.ts +14 -0
- package/dist/components/Swatch.svelte +174 -0
- package/dist/components/Swatch.svelte.d.ts +58 -0
- package/dist/components/Tab.svelte +88 -0
- package/dist/components/Tab.svelte.d.ts +11 -0
- package/dist/components/Tabs.svelte +306 -0
- package/dist/components/Tabs.svelte.d.ts +20 -0
- package/dist/components/Tag.svelte +71 -0
- package/dist/components/Tag.svelte.d.ts +16 -0
- package/dist/components/TextField.svelte +100 -0
- package/dist/components/TextField.svelte.d.ts +18 -0
- package/dist/components/ThemeToggle.svelte +122 -0
- package/dist/components/ThemeToggle.svelte.d.ts +11 -0
- package/dist/components/Tile.svelte +115 -0
- package/dist/components/Tile.svelte.d.ts +14 -0
- package/dist/components/TileScroller.svelte +199 -0
- package/dist/components/TileScroller.svelte.d.ts +25 -0
- package/dist/components/container-context.d.ts +1 -0
- package/dist/components/container-context.js +6 -0
- package/dist/components/deer-mark-path.d.ts +16 -0
- package/dist/components/deer-mark-path.js +16 -0
- package/dist/components/nav.d.ts +25 -0
- package/dist/components/nav.js +17 -0
- package/dist/components/radio-context.d.ts +6 -0
- package/dist/components/radio-context.js +1 -0
- package/dist/components/tabs-context.d.ts +5 -0
- package/dist/components/tabs-context.js +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +37 -0
- package/dist/theme/base.css +439 -0
- package/dist/theme/extras.css +2 -0
- package/dist/theme/fonts.css +107 -0
- package/dist/theme/tokens.css +208 -0
- package/dist/utils/chapter-nav-dock.svelte.d.ts +23 -0
- package/dist/utils/chapter-nav-dock.svelte.js +65 -0
- package/dist/utils/cn.d.ts +2 -0
- package/dist/utils/cn.js +27 -0
- package/package.json +79 -0
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import { cn } from '../utils/cn.js';
|
|
5
|
+
import Button from './Button.svelte';
|
|
6
|
+
import { createChapterNavDock } from '../utils/chapter-nav-dock.svelte.js';
|
|
7
|
+
import { DEER_MARK_PATH } from './deer-mark-path.js';
|
|
8
|
+
import { isActive, type NavItem } from './nav.js';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
/**
|
|
12
|
+
* Escape hatch: a snippet rendering custom mark content in place of the
|
|
13
|
+
* bundled, re-theming deer mark (e.g. a division-specific logo). Most
|
|
14
|
+
* consumers should leave this unset and get the guide's own mark.
|
|
15
|
+
*/
|
|
16
|
+
logo?: Snippet;
|
|
17
|
+
logoAlt?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Where the logo/mark links to. Defaults to `/`, the guide's own
|
|
20
|
+
* literal href (partials/header.html:3) — set this when the consuming
|
|
21
|
+
* app is not rooted at `/`, e.g. mounted under a base path.
|
|
22
|
+
*/
|
|
23
|
+
logoHref?: string;
|
|
24
|
+
subtitle?: string;
|
|
25
|
+
items?: NavItem[];
|
|
26
|
+
active?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Set when a `ChapterNav` is rendered directly below this header, to
|
|
29
|
+
* reproduce the guide's joined-panel seam (jelinek.css:604-620): the
|
|
30
|
+
* header loses its bottom border/corners/shadow so it flows into the
|
|
31
|
+
* nav strip, until the reader scrolls past 50px and it detaches into
|
|
32
|
+
* its own rounded, shadowed bar (assets/js/jelinek.js:354). SiteHeader
|
|
33
|
+
* tracks that scroll threshold itself — a consumer never has to wire
|
|
34
|
+
* up a listener. Leave unset (default `false`) for the normal,
|
|
35
|
+
* always-detached header, e.g. the rozcestník, which has no chapter
|
|
36
|
+
* nav at all.
|
|
37
|
+
*/
|
|
38
|
+
hasChapterNav?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Accessible name for the `<nav>` landmark holding the top-level
|
|
41
|
+
* links. Czech by default, like every other user-facing string this
|
|
42
|
+
* kit ships (see Pager's `prevLabel`/`nextLabel`).
|
|
43
|
+
*/
|
|
44
|
+
navLabel?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Chapter links to repeat inside the phone menu panel. Below 640px the
|
|
47
|
+
* guide's chapter bar is `display: none` (see ChapterNav.svelte), so the
|
|
48
|
+
* chapters have nowhere else to live on a phone; the guide's own script
|
|
49
|
+
* clones them out of the rendered bar, and this prop is that same content
|
|
50
|
+
* passed declaratively instead. Leave unset on pages with no chapter nav
|
|
51
|
+
* (the rozcestník) — the divider and the block disappear with it. They
|
|
52
|
+
* are hidden again from 640px up, where the bar itself is back.
|
|
53
|
+
*/
|
|
54
|
+
chapterItems?: NavItem[];
|
|
55
|
+
/**
|
|
56
|
+
* Accessible name for the phone hamburger button while the menu is SHUT.
|
|
57
|
+
* It has no visible text — it is three bars that fold into a cross — so
|
|
58
|
+
* this is the only name a screen reader gets. See BURGER below.
|
|
59
|
+
*/
|
|
60
|
+
menuLabel?: string;
|
|
61
|
+
/** Accessible name for the same button while the menu is OPEN. */
|
|
62
|
+
menuLabelClose?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Task 37 — TRAILING CONTENT INSIDE THE NAV, which the guide requires and
|
|
65
|
+
* this component previously refused.
|
|
66
|
+
*
|
|
67
|
+
* partials/header.html:6-13 nests the theme toggle as the LAST CHILD of
|
|
68
|
+
* `.site-header__nav`, so it sits in the nav row on a desktop and moves
|
|
69
|
+
* into the burger panel on a phone along with the links. The earlier
|
|
70
|
+
* decision here was to keep ThemeToggle separate and let a consumer
|
|
71
|
+
* "render both together in its own layout" — but a consumer cannot put
|
|
72
|
+
* anything inside this component's nav from outside it, so what that
|
|
73
|
+
* actually produced was a toggle stranded in the page body, at a different
|
|
74
|
+
* place from the guide and gone from the phone menu entirely.
|
|
75
|
+
*
|
|
76
|
+
* Rendered into BOTH navs, after the links, exactly like `items`. Keep it
|
|
77
|
+
* to controls that belong to the header (the toggle, a division switch) —
|
|
78
|
+
* it is not a general slot.
|
|
79
|
+
*/
|
|
80
|
+
actions?: Snippet;
|
|
81
|
+
class?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let {
|
|
85
|
+
logo,
|
|
86
|
+
logoAlt = 'JELÍNEK',
|
|
87
|
+
logoHref = '/',
|
|
88
|
+
subtitle,
|
|
89
|
+
items = [],
|
|
90
|
+
active,
|
|
91
|
+
hasChapterNav = false,
|
|
92
|
+
chapterItems = [],
|
|
93
|
+
navLabel = 'Hlavní navigace',
|
|
94
|
+
menuLabel = 'Otevřít menu',
|
|
95
|
+
menuLabelClose = 'Zavřít menu',
|
|
96
|
+
actions,
|
|
97
|
+
class: className,
|
|
98
|
+
...rest
|
|
99
|
+
}: Props & HTMLAttributes<HTMLElement> = $props();
|
|
100
|
+
|
|
101
|
+
// Task 15 — see chapter-nav-dock.svelte.ts for the full reasoning (the
|
|
102
|
+
// :root:has()/scroll-linked-<html>-class trick this replaces, and why the
|
|
103
|
+
// scroll tracking lives in its own module). `dock.docked` is true only
|
|
104
|
+
// while BOTH a chapter nav is present (hasChapterNav) AND the page hasn't
|
|
105
|
+
// scrolled past 50px — reproduces
|
|
106
|
+
// `:root:has(.chapter-nav):not(.is-subnav-scrolled)` without reaching
|
|
107
|
+
// outside this component.
|
|
108
|
+
const dock = createChapterNavDock(() => hasChapterNav);
|
|
109
|
+
|
|
110
|
+
// Task 26 — phone disclosure state. See MENU_BUTTON below for why the
|
|
111
|
+
// links collapse at all. `$props.id()` gives the panel a per-instance id
|
|
112
|
+
// so `aria-controls` still resolves when a page renders more than one
|
|
113
|
+
// header (a showcase or a docs page embedding the component).
|
|
114
|
+
const uid = $props.id();
|
|
115
|
+
const panelId = `${uid}-links`;
|
|
116
|
+
let menuOpen = $state(false);
|
|
117
|
+
let menuButton = $state<HTMLButtonElement | null>(null);
|
|
118
|
+
let linksEl = $state<HTMLElement | null>(null);
|
|
119
|
+
|
|
120
|
+
// Closing the panel while focus is still inside it would drop focus onto
|
|
121
|
+
// <body>, restarting keyboard navigation from the top of the page. Every
|
|
122
|
+
// close path goes through here so that cannot happen — Escape, a click
|
|
123
|
+
// outside, and the breakpoint crossing below.
|
|
124
|
+
function closeMenu() {
|
|
125
|
+
if (linksEl?.contains(document.activeElement)) {
|
|
126
|
+
const button = menuButton;
|
|
127
|
+
button?.focus();
|
|
128
|
+
// A pointerdown-driven close is not finished when this runs: the
|
|
129
|
+
// browser's own mousedown focus handling comes next and overwrites
|
|
130
|
+
// what we just set — with the clicked element if it is focusable, or
|
|
131
|
+
// with <body> if it is not. Reclaim focus on the following task, but
|
|
132
|
+
// only from <body>: that is the actual defect (keyboard navigation
|
|
133
|
+
// restarting from the top of the document), and a real click on a
|
|
134
|
+
// real control should keep the focus it earned.
|
|
135
|
+
setTimeout(() => {
|
|
136
|
+
if (document.activeElement === document.body) button?.focus();
|
|
137
|
+
}, 0);
|
|
138
|
+
}
|
|
139
|
+
menuOpen = false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Dismissal, matching assets/js/jelinek.js's own initHeaderMenu().
|
|
143
|
+
// `pointerdown`, not `click`: iOS Safari does not propagate a click from an
|
|
144
|
+
// element that is not itself clickable up to `document`, so a tap on body
|
|
145
|
+
// text would leave the panel open. Listeners exist only while the menu is
|
|
146
|
+
// open — nothing is attached on a desktop render, or on a phone render with
|
|
147
|
+
// the menu shut.
|
|
148
|
+
$effect(() => {
|
|
149
|
+
if (!menuOpen) return;
|
|
150
|
+
function onPointerDown(event: PointerEvent) {
|
|
151
|
+
const target = event.target as Node | null;
|
|
152
|
+
if (target && (linksEl?.contains(target) || menuButton?.contains(target))) return;
|
|
153
|
+
closeMenu();
|
|
154
|
+
}
|
|
155
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
156
|
+
if (event.key !== 'Escape') return;
|
|
157
|
+
menuButton?.focus();
|
|
158
|
+
menuOpen = false;
|
|
159
|
+
}
|
|
160
|
+
document.addEventListener('pointerdown', onPointerDown);
|
|
161
|
+
document.addEventListener('keydown', onKeyDown);
|
|
162
|
+
return () => {
|
|
163
|
+
document.removeEventListener('pointerdown', onPointerDown);
|
|
164
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// From 640px up the button is display:none and the links are back in the
|
|
169
|
+
// row. Leaving `menuOpen` true across that boundary means the panel springs
|
|
170
|
+
// open again unasked on the way back down, and `aria-expanded="true"`
|
|
171
|
+
// lingers on a hidden button in between.
|
|
172
|
+
$effect(() => {
|
|
173
|
+
const wide = window.matchMedia('(min-width: 640px)');
|
|
174
|
+
// Plain write, not closeMenu(), for the mount case: on mount nothing can
|
|
175
|
+
// have focus inside a panel that has never been open, and calling
|
|
176
|
+
// closeMenu() here would read `linksEl`/`menuButton` inside the effect
|
|
177
|
+
// body and make this effect re-run every time those bindings settle.
|
|
178
|
+
if (wide.matches) menuOpen = false;
|
|
179
|
+
function onChange() {
|
|
180
|
+
if (wide.matches) closeMenu();
|
|
181
|
+
}
|
|
182
|
+
wide.addEventListener('change', onChange);
|
|
183
|
+
return () => wide.removeEventListener('change', onChange);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// jelinek.css:442-476 (.site-header and its descendants) plus the dark
|
|
187
|
+
// treatment at :276-278. Per direction, the guide is reproduced as this
|
|
188
|
+
// component's own default — floating positioning, blur and all — not
|
|
189
|
+
// softened into something a consumer must opt into. A consumer who needs
|
|
190
|
+
// different positioning has `class` (merged last via cn()) to override
|
|
191
|
+
// `absolute`/`fixed`/`top-0`/etc.
|
|
192
|
+
//
|
|
193
|
+
// The `min-[620.02px]:` epsilon this file used to carry is GONE, and has been
|
|
194
|
+
// since Task 26: it only ever existed to convert the guide's INCLUSIVE
|
|
195
|
+
// `@media (max-width: 620px)` into a mobile-first `min-width` without leaving
|
|
196
|
+
// a gap (Tailwind's `max-[620px]:` compiles to the exclusive
|
|
197
|
+
// `width < 620px`). The guide declares the phone layout unconditionally and
|
|
198
|
+
// enhances it in `@media (min-width: 640px)`, so `min-[640px]:` is an EXACT
|
|
199
|
+
// translation. Task 30 moved the boundary from 621 to 640, onto the
|
|
200
|
+
// designer's 640/1024/1440 ladder.
|
|
201
|
+
//
|
|
202
|
+
// TASK 33 — THE PHONE HEADER IS THE DESIGNER'S, AND IT IS A DIFFERENT SHAPE.
|
|
203
|
+
// Three of his commits land here (36354b9, 6750ee8, 74ef043, a430f9c) and
|
|
204
|
+
// they supersede this component's previous phone treatment wholesale:
|
|
205
|
+
//
|
|
206
|
+
// POSITION. `absolute inset-x-0 top-0 z-50`, not `sticky`. His 74ef043 made
|
|
207
|
+
// this change deliberately and says why: with the bar in flow (or sticky at
|
|
208
|
+
// the top) the hero's grey band started BELOW it, leaving a visible seam in
|
|
209
|
+
// the page background right above the bar. Absolute takes the bar out of
|
|
210
|
+
// flow so the band runs under it. The cost is that the bar scrolls away on a
|
|
211
|
+
// phone (it did not while it was sticky) and that the hero must now reserve
|
|
212
|
+
// `--header-h` at every width — both accepted with the design; PageHero and
|
|
213
|
+
// +layout.svelte carry the matching padding change.
|
|
214
|
+
//
|
|
215
|
+
// SIZE. There is no separate smaller phone bar any more. `--logo-mark` is
|
|
216
|
+
// 46px at EVERY width (it was 36px on a phone), the outer gutter is ZERO on a
|
|
217
|
+
// phone (`p-0`, so the bar spans the full viewport edge to edge) and 12px/24px
|
|
218
|
+
// from 640px up, and the inner padding is a flat `12px 24px` throughout. Bar
|
|
219
|
+
// height on a phone is therefore 46 + 2×12 + 1 (bottom border) = 71px, which
|
|
220
|
+
// is what `--header-h: 70px` reserves.
|
|
221
|
+
//
|
|
222
|
+
// FRAME. On a phone the bar is a full-width strip, not a floating card: no
|
|
223
|
+
// max-width (`max-w-none`), square corners, and only a BOTTOM hairline
|
|
224
|
+
// (`border-x-0 border-t-0`) — his a430f9c. From 640px up it becomes the
|
|
225
|
+
// floating card again: `max-w-page`, all four borders, `rounded-card`.
|
|
226
|
+
//
|
|
227
|
+
// INNER (.site-header__inner): flex gap computed from --logo-mark
|
|
228
|
+
// (`calc(var(--logo-mark) * 107 / 400)` — the mark's own right-paroh
|
|
229
|
+
// margin), centred with `mx-auto`, its OWN padding (a distinct box from the
|
|
230
|
+
// outer gutter — both are real), and the glass.
|
|
231
|
+
//
|
|
232
|
+
// Task 31 — THE GLASS IS THE SHARED TOKEN SET, not this component's own
|
|
233
|
+
// values: `background: var(--glass-fill); backdrop-filter:
|
|
234
|
+
// var(--glass-filter); border: 1px solid var(--glass-border)`, the same three
|
|
235
|
+
// tokens .glassmorphism, .chapter-nav__inner and .is-sticky use. Every value
|
|
236
|
+
// moved there (fill from a hand-written rgba pair per theme to one flipping
|
|
237
|
+
// token, filter from `blur(20px) brightness(150%)`/`brightness(40%)` to
|
|
238
|
+
// `blur(25px) saturate(140%)` in both themes, border from --border-subtle to
|
|
239
|
+
// the deliberately lighter --glass-border). See Glass.svelte for why the
|
|
240
|
+
// filter is written as an arbitrary property pair rather than
|
|
241
|
+
// backdrop-blur-*/backdrop-saturate-* (token tracking — a literal here would
|
|
242
|
+
// drift with no gate to catch it). The dark hairline got softer again in his
|
|
243
|
+
// 54082b9 (--glass-border 25% white in dark); that is inside the token, so
|
|
244
|
+
// nothing changes here.
|
|
245
|
+
//
|
|
246
|
+
// THE JOINED-PANEL SEAM IS NOW ≥640px ONLY. jelinek.css:611-628 moved the
|
|
247
|
+
// whole `:root:has(.chapter-nav):not(.is-subnav-scrolled)` block inside
|
|
248
|
+
// `@media (min-width: 640px)`, because on a phone there is nothing to join
|
|
249
|
+
// to: the chapter bar is hidden and the header is already a square-cornered
|
|
250
|
+
// full-width strip. So INNER_JOINED/INNER_DETACHED are min-[640px]-scoped,
|
|
251
|
+
// and the phone frame above is unconditional.
|
|
252
|
+
//
|
|
253
|
+
// The dark inset top highlight (`inset 0 2px 1px -1px var(--box-primary)`,
|
|
254
|
+
// :278) still applies in EITHER case at every width — see :627, where the
|
|
255
|
+
// dark joined case keeps this same inset instead of dropping to
|
|
256
|
+
// `shadow: none`. TASK 33: --box-primary is now a real passthrough token
|
|
257
|
+
// (his 4ec82cd promoted it to one neutral `:root` value, hsl(0,0%,50%), and
|
|
258
|
+
// deleted the `.site-header__inner { --box-primary: var(--JELINEK-BLACK) }`
|
|
259
|
+
// local this file used to substitute). The old stand-in,
|
|
260
|
+
// var(--color-jelinek-black), is now WRONG rather than equivalent:
|
|
261
|
+
// --JELINEK-BLACK inverts to hsl(0,0%,95%) in dark mode, so it would paint a
|
|
262
|
+
// near-white hairline where the guide asks for 50% grey.
|
|
263
|
+
//
|
|
264
|
+
// Space reservation below this header is still the consuming app's job —
|
|
265
|
+
// SiteHeader/ChapterNav never add `padding-top` to a sibling
|
|
266
|
+
// (`--header-h`/`--subnav-h` are exported passthrough tokens precisely so a
|
|
267
|
+
// consumer's own layout can do that). PageHero and the showcase's
|
|
268
|
+
// `+layout.svelte` model exactly that.
|
|
269
|
+
const OUTER =
|
|
270
|
+
'absolute inset-x-0 top-0 z-50 p-0 [--logo-mark:46px] ' +
|
|
271
|
+
'min-[640px]:fixed min-[640px]:px-6 min-[640px]:py-3';
|
|
272
|
+
const INNER_BASE =
|
|
273
|
+
'mx-auto flex max-w-none items-center gap-[calc(var(--logo-mark)*107/400)] ' +
|
|
274
|
+
'border border-x-0 border-t-0 border-[var(--glass-border)] rounded-none ' +
|
|
275
|
+
'bg-[var(--glass-fill)] px-6 py-3 shadow-resting ' +
|
|
276
|
+
'dark:shadow-[var(--shadow-resting),inset_0_2px_1px_-1px_var(--box-primary)] ' +
|
|
277
|
+
'[-webkit-backdrop-filter:var(--glass-filter)] [backdrop-filter:var(--glass-filter)] ' +
|
|
278
|
+
'min-[640px]:max-w-page min-[640px]:border-x min-[640px]:border-t ' +
|
|
279
|
+
'min-[640px]:rounded-card';
|
|
280
|
+
const INNER_JOINED =
|
|
281
|
+
'min-[640px]:rounded-b-none min-[640px]:border-b-0 min-[640px]:shadow-none ' +
|
|
282
|
+
'min-[640px]:dark:shadow-[inset_0_2px_1px_-1px_var(--box-primary)]';
|
|
283
|
+
const INNER_DETACHED = '';
|
|
284
|
+
const inner = $derived(cn(INNER_BASE, dock.docked ? INNER_JOINED : INNER_DETACHED));
|
|
285
|
+
|
|
286
|
+
// jelinek.css:444-445 (.site-header__logo, .site-header__logo img /
|
|
287
|
+
// .site-header__mark): inline-flex, no shrink/grow, line-height:0
|
|
288
|
+
// (leading-none), border-radius:radius-button, overflow hidden — the
|
|
289
|
+
// mark itself is a square var(--logo-mark) box.
|
|
290
|
+
const LOGO_LINK = 'inline-flex flex-none leading-none rounded-button overflow-hidden';
|
|
291
|
+
const MARK = 'block h-[var(--logo-mark)] w-[var(--logo-mark)]';
|
|
292
|
+
|
|
293
|
+
// jelinek.css:447 (.site-header__title): font-primary (GT Walsheim),
|
|
294
|
+
// weight 700, line-height 1, size proportional to the mark
|
|
295
|
+
// (calc(logo-mark * 71/170)), letter-spacing -0.01em. The brief's draft
|
|
296
|
+
// dropped this text entirely and rendered only the subtitle — the real
|
|
297
|
+
// markup (partials/header.html:5) is always `JELÍNEK<span>{subtitle}</span>`,
|
|
298
|
+
// the brand name is never itself a prop (every real usage renders the
|
|
299
|
+
// same literal word), so it's hardcoded here and only the trailing
|
|
300
|
+
// <span> is conditional on the `subtitle` prop. No whitespace is placed
|
|
301
|
+
// between "JELÍNEK" and the {#if} block in the markup below — Svelte
|
|
302
|
+
// collapses template whitespace, and the source has none there either
|
|
303
|
+
// (the visual line break between them comes entirely from the span's
|
|
304
|
+
// own `display:block`, not from a text-level space).
|
|
305
|
+
//
|
|
306
|
+
// jelinek.css:448-454 (.site-header__title span, the subtitle): colour
|
|
307
|
+
// text-muted1, an explicit "Avenir Next LT Pro"/"Avenir Next" stack —
|
|
308
|
+
// NOT the --font-secondary token (which currently also resolves to GT
|
|
309
|
+
// Walsheim Pro in this guide's fonts, see tokens.css) — kept as a literal
|
|
310
|
+
// inline `style`, same technique Tag.svelte uses for its own
|
|
311
|
+
// custom-property values, since Tailwind's bracket syntax for a
|
|
312
|
+
// comma-separated, quoted font stack is fragile. Weight 600 (Avenir
|
|
313
|
+
// Demi), size calc(logo-mark*35/170), tracking 0.064em, uppercase,
|
|
314
|
+
// margin-top 0.3em, line-height 1.1, display block.
|
|
315
|
+
//
|
|
316
|
+
// Task 26 added `min-w-0` here, reproducing the guide's
|
|
317
|
+
// `:root.has-js .site-header__title { min-width: 0 }`. TASK 33 REMOVES IT,
|
|
318
|
+
// because the guide's rule is gone with the layout that needed it: it existed
|
|
319
|
+
// only so the lockup could shrink instead of pushing a `flex-nowrap` row of
|
|
320
|
+
// three link pills plus a theme toggle onto a second line. The phone row now
|
|
321
|
+
// holds the lockup and a 44px hamburger and nothing else — measured at a
|
|
322
|
+
// 320px viewport: 46px mark + 12px gap + ~75px min-content lockup + 44px
|
|
323
|
+
// button + 48px inner padding = 225px, with 95px to spare — so nothing needs
|
|
324
|
+
// to be crushed, and keeping `min-w-0` would shrink the lockup for no reason
|
|
325
|
+
// on the narrowest phones. Verify at 320px if the burger ever grows a label.
|
|
326
|
+
const TITLE = 'font-primary text-[calc(var(--logo-mark)*71/170)] leading-none font-bold tracking-[-0.01em]';
|
|
327
|
+
const SUBTITLE =
|
|
328
|
+
'mt-[0.3em] block text-[calc(var(--logo-mark)*35/170)] leading-[1.1] font-semibold ' +
|
|
329
|
+
'tracking-[0.064em] text-fg-muted1 uppercase';
|
|
330
|
+
const SUBTITLE_STYLE = "font-family: 'Avenir Next LT Pro', 'Avenir Next', Arial, sans-serif;";
|
|
331
|
+
|
|
332
|
+
// TASK 33 — THE PHONE MENU IS THE DESIGNER'S HAMBURGER PANEL, AND THE NAV
|
|
333
|
+
// ITEMS ARE BUTTONS. Both halves are his (6750ee8, 36354b9) and both replace
|
|
334
|
+
// what this component did before, rather than sitting alongside it.
|
|
335
|
+
//
|
|
336
|
+
// WHY IT EXISTS AT ALL. Below 640px the top-level links do not fit beside the
|
|
337
|
+
// mark. Measured in the guide, in-browser, at the real font: the three pills
|
|
338
|
+
// come to 273px and the theme toggle to another 60px, against 270px of usable
|
|
339
|
+
// width inside the bar at a 320px viewport. There is no one-row arrangement,
|
|
340
|
+
// so the links collapse — that finding (audit M3) still stands; only the
|
|
341
|
+
// mechanism changed.
|
|
342
|
+
//
|
|
343
|
+
// WHAT REPLACED THE "Menu" BUTTON + DROPDOWN. His version is a labelless
|
|
344
|
+
// 44px hamburger (three 2px bars that fold into a cross) and a FULL-WIDTH
|
|
345
|
+
// GLASS PANEL hanging off the bottom edge of the bar, not a 200px solid
|
|
346
|
+
// dropdown inset inside it. This component's previous panel was a deliberate
|
|
347
|
+
// deviation — a solid `bg-surface` sheet, because the header's own 60%-white
|
|
348
|
+
// glass left links unreadable over page text. That deviation is dropped: he
|
|
349
|
+
// solved the same problem differently, by hanging the panel off the OUTER
|
|
350
|
+
// header rather than nesting it inside the glass bar (glass inside glass has
|
|
351
|
+
// nothing to blur, so content showed through sharp), and by using the shared
|
|
352
|
+
// --glass-fill, which is a heavier 50%/60% fill than the value that was
|
|
353
|
+
// unreadable. His arrangement is the live one and it is his call.
|
|
354
|
+
//
|
|
355
|
+
// `display: contents` IS GONE TOO. It was how the panel dissolved back into
|
|
356
|
+
// the nav row from 640px up. His CSS instead styles the `<nav>` ITSELF as the
|
|
357
|
+
// panel and returns it to a plain `position: static` flex row at 640px, so
|
|
358
|
+
// there is no wrapper element to dissolve. That is why NAV below carries two
|
|
359
|
+
// completely different rule sets rather than a wrapper plus a modifier.
|
|
360
|
+
//
|
|
361
|
+
// HIS REPARENTING IS REPRODUCED AS TWO ELEMENTS, NOT AS DOM SURGERY, and this
|
|
362
|
+
// is the one place the kit's shape differs from his markup. His script moves
|
|
363
|
+
// the single `<nav>` between `.site-header__inner` (≥640px) and
|
|
364
|
+
// `.site-header` (phone) on a matchMedia change, and that move is LOAD-
|
|
365
|
+
// BEARING, not tidiness: `.site-header__inner` has `backdrop-filter`, which
|
|
366
|
+
// makes it a BACKDROP ROOT, and a `backdrop-filter` descendant of a backdrop
|
|
367
|
+
// root has nothing left to filter. MEASURED, after a first attempt that
|
|
368
|
+
// nested the panel inside `__inner` and reasoned it would be fine because
|
|
369
|
+
// `backdrop-filter` also makes an element a containing block: at 375px, over
|
|
370
|
+
// scrolled page content, the nested panel rendered with NO blur AND NO
|
|
371
|
+
// visible fill — page text read straight through the links. Moving that exact
|
|
372
|
+
// same element onto `<header>` in the same page, with no CSS change, restored
|
|
373
|
+
// both. His comment says as much ("sklo vnořené ve skle nemá co rozostřovat")
|
|
374
|
+
// and it is right.
|
|
375
|
+
//
|
|
376
|
+
// A component cannot do the move itself. Svelte owns this subtree; relocating
|
|
377
|
+
// a node it created out from under its `{#if}` block leaves the block's
|
|
378
|
+
// first/last node range pointing into a sibling chain the node has left, so
|
|
379
|
+
// teardown either misses it or throws. So the panel and the row are two
|
|
380
|
+
// separate `<nav>` elements in their two correct parents, and only ever ONE of
|
|
381
|
+
// them is displayed:
|
|
382
|
+
// - the ROW lives inside `__inner`, `hidden` below 640px;
|
|
383
|
+
// - the PANEL is a sibling of `__inner`, hidden FROM 640px up.
|
|
384
|
+
// The `display: none` one is out of the accessibility tree entirely, so there
|
|
385
|
+
// is exactly one nav landmark at any width, and the link markup is not
|
|
386
|
+
// duplicated either — both render the same `{#snippet navLinks}`.
|
|
387
|
+
//
|
|
388
|
+
// NO `.has-js` EQUIVALENT, and that is deliberate rather than an omission.
|
|
389
|
+
// The guide gates its whole collapse on a `.has-js` class its inline header
|
|
390
|
+
// script sets, so a visitor without JS keeps the links in the bar instead of
|
|
391
|
+
// behind a button that cannot open — this merge added that guard to the
|
|
392
|
+
// guide, since his version locked the menu shut without JS. A Svelte
|
|
393
|
+
// component only renders interactively where JS runs, so there is nothing to
|
|
394
|
+
// guard here. A consumer server-rendering this kit and caring about the no-JS
|
|
395
|
+
// case must mirror the guide's own `.has-js` rules.
|
|
396
|
+
//
|
|
397
|
+
// PANEL (the phone half of jelinek.css:498-509): `absolute`, hung off
|
|
398
|
+
// `top-full` on the outer `<header>` (which is `absolute`, so it is the
|
|
399
|
+
// containing block), full viewport width, column layout, 12px padding,
|
|
400
|
+
// bottom-rounded, the shared glass, the resting shadow.
|
|
401
|
+
//
|
|
402
|
+
// ROW (the ≥640px half, jelinek.css:589-596): a plain right-aligned flex row
|
|
403
|
+
// with an 8px gap and none of the panel chrome. It carries no phone styling at
|
|
404
|
+
// all — it is simply not displayed there — which is why this is two constants
|
|
405
|
+
// rather than one with a pile of `min-[640px]:` resets.
|
|
406
|
+
// Task 37 — THE PANEL HAS TO SCROLL, and until now it silently could not.
|
|
407
|
+
// It is `absolute`, so its height is whatever its content needs and nothing
|
|
408
|
+
// clips it. With three top-level links, ten chapters and the theme toggle it
|
|
409
|
+
// measured 674px tall on a 375×720 phone, starting at y=71 — bottom edge at
|
|
410
|
+
// 745, i.e. the toggle and the last chapter were 25px BELOW the fold with no
|
|
411
|
+
// way to reach them: the page behind cannot scroll the panel, because the
|
|
412
|
+
// panel is not part of its flow.
|
|
413
|
+
//
|
|
414
|
+
// `max-h` is the space between the header and the bottom of the viewport
|
|
415
|
+
// (minus 12px so it never sits flush against the edge), and `overflow-y-auto`
|
|
416
|
+
// makes the surplus reachable. `dvh` rather than `vh` because on iOS Safari
|
|
417
|
+
// `vh` is the tallest-possible viewport, which is exactly the amount by which
|
|
418
|
+
// the URL bar would hide the end of the list. `overscroll-contain` keeps a
|
|
419
|
+
// flick at the end of the list from scrolling the page underneath instead.
|
|
420
|
+
const NAV_PANEL =
|
|
421
|
+
'absolute top-full inset-x-0 z-[48] flex-col items-stretch gap-1 p-3 ' +
|
|
422
|
+
'rounded-b-card border border-x-0 border-t-0 border-[var(--glass-border)] ' +
|
|
423
|
+
'bg-[var(--glass-fill)] shadow-resting min-[640px]:hidden ' +
|
|
424
|
+
'max-h-[calc(100dvh_-_var(--header-h)_-_12px)] overflow-y-auto overscroll-contain ' +
|
|
425
|
+
'[-webkit-backdrop-filter:var(--glass-filter)] [backdrop-filter:var(--glass-filter)]';
|
|
426
|
+
const NAV_ROW = 'hidden ml-auto items-center gap-2 min-[640px]:flex';
|
|
427
|
+
const navPanel = $derived(cn(NAV_PANEL, menuOpen ? 'flex' : 'hidden'));
|
|
428
|
+
|
|
429
|
+
// NAV LINKS (jelinek.css:472 + partials/header.html:6-8). His 36354b9 turned
|
|
430
|
+
// them into TERTIARY BUTTONS: the markup is literally
|
|
431
|
+
// `class="btn btn--ghost btn--black"` with `is-active` for the current page,
|
|
432
|
+
// so appearance, hover and the full-fill active state all come from the
|
|
433
|
+
// button system now. The previous bespoke pill (`rounded-pill px-3.5 py-2`,
|
|
434
|
+
// `bg-surface-black` when active) is deleted rather than kept — reproducing it
|
|
435
|
+
// alongside would leave the kit with a nav-link style the guide no longer has.
|
|
436
|
+
//
|
|
437
|
+
// Rendered through this kit's own <Button href variant="ghost" color="black">
|
|
438
|
+
// for the same reason: it IS `.btn--ghost.btn--black`, and duplicating those
|
|
439
|
+
// classes by hand here is exactly the drift the css-lock gate exists to
|
|
440
|
+
// catch. `active` maps to `is-active`.
|
|
441
|
+
//
|
|
442
|
+
// The three declarations left in the guide's own rule are the size trim, and
|
|
443
|
+
// they are passed as the external `class` (cn() merges it last, so it beats
|
|
444
|
+
// Button's own `px-[26px] py-[11px] text-[0.98rem]`): `padding: 9px
|
|
445
|
+
// var(--space-2)` → `px-4 py-[9px]`, `font-size: 0.95rem`, and
|
|
446
|
+
// `text-align: left` — which is invisible on a content-width row but does
|
|
447
|
+
// matter in the phone panel, where `items-stretch` makes every link
|
|
448
|
+
// full-width.
|
|
449
|
+
// `justify-start` carries the guide's `text-align: left` across this kit's
|
|
450
|
+
// one documented Button deviation: the guide's `.btn` is `display:
|
|
451
|
+
// inline-block; text-align: center`, Button.svelte is `inline-flex ...
|
|
452
|
+
// justify-center` (so it can hold an icon), and on a flex box
|
|
453
|
+
// `text-align` no longer moves the label — `justify-*` does. Both are
|
|
454
|
+
// invisible on the ≥640px row, where the button is content-width; it only
|
|
455
|
+
// shows in the panel, where `items-stretch` makes every entry full-width.
|
|
456
|
+
// `leading-[1.2]` is re-asserted here even though Button's BASE already has
|
|
457
|
+
// it: tailwind-merge treats a font-size utility as also owning line-height
|
|
458
|
+
// (Tailwind's text-* sizes set both), so the `text-[0.95rem]` override
|
|
459
|
+
// silently strips Button's `leading-[1.2]` and the link falls back to the
|
|
460
|
+
// inherited 1.6 — six extra pixels of height vs. the guide's header.
|
|
461
|
+
const NAV_LINK = 'justify-start px-4 py-[9px] text-[0.95rem] leading-[1.2] text-left';
|
|
462
|
+
|
|
463
|
+
// BURGER (jelinek.css:481-497). A 44px square — his comment calls it out as
|
|
464
|
+
// a comfortable finger target — holding three 2px bars with a 5px gap, no
|
|
465
|
+
// border, no background. Open, the outer two rotate 45° into a cross and the
|
|
466
|
+
// middle one fades out. `ml-auto` pushes it to the right of the lockup, the
|
|
467
|
+
// job the nav row's own `ml-auto` does from 640px up. Hidden from 640px up.
|
|
468
|
+
//
|
|
469
|
+
// It has NO VISIBLE LABEL — the previous version's "Menu" text is gone with
|
|
470
|
+
// his design — so `aria-label` is the only accessible name it has, and it
|
|
471
|
+
// changes with state exactly as his script changes it (`Otevřít menu` /
|
|
472
|
+
// `Zavřít menu`, the `menuLabel`/`menuLabelClose` props).
|
|
473
|
+
const BURGER =
|
|
474
|
+
'ml-auto inline-flex h-11 w-11 flex-none cursor-pointer flex-col justify-center gap-[5px] ' +
|
|
475
|
+
'rounded-button border-0 bg-transparent px-2.5 min-[640px]:hidden';
|
|
476
|
+
const BAR =
|
|
477
|
+
'block h-0.5 rounded-[2px] bg-fg ' +
|
|
478
|
+
'transition-[transform,opacity] duration-[var(--dur-base)] ease-[var(--ease)]';
|
|
479
|
+
const bar1 = $derived(cn(BAR, menuOpen && 'translate-y-[7px] rotate-45'));
|
|
480
|
+
const bar2 = $derived(cn(BAR, menuOpen && 'opacity-0'));
|
|
481
|
+
const bar3 = $derived(cn(BAR, menuOpen && '-translate-y-[7px] -rotate-45'));
|
|
482
|
+
|
|
483
|
+
// CHAPTERS INSIDE THE PANEL (jelinek.css:504-509, `.site-menu__chapters`).
|
|
484
|
+
// The chapter bar is `display: none` below 640px now (ChapterNav.svelte), so
|
|
485
|
+
// without this a phone reader has no route to the chapters at all. The guide
|
|
486
|
+
// solves it in script, by cloning the anchors out of the rendered
|
|
487
|
+
// `.chapter-nav__inner` and appending them to the panel; the kit takes them as
|
|
488
|
+
// the `chapterItems` prop instead — same content, declared rather than
|
|
489
|
+
// cloned, and it works under SSR where there is no rendered bar to read.
|
|
490
|
+
//
|
|
491
|
+
// A 1px divider above, 8px of separation, 2px between entries, and a smaller
|
|
492
|
+
// size trim on the buttons (`font-size: 0.9rem; padding: 7px var(--space-2)`).
|
|
493
|
+
// Hidden from 640px up, where the bar itself is back.
|
|
494
|
+
const CHAPTERS =
|
|
495
|
+
'mt-2 flex flex-col gap-0.5 border-t border-[var(--glass-border)] pt-2 min-[640px]:hidden';
|
|
496
|
+
// Same leading-[1.2] re-assertion as NAV_LINK — text-[0.9rem] would strip it.
|
|
497
|
+
const CHAPTER_LINK = 'justify-start px-4 py-[7px] text-[0.9rem] leading-[1.2] text-left';
|
|
498
|
+
|
|
499
|
+
// The `actions` block inside the phone panel. Its own divider, matching the
|
|
500
|
+
// one CHAPTERS draws above the chapter list, so the toggle reads as a separate
|
|
501
|
+
// control rather than as one more menu entry. Left-aligned with the links.
|
|
502
|
+
const PANEL_ACTIONS =
|
|
503
|
+
'mt-2 flex items-center border-t border-[var(--glass-border)] px-4 pt-3 min-[640px]:hidden';
|
|
504
|
+
|
|
505
|
+
// The theme toggle (and anything else that belongs in the nav) arrives
|
|
506
|
+
// through the `actions` snippet — see its prop doc for why this stopped being
|
|
507
|
+
// "render it yourself outside the component".
|
|
508
|
+
</script>
|
|
509
|
+
|
|
510
|
+
<!-- One definition of the top-level links, rendered into both navs. `linkClass`
|
|
511
|
+
exists only so a future caller of this snippet can trim differently; today
|
|
512
|
+
both call sites pass NAV_LINK. -->
|
|
513
|
+
{#snippet navLinks(linkClass: string)}
|
|
514
|
+
{#each items as item (item.href)}
|
|
515
|
+
<Button
|
|
516
|
+
href={item.href}
|
|
517
|
+
variant="ghost"
|
|
518
|
+
color="black"
|
|
519
|
+
active={isActive(item, active)}
|
|
520
|
+
aria-current={isActive(item, active) ? 'page' : undefined}
|
|
521
|
+
class={linkClass}
|
|
522
|
+
>
|
|
523
|
+
{item.label}
|
|
524
|
+
</Button>
|
|
525
|
+
{/each}
|
|
526
|
+
{/snippet}
|
|
527
|
+
|
|
528
|
+
<header {...rest} class={cn(OUTER, className)}>
|
|
529
|
+
<div class={inner}>
|
|
530
|
+
<a href={logoHref} aria-label={logoAlt} class={LOGO_LINK}>
|
|
531
|
+
{#if logo}
|
|
532
|
+
{@render logo()}
|
|
533
|
+
{:else}
|
|
534
|
+
<!-- Path data from the shared deer-mark-path.js module (Task 23 —
|
|
535
|
+
extracted so this and ContentLogo.svelte's own copy of the
|
|
536
|
+
SVG mark can't silently drift apart; see that module's own
|
|
537
|
+
comment for the verification that both were byte-identical to
|
|
538
|
+
partials/header.html:4 before the extraction). The source
|
|
539
|
+
re-themes via inline style="fill:var(--JELINEK-BLACK)" (plus a
|
|
540
|
+
static fill="#000" fallback attribute); reproduced here with the
|
|
541
|
+
kit's own fill-jelinek-black/fill-jelinek-white utilities
|
|
542
|
+
instead — they resolve to the exact same
|
|
543
|
+
var(--JELINEK-BLACK)/var(--JELINEK-WHITE), and every other
|
|
544
|
+
token-driven colour in this kit goes through a Tailwind utility
|
|
545
|
+
rather than an inline style, so the static fallback attribute
|
|
546
|
+
(meaningful only without CSS custom-property support) is dropped
|
|
547
|
+
as unneeded for this target stack. -->
|
|
548
|
+
<svg
|
|
549
|
+
class={MARK}
|
|
550
|
+
viewBox="0 0 400 400"
|
|
551
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
552
|
+
aria-hidden="true"
|
|
553
|
+
>
|
|
554
|
+
<rect width="400" height="400" class="fill-jelinek-black" />
|
|
555
|
+
<path d={DEER_MARK_PATH} class="fill-jelinek-white" />
|
|
556
|
+
</svg>
|
|
557
|
+
{/if}
|
|
558
|
+
</a>
|
|
559
|
+
<div class={TITLE}>JELÍNEK{#if subtitle}<span class={SUBTITLE} style={SUBTITLE_STYLE}>{subtitle}</span>{/if}</div>
|
|
560
|
+
{#if items.length > 0}
|
|
561
|
+
<!-- The burger precedes the panel in source order so Tab from it lands on
|
|
562
|
+
the first link it just revealed, and because that is the order in
|
|
563
|
+
partials/header.html:6-7. -->
|
|
564
|
+
<button
|
|
565
|
+
type="button"
|
|
566
|
+
bind:this={menuButton}
|
|
567
|
+
class={BURGER}
|
|
568
|
+
aria-expanded={menuOpen}
|
|
569
|
+
aria-controls={panelId}
|
|
570
|
+
aria-label={menuOpen ? menuLabelClose : menuLabel}
|
|
571
|
+
onclick={() => (menuOpen = !menuOpen)}
|
|
572
|
+
>
|
|
573
|
+
<span class={bar1}></span>
|
|
574
|
+
<span class={bar2}></span>
|
|
575
|
+
<span class={bar3}></span>
|
|
576
|
+
</button>
|
|
577
|
+
<!-- The ≥640px row. Inside the glass bar, so it is a flex item of it. -->
|
|
578
|
+
<nav class={NAV_ROW} aria-label={navLabel}>
|
|
579
|
+
{@render navLinks(NAV_LINK)}
|
|
580
|
+
{#if actions}{@render actions()}{/if}
|
|
581
|
+
</nav>
|
|
582
|
+
{/if}
|
|
583
|
+
</div>
|
|
584
|
+
{#if items.length > 0}
|
|
585
|
+
<!-- The phone panel. A SIBLING of the glass bar, not a child — see
|
|
586
|
+
NAV_PANEL's comment for the measurement that forced that. Hidden from
|
|
587
|
+
640px up, so only one of the two navs is ever in the a11y tree. -->
|
|
588
|
+
<nav id={panelId} bind:this={linksEl} class={navPanel} aria-label={navLabel}>
|
|
589
|
+
{@render navLinks(NAV_LINK)}
|
|
590
|
+
{#if chapterItems.length > 0}
|
|
591
|
+
<div class={CHAPTERS}>
|
|
592
|
+
{#each chapterItems as chapter (chapter.href)}
|
|
593
|
+
<Button
|
|
594
|
+
href={chapter.href}
|
|
595
|
+
variant="ghost"
|
|
596
|
+
color="black"
|
|
597
|
+
active={isActive(chapter, active)}
|
|
598
|
+
aria-current={isActive(chapter, active) ? 'page' : undefined}
|
|
599
|
+
class={CHAPTER_LINK}
|
|
600
|
+
>
|
|
601
|
+
{chapter.label}
|
|
602
|
+
</Button>
|
|
603
|
+
{/each}
|
|
604
|
+
</div>
|
|
605
|
+
{/if}
|
|
606
|
+
<!-- Last in the panel, as in the guide's nav: on a phone the toggle is
|
|
607
|
+
inside the burger menu, not floating in the page. -->
|
|
608
|
+
{#if actions}
|
|
609
|
+
<div class={PANEL_ACTIONS}>{@render actions()}</div>
|
|
610
|
+
{/if}
|
|
611
|
+
</nav>
|
|
612
|
+
{/if}
|
|
613
|
+
</header>
|