@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.
Files changed (84) hide show
  1. package/README.md +492 -0
  2. package/dist/components/Alert.svelte +59 -0
  3. package/dist/components/Alert.svelte.d.ts +13 -0
  4. package/dist/components/Button.svelte +178 -0
  5. package/dist/components/Button.svelte.d.ts +19 -0
  6. package/dist/components/Card.svelte +68 -0
  7. package/dist/components/Card.svelte.d.ts +11 -0
  8. package/dist/components/ChapterNav.svelte +290 -0
  9. package/dist/components/ChapterNav.svelte.d.ts +18 -0
  10. package/dist/components/Checkbox.svelte +52 -0
  11. package/dist/components/Checkbox.svelte.d.ts +11 -0
  12. package/dist/components/CodeCopy.svelte +294 -0
  13. package/dist/components/CodeCopy.svelte.d.ts +51 -0
  14. package/dist/components/Container.svelte +34 -0
  15. package/dist/components/Container.svelte.d.ts +10 -0
  16. package/dist/components/ContentLogo.svelte +151 -0
  17. package/dist/components/ContentLogo.svelte.d.ts +37 -0
  18. package/dist/components/Demo.svelte +111 -0
  19. package/dist/components/Demo.svelte.d.ts +12 -0
  20. package/dist/components/Eyebrow.svelte +70 -0
  21. package/dist/components/Eyebrow.svelte.d.ts +12 -0
  22. package/dist/components/Glass.svelte +75 -0
  23. package/dist/components/Glass.svelte.d.ts +10 -0
  24. package/dist/components/Grid.svelte +87 -0
  25. package/dist/components/Grid.svelte.d.ts +11 -0
  26. package/dist/components/Label.svelte +37 -0
  27. package/dist/components/Label.svelte.d.ts +11 -0
  28. package/dist/components/PageHero.svelte +312 -0
  29. package/dist/components/PageHero.svelte.d.ts +55 -0
  30. package/dist/components/Pager.svelte +98 -0
  31. package/dist/components/Pager.svelte.d.ts +13 -0
  32. package/dist/components/Panel.svelte +89 -0
  33. package/dist/components/Panel.svelte.d.ts +12 -0
  34. package/dist/components/ProductCard.svelte +81 -0
  35. package/dist/components/ProductCard.svelte.d.ts +18 -0
  36. package/dist/components/Radio.svelte +54 -0
  37. package/dist/components/Radio.svelte.d.ts +11 -0
  38. package/dist/components/RadioGroup.svelte +42 -0
  39. package/dist/components/RadioGroup.svelte.d.ts +12 -0
  40. package/dist/components/Select.svelte +65 -0
  41. package/dist/components/Select.svelte.d.ts +20 -0
  42. package/dist/components/SiteFooter.svelte +121 -0
  43. package/dist/components/SiteFooter.svelte.d.ts +13 -0
  44. package/dist/components/SiteHeader.svelte +613 -0
  45. package/dist/components/SiteHeader.svelte.d.ts +80 -0
  46. package/dist/components/Slider.svelte +55 -0
  47. package/dist/components/Slider.svelte.d.ts +14 -0
  48. package/dist/components/Swatch.svelte +174 -0
  49. package/dist/components/Swatch.svelte.d.ts +58 -0
  50. package/dist/components/Tab.svelte +88 -0
  51. package/dist/components/Tab.svelte.d.ts +11 -0
  52. package/dist/components/Tabs.svelte +306 -0
  53. package/dist/components/Tabs.svelte.d.ts +20 -0
  54. package/dist/components/Tag.svelte +71 -0
  55. package/dist/components/Tag.svelte.d.ts +16 -0
  56. package/dist/components/TextField.svelte +100 -0
  57. package/dist/components/TextField.svelte.d.ts +18 -0
  58. package/dist/components/ThemeToggle.svelte +122 -0
  59. package/dist/components/ThemeToggle.svelte.d.ts +11 -0
  60. package/dist/components/Tile.svelte +115 -0
  61. package/dist/components/Tile.svelte.d.ts +14 -0
  62. package/dist/components/TileScroller.svelte +199 -0
  63. package/dist/components/TileScroller.svelte.d.ts +25 -0
  64. package/dist/components/container-context.d.ts +1 -0
  65. package/dist/components/container-context.js +6 -0
  66. package/dist/components/deer-mark-path.d.ts +16 -0
  67. package/dist/components/deer-mark-path.js +16 -0
  68. package/dist/components/nav.d.ts +25 -0
  69. package/dist/components/nav.js +17 -0
  70. package/dist/components/radio-context.d.ts +6 -0
  71. package/dist/components/radio-context.js +1 -0
  72. package/dist/components/tabs-context.d.ts +5 -0
  73. package/dist/components/tabs-context.js +1 -0
  74. package/dist/index.d.ts +37 -0
  75. package/dist/index.js +37 -0
  76. package/dist/theme/base.css +439 -0
  77. package/dist/theme/extras.css +2 -0
  78. package/dist/theme/fonts.css +107 -0
  79. package/dist/theme/tokens.css +208 -0
  80. package/dist/utils/chapter-nav-dock.svelte.d.ts +23 -0
  81. package/dist/utils/chapter-nav-dock.svelte.js +65 -0
  82. package/dist/utils/cn.d.ts +2 -0
  83. package/dist/utils/cn.js +27 -0
  84. package/package.json +79 -0
@@ -0,0 +1,178 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
4
+ import { cn } from '../utils/cn.js';
5
+
6
+ type Variant = 'primary' | 'secondary' | 'ghost';
7
+ type Color = 'brown' | 'magenta' | 'black';
8
+
9
+ interface Props {
10
+ variant?: Variant;
11
+ color?: Color;
12
+ href?: string;
13
+ disabled?: boolean;
14
+ active?: boolean;
15
+ fullWidth?: boolean;
16
+ class?: string;
17
+ onclick?: (event: MouseEvent) => void;
18
+ children: Snippet;
19
+ }
20
+
21
+ let {
22
+ variant = 'primary',
23
+ color = 'brown',
24
+ href,
25
+ disabled = false,
26
+ active = false,
27
+ fullWidth = false,
28
+ class: className,
29
+ onclick,
30
+ children,
31
+ ...rest
32
+ }: Props &
33
+ Omit<
34
+ HTMLButtonAttributes & HTMLAnchorAttributes,
35
+ 'href' | 'disabled' | 'onclick'
36
+ > = $props();
37
+
38
+ // jelinek.css:734-741 fixes the button's type size, line height and padding
39
+ // to exact pixel/rem values, not the modular type scale or 8pt spacing grid.
40
+ // Fidelity to the guide beats token purity for this component — do not
41
+ // "tidy" text-[0.98rem]/leading-[1.2]/px-[26px]/py-[11px] back to scale
42
+ // tokens.
43
+ //
44
+ // Task 31 — the transition is now the guide's OWN property list and its own
45
+ // duration/easing tokens (jelinek.css:739: `transition: background
46
+ // var(--dur-base) var(--ease), color …, border-color …, box-shadow …`),
47
+ // replacing a bare `transition-all`. Two separate corrections in one:
48
+ // - PROPERTY LIST. `transition-all` animates every animatable property,
49
+ // including ones the guide deliberately leaves alone. The guide names
50
+ // four; so does this now.
51
+ // - EASING, and this one was a real (if invisible) mismatch even before
52
+ // the merge. Tailwind's default timing function is
53
+ // `cubic-bezier(0.4, 0, 0.2, 1)` (verified in
54
+ // node_modules/tailwindcss/theme.css's --default-transition-timing-
55
+ // function), while the guide's --ease is the CSS keyword `ease` =
56
+ // `cubic-bezier(0.25, 0.1, 0.25, 1)`. A bare `transition-*` utility
57
+ // therefore never matched the guide's curve. Naming --ease explicitly
58
+ // fixes that and makes the kit track the token.
59
+ // Duration happened to agree (--dur-base 0.15s = Tailwind's 150ms
60
+ // default), but it is spelled out for the same tracking reason: the
61
+ // agreement is a property of today's value, not of the system.
62
+ // NOT done as a global `--default-transition-*` override in extras.css,
63
+ // which would have covered every component at once: extras.css is OPTIONAL
64
+ // (package.json exports it separately), so a consumer who skips it would get
65
+ // silently un-branded transitions everywhere. Components must not depend on
66
+ // optional CSS for correctness — the same call base.css's own header makes.
67
+ const BASE =
68
+ 'inline-flex items-center justify-center gap-2 border-2 border-transparent rounded-button ' +
69
+ 'px-[26px] py-[11px] font-primary text-[0.98rem] leading-[1.2] font-bold no-underline ' +
70
+ 'cursor-pointer shadow-resting ' +
71
+ 'transition-[background,color,border-color,box-shadow] ' +
72
+ 'duration-[var(--dur-base)] ease-[var(--ease)]';
73
+
74
+ const PRIMARY: Record<Color, string> = {
75
+ brown:
76
+ 'bg-jelinek-brown text-fg-inverse hover:bg-jelinek-brown-highlight hover:shadow-interactive',
77
+ magenta:
78
+ 'bg-jelinek-magenta text-fg-inverse hover:bg-jelinek-magenta-highlight hover:shadow-interactive',
79
+ black:
80
+ 'bg-jelinek-black text-on-jelinek-black hover:bg-jelinek-black-highlight hover:shadow-interactive'
81
+ };
82
+
83
+ const SECONDARY: Record<Color, string> = {
84
+ brown:
85
+ 'bg-transparent text-jelinek-brown border-jelinek-brown ' +
86
+ 'hover:bg-jelinek-brown-highlight hover:text-fg-inverse hover:border-transparent hover:shadow-interactive',
87
+ magenta:
88
+ 'bg-transparent text-jelinek-magenta border-jelinek-magenta ' +
89
+ 'hover:bg-jelinek-magenta-highlight hover:text-fg-inverse hover:border-transparent hover:shadow-interactive',
90
+ black:
91
+ 'bg-transparent text-jelinek-black border-jelinek-black ' +
92
+ 'hover:bg-jelinek-black-highlight hover:text-on-jelinek-black hover:border-transparent hover:shadow-interactive'
93
+ };
94
+
95
+ // jelinek.css:765 (and its :hover at :769) keeps ghost buttons flat — no
96
+ // resting or hover shadow — unlike primary/secondary which get shadow-resting
97
+ // from BASE and shadow-interactive on hover/active.
98
+ const GHOST: Record<Color, string> = {
99
+ brown:
100
+ 'bg-transparent border-transparent shadow-none text-jelinek-brown hover:text-jelinek-brown-highlight',
101
+ magenta:
102
+ 'bg-transparent border-transparent shadow-none text-jelinek-magenta hover:text-jelinek-magenta-highlight',
103
+ black:
104
+ 'bg-transparent border-transparent shadow-none text-jelinek-black hover:text-jelinek-black-highlight'
105
+ };
106
+
107
+ // Active always means a full fill in the accent colour — the highlight shade
108
+ // is reserved for hover. (ui/komponenty.html, "Aktivní / vybraný stav")
109
+ // jelinek.css:776-779 also raises active buttons to shadow-interactive.
110
+ const ACTIVE: Record<Color, string> = {
111
+ brown: 'bg-jelinek-brown text-fg-inverse border-transparent shadow-interactive',
112
+ magenta: 'bg-jelinek-magenta text-fg-inverse border-transparent shadow-interactive',
113
+ black: 'bg-jelinek-black text-on-jelinek-black border-transparent shadow-interactive'
114
+ };
115
+
116
+ const DISABLED =
117
+ 'bg-surface-gray text-fg-muted2 border-transparent shadow-none cursor-not-allowed ' +
118
+ 'pointer-events-none hover:bg-surface-gray hover:text-fg-muted2 hover:shadow-none';
119
+
120
+ const byVariant = $derived(
121
+ variant === 'primary' ? PRIMARY[color] : variant === 'secondary' ? SECONDARY[color] : GHOST[color]
122
+ );
123
+
124
+ const classes = $derived(
125
+ cn(
126
+ BASE,
127
+ byVariant,
128
+ active && ACTIVE[color],
129
+ disabled && DISABLED,
130
+ fullWidth && 'w-full',
131
+ className
132
+ )
133
+ );
134
+
135
+ // A disabled link keeps its genuine href and native `link` role — a
136
+ // screen-reader user must still land on something announced as a link,
137
+ // not on an inert generic element. Activation is blocked at the handler
138
+ // level instead: mouse/AT clicks are intercepted here, and keyboard
139
+ // activation (Enter on a focused <a href>) is intercepted separately,
140
+ // rather than relying on `pointer-events-none` alone (it only stops
141
+ // pointer input, not keyboard or assistive-technology activation).
142
+ function handleClick(event: MouseEvent) {
143
+ if (disabled) {
144
+ event.preventDefault();
145
+ return;
146
+ }
147
+ onclick?.(event);
148
+ }
149
+
150
+ // Only ever wired to the <a> branch below, so the event is typed to match
151
+ // exactly the overload `rest.onkeydown` resolves to there — a plain
152
+ // `KeyboardEvent` doesn't satisfy either half of HTMLButtonAttributes &
153
+ // HTMLAnchorAttributes's overloaded onkeydown at once.
154
+ function handleKeydown(event: KeyboardEvent & { currentTarget: EventTarget & HTMLAnchorElement }) {
155
+ if (disabled && event.key === 'Enter') {
156
+ event.preventDefault();
157
+ }
158
+ rest.onkeydown?.(event);
159
+ }
160
+ </script>
161
+
162
+ {#if href}
163
+ <a
164
+ {...rest}
165
+ {href}
166
+ aria-disabled={disabled ? 'true' : rest['aria-disabled']}
167
+ tabindex={disabled ? -1 : rest.tabindex}
168
+ onclick={handleClick}
169
+ onkeydown={handleKeydown}
170
+ class={classes}
171
+ >
172
+ {@render children()}
173
+ </a>
174
+ {:else}
175
+ <button {...rest} type={rest.type ?? 'button'} {disabled} onclick={handleClick} class={classes}>
176
+ {@render children()}
177
+ </button>
178
+ {/if}
@@ -0,0 +1,19 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
3
+ type Variant = 'primary' | 'secondary' | 'ghost';
4
+ type Color = 'brown' | 'magenta' | 'black';
5
+ interface Props {
6
+ variant?: Variant;
7
+ color?: Color;
8
+ href?: string;
9
+ disabled?: boolean;
10
+ active?: boolean;
11
+ fullWidth?: boolean;
12
+ class?: string;
13
+ onclick?: (event: MouseEvent) => void;
14
+ children: Snippet;
15
+ }
16
+ type $$ComponentProps = Props & Omit<HTMLButtonAttributes & HTMLAnchorAttributes, 'href' | 'disabled' | 'onclick'>;
17
+ declare const Button: import("svelte").Component<$$ComponentProps, {}, "">;
18
+ type Button = ReturnType<typeof Button>;
19
+ export default Button;
@@ -0,0 +1,68 @@
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
+
6
+ interface Props {
7
+ padded?: boolean;
8
+ class?: string;
9
+ children: Snippet;
10
+ }
11
+
12
+ let {
13
+ padded = true,
14
+ class: className,
15
+ children,
16
+ ...rest
17
+ }: Props & HTMLAttributes<HTMLDivElement> = $props();
18
+
19
+ // jelinek.css:474-481 draws the card border as a two-layer gradient
20
+ // (padding-box fill + border-box gradient from --box-accent to
21
+ // --border-subtle) so a divisional --box-accent can tint just the edge.
22
+ // This component has no colour-variant prop, and --box-accent defaults to
23
+ // --border-subtle, so both gradient stops are identical and the technique
24
+ // collapses to a flat 1px border-border-subtle — reproduced directly
25
+ // rather than replicating the (here invisible) gradient machinery.
26
+ // jelinek.css:322-325 puts .card on the secondary (Avenir) family at
27
+ // weight 300 (Light) as its body text, same as .alert/.panel/etc.
28
+ // jelinek.css:483's `padding: 24px` matches p-6 exactly — p-6 resolves to
29
+ // a fixed 24px at every viewport width because theme.css pins Tailwind's
30
+ // `--spacing` to a literal 4px (gen-tokens.js), not its rem-relative
31
+ // default; see that file's comment for why a rem-based p-6 would have
32
+ // drifted below the guide's 24px once base.css's html got a responsive
33
+ // font-size (Task 20).
34
+ // Deliberately NOT reproduced: the same line's `margin-bottom: 1.5rem`.
35
+ // Same reasoning as Alert's dropped margin: a reusable Card shouldn't
36
+ // impose spacing on whatever a consumer stacks it in, and the guide's own
37
+ // CSS zeroes this exact margin inside `.grid` (line 485) — treating it as
38
+ // a page-layout convenience, not a property of "card". Flagging for
39
+ // adjudication per the task brief.
40
+ // jelinek.css:652 (`.card h3, .tile h3`): headings inside components are
41
+ // deliberately NOT the content hierarchy — they hold a smaller,
42
+ // componentised --step1 size at line-height 1.3, not the page's large h3
43
+ // modular-scale step. (Tile's own h3 rule also adds a 6px margin-bottom
44
+ // via a Tile-only selector, jelinek.css:648 — that one is not shared with
45
+ // .card, so it is not reproduced here.) jelinek.css:327-331 (the same
46
+ // shared strong/b block that gives the card itself font-secondary/
47
+ // font-light) raises nested strong/b back to font-weight 500. Both
48
+ // reproduced as compound arbitrary variants on the root element, matching
49
+ // the pattern Tile.svelte uses for its own equivalent rules, since Card's
50
+ // children are an opaque Snippet this component doesn't otherwise touch.
51
+ // TASK 34 — the dark-mode inset top highlight, jelinek.css:251-257:
52
+ // `:root[data-theme="dark"] .panel, .card, .demo, .swatch, .logo-tool {
53
+ // box-shadow: inset 0 2px 1px -1px var(--box-primary) }`. `.card` is in that
54
+ // selector list and this was missing, so the guide's card computed
55
+ // `inset 0 2px 1px -1px rgb(128,128,128)` in dark where the kit's computed
56
+ // `none`. See Panel.svelte for why it was dropped and why that was wrong.
57
+ // This component has no colour-variant prop, so jelinek.css:705-706's
58
+ // `.card--brown`/`.card--magenta` --box-primary overrides have no
59
+ // counterpart to attach to; the highlight is always the :root 50% grey here.
60
+ const BASE =
61
+ 'rounded-card border border-border-subtle bg-surface font-secondary font-light ' +
62
+ '[&_h3]:text-step1 [&_h3]:leading-[1.3] [&_strong]:font-medium [&_b]:font-medium ' +
63
+ 'dark:shadow-[inset_0_2px_1px_-1px_var(--box-primary)]';
64
+ </script>
65
+
66
+ <div {...rest} class={cn(BASE, padded && 'p-6', className)}>
67
+ {@render children()}
68
+ </div>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ interface Props {
4
+ padded?: boolean;
5
+ class?: string;
6
+ children: Snippet;
7
+ }
8
+ type $$ComponentProps = Props & HTMLAttributes<HTMLDivElement>;
9
+ declare const Card: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type Card = ReturnType<typeof Card>;
11
+ export default Card;
@@ -0,0 +1,290 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { cn } from '../utils/cn.js';
4
+ import { isActive, type NavItem } from './nav.js';
5
+
6
+ interface Props {
7
+ items: NavItem[];
8
+ active?: string;
9
+ /**
10
+ * Accessible name for the `<nav>` landmark. The bar is a horizontal
11
+ * scroll region with `tabindex="0"` (see INNER below), so it needs a
12
+ * name for a screen-reader user to know what they have landed in.
13
+ * Czech by default, like every other user-facing string this kit ships.
14
+ */
15
+ label?: string;
16
+ class?: string;
17
+ }
18
+
19
+ let {
20
+ items,
21
+ active,
22
+ label = 'Kapitoly',
23
+ class: className,
24
+ ...rest
25
+ }: Props & HTMLAttributes<HTMLElement> = $props();
26
+
27
+ // jelinek.css:559-638 (.chapter-nav / .chapter-nav__inner) — the guide's
28
+ // floating pill bar, tucked under the (also floating) SiteHeader. Per
29
+ // direction, this is reproduced as the component's own default rather
30
+ // than left to a consuming app.
31
+ //
32
+ // Task 26 — mobile-first, and the `min-[620.02px]:` epsilon is GONE.
33
+ // The guide used to express the phone case as
34
+ // `@media (max-width: 620px) { .site-header, .chapter-nav { position: static } }`
35
+ // and the epsilon existed purely to convert that INCLUSIVE max-width
36
+ // boundary into a mobile-first `min-width` without a gap (Tailwind's
37
+ // `max-[620px]:` compiles to the exclusive `width < 620px`). That block is
38
+ // gone: the guide declares the phone layout unconditionally and enhances it
39
+ // in `@media (min-width: 640px)`, so `min-[640px]:` is an EXACT translation.
40
+ // Task 30 moved the boundary from 621 to 640: the guide converged on the
41
+ // designer's 640/1024/1440 ladder, and 621 was an artifact of the
42
+ // `--ratio` tiers that no longer exist. Grid no longer needs an epsilon
43
+ // either — its `.grid--*` rules are mobile-first `min-width` now too.
44
+ //
45
+ // TASK 33 — ON A PHONE THIS BAR IS NOT RENDERED AT ALL. jelinek.css:521-525
46
+ // now reads `.chapter-nav { display: none; position: static; padding: 0
47
+ // var(--space-3) }`, and `@media (min-width: 640px)` turns it back on with
48
+ // `display: block; position: absolute`. That is the designer's decision
49
+ // (6750ee8): below 640px the chapters live inside the header's burger panel
50
+ // instead, which is why SiteHeader now takes a `chapterItems` prop — a phone
51
+ // reader has to be able to reach them somewhere, and a second glass strip
52
+ // under a full-width glass header was the arrangement he replaced.
53
+ //
54
+ // Everything the previous phone base carried is gone with it: the `-mt-1.5`
55
+ // that cancelled the old `py-1.5` header gutter (the header has no gutter on
56
+ // a phone any more, `padding: 0`), the `pb-2`, and the narrower `px-3`. The
57
+ // padding is now a flat 24px (`px-6` = --space-3) at every width, so it does
58
+ // not need a variant either.
59
+ //
60
+ // `position: static` in the base is the guide's own wording and is harmless
61
+ // while `display: none` holds; from 640px up the bar is `absolute`, which is
62
+ // what makes it the containing block for the scroll-affordance fades below.
63
+ const OUTER =
64
+ 'hidden z-[49] px-6 ' +
65
+ 'min-[640px]:block min-[640px]:absolute min-[640px]:inset-x-0 ' +
66
+ 'min-[640px]:top-[calc(var(--header-h)-11px)]';
67
+
68
+ // The inner bar itself (jelinek.css:593-604): flex/gap-1.5 (6px), 16px/8px
69
+ // padding (py-2 px-4), max-w-page + mx-auto centering, the glass, and
70
+ // shadow-resting. The dark variant's box-shadow is the SAME `shadow-resting`
71
+ // as light — no extra inset highlight here (that's SiteHeader-only, see
72
+ // there).
73
+ //
74
+ // Task 31, part 1 — THE GLASS IS THE SHARED TOKEN SET NOW. jelinek.css:597-600
75
+ // reads `background: var(--glass-fill); backdrop-filter: var(--glass-filter);
76
+ // border: 1px solid var(--glass-border)`, identical to .site-header__inner
77
+ // and .glassmorphism. Fill was `rgba(255,255,255,.6)` light /
78
+ // `rgba(13,13,13,.6)` dark (both hand-written here) and is now one token that
79
+ // flips itself; filter was `blur(20px) brightness(150%)`/`brightness(40%)`
80
+ // and is now `blur(25px) saturate(140%)` in both themes; border was
81
+ // --border-subtle and is now --glass-border. Both `dark:` utilities are gone
82
+ // with the guide's own dark rules — jelinek.css:262-264 now only sets the
83
+ // FILL token in dark, and the filter override was deleted outright. See
84
+ // Glass.svelte for why the filter is an arbitrary property pair.
85
+ //
86
+ // Task 31, part 2 — the guide gave this bar the SAME scroller treatment as
87
+ // `.tabs`, in a rule that names both (jelinek.css:834-846):
88
+ // `flex-wrap: nowrap; overflow-x: auto; scroll-behavior: smooth;
89
+ // scrollbar-width: none; -ms-overflow-style: none` plus
90
+ // `::-webkit-scrollbar { display: none }`. Two of those are real changes
91
+ // here: `scrollbar-width` was `thin` and is now `none`, and
92
+ // `scroll-behavior: smooth` is new (it makes the mount-time "scroll the
93
+ // active chapter into view" below animate instead of jumping). `flex-wrap:
94
+ // nowrap` is now explicit in the guide where it was previously only the
95
+ // flex default — spelled out here for the same reason.
96
+ //
97
+ // HIDING THE SCROLLBAR COSTS THIS COMPONENT NOTHING, which is why it is
98
+ // reproduced as-is: unlike Tabs (see Tabs.svelte's own note on the same
99
+ // change), this bar already carries the guide's replacement affordance —
100
+ // the gradient edge fades below, driven by real scroll position — plus
101
+ // `tabindex="0"` for keyboard scrolling. The guide reached the same
102
+ // conclusion for the same element: assets/js/jelinek.js:467-474 deliberately
103
+ // EXCLUDES `.chapter-nav__inner` from the arrow-button wrapper because it
104
+ // already has the fades, and says so ("Dvě různá vodítka na jednom prvku …
105
+ // nenavrhl nikdo" — nobody designed two different affordances on one
106
+ // element). The `.chapter-nav .tabs-scroll*` rules at jelinek.css:853-855
107
+ // are dead code kept only so that decision is one step to reverse; there is
108
+ // nothing there for this component to reproduce.
109
+ //
110
+ // Fix round 1: NO `flex-wrap: wrap` — wrapping would be self-contradictory
111
+ // (a wrapped row never overflows, so the horizontal scroll this bar is built
112
+ // around never engages) and would break `--subnav-h: 58px`, a fixed
113
+ // single-row-height token other layout maths depends on.
114
+ //
115
+ // Task 26 (audit M4) — this stayed a scroller rather than becoming a
116
+ // wrapping row, and that was measured rather than assumed: the guide's nine
117
+ // brand chapters have 860px of intrinsic pill width, which at a 320px
118
+ // viewport wraps to four rows, ~164px — it would hand back most of what the
119
+ // header redesign just saved. So the scroller keeps its single row and
120
+ // gains the two things it was missing:
121
+ // - `tabindex="0"` + a focus ring, so the region is keyboard-operable
122
+ // (WCAG 2.1.1). Note the links inside were already reachable — focusing
123
+ // an off-screen link scrolls it into view — so this closes the
124
+ // "scroll the region without moving focus link-by-link" case, not a
125
+ // total lockout.
126
+ // - an edge fade on whichever side still has content, driven by the real
127
+ // scroll position (see `atStart`/`atEnd` below), so it never appears
128
+ // where there is nothing to scroll.
129
+ // `rounded-b-card` + `border-t-0`, NOT `rounded-card`/`rounded-pill`: the
130
+ // guide rounds only the bottom two corners and zeroes the top border,
131
+ // UNCONDITIONALLY — this is the static half of the joined-header seam,
132
+ // reproduced as-is regardless of scroll position or of whether a SiteHeader
133
+ // is actually rendered above this component. The OTHER, scroll-linked half
134
+ // (the header's own top-only radius, dropped bottom border and shadow)
135
+ // lives in SiteHeader's `hasChapterNav` prop (Task 15;
136
+ // chapter-nav-dock.svelte.ts). That module is unchanged by Task 26 — its
137
+ // 50px threshold and its meaning are untouched — but the state it exposes
138
+ // now matters on phones too, where the header did not previously stay on
139
+ // screen at all.
140
+ // Task 32 — THE FOCUS RING IS ON THE FOCUS TOKENS, not --JELINEK-BROWN.
141
+ // jelinek.css:637-640 now reads `outline: var(--focus-width) solid
142
+ // var(--focus-color); outline-offset: calc(-1 * var(--focus-offset))`. The
143
+ // colour is the point: --focus-color is --JELINEK-BROWN in light mode but
144
+ // --JELINEK-BROWN-HIGHLIGHT in dark (tokens.css:47/100), so the previous
145
+ // hard-coded `outline-jelinek-brown` here rendered rgb(147,101,57) against
146
+ // the rgb(201,157,115) every other focusable element in the kit shows in
147
+ // dark mode — measurably darker, on the one control whose ring sits on
148
+ // glass. See TextField.svelte for the same reasoning and the same
149
+ // `[length:…]` hint (Tailwind needs it to read a var() as a width rather
150
+ // than a colour).
151
+ //
152
+ // The OFFSET stays negative — the ring is drawn inside the bar, because an
153
+ // outward ring would collide with the SiteHeader this bar is docked to —
154
+ // but it is now derived from --focus-offset rather than a literal -2px, so
155
+ // the three focus tokens move together.
156
+ const INNER =
157
+ 'mx-auto flex max-w-page flex-nowrap items-center gap-1.5 rounded-b-card ' +
158
+ 'border border-t-0 border-[var(--glass-border)] bg-[var(--glass-fill)] px-4 py-2 shadow-resting ' +
159
+ '[-webkit-backdrop-filter:var(--glass-filter)] [backdrop-filter:var(--glass-filter)] ' +
160
+ 'overflow-x-auto [scroll-behavior:smooth] [scrollbar-width:none] ' +
161
+ '[-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden ' +
162
+ 'focus-visible:outline-[length:var(--focus-width)] ' +
163
+ 'focus-visible:outline-offset-[calc(-1*var(--focus-offset))] ' +
164
+ 'focus-visible:outline-[var(--focus-color)]';
165
+
166
+ // jelinek.css:622-629. Resting colour is text-muted1 with hover adding
167
+ // BOTH a text colour change and a background. The active pill is
168
+ // `color: var(--text-black); border-color: var(--JELINEK-BROWN);
169
+ // background: var(--bg-brown)` — a light brown-bordered pill with full-
170
+ // strength text, NOT a solid black fill. `border: 1px solid transparent`
171
+ // is kept on rest/hover so the active item's border doesn't shift layout
172
+ // when it appears — tailwind-merge drops it in favour of the active
173
+ // variant's `border-jelinek-brown` since both target the same
174
+ // border-color group.
175
+ const REST = 'text-fg-muted1 hover:bg-surface-gray hover:text-fg';
176
+ const ACTIVE = 'border-jelinek-brown bg-surface-brown text-fg';
177
+
178
+ // Scroll affordance. The fades are real elements rather than `before:`/
179
+ // `after:` utilities because a component owns its markup — the same call
180
+ // PageHero makes about the guide's `<hr class="rule">` marker. They are
181
+ // positioned against OUTER (`relative` on a phone, `absolute` from 640px
182
+ // up — both are positioned, so both establish the containing block), and
183
+ // inset 1px so they never blur the bar's own border.
184
+ //
185
+ // State is read from the scroller, not guessed from a breakpoint: a nav
186
+ // with three chapters does not overflow at any width and correctly shows
187
+ // no fade at all. A ResizeObserver covers the viewport changing under a
188
+ // fixed set of items; the scroll listener covers the reader moving.
189
+ let scroller = $state<HTMLElement | null>(null);
190
+ let scrollLeft = $state(0);
191
+ let scrollMax = $state(0);
192
+ const showStart = $derived(scrollMax > 1 && scrollLeft > 1);
193
+ const showEnd = $derived(scrollMax > 1 && scrollLeft < scrollMax - 1);
194
+
195
+ $effect(() => {
196
+ const el = scroller;
197
+ if (!el) return;
198
+ function measure() {
199
+ if (!el) return;
200
+ scrollLeft = el.scrollLeft;
201
+ scrollMax = el.scrollWidth - el.clientWidth;
202
+ }
203
+ // Bring the active chapter into view on mount, mirroring
204
+ // assets/js/jelinek.js's initChapterNavScroll(): a nav whose own current
205
+ // entry is off-screen is worse than no scroller. Only when it really is
206
+ // clipped, though — centring unconditionally would also move the bar on
207
+ // the (common, wider) renders where the active pill is already visible,
208
+ // which is a change nobody asked for.
209
+ const current = el.querySelector<HTMLElement>('[aria-current="page"]');
210
+ if (current) {
211
+ const pill = current.getBoundingClientRect();
212
+ const port = el.getBoundingClientRect();
213
+ if (pill.left < port.left - 1 || pill.right > port.right + 1) {
214
+ const offset = pill.left - port.left + el.scrollLeft;
215
+ el.scrollLeft = Math.max(0, offset - (el.clientWidth - current.offsetWidth) / 2);
216
+ }
217
+ }
218
+ measure();
219
+ el.addEventListener('scroll', measure, { passive: true });
220
+ const ro = new ResizeObserver(measure);
221
+ ro.observe(el);
222
+ return () => {
223
+ el.removeEventListener('scroll', measure);
224
+ ro.disconnect();
225
+ };
226
+ });
227
+
228
+ // jelinek.css:573-592 (.chapter-nav::before/::after) + :670 (the ≥640px
229
+ // `bottom: 1px`). The fade's `transition` used to be the ONE in the guide
230
+ // still spelled as a literal `0.15s ease` while everything else had moved
231
+ // onto --dur-*/--ease; this component referenced the tokens anyway, and
232
+ // Task 31 reported the literal as a likely oversight rather than copying it
233
+ // as a divergence. Task 32 closed it in the guide itself (:581 now reads
234
+ // `transition: opacity var(--dur-base) var(--ease)`), so the two spellings
235
+ // agree and no divergence is left to record. The explicit easing still
236
+ // matters, because Tailwind's default curve is not `ease` (see
237
+ // Button.svelte).
238
+ // Task 33 — `bottom: 1px` is unconditional in the guide now (the phone-only
239
+ // `bottom: 9px`, which cleared the old `pb-2`, went with the phone layout),
240
+ // and so are the 25px insets below: the bar's own padding is a flat
241
+ // --space-3 (24px) plus its 1px border at every width.
242
+ const FADE =
243
+ 'pointer-events-none absolute top-px bottom-px w-9 ' +
244
+ 'transition-opacity duration-[var(--dur-base)] ease-[var(--ease)]';
245
+ </script>
246
+
247
+ <nav {...rest} class={cn(OUTER, className)} aria-label={label}>
248
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
249
+ <!-- The tabindex is deliberate and is the fix for audit M4, not an
250
+ oversight the linter caught: WCAG 2.1.1 requires a scrollable region to
251
+ be operable from the keyboard, and a scroll container is only
252
+ scrollable by arrow key once it can hold focus. The rule fires because
253
+ a plain <div> is non-interactive, which is true and is exactly the
254
+ situation the success criterion covers. No `role` is added: the
255
+ enclosing <nav> landmark already carries the accessible name, and
256
+ `role="group"` here would make a screen reader announce "Kapitoly"
257
+ twice. -->
258
+ <div bind:this={scroller} class={INNER} tabindex="0">
259
+ {#each items as item (item.href)}
260
+ <a
261
+ href={item.href}
262
+ aria-current={isActive(item, active) ? 'page' : undefined}
263
+ class={cn(
264
+ 'whitespace-nowrap rounded-pill border border-transparent px-[13px] py-1.5 font-secondary text-[0.88rem] font-medium no-underline transition-colors',
265
+ isActive(item, active) ? ACTIVE : REST
266
+ )}
267
+ >
268
+ {item.label}
269
+ </a>
270
+ {/each}
271
+ </div>
272
+ <span
273
+ aria-hidden="true"
274
+ class={cn(
275
+ FADE,
276
+ 'left-[25px] rounded-bl-[calc(var(--radius-card)-1px)]',
277
+ 'bg-[linear-gradient(to_right,var(--color-surface)_20%,transparent)]',
278
+ showStart ? 'opacity-100' : 'opacity-0'
279
+ )}
280
+ ></span>
281
+ <span
282
+ aria-hidden="true"
283
+ class={cn(
284
+ FADE,
285
+ 'right-[25px] rounded-br-[calc(var(--radius-card)-1px)]',
286
+ 'bg-[linear-gradient(to_left,var(--color-surface)_20%,transparent)]',
287
+ showEnd ? 'opacity-100' : 'opacity-0'
288
+ )}
289
+ ></span>
290
+ </nav>
@@ -0,0 +1,18 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import { type NavItem } from './nav.js';
3
+ interface Props {
4
+ items: NavItem[];
5
+ active?: string;
6
+ /**
7
+ * Accessible name for the `<nav>` landmark. The bar is a horizontal
8
+ * scroll region with `tabindex="0"` (see INNER below), so it needs a
9
+ * name for a screen-reader user to know what they have landed in.
10
+ * Czech by default, like every other user-facing string this kit ships.
11
+ */
12
+ label?: string;
13
+ class?: string;
14
+ }
15
+ type $$ComponentProps = Props & HTMLAttributes<HTMLElement>;
16
+ declare const ChapterNav: import("svelte").Component<$$ComponentProps, {}, "">;
17
+ type ChapterNav = ReturnType<typeof ChapterNav>;
18
+ export default ChapterNav;
@@ -0,0 +1,52 @@
1
+ <script lang="ts">
2
+ import type { HTMLInputAttributes } from 'svelte/elements';
3
+ import { cn } from '../utils/cn.js';
4
+
5
+ interface Props {
6
+ checked?: boolean;
7
+ label: string;
8
+ id?: string;
9
+ class?: string;
10
+ }
11
+
12
+ let {
13
+ checked = $bindable(false),
14
+ label,
15
+ id,
16
+ class: className,
17
+ ...rest
18
+ }: Props & Omit<HTMLInputAttributes, 'checked' | 'type'> = $props();
19
+
20
+ const uid = $props.id();
21
+ const inputId = $derived(id ?? uid);
22
+
23
+ // jelinek.css has NO rule at all for checkbox inputs or their wrapper —
24
+ // ui/komponenty.html renders them as bare
25
+ // `<label style="display:flex;gap:10px;...">` with no class. The label
26
+ // TEXT still ends up on Avenir in the real guide page, though: that demo
27
+ // markup sits inside `<div class="demo">`, and `.card, .panel, .alert,
28
+ // .tile, .demo, .swatch { font-family: var(--font-secondary); ... }`
29
+ // (jelinek.css:322-324) applies font-secondary to it ambiently. Setting
30
+ // font-secondary/text-fg explicitly here reproduces that real rendering
31
+ // without depending on an ambient .demo wrapper the consumer may not
32
+ // have. gap-2.5 matches the 10px inline gap.
33
+ //
34
+ // ACCENT COLOUR — no CSS source of truth exists, so this is a judgement
35
+ // call, flagged for adjudication (see task-7-report.md). The checkbox
36
+ // square itself uses accent-jelinek-brown, NOT accent-jelinek-magenta:
37
+ // magenta is reserved for the Zdravý spánek (mattress) division and the
38
+ // guide is explicit brown/magenta never mix in one interface, so baking
39
+ // magenta into every checkbox would leak the mattress accent into
40
+ // furniture-division UIs. jelinek-brown is also what TextField/Select's
41
+ // own focus ring already uses (jelinek.css:661), and the token layer
42
+ // already re-themes --JELINEK-BROWN to magenta under
43
+ // [data-division="mattress"] (tokens.css:161-166) — the same mechanism
44
+ // Button relies on for its brand colours — so this accent still turns
45
+ // magenta automatically on mattress-themed pages, without hardcoding it.
46
+ const LABEL = 'flex cursor-pointer items-center gap-2.5 font-secondary text-step0 text-fg';
47
+ </script>
48
+
49
+ <label for={inputId} class={cn(LABEL, className)}>
50
+ <input {...rest} id={inputId} type="checkbox" bind:checked class="accent-jelinek-brown" />
51
+ {label}
52
+ </label>
@@ -0,0 +1,11 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ interface Props {
3
+ checked?: boolean;
4
+ label: string;
5
+ id?: string;
6
+ class?: string;
7
+ }
8
+ type $$ComponentProps = Props & Omit<HTMLInputAttributes, 'checked' | 'type'>;
9
+ declare const Checkbox: import("svelte").Component<$$ComponentProps, {}, "checked">;
10
+ type Checkbox = ReturnType<typeof Checkbox>;
11
+ export default Checkbox;