@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
package/README.md ADDED
@@ -0,0 +1,492 @@
1
+ # @jelinek/ui
2
+
3
+ Svelte 5 + Tailwind v4 component kit generated from the JELÍNEK brand guide.
4
+
5
+ **The design authority is `assets/css/jelinek.css`** in the root of this
6
+ repository (the brand guide site), not this package. This kit reimplements
7
+ a subset of that CSS as Svelte components; when the two disagree, the guide
8
+ is right and the kit needs fixing. See "Sync contract" below for how (and
9
+ how incompletely) that is enforced.
10
+
11
+ ## Install
12
+
13
+ Published **public on npmjs.com** as `@jelinek/ui` — no token, no `.npmrc`,
14
+ nothing to log into:
15
+
16
+ ```
17
+ pnpm add @jelinek/ui
18
+ ```
19
+
20
+ Up to 0.2.0 it was `@jelinek-nabytek-a-matrace/ui` on GitHub Packages, which
21
+ required a personal access token even for public packages, and consuming apps
22
+ therefore aliased it to the short name. Both the alias and the `.npmrc` lines
23
+ are now dead weight — delete them, or `pnpm install` will keep asking GitHub
24
+ Packages for a package that is no longer published there:
25
+
26
+ ```json
27
+ {
28
+ "dependencies": {
29
+ "@jelinek/ui": "^0.3.0"
30
+ }
31
+ }
32
+ ```
33
+
34
+ The package ships component sources and CSS only. It carries **no font
35
+ files** — `fonts.css` merely references `/assets/fonts/*.woff2`, which the
36
+ consuming app must serve itself. The fonts are licensed and stay out of the
37
+ published tarball.
38
+
39
+ ```js
40
+ import { Button } from '@jelinek/ui';
41
+ ```
42
+
43
+ ## Setup in a consuming app
44
+
45
+ The package's `exports` map (from `package.json`):
46
+
47
+ ```json
48
+ {
49
+ ".": {
50
+ "types": "./dist/index.d.ts",
51
+ "svelte": "./dist/index.js",
52
+ "default": "./dist/index.js"
53
+ },
54
+ "./theme.css": "./dist/theme/tokens.css",
55
+ "./extras.css": "./dist/theme/extras.css",
56
+ "./base.css": "./dist/theme/base.css",
57
+ "./fonts.css": "./dist/theme/fonts.css"
58
+ }
59
+ ```
60
+
61
+ In your app's global CSS entry point:
62
+
63
+ ```css
64
+ @import "tailwindcss";
65
+ @import "@jelinek/ui/theme.css";
66
+ /* @import "@jelinek/ui/extras.css"; -- optional, hand-written theme additions, import after theme.css */
67
+ /* @import "@jelinek/ui/base.css"; -- optional but recommended, import after theme.css (and extras.css, if used) */
68
+
69
+ @source "../node_modules/@jelinek/ui/dist";
70
+ ```
71
+
72
+ `theme.css` (`dist/theme/tokens.css`) is generated — see "Sync contract"
73
+ below — and defines the semantic color/spacing/radius/shadow/typography
74
+ variables plus the `@theme inline` block that maps them onto Tailwind
75
+ utilities. The `@source` line is required: without it, Tailwind never scans
76
+ the kit's own compiled class names and utilities the kit relies on (e.g.
77
+ `rounded-(--radius-button)`) can be missing from your build. Adjust the
78
+ relative path to match where `node_modules` actually sits from your CSS
79
+ file — the path above assumes a typical `src/app.css` one level under the
80
+ app root.
81
+
82
+ **Consumer implication — `theme.css` pins Tailwind's `--spacing` to a fixed
83
+ `4px`.** Tailwind v4's own default, `--spacing: 0.25rem`, is `rem`-relative,
84
+ so it tracks whatever root `font-size` is in play. That root is a flat
85
+ `16px` today (see below), which makes `0.25rem` and `4px` agree — but the
86
+ agreement is a property of the current root value, not of the system, and
87
+ before this kit's type-scale rework the root was
88
+ `clamp(13px, 8px + 1.4vw, 16px)` and the unpinned default shrank every
89
+ `p-*`/`px-*`/`m-*`/`gap-*`/`w-*` utility, kit-wide, by up to 19% below
90
+ roughly 571px viewport width. Type scales with the viewport; spacing does
91
+ not, and the two must not share one lever. This import is required (unlike `base.css`/`extras.css`
92
+ below), which is deliberate: components reproduce guide padding/margin/gap
93
+ values as bare Tailwind spacing utilities (`Card`'s `p-6` for the guide's
94
+ `padding: 24px`, for example) and rely on this pin to stay correct at every
95
+ width, so it has to live in the one file every consumer of the components
96
+ necessarily imports, not an optional one. If your app sets its own
97
+ `--spacing` after this import, components using the bare numeric scale will
98
+ resize with it — same tradeoff as overriding any other token here.
99
+
100
+ `@jelinek/ui/base.css` (`dist/theme/base.css`) is optional but recommended.
101
+ Without it, this kit gives you JELÍNEK colours and components but plain
102
+ markup — a bare `<h2>`, a `<p>`, an `<a>` — still looks generic, because
103
+ `theme.css` only defines custom properties, never element defaults. It
104
+ reproduces the guide's global element layer (`*`, `html`, `body`, `main`,
105
+ `h1`-`h6`, `p`, `a`, `img`, `code`, plus the
106
+ `.muted`/`.muted2`/`.serif`/`.small`/`.tiny`/`.sub` text-role utilities)
107
+ inside `@layer base` — the same layer Tailwind's own preflight populates —
108
+ so import it **after** `tailwindcss` (same layer, later source wins the
109
+ preflight `h1`-`h6` reset) and it will still lose to any Tailwind utility
110
+ class you apply (`text-step2`, `font-bold`, …), since `utilities` is a
111
+ later layer than `base` and layer order always beats specificity. Position
112
+ relative to `theme.css`/`extras.css` doesn't affect that layering — those
113
+ two are unlayered token files — but importing it after them means every
114
+ token it references (`--font-primary`, `--text-black`, the `--step*`
115
+ scale, …) is already defined, including any you've added yourself in
116
+ `extras.css`.
117
+
118
+ **Consumer implication — a fixed 16px root font size, with the fluidity on
119
+ the steps.** `html`'s `font-size` is a flat `16px`. It used to be
120
+ `clamp(13px, 8px + 1.4vw, 16px)`, which made the root — and with it every
121
+ `rem` in the system — *shrink* to 13px on the narrowest phones; the guide
122
+ now keeps the root fixed so body copy is 16px at every viewport width, and
123
+ carries the responsiveness on the individual scale steps instead. Every
124
+ `--stepN` token (`theme.css`) is its own `clamp()` interpolating over
125
+ 320–1024px viewport width, so headings still grow with the viewport
126
+ (`--step6`, i.e. `h1`, runs 33px → 48px) while `--step0` (body) stays
127
+ 16px and `--step-down1`/`--step-down2` (`.small`/`.tiny`) are fixed at
128
+ 15px/13px. Importing base.css therefore gives plain unstyled markup the
129
+ same type behaviour as brand.jlnk.cz.
130
+
131
+ If your app needs the root to follow the reader's own browser font-size
132
+ setting, override `html`'s `font-size` (e.g. to `100%`) in your own CSS
133
+ after this import. Be aware of what that does and does not carry: each
134
+ step's **min, max and intercept are in `rem`** and scale with the new root,
135
+ but its **slope is in `vw`** and does not. The interpolation window
136
+ therefore moves outward in proportion to the root: at a 24px root the
137
+ 320–1024px range becomes 480–1536px, so a step reaches its (now larger)
138
+ maximum at a *wider* viewport, not a narrower one, and the ramp between min
139
+ and max is no longer a clean scale-up of the 16px curve — still monotonic,
140
+ still bounded by the scaled min/max, just a different shape in between. The
141
+ guide keeps `16px` for now for a different reason (`--header-h`/`--subnav-h`
142
+ are hand-measured px while the header's own text is `rem`, so an unpinned
143
+ root exposes that seam); if you take the override, sanity-check your own
144
+ layout at a raised browser default rather than assuming. Note that seam is
145
+ now a **floating-header-only** problem: since Task 26 the guide's phone
146
+ header is `position: sticky` and reserves no space through those constants
147
+ at all, and re-measuring the raised-root sweep confirms it — at a 24px
148
+ browser default with `font-size: 100%` the two glass bars still meet with a
149
+ 0px seam at every width below 640px, and the overlap above it is a flat
150
+ 6.5px (measured at 640, 700, 768, 1024, 1440 and 1920px: the fixed header
151
+ grows to 101.5px against the hand-measured `--header-h: 94px`). Task 30's
152
+ move to the designer's 640px ladder removed the anomaly this note used to
153
+ call out — the old 621px boundary produced a much larger overlap in a
154
+ narrow band right at the switch, and with header and chapter-nav now
155
+ flipping on the same 640px query there is no band left. At the pinned 16px
156
+ root the seam is 0px at every width, 320 to 1920.
157
+
158
+ **Consumer implication — `box-sizing: border-box` on every element.** This
159
+ matches Tailwind's own preflight exactly, so it changes nothing for an app
160
+ that already runs preflight; it only matters if you import base.css
161
+ without Tailwind preflight (or without Tailwind at all), in which case
162
+ your elements now size padding/border inward instead of the browser's
163
+ default content-box behaviour.
164
+
165
+ **Not included — `scroll-behavior: smooth`.** The guide sets this on
166
+ `html`, but base.css deliberately leaves it out: it's a global scroll
167
+ *behaviour*, not a visual default, it isn't load-bearing for anything the
168
+ guide itself does (its own chapter navigation is cross-page links, not
169
+ in-page anchors), it can conflict with an app's own scroll management
170
+ (virtualised lists, scroll-linked animation, router-driven scroll
171
+ restoration), and the guide doesn't gate it behind
172
+ `prefers-reduced-motion`. Add `html { scroll-behavior: smooth }` yourself,
173
+ after this import, if you want it.
174
+
175
+ `html` also gets a static `background-color: var(--bg-white)` — the
176
+ canvas colour the guide reveals on elastic overscroll (rubber-banding past
177
+ the top/bottom of the page). The guide swaps this dynamically by scroll
178
+ position via its own `<script>` (`assets/js/jelinek.js`); base.css ships
179
+ only the static starting colour, not that JS behaviour.
180
+
181
+ `body`'s `background-color`/`color` now transition (`0.2s ease`): if you
182
+ use this kit's `ThemeToggle`, flipping `data-theme` animates instead of
183
+ snapping. Override `body`'s `transition` after import if you don't want
184
+ that.
185
+
186
+ **Consumer implication — this assumes a page shell.** `body` is
187
+ `min-height: 100%; display: flex; flex-direction: column` and `main` is
188
+ `flex: 1 0 auto` (`html` gets `height: 100%` to make the percentage mean
189
+ anything) — the guide's sticky-footer trick, so a page with little content
190
+ still holds its footer to the bottom of the viewport instead of leaving a
191
+ white gap below it. That means base.css expects your page's direct `body`
192
+ children to be exactly: a header, zero or more chrome elements, one
193
+ `<main>`, and a footer — the same shape this package's own showcase uses
194
+ (`src/routes/+layout.svelte`: `SiteHeader`, optionally `ChapterNav`,
195
+ `<main>`, `SiteFooter`, with nothing wrapping them so they're real flex
196
+ children of `body`). If your app already owns a different page shell
197
+ (its own scroll container, a fixed-height app frame, more than one
198
+ `<main>`, …), either don't import base.css and take its other rules
199
+ (typography, links, …) from your own reset, or import it and override
200
+ `body`'s `display`/`flex-direction`/`min-height` afterwards in your own
201
+ CSS — everything else base.css provides (headings, links, the text
202
+ utilities) is independent of this and keeps working.
203
+
204
+ `@jelinek/ui/fonts.css` (`dist/theme/fonts.css`) is optional. It declares
205
+ `@font-face` rules assuming the `.woff2` files are served from
206
+ `/assets/fonts/` in your app (the same absolute path the brand guide site
207
+ itself uses). If you host fonts elsewhere, skip this import and declare
208
+ your own `@font-face` rules — the kit only depends on the
209
+ `--font-primary`/`--font-secondary`/`--font-serif` variables, which all
210
+ have fallbacks.
211
+
212
+ This package's own showcase is the example of hosting fonts
213
+ elsewhere-but-really-the-same-place, and it needs two different answers
214
+ because it's served two ways:
215
+
216
+ - Deployed behind the `brand` Worker at `/ui/`, `/assets/fonts/...`
217
+ already resolves on the same origin for free — `wrangler.jsonc` serves
218
+ the whole repo root as static assets, and `assets/fonts/*.woff2` (the
219
+ one canonical source, per `assets/fonts/README.md`) already lives there.
220
+ Nothing to configure.
221
+ - Standalone `pnpm dev` / `pnpm preview` only know about the `svelte/`
222
+ project, so `vite.config.ts` adds a small dev-only middleware that reads
223
+ `/assets/fonts/*.woff2` straight out of the repo root's `assets/fonts/`
224
+ and serves it at that same unprefixed path. A `static/` symlink was
225
+ tried first and rejected: this project sets `paths.base: '/ui'`
226
+ (it's deployed at `/ui/`, not the site root), and SvelteKit's dev
227
+ server serves everything under `static/` — symlinked or not — prefixed
228
+ by that base, so a symlink would only ever be reachable at
229
+ `/ui/assets/fonts/...`, never at the unprefixed path `fonts.css`
230
+ actually requests. The middleware reproduces the literal production URL
231
+ instead. Either way, the licensed font files themselves are never
232
+ copied into `svelte/`.
233
+
234
+ ## Use with htmx / server-rendered HTML
235
+
236
+ The kit is a Svelte library, but a consuming app doesn't have to be a
237
+ Svelte app. An htmx project (or any server-rendered site) can use it on
238
+ two levels; both need one small asset build, because the package ships
239
+ **uncompiled `.svelte` source** (that's what `svelte-package` produces —
240
+ see `dist/`), and someone has to run the Svelte compiler and Tailwind. In
241
+ an htmx project that someone is a tiny Vite build producing one JS and one
242
+ CSS file that your server-rendered pages link.
243
+
244
+ **Level 1 — CSS layers only.** Import the CSS entry points from "Setup in
245
+ a consuming app" above (`theme.css`, optionally `extras.css`/`base.css`/
246
+ `fonts.css`) into a stylesheet compiled by Tailwind, and add a `@source`
247
+ line pointing at your own template directory so utilities you use in
248
+ server-rendered markup get generated:
249
+
250
+ ```css
251
+ @import "tailwindcss";
252
+ @import "@jelinek/ui/theme.css";
253
+ @import "@jelinek/ui/base.css";
254
+
255
+ @source "../node_modules/@jelinek/ui/dist";
256
+ @source "../templates"; /* your server's HTML templates */
257
+ ```
258
+
259
+ This gives server-rendered markup the brand's tokens, element defaults
260
+ (headings, links, body copy) and the full utility vocabulary. It does
261
+ **not** give you the components themselves — hand-copying a component's
262
+ class string into a template will drift the first time the component
263
+ changes. For anything that looks like a component, use level 2.
264
+
265
+ **Level 2 — components as islands.** Interactive components mount into
266
+ server-rendered pages as Svelte islands. Author each island as a small
267
+ `.svelte` wrapper file in your asset bundle — not by constructing props
268
+ from plain JS — because many components take `Snippet` props (`Button`'s
269
+ `children`, for example), and snippets are markup, which you want to write
270
+ in a `.svelte` file anyway:
271
+
272
+ ```svelte
273
+ <!-- islands/ProductTabs.svelte -->
274
+ <script>
275
+ import { Tab, Tabs } from '@jelinek/ui';
276
+
277
+ let { value = 'popis' } = $props();
278
+ </script>
279
+
280
+ <Tabs bind:value>
281
+ <Tab value="popis">Popis</Tab>
282
+ <Tab value="parametry">Parametry</Tab>
283
+ </Tabs>
284
+ ```
285
+
286
+ The entry point mounts islands on load and after every htmx swap, and
287
+ unmounts them when htmx removes their element — without the cleanup
288
+ listener, every swap leaks the previous instance:
289
+
290
+ ```js
291
+ // islands.js — the single <script type="module"> your layout links
292
+ import { mount, unmount } from 'svelte';
293
+ import './app.css'; // the stylesheet from level 1
294
+ import ProductTabs from './islands/ProductTabs.svelte';
295
+
296
+ const REGISTRY = { ProductTabs };
297
+ const mounted = new WeakMap();
298
+
299
+ function hydrate(root) {
300
+ const targets = [root, ...root.querySelectorAll('[data-island]')];
301
+ for (const el of targets) {
302
+ if (!el.matches?.('[data-island]') || mounted.has(el)) continue;
303
+ const Component = REGISTRY[el.dataset.island];
304
+ if (!Component) continue;
305
+ const props = el.dataset.props ? JSON.parse(el.dataset.props) : {};
306
+ mounted.set(el, mount(Component, { target: el, props }));
307
+ }
308
+ }
309
+
310
+ // Runs once on page load and again for every htmx swap.
311
+ htmx.onLoad(hydrate);
312
+
313
+ // htmx removes swapped-out elements itself; Svelte has to be told.
314
+ document.body.addEventListener('htmx:beforeCleanupElement', (e) => {
315
+ const app = mounted.get(e.target);
316
+ if (app) {
317
+ unmount(app);
318
+ mounted.delete(e.target);
319
+ }
320
+ });
321
+ ```
322
+
323
+ Server-rendered pages then place islands declaratively; htmx attributes
324
+ and islands coexist freely:
325
+
326
+ ```html
327
+ <div data-island="ProductTabs" data-props='{"value":"parametry"}'></div>
328
+ ```
329
+
330
+ The Vite project around this is three files (`package.json` with
331
+ `@jelinek/ui`, `svelte`, `vite`, `@sveltejs/vite-plugin-svelte`,
332
+ `@tailwindcss/vite`; a `vite.config.js` with the two plugins and
333
+ `build.rollupOptions.input: 'islands.js'`; the `islands.js` above). Point
334
+ `build.outDir` wherever your server serves static assets from.
335
+
336
+ Two caveats, both inherited from the setup section above: the `@source`
337
+ line for the kit's `dist/` is still required (island components' own
338
+ classes live there), and `fonts.css` still assumes `/assets/fonts/` on
339
+ your origin — host the `.woff2` files there or write your own
340
+ `@font-face`. And a boundary worth knowing before you commit to it:
341
+ paired components that talk through Svelte context (`Tabs` → `Tab`,
342
+ `RadioGroup` → `Radio`, `Container` → `PageHero`) must live inside **one**
343
+ island — context does not cross island boundaries, so a `Tab` in one
344
+ `data-island` cannot report to a `Tabs` in another.
345
+
346
+ ## Dark mode and divisions
347
+
348
+ - Dark mode: set `data-theme="dark"` on the document element (or add class
349
+ `.dark` — both are supported so pim/admin's existing `.dark` convention
350
+ keeps working).
351
+ - Division re-theme: `data-division="mattress"` swaps the brand's brown
352
+ accent for magenta (Zdravý spánek / mattress division).
353
+ `data-theme="mattress"` is kept as an alias for the same effect, for
354
+ apps whose existing markup already sets it that way.
355
+
356
+ **`ThemeToggle`'s `theme` prop defaults to `undefined`, not `'light'`.**
357
+ When left unbound, it seeds itself from whatever is already on the
358
+ document — `document.documentElement.dataset.theme` if you've already set
359
+ `data-theme` yourself (a persisted choice from localStorage, a cookie, an
360
+ SSR-rendered attribute), falling back to `prefers-color-scheme` only if
361
+ nothing is set — instead of writing a hardcoded default back onto `<html>`
362
+ on mount and clobbering whatever you'd already put there. If you do bind
363
+ `theme` yourself (`bind:theme={yourState}`), that binding is authoritative
364
+ as before; the DOM-seeding only applies when it's left unbound.
365
+
366
+ ## Components
367
+
368
+ Alert, Button, Card, ChapterNav, Checkbox, CodeCopy, Container, ContentLogo,
369
+ Demo, Eyebrow, Glass, Grid, Label, PageHero, Pager, Panel, ProductCard, Radio,
370
+ RadioGroup, Select, SiteFooter, SiteHeader, Slider, Swatch, Tab, Tabs, Tag,
371
+ TextField, ThemeToggle, Tile, TileScroller.
372
+
373
+ See `src/lib/index.ts` for the exact export list and `src/lib/components/`
374
+ for source. Per-component contracts, including the two below, are printed at
375
+ `/ui/api`.
376
+
377
+ ### Two constraints worth knowing before you use them
378
+
379
+ **`Tabs` — the scroll arrows are pinned to the WRAPPER, not to the row of
380
+ pills.** When the segments overflow, `Tabs` grows a wrapper with a back/forward
381
+ arrow on each side, and those arrows sit at the wrapper's edges. If the row is
382
+ narrower than its wrapper — which is what happens the moment you constrain it
383
+ with `w-3/4`, `max-w-*` or a `mx-auto` width — the arrows detach from the pills
384
+ and point at empty space. Measured: 439px adrift with a 200px row inside a
385
+ 1082px wrapper, and already 68px off at this showcase's own `min-[640px]:w-3/4`
386
+ call site (invisible there only because nothing overflows at that width). Keep
387
+ `Tabs` at the full width of its wrapper anywhere the segments can overflow, or
388
+ constrain the wrapper rather than the component.
389
+
390
+ **`SiteHeader` floats at every width, so the page must reserve its height.**
391
+ The bar is `absolute` below 640px and `fixed` from 640px up; it never adds
392
+ padding to a sibling. Reserve `--header-h` (70px phone, 94px from 640px) plus
393
+ `--subnav-h` (58px) when a `ChapterNav` is present — `PageHero` and this
394
+ showcase's `+layout.svelte` both model it. And below 640px `ChapterNav` renders
395
+ nothing at all: pass the same chapter list to `SiteHeader`'s `chapterItems` so
396
+ a phone reader can still reach the chapters from the menu panel.
397
+
398
+ ## Sync contract — and its limits
399
+
400
+ Two automated checks keep this kit from silently drifting away from the
401
+ guide. Both run in CI (see below) and both are honest about what they do
402
+ *not* catch.
403
+
404
+ **`pnpm check:tokens`** (`scripts/gen-tokens.js --check`) regenerates
405
+ `src/lib/theme/tokens.css` from the guide's `:root` / `:root[data-theme="dark"]`
406
+ blocks and fails if the committed file is stale. It also fails the build
407
+ outright — `gen-tokens.js`, unchecked — if the guide defines a `:root`
408
+ custom property that is in neither `TOKEN_MAP` nor `PASSTHROUGH` in
409
+ `scripts/token-map.js`. That's deliberate: a brand-new design token
410
+ (a new color, spacing step, radius, etc.) must become a visible task for
411
+ someone to map, not a silent omission. `tokens.css` is **generated —
412
+ never hand-edit it**; run `pnpm gen:tokens` and commit the result.
413
+ Hand-written theme additions belong in `src/lib/theme/extras.css`, which
414
+ the generator never touches.
415
+
416
+ **`pnpm check:css-lock`** (`scripts/check-css-lock.js`) hashes the guide's
417
+ CSS rule groups for two fixed, hardcoded allow-lists in that file:
418
+ `GUARDED` — class names (`btn`, `tag`, `alert`, `tile`, `panel`, `input`,
419
+ `grid`, `site-header`, and so on, one entry per class a component in this
420
+ kit reimplements — including the six text-role utility classes `.muted`,
421
+ `.muted2`, `.serif`, `.small`, `.tiny`, `.sub`, whose "component" is
422
+ `src/lib/theme/base.css` itself) — and `GUARDED_ELEMENTS` — bare type
423
+ selectors (`*`, `html`, `body`, `main`, `h1`-`h6`, `p`, `a`, `img`, `code`)
424
+ that base.css also reproduces and that have no class for `GUARDED` to
425
+ match. Both are compared against `component-css.lock.json`. If a guarded
426
+ name's rules change in the guide, the check fails and names which
427
+ component needs review (`.grid rules changed` for a class, `body rules
428
+ changed` for an element — no leading dot); `--write` refreshes the lock
429
+ once you've handled it.
430
+
431
+ **The gap, stated plainly:** `check:css-lock` only re-checks names that are
432
+ *already* on `GUARDED` or `GUARDED_ELEMENTS`. It cannot detect that a
433
+ **brand-new** component-shaped class — or bare element rule — was added to
434
+ the guide — say, a designer adds `.badge-new-something { ... }` to
435
+ `jelinek.css` for a component this kit doesn't have yet, or a new
436
+ `blockquote { ... }` that base.css doesn't reproduce. That rule gets no
437
+ bucket in the hash and the check reports "no drift", exit 0, with nothing
438
+ in the kit reviewing it. This was verified by executing exactly that
439
+ experiment during the final branch review. It is not a bug: reliably
440
+ telling "this new CSS rule is a component that needs tracking" from "this
441
+ is a one-off utility" cannot be inferred from selector shape alone, so
442
+ `GUARDED` and `GUARDED_ELEMENTS` stay manual allow-lists rather than
443
+ something auto-detected. In practice this means: **when you add a new
444
+ component to this kit (or extend base.css), add its guide class name to
445
+ `GUARDED` — or its bare element name to `GUARDED_ELEMENTS` — in
446
+ `scripts/check-css-lock.js` and run `--write`** — don't rely on the check
447
+ to remind you it's missing. See the header comment in that file for the
448
+ full reasoning.
449
+
450
+ ## CI
451
+
452
+ `.github/workflows/ui.yml` runs on pull requests touching `svelte/**` or
453
+ `assets/css/jelinek.css`, and on push to `master`. It does **not** run on
454
+ a bare feature-branch push (no PR yet) — only PR and `master` events
455
+ trigger the `verify` job: token drift, CSS-lock drift, `check`, `test`,
456
+ `build`, `build:showcase` (the only step that actually compiles Tailwind
457
+ and prerenders — see its own comment in the workflow for why it's
458
+ separate from `build`), and finally a Playwright e2e suite
459
+ (`e2e/theme-division.spec.ts`) run against that showcase build — browser-
460
+ level regression coverage for the theme × division custom-property
461
+ cascade that `pnpm test`'s jsdom environment structurally cannot check
462
+ (jsdom never resolves a CSS custom-property cascade at all).
463
+
464
+ The `publish` job is separate and gated on a tag matching `ui-v*` — it
465
+ does not run on ordinary merges to `master`, only when someone pushes a
466
+ `ui-v*` tag, and it also depends on `verify` passing first. It publishes
467
+ to **npmjs.org with `--access public`**, authenticating with the
468
+ `NPM_TOKEN` repo secret (a granular automation token for the `jelinek`
469
+ npm org — a different namespace from the GitHub org, which is named
470
+ `JELINEK-nabytek-a-matrace`). Two things to know about that token: npm
471
+ expires granular tokens, so a publish failing with a 401 after months of
472
+ silence means it needs regenerating; and npm is phasing out tokens that
473
+ bypass 2FA (account changes Aug 2026, direct publishing Jan 2027), whose
474
+ replacement is trusted publishing via GitHub Actions OIDC. No provenance
475
+ (`--provenance`) is generated — npm rejects it for builds from a private
476
+ repository, and this repo is private.
477
+
478
+ ## Known design debt
479
+
480
+ The guide does not yet define everything this kit's consumers need. See:
481
+
482
+ - `docs/chybejici-komponenty.md` — missing interface components (icons,
483
+ form states, modals/toasts, tables, e-shop pieces, navigation), with
484
+ priority.
485
+ - `docs/chybejici-bloky-cms.md` — the 16 CMS content blocks that exist in
486
+ code but were never designed against the brand guide.
487
+
488
+ Five components in this kit have **no corresponding CSS in the guide** and
489
+ were necessarily invented rather than reimplemented: `Checkbox`, `Radio`,
490
+ `RadioGroup`, `Slider`, and `ProductCard`. Treat their current appearance
491
+ as a placeholder, not as an extension of the brand guide's authority —
492
+ they should be revisited once the guide actually specifies them.
@@ -0,0 +1,59 @@
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 = 'success' | 'error' | 'warning' | 'info';
7
+
8
+ interface Props {
9
+ variant?: Variant;
10
+ title?: string;
11
+ class?: string;
12
+ children: Snippet;
13
+ }
14
+
15
+ let {
16
+ variant = 'info',
17
+ title,
18
+ class: className,
19
+ children,
20
+ ...rest
21
+ }: Props & HTMLAttributes<HTMLDivElement> = $props();
22
+
23
+ // jelinek.css:322-325 puts .alert (with .card/.panel/.tile/.demo/.swatch) on
24
+ // the secondary (Avenir) family at weight 300 (Light) as its body text.
25
+ // jelinek.css (.alert): 16px/20px padding (p-4 alone would drop the 20px
26
+ // horizontal half).
27
+ // Task 43 — the size is --step-down1 (.small's own step). It used to be a
28
+ // hardcoded 0.8rem, which the guide's OWN comment on the rule contradicted
29
+ // ("text v sémantických boxech = velikost small"): 0.8rem is 12.8px, i.e.
30
+ // the TINY step, and the reader caught it. The literal predated the scale
31
+ // change that put .small at 15px; the guide rule now reads
32
+ // var(--step-down1) and this component follows it as text-step-down1.
33
+ // Deliberately NOT reproduced: jelinek.css:600's `margin: 0 0 14px`. Baking
34
+ // an external margin into a reusable atom fights a consumer's own layout
35
+ // (flex/grid gap); the guide itself zeroes the equivalent margin on cards
36
+ // inside a `.grid` (line 485), which reads as confirmation that this
37
+ // margin is page-authoring convenience, not an intrinsic part of "alert".
38
+ // Flagging for adjudication per the task brief.
39
+ const BASE =
40
+ 'rounded-card py-4 px-5 font-secondary font-light text-step-down1 leading-[1.1] text-fg dark:text-fg-inverse';
41
+
42
+ const VARIANT: Record<Variant, string> = {
43
+ success: 'bg-sem-success',
44
+ error: 'bg-sem-error',
45
+ warning: 'bg-sem-warning',
46
+ info: 'bg-sem-info'
47
+ };
48
+ </script>
49
+
50
+ <div
51
+ role={variant === 'error' ? 'alert' : 'status'}
52
+ {...rest}
53
+ class={cn(BASE, VARIANT[variant], className)}
54
+ >
55
+ {#if title}
56
+ <strong class="mb-0.5 block font-medium">{title}</strong>
57
+ {/if}
58
+ {@render children()}
59
+ </div>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ type Variant = 'success' | 'error' | 'warning' | 'info';
4
+ interface Props {
5
+ variant?: Variant;
6
+ title?: string;
7
+ class?: string;
8
+ children: Snippet;
9
+ }
10
+ type $$ComponentProps = Props & HTMLAttributes<HTMLDivElement>;
11
+ declare const Alert: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type Alert = ReturnType<typeof Alert>;
13
+ export default Alert;