@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,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>
|