@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,312 @@
1
+ <script lang="ts">
2
+ import { hasContext, type Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { cn } from '../utils/cn.js';
5
+ import { CONTAINER_CONTEXT } from './container-context.js';
6
+ import Eyebrow from './Eyebrow.svelte';
7
+
8
+ type Variant = 'brown' | 'magenta' | 'black';
9
+ type Level = 'h1' | 'h2' | 'h3' | 'h4';
10
+
11
+ interface Props {
12
+ /** Colour shared by the eyebrow and the underline bar — jelinek.css
13
+ * never pairs `.eyebrow--magenta` with `.rule--black` or similar (see
14
+ * ui/barvy-v-akci.html and brand/znacka.html), so one prop drives both
15
+ * instead of two that could disagree. */
16
+ variant?: Variant;
17
+ /** Heading tag. Every real `.page-hero` in the guide uses `<h1>` — kept
18
+ * overridable since jelinek.css's underline selector,
19
+ * `:is(h1, h2, h3):has(+ .rule)`, supports three levels (the
20
+ * guide's OWN non-hero usage of the same rule, e.g.
21
+ * ui/barvy-v-akci.html's `.demo` blocks, underlines a bare `<h2>`).
22
+ *
23
+ * `h4` is still accepted and still gets h4 typography, but as of the
24
+ * Task 31 merge it gets NO underline bar — the guide narrowed that
25
+ * selector to h1-h3. See HEADING_BASE / RULE_BAR below. */
26
+ level?: Level;
27
+ /** Set when a `ChapterNav` renders directly above this hero — same
28
+ * flag SiteHeader already takes, see its prop for the full seam
29
+ * explanation. Changes how much top padding this hero reserves for
30
+ * the floating header (+ chapter nav) above it. */
31
+ hasChapterNav?: boolean;
32
+ /** Whether this hero's content already sits inside something that
33
+ * insets it — a `Container` ancestor, or your own app's equivalent
34
+ * max-width/padding wrapper. Left `undefined` (the default), this is
35
+ * auto-detected via `Container`'s own Svelte context: present ->
36
+ * `PageHero` skips its internal `.container`-equivalent wrapper
37
+ * (jelinek.css never applies that inset twice); absent -> `PageHero`
38
+ * supplies it itself, so standalone usage (this hero as the first
39
+ * thing on a page, no `Container` anywhere) still matches the guide
40
+ * instead of rendering flush against the browser edge.
41
+ *
42
+ * Context can only see the KIT's own `Container` — it can't detect a
43
+ * consumer's OWN, non-kit layout div doing the same job. Set this
44
+ * explicitly to override the auto-detection for that case: `true` if
45
+ * something ALREADY insets this hero (suppresses the internal
46
+ * wrapper, even with no `Container` in the tree), `false` to force
47
+ * the internal wrapper on (even inside a `Container`, if you
48
+ * deliberately want the extra inset). */
49
+ contained?: boolean;
50
+ eyebrow?: Snippet;
51
+ heading: Snippet;
52
+ /** Content below the underline — a lede paragraph, buttons, etc. Every
53
+ * real `.page-hero` in the guide has this; kept optional since
54
+ * jelinek.css itself allows a bare eyebrow+heading (brand/znacka.html). */
55
+ children?: Snippet;
56
+ class?: string;
57
+ }
58
+
59
+ let {
60
+ variant = 'brown',
61
+ level = 'h1',
62
+ hasChapterNav = false,
63
+ contained,
64
+ eyebrow,
65
+ heading,
66
+ children,
67
+ class: className,
68
+ ...rest
69
+ }: Props & HTMLAttributes<HTMLElement> = $props();
70
+
71
+ // `hasContext` must run synchronously during this component's own
72
+ // initialization (Svelte's lifecycle rule for context APIs) — captured
73
+ // into a plain const here, NOT inside `$derived`'s callback, since that
74
+ // callback's re-run timing isn't guaranteed to still be within this
75
+ // component's setup phase. `contained` (the escape hatch above) wins
76
+ // when the consumer sets it explicitly; otherwise fall back to whatever
77
+ // `Container` (if any encloses this component) published.
78
+ const ambientContainer = hasContext(CONTAINER_CONTEXT);
79
+ const isContained = $derived(contained ?? ambientContainer);
80
+
81
+ // jelinek.css:672-677 (.page-hero): background var(--bg-gray) ->
82
+ // bg-surface-gray. The box-shadow/clip-path pair is the guide's own
83
+ // full-bleed technique (its comment: "celoplošný šedý pruh bez vodorovného
84
+ // posuvníku; sahá i za menu" — a full-width grey stripe with no horizontal
85
+ // scrollbar, reaching behind the floating header too) — reproduced as
86
+ // BOTH arbitrary values together, per task direction, since either alone
87
+ // would either not bleed or reintroduce the scrollbar. `var(--bg-gray)`
88
+ // is written via its mapped Tailwind theme name, --color-surface-gray,
89
+ // same technique SiteHeader.svelte's dark inset shadow uses for
90
+ // --JELINEK-BLACK, so this still tracks dark mode (--bg-gray flips at
91
+ // tokens.css:77) with no separate dark: variant needed.
92
+ // padding-bottom is the passthrough token --space-section (token-map.js
93
+ // keeps space-* off the Tailwind theme on purpose — see its own comment —
94
+ // so this stays an arbitrary var() reference, not a spacing utility).
95
+ const OUTER =
96
+ 'bg-surface-gray shadow-[0_0_0_100vmax_var(--color-surface-gray)] ' +
97
+ '[clip-path:inset(0_-100vmax)] pb-[var(--space-section)]';
98
+
99
+ // jelinek.css:339 (.container) — jelinek.css:672 gives `.page-hero`
100
+ // itself explicit ZERO horizontal padding, relying ENTIRELY on
101
+ // `.page-hero` always being a direct child of `<main class="container">`
102
+ // (ui/komponenty.html, brand/*.html) for its content's inset. The guide
103
+ // therefore applies exactly ONE `.container`'s worth of padding to that
104
+ // content, never zero, never two.
105
+ //
106
+ // review round 2 (Critical) found this component reproduced neither —
107
+ // no inset at all when rendered with no ambient Container (its most
108
+ // natural standalone usage: this hero as the first thing on a page).
109
+ // review round 3 (Critical, follow-up) found the round-2 fix overcorrected:
110
+ // unconditionally rendering this wrapper matches the STANDALONE case but
111
+ // DOUBLES the padding in the NESTED case (this kit's own showcase, which
112
+ // wraps every route's content in `Container`) — `section.x`/`h1.x`
113
+ // measured 24px apart at every viewport width tested, on top of
114
+ // whatever the ambient Container already contributed, where the guide's
115
+ // own total is one `px-6` (24px), not two.
116
+ //
117
+ // Fixed by rendering this wrapper CONDITIONALLY on `isContained` (see
118
+ // `contained` prop + `CONTAINER_CONTEXT` above): a `Container` ancestor
119
+ // already does this component's job, so render nothing extra; no
120
+ // `Container` ancestor (context absent) means this hero must do it
121
+ // itself, exactly once. `.container`'s exact values, reproduced as an
122
+ // inner wrapper around the content only — NOT by composing the
123
+ // `Container` component even in the "render it" branch, so that a
124
+ // consumer who imports both `Container` and `PageHero` from this kit
125
+ // never has two literally-different-but-identical wrapper implementations
126
+ // to keep in sync. Same "carry your own centering rather than assume an
127
+ // ancestor" call SiteHeader.svelte's `__inner` already makes, refined
128
+ // here to not assume the OPPOSITE (no ancestor) unconditionally either.
129
+ const INNER = 'mx-auto w-full max-w-page px-6';
130
+
131
+ // jelinek.css:657-667 (padding-top). Same mobile-first shape and SAME
132
+ // `min-[640px]` breakpoint as +layout.svelte's own `mainPadding` — this
133
+ // component models exactly the condition that calculation exists for, so
134
+ // it must follow it rather than invent a second breakpoint value that
135
+ // could drift from it.
136
+ //
137
+ // Task 26 — the `.02px` epsilon is gone. It existed only to convert the
138
+ // guide's INCLUSIVE `@media (max-width: 620px)` into a mobile-first
139
+ // `min-width` without leaving a gap. The guide's hero rule is itself
140
+ // mobile-first now. `min-[640px]:` is therefore an exact translation.
141
+ // Task 30 moved the boundary from 621 to 640: the guide converged on the
142
+ // designer's 640/1024/1440 ladder, and 621 was an artifact of the
143
+ // `--ratio` tiers that no longer exist. Grid no longer needs an epsilon
144
+ // either — its `.grid--*` rules are mobile-first `min-width` now too.
145
+ //
146
+ // TASK 33 — THE FLAT PHONE `pt-7` IS GONE, AND SO IS THE BREAKPOINT IN THE
147
+ // no-chapter-nav CASE. The designer's 74ef043 made the header float on the
148
+ // phone too (`position: absolute`, previously in normal flow / then briefly
149
+ // `sticky` on our side), precisely so the hero's grey band runs UNDER it and
150
+ // no seam shows above the bar. The hero therefore has to reserve the header's
151
+ // height at EVERY width, and the guide's base rule now reads
152
+ // `padding: calc(var(--header-h) + 28px) 0 …` with no `@media` companion —
153
+ // `--header-h` re-resolves itself at the breakpoint (70px phone → 94px from
154
+ // 640px up, tokens.css), so one expression covers both.
155
+ //
156
+ // What still needs the breakpoint is the chapter-nav case, and for a
157
+ // different reason: the chapter bar is `display: none` below 640px (its
158
+ // entries move into the header's burger panel), so there is no strip to
159
+ // reserve until the bar exists.
160
+ const paddingTop = $derived(
161
+ hasChapterNav
162
+ ? 'pt-[calc(var(--header-h)_+_28px)] min-[640px]:pt-[calc(var(--header-h)_+_var(--subnav-h)_+_20px)]'
163
+ : 'pt-[calc(var(--header-h)_+_28px)]'
164
+ );
165
+
166
+ // The heading-underline mechanism. jelinek.css draws this via
167
+ // `:is(h1, h2, h3):has(+ .rule)` on the HEADING (jelinek.css:407-414)
168
+ // — an `<hr class="rule">` sibling that is itself `display: none`
169
+ // (jelinek.css:406) and exists only as a marker for `:has()` to find,
170
+ // because the guide is hand-written HTML with no components. A component
171
+ // controls its own markup, so this is reproduced as `after:*` utilities
172
+ // on the heading directly instead — no marker element, no sibling
173
+ // selector, no `.rule` class name in a kit that otherwise ships no CSS
174
+ // class names of its own. Per task direction: if this were wrong it
175
+ // should be argued against rather than implemented, and on reading the
176
+ // CSS it is not wrong — `:has()` is a hook for the guide's own flat HTML,
177
+ // not a rule about how the bar itself is drawn, and dropping it changes
178
+ // no rendered pixel.
179
+ //
180
+ // TASK 31 — THE SELECTOR LOST h4. It was `:is(h1, h2, h3, h4)` and is now
181
+ // `:is(h1, h2, h3)` across all five of the guide's rules (:407-414). So an
182
+ // `<h4>` followed by `<hr class="rule">` now renders NO bar in the guide,
183
+ // and neither does `level="h4"` here. The `<hr>` stays hidden either way
184
+ // (`.rule { display: none }` is unconditional), so nothing about the guide's
185
+ // own pages changed visibly: grepping every heading/`.rule` pairing in
186
+ // brand/*.html, ui-old/*.html and index.html finds only h1 (16 of them) and
187
+ // h2 (69) — no h3 or h4 pairing exists anywhere, so h4 was already dead in
188
+ // that selector before it was removed. That is also why this is reproduced
189
+ // as "h4 keeps its typography but loses the bar" rather than by narrowing
190
+ // the `Level` type: the change removes an underline, not a heading level,
191
+ // and the guide's h1-h6 typography rule (:355-361) still covers h4
192
+ // unchanged. Narrowing the prop type would be a breaking API change the
193
+ // stylesheet does not ask for.
194
+ //
195
+ // The three declarations below split accordingly. HEADING_BASE holds what
196
+ // the shared `h1…h6` rule gives EVERY level; RULE_BAR holds everything that
197
+ // comes from the `:has(+ .rule)` rules and so applies to h1-h3 only.
198
+ //
199
+ // jelinek.css:355 (the shared `h1, h2, h3, h4, h5, h6` rule): font-primary
200
+ // (GT Walsheim Bold), font-weight 700, `margin: 0 0 0.4em`.
201
+ // This heading is markup this component GENERATES itself (unlike, say,
202
+ // ProductCard's <h3>, which already sits inside Card's own DOM and so
203
+ // reaches a real ambient `<h3>` element rule) — every other component in
204
+ // this kit that touches a heading relies on `base.css` reproducing
205
+ // jelinek.css:355-361 for exactly that reason: `base.css` is OPTIONAL
206
+ // (its own file header documents legitimate reasons to skip it), and a
207
+ // consumer who does is entitled to every OTHER component still matching
208
+ // the guide by default. This is the one component that renders a bare
209
+ // heading tag as self-generated markup, so it is the one exposed to that
210
+ // gap, and must carry its own typography rather than assume it.
211
+ // `mb-[0.4em]` is that shorthand's bottom half, and it is what an h4 now
212
+ // actually gets — RULE_BAR's `mb-0` overrides it for h1-h3, exactly as
213
+ // jelinek.css:407 does.
214
+ const HEADING_BASE = 'font-primary font-bold mt-0 mb-[0.4em]';
215
+
216
+ // jelinek.css:407 (`width: fit-content; max-width: 100%; margin-bottom:
217
+ // 0`) -> w-fit max-w-full mb-0. This is what makes the bar exactly 75% of
218
+ // the heading's OWN TEXT width rather than 75% of the section's width:
219
+ // without `w-fit` the heading (a block element) would stretch to fill its
220
+ // container, and the `after:w-[75%]` bar below inherits ITS OWN
221
+ // containing block from the heading's fitted box, not the page-hero's.
222
+ // jelinek.css:408-411 (`content: ""; display: block; height: 3px; width:
223
+ // 75%; border-radius: 2px; margin: 16px 0 24px`) -> the after: block.
224
+ // margin is a 3-value shorthand (top 16px, left/right 0, bottom 24px) ->
225
+ // after:mt-4 after:mb-6 (no explicit mx-0: Tailwind's default margin on a
226
+ // generated pseudo-element is already 0). height/border-radius are not on
227
+ // any Tailwind scale (nearest radius token, radius-button, is 8px) ->
228
+ // kept arbitrary, same as Tag/CodeCopy's own off-scale literals.
229
+ const RULE_BAR =
230
+ "w-fit max-w-full mb-0 after:content-[''] " +
231
+ 'after:block after:h-[3px] after:w-[75%] after:rounded-[2px] after:mt-4 after:mb-6';
232
+
233
+ // Which levels the guide's `:is(h1, h2, h3):has(+ .rule)` covers.
234
+ const hasRule = $derived(level !== 'h4');
235
+
236
+ // jelinek.css:356-359 (h1-h4; h5/h6 have no counterpart here since `Level`
237
+ // stops at h4). Note this list is NOT the same set as the underline's
238
+ // `:is(h1,h2,h3)` any more — h4 is a real, styled level that simply has no
239
+ // bar (see above).
240
+ // Only size/line-height (+ h1's own letter-spacing) vary per level, per
241
+ // the guide's own comment at jelinek.css:345-347 ("Mění se jen velikost a
242
+ // řádkování; řez zůstává" — only size and leading change; the typeface
243
+ // stays constant, already covered by HEADING_BASE above).
244
+ //
245
+ // `Level` is typed to exactly `'h1' | 'h2' | 'h3' | 'h4'`, so `pnpm check`
246
+ // rejects `level="h5"` at compile time — but a consumer bypassing
247
+ // TypeScript (plain JS, or an `as any`) could still pass one through at
248
+ // runtime. `LEVEL_TYPE[level]` would then be `undefined`, and `cn()`
249
+ // (clsx underneath) silently drops an `undefined` argument rather than
250
+ // throwing — the heading would still render, just with no size/leading
251
+ // of its own (falling back to the browser's UA default, or base.css's if
252
+ // imported), no error to signal why. Low severity (needs bypassing the
253
+ // type system to hit) but worth naming, since it fails silently rather
254
+ // than loudly.
255
+ const LEVEL_TYPE: Record<Level, string> = {
256
+ h1: 'text-step6 leading-[1.1] tracking-[-0.01em]',
257
+ h2: 'text-step5 leading-[1.1]',
258
+ h3: 'text-step4 leading-[1.2]',
259
+ h4: 'text-step3 leading-[1.2]'
260
+ };
261
+
262
+ // jelinek.css:410 (default, --JELINEK-BROWN), :412 (--magenta), :413
263
+ // (--black) -> after:bg-jelinek-*. Same brown/magenta/black set as
264
+ // Eyebrow's own VARIANT, and for the same reason (division re-theme
265
+ // remaps --JELINEK-BROWN under [data-division="mattress"], magenta/black
266
+ // don't move). :414's white variant is deliberately NOT included here: it
267
+ // pairs with `.eyebrow--white`, which does not exist in jelinek.css, and
268
+ // every real `.rule--white` usage sits on a dark PANEL, never inside a
269
+ // `.page-hero` (grep confirms: brand/aplikace.html's is standalone) — a
270
+ // white bar on this component's own always-grey band has no real-world
271
+ // counterpart to be faithful to.
272
+ const RULE_VARIANT: Record<Variant, string> = {
273
+ brown: 'after:bg-jelinek-brown',
274
+ magenta: 'after:bg-jelinek-magenta',
275
+ black: 'after:bg-jelinek-black'
276
+ };
277
+ </script>
278
+
279
+ <!-- Factored into a local snippet so the "already contained" and "must
280
+ contain itself" branches below render the identical markup instead of
281
+ two copies drifting apart. -->
282
+ {#snippet content()}
283
+ {#if eyebrow}
284
+ <Eyebrow {variant}>{@render eyebrow()}</Eyebrow>
285
+ {/if}
286
+ <!-- RULE_BAR and RULE_VARIANT are both gated on `hasRule`: an h4 gets the
287
+ guide's h4 typography and no underline, per `:is(h1, h2, h3)`. -->
288
+ <svelte:element
289
+ this={level}
290
+ class={cn(
291
+ HEADING_BASE,
292
+ LEVEL_TYPE[level],
293
+ hasRule && RULE_BAR,
294
+ hasRule && RULE_VARIANT[variant]
295
+ )}
296
+ >
297
+ {@render heading()}
298
+ </svelte:element>
299
+ {#if children}
300
+ {@render children()}
301
+ {/if}
302
+ {/snippet}
303
+
304
+ <section {...rest} class={cn(OUTER, paddingTop, className)}>
305
+ {#if isContained}
306
+ {@render content()}
307
+ {:else}
308
+ <div class={INNER}>
309
+ {@render content()}
310
+ </div>
311
+ {/if}
312
+ </section>
@@ -0,0 +1,55 @@
1
+ import { type Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ type Variant = 'brown' | 'magenta' | 'black';
4
+ type Level = 'h1' | 'h2' | 'h3' | 'h4';
5
+ interface Props {
6
+ /** Colour shared by the eyebrow and the underline bar — jelinek.css
7
+ * never pairs `.eyebrow--magenta` with `.rule--black` or similar (see
8
+ * ui/barvy-v-akci.html and brand/znacka.html), so one prop drives both
9
+ * instead of two that could disagree. */
10
+ variant?: Variant;
11
+ /** Heading tag. Every real `.page-hero` in the guide uses `<h1>` — kept
12
+ * overridable since jelinek.css's underline selector,
13
+ * `:is(h1, h2, h3):has(+ .rule)`, supports three levels (the
14
+ * guide's OWN non-hero usage of the same rule, e.g.
15
+ * ui/barvy-v-akci.html's `.demo` blocks, underlines a bare `<h2>`).
16
+ *
17
+ * `h4` is still accepted and still gets h4 typography, but as of the
18
+ * Task 31 merge it gets NO underline bar — the guide narrowed that
19
+ * selector to h1-h3. See HEADING_BASE / RULE_BAR below. */
20
+ level?: Level;
21
+ /** Set when a `ChapterNav` renders directly above this hero — same
22
+ * flag SiteHeader already takes, see its prop for the full seam
23
+ * explanation. Changes how much top padding this hero reserves for
24
+ * the floating header (+ chapter nav) above it. */
25
+ hasChapterNav?: boolean;
26
+ /** Whether this hero's content already sits inside something that
27
+ * insets it — a `Container` ancestor, or your own app's equivalent
28
+ * max-width/padding wrapper. Left `undefined` (the default), this is
29
+ * auto-detected via `Container`'s own Svelte context: present ->
30
+ * `PageHero` skips its internal `.container`-equivalent wrapper
31
+ * (jelinek.css never applies that inset twice); absent -> `PageHero`
32
+ * supplies it itself, so standalone usage (this hero as the first
33
+ * thing on a page, no `Container` anywhere) still matches the guide
34
+ * instead of rendering flush against the browser edge.
35
+ *
36
+ * Context can only see the KIT's own `Container` — it can't detect a
37
+ * consumer's OWN, non-kit layout div doing the same job. Set this
38
+ * explicitly to override the auto-detection for that case: `true` if
39
+ * something ALREADY insets this hero (suppresses the internal
40
+ * wrapper, even with no `Container` in the tree), `false` to force
41
+ * the internal wrapper on (even inside a `Container`, if you
42
+ * deliberately want the extra inset). */
43
+ contained?: boolean;
44
+ eyebrow?: Snippet;
45
+ heading: Snippet;
46
+ /** Content below the underline — a lede paragraph, buttons, etc. Every
47
+ * real `.page-hero` in the guide has this; kept optional since
48
+ * jelinek.css itself allows a bare eyebrow+heading (brand/znacka.html). */
49
+ children?: Snippet;
50
+ class?: string;
51
+ }
52
+ type $$ComponentProps = Props & HTMLAttributes<HTMLElement>;
53
+ declare const PageHero: import("svelte").Component<$$ComponentProps, {}, "">;
54
+ type PageHero = ReturnType<typeof PageHero>;
55
+ export default PageHero;
@@ -0,0 +1,98 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { cn } from '../utils/cn.js';
4
+ import type { NavItem } from './nav.js';
5
+
6
+ interface Props {
7
+ prev?: NavItem;
8
+ next?: NavItem;
9
+ prevLabel?: string;
10
+ nextLabel?: string;
11
+ class?: string;
12
+ }
13
+
14
+ let {
15
+ prev,
16
+ next,
17
+ prevLabel = 'Předchozí kapitola',
18
+ nextLabel = 'Další kapitola',
19
+ class: className,
20
+ ...rest
21
+ }: Props & HTMLAttributes<HTMLElement> = $props();
22
+
23
+ // jelinek.css:1114 (.pager): flex, space-between, 16px gap, and — this is
24
+ // what the brief got wrong — `margin: 44px 0 10px`, an asymmetric
25
+ // top/bottom margin, NOT the brief's `pt-6 border-t` (that border doesn't
26
+ // exist anywhere in the source). Both values land exactly on Tailwind's
27
+ // default scale: 44px = mt-11, 10px = mb-2.5. Kept as literal margin
28
+ // (unlike Card/Alert's dropped margins) on the same "chrome, not a
29
+ // reusable content atom" reasoning as SiteFooter's mt-[60px] — see that
30
+ // component's comment.
31
+ //
32
+ // Task 26 (audit M3, third part) — `.pager` now wraps. It used to be
33
+ // `display: flex` with no wrap and `flex: 1` on the links, so on a narrow
34
+ // viewport the two boxes never broke onto separate lines; they just shrank
35
+ // together, down to a measured 156px each on a 375px phone. The guide now
36
+ // carries `flex-wrap: wrap` (jelinek.css:1114) with a 200px flex-basis on
37
+ // the links (:1116), which puts the break where the CONTENT stops fitting
38
+ // rather than at a breakpoint: two 200px cards plus the 16px gap need
39
+ // 416px of content width, so they stack below roughly a 464px viewport
40
+ // and sit side by side above it. No media query, so nothing here is
41
+ // variant-prefixed either.
42
+ const ROOT = 'mt-11 mb-2.5 flex flex-wrap justify-between gap-4';
43
+
44
+ // jelinek.css:1115-1121 (.pager a): 16px/20px padding (py-4 px-5), border,
45
+ // rounded-card, colour text-black (text-fg), shadow-resting resting →
46
+ // shadow-interactive on hover.
47
+ // Task 31 — the transition is now `box-shadow var(--dur-base) var(--ease)`
48
+ // (jelinek.css:1120), so the duration and easing are named rather than left
49
+ // to Tailwind's defaults. Duration agreed already (0.15s = 150ms); the
50
+ // EASING did not — Tailwind's default is cubic-bezier(0.4, 0, 0.2, 1) and
51
+ // the guide's --ease is the `ease` keyword. See Button.svelte for the full
52
+ // note, including why this is not fixed once globally in extras.css.
53
+ // jelinek.css:337 (`.tile, .pager a { font-weight: 400 }`) overrides the
54
+ // global bold anchor default — font-normal. jelinek.css:373-377 puts pager
55
+ // links (and their nested spans) on font-secondary.
56
+ // jelinek.css:240 (dark mode only): the top border lightens to
57
+ // --border-highlight-white — not in the token map's Utility column, so
58
+ // referenced directly as the raw custom property via an arbitrary value
59
+ // rather than a named class.
60
+ // `flex-[1_1_200px]`, not `flex-1` — Tailwind's `flex-1` is `flex: 1 1 0%`,
61
+ // a zero basis, which is exactly why the old row never wrapped (items with
62
+ // a 0 basis can always be squeezed into the line). jelinek.css:1116 is
63
+ // `flex: 1 1 200px`; the 200px basis is what makes ROOT's `flex-wrap`
64
+ // actually fire. Changing one without the other is a no-op.
65
+ const LINK =
66
+ 'max-w-[340px] flex-[1_1_200px] rounded-card border border-border-subtle px-5 py-4 font-secondary ' +
67
+ 'font-normal text-fg no-underline shadow-resting hover:shadow-interactive ' +
68
+ 'transition-shadow duration-[var(--dur-base)] ease-[var(--ease)] ' +
69
+ 'dark:border-t-[var(--border-highlight-white)]';
70
+
71
+ // jelinek.css:1123 (.pager a span): block, 0.78rem (not on the --step
72
+ // scale), text-muted1.
73
+ const SUBLABEL = 'block text-[0.78rem] text-fg-muted1';
74
+ </script>
75
+
76
+ <nav {...rest} class={cn(ROOT, className)}>
77
+ {#if prev}
78
+ <a href={prev.href} class={LINK}>
79
+ <span class={SUBLABEL}>{prevLabel}</span>
80
+ <!-- No classes on <strong>: it inherits colour (text-fg) and font
81
+ (font-secondary) from the <a> above, and browsers already bold
82
+ <strong> by default — jelinek.css has no rule that changes its
83
+ size or family beyond what the parent link already sets. -->
84
+ <strong>← {prev.label}</strong>
85
+ </a>
86
+ {/if}
87
+ {#if next}
88
+ <!-- jelinek.css:1124 (.pager a.next): text-align:right + margin-left:auto,
89
+ applied unconditionally whenever a "next" link exists — kept
90
+ unconditional here too (harmless when prev is also present, since
91
+ justify-between already separates them; load-bearing when prev is
92
+ absent, keeping the lone link right-aligned rather than flex-start). -->
93
+ <a href={next.href} class={cn(LINK, 'ml-auto text-right')}>
94
+ <span class={SUBLABEL}>{nextLabel}</span>
95
+ <strong>{next.label} →</strong>
96
+ </a>
97
+ {/if}
98
+ </nav>
@@ -0,0 +1,13 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import type { NavItem } from './nav.js';
3
+ interface Props {
4
+ prev?: NavItem;
5
+ next?: NavItem;
6
+ prevLabel?: string;
7
+ nextLabel?: string;
8
+ class?: string;
9
+ }
10
+ type $$ComponentProps = Props & HTMLAttributes<HTMLElement>;
11
+ declare const Pager: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type Pager = ReturnType<typeof Pager>;
13
+ export default Pager;
@@ -0,0 +1,89 @@
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
+ type Variant = 'light' | 'dark' | 'brown' | 'magenta';
7
+
8
+ interface Props {
9
+ variant?: Variant;
10
+ class?: string;
11
+ children: Snippet;
12
+ }
13
+
14
+ let {
15
+ variant,
16
+ class: className,
17
+ children,
18
+ ...rest
19
+ }: Props & HTMLAttributes<HTMLDivElement> = $props();
20
+
21
+ // CSS FIDELITY — the brief gave Panel a single hardcoded look
22
+ // (bg-surface-gray). jelinek.css:486-492 actually defines FOUR modifier
23
+ // variants on top of the unmodified default:
24
+ //
25
+ // jelinek.css:474-482 (shared box system with .card/.demo/.tile/.swatch):
26
+ // --box-fill defaults to var(--bg-white) and --box-accent is never
27
+ // touched by any of these variants (only --box-fill/--box-primary are),
28
+ // so — same reasoning as Card/Tile — the border gradient always
29
+ // collapses to a flat 1px border-border-subtle regardless of variant;
30
+ // only the fill changes.
31
+ // jelinek.css:483 `.panel, .card, .demo { padding: 24px; margin-bottom:
32
+ // 1.5rem }` — 24px is exactly p-6 (the brief's p-8/32px was wrong).
33
+ // margin-bottom is dropped per standing rule 5 (content atom, N per
34
+ // grid; jelinek.css:485 zeroes this exact margin inside `.grid`, which is
35
+ // the rule's own confirmation that it's page-layout convenience, not a
36
+ // property of "panel" — same call Card already made).
37
+ // jelinek.css:322-325/327-331 (shared block): font-secondary font-light,
38
+ // nested strong/b → font-medium.
39
+ //
40
+ // jelinek.css:486 `.panel--light { --box-fill: var(--bg-gray) }` →
41
+ // bg-surface-gray.
42
+ // jelinek.css:487-488 `.panel--dark { --box-fill: var(--bg-black); color:
43
+ // var(--text-white) }` plus `.panel--dark h1, h2, h3 { color:
44
+ // var(--text-white) }` → bg-surface-black text-fg-inverse, and nested
45
+ // h1/h2/h3 pinned to text-fg-inverse too (reproduced as compound
46
+ // arbitrary variants on the root, same pattern Tile/Card use for their
47
+ // own nested-element rules, since children is an opaque Snippet).
48
+ // jelinek.css:491 `.panel--brown { --box-fill: var(--bg-brown);
49
+ // --box-primary: var(--JELINEK-BROWN) }` → bg-surface-brown.
50
+ // jelinek.css:492 `.panel--magenta { --box-fill: var(--bg-magenta);
51
+ // --box-primary: var(--JELINEK-MAGENTA) }` → bg-surface-magenta.
52
+ // TASK 34 — CORRECTION. This comment used to claim --box-primary "only ever
53
+ // feeds the dark-mode inset highlight on `.tile`" and that ".panel has no
54
+ // box-shadow rule that consumes it anywhere", and dropped it on that basis.
55
+ // The claim was false. jelinek.css:251-257 is exactly that rule:
56
+ //
57
+ // :root[data-theme="dark"] .panel, .card, .demo, .swatch, .logo-tool {
58
+ // box-shadow: inset 0 2px 1px -1px var(--box-primary);
59
+ // }
60
+ //
61
+ // .panel is the FIRST selector in it. So the guide's .panel in dark mode
62
+ // computes `inset 0 2px 1px -1px rgb(128,128,128)` (--box-primary is the
63
+ // flat 50% grey his 4ec82cd put on :root, jelinek.css:135) while the kit
64
+ // computed `box-shadow: none` — a live visual divergence on four guarded
65
+ // lock groups. Reproduced here rather than merely documented, because the
66
+ // rule is unconditional for these surfaces in dark mode and there is
67
+ // nothing to interpret.
68
+ //
69
+ // Consequence for the variants: --box-primary is NO LONGER inert, so
70
+ // jelinek.css:705-706's per-variant overrides have to come with it. Brown
71
+ // and magenta panels tint their own top highlight with the divisional
72
+ // colour; light/dark do not touch it (jelinek.css:700-701) and keep the
73
+ // :root grey.
74
+ const BASE =
75
+ 'rounded-card border border-border-subtle bg-surface p-6 font-secondary font-light ' +
76
+ '[&_strong]:font-medium [&_b]:font-medium ' +
77
+ 'dark:shadow-[inset_0_2px_1px_-1px_var(--box-primary)]';
78
+
79
+ const VARIANT: Record<Variant, string> = {
80
+ light: 'bg-surface-gray',
81
+ dark: 'bg-surface-black text-fg-inverse [&_h1]:text-fg-inverse [&_h2]:text-fg-inverse [&_h3]:text-fg-inverse',
82
+ brown: 'bg-surface-brown [--box-primary:var(--JELINEK-BROWN)]',
83
+ magenta: 'bg-surface-magenta [--box-primary:var(--JELINEK-MAGENTA)]'
84
+ };
85
+ </script>
86
+
87
+ <div {...rest} class={cn(BASE, variant && VARIANT[variant], className)}>
88
+ {@render children()}
89
+ </div>
@@ -0,0 +1,12 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ type Variant = 'light' | 'dark' | 'brown' | 'magenta';
4
+ interface Props {
5
+ variant?: Variant;
6
+ class?: string;
7
+ children: Snippet;
8
+ }
9
+ type $$ComponentProps = Props & HTMLAttributes<HTMLDivElement>;
10
+ declare const Panel: import("svelte").Component<$$ComponentProps, {}, "">;
11
+ type Panel = ReturnType<typeof Panel>;
12
+ export default Panel;
@@ -0,0 +1,81 @@
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 Card from './Card.svelte';
7
+
8
+ interface Props {
9
+ title: string;
10
+ description?: string;
11
+ image?: string;
12
+ imageAlt?: string;
13
+ href?: string;
14
+ ctaLabel?: string;
15
+ color?: 'brown' | 'magenta' | 'black';
16
+ /** Optional badge rendered above the title, e.g. <Tag variant="instock">. */
17
+ tag?: Snippet;
18
+ class?: string;
19
+ }
20
+
21
+ let {
22
+ title,
23
+ description,
24
+ image,
25
+ imageAlt = '',
26
+ href,
27
+ ctaLabel = 'Detail produktu',
28
+ color = 'brown',
29
+ tag,
30
+ class: className,
31
+ ...rest
32
+ }: Props & HTMLAttributes<HTMLDivElement> = $props();
33
+
34
+ // No `.product-card` class exists in jelinek.css — this is a composition
35
+ // of Card + Button + (via the `tag` snippet) Tag, not a standalone CSS
36
+ // rule. Its structure and spacing are taken from the guide's own worked
37
+ // example, ui/komponenty.html:253-281, reproduced value-for-value where
38
+ // that markup gives one (18px inner padding, 10px gap under the tag,
39
+ // 8px/4px heading margin), and by ordinary judgment where it doesn't
40
+ // (there's no other source of truth to check those calls against).
41
+ </script>
42
+
43
+ <Card {...rest} padded={false} class={cn('overflow-hidden', className)}>
44
+ {#if image}
45
+ <!-- jelinek.css:510 (.photo): rounded-image object-cover w-full. The
46
+ demo (ui/komponenty.html:255) overrides the radius to 0 inline and
47
+ adds aspect-ratio:4/3: the image sits flush against the card's own
48
+ rounded top corners, and `overflow-hidden` on the card above does
49
+ the rounding instead — a rounded image here would double up as a
50
+ visible seam inside the rounded card. `rounded-none` is written
51
+ explicitly, not just omitted, to keep that override visible. -->
52
+ <img src={image} alt={imageAlt} class="aspect-[4/3] w-full rounded-none object-cover" />
53
+ {/if}
54
+ <div class="p-[18px]">
55
+ {#if tag}
56
+ <div class="mb-2.5">{@render tag()}</div>
57
+ {/if}
58
+ <!-- jelinek.css:652 (`.card h3`) already reaches this <h3> through
59
+ Card's own [&_h3]:text-step1 [&_h3]:leading-[1.3] descendant rule
60
+ (Card.svelte) — repeating text-step1/leading here would duplicate
61
+ Card, not correct it. text-fg is still needed: Card sets no text
62
+ color of its own (the static site's .card h3 inherits its color
63
+ from body), so without it this heading would fall back to
64
+ whichever default the consuming app happens to have. mt-2/mb-1
65
+ reproduce the demo's inline `margin:8px 0 4px` exactly — no CSS
66
+ rule sets this margin (it's specific to that one demo instance),
67
+ but both values already sit on Tailwind's default spacing scale. -->
68
+ <h3 class="mt-2 mb-1 text-fg">{title}</h3>
69
+ {#if description}
70
+ <!-- jelinek.css:306 (.small) + shared .muted (jelinek.css:303): a
71
+ fixed --step-down1 size at line-height 1.1 (not on Tailwind's
72
+ leading scale, kept arbitrary) and the muted1 text color.
73
+ font-weight 300 already comes from Card's own
74
+ font-secondary/font-light, inherited here — not re-stated. -->
75
+ <p class="mb-4 text-step-down1 leading-[1.1] text-fg-muted1">{description}</p>
76
+ {/if}
77
+ {#if href}
78
+ <Button {href} {color} fullWidth>{ctaLabel}</Button>
79
+ {/if}
80
+ </div>
81
+ </Card>