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