@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,306 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { setContext, type Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import { cn } from '../utils/cn.js';
|
|
5
|
+
import { TABS_CONTEXT, type TabsContext } from './tabs-context.js';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
value?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Accessible names for the two scroll arrows. Verbatim from
|
|
11
|
+
* assets/js/jelinek.js:502-503, which passes exactly these strings to
|
|
12
|
+
* `arrow()` and sets them as the buttons' `aria-label`. Czech by default,
|
|
13
|
+
* like every other user-facing string this kit ships (see ChapterNav's
|
|
14
|
+
* `label`).
|
|
15
|
+
*/
|
|
16
|
+
prevLabel?: string;
|
|
17
|
+
nextLabel?: string;
|
|
18
|
+
class?: string;
|
|
19
|
+
children: Snippet;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
value = $bindable(''),
|
|
24
|
+
prevLabel = 'Posunout na předchozí položky',
|
|
25
|
+
nextLabel = 'Posunout na další položky',
|
|
26
|
+
class: className,
|
|
27
|
+
children,
|
|
28
|
+
...rest
|
|
29
|
+
}: Props & HTMLAttributes<HTMLDivElement> = $props();
|
|
30
|
+
|
|
31
|
+
setContext<TabsContext>(TABS_CONTEXT, {
|
|
32
|
+
current: () => value,
|
|
33
|
+
select: (next) => (value = next)
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// jelinek.css:834-838 (.tabs): display:flex, 6px gap. gap-1.5 = 6px —
|
|
37
|
+
// a fixed px, not 1.5 × a rem-relative default; see theme.css's
|
|
38
|
+
// `--spacing: 4px` pin (gen-tokens.js) for why that distinction matters
|
|
39
|
+
// under base.css's responsive root font-size (Task 20).
|
|
40
|
+
//
|
|
41
|
+
// TASK 31 — `.tabs` STOPPED WRAPPING AND BECAME A SIDEWAYS SCROLLER. The
|
|
42
|
+
// guide now reads `flex-wrap: nowrap; overflow-x: auto; scroll-behavior:
|
|
43
|
+
// smooth; scrollbar-width: none; -ms-overflow-style: none`, plus
|
|
44
|
+
// `.tabs::-webkit-scrollbar { display: none }` (:844-846, a rule shared with
|
|
45
|
+
// .chapter-nav__inner and .logo-tool__bgtabs, which get the same scroller
|
|
46
|
+
// declarations at :840-843). Its own comment states the intent: when the
|
|
47
|
+
// segments do not fit, they "NEZALOMÍ se do dalšího řádku, ale posouvají se
|
|
48
|
+
// do strany" — they no longer break onto a second line, they scroll
|
|
49
|
+
// sideways.
|
|
50
|
+
//
|
|
51
|
+
// This is a PAIRED change with Tab.svelte's `flex: 1 0 auto` (jelinek.css:880,
|
|
52
|
+
// was `flex: 1`). Do not touch one without the other: `flex: 1` carries a 0%
|
|
53
|
+
// basis, so segments would happily shrink below their own text inside a
|
|
54
|
+
// nowrap row and nothing would ever overflow — the scroller would never
|
|
55
|
+
// engage and the labels would just squash. `1 0 auto` grows to fill a wide
|
|
56
|
+
// row but never shrinks, which is what produces the overflow this rule
|
|
57
|
+
// scrolls.
|
|
58
|
+
// TASK 33 — the pill joined the surface system. The designer's 8d68770
|
|
59
|
+
// ("pilulky: stejný border a horní highlight jako ostatní povrchy") replaced
|
|
60
|
+
// `border: 0` with `border: 1px solid var(--border-subtle)` — the same
|
|
61
|
+
// hairline .panel/.card/.tile carry — and appended the shared dark-mode top
|
|
62
|
+
// highlight `inset 0 2px 1px -1px var(--box-primary)` to the dark inset
|
|
63
|
+
// shadow. The shadow's own two layers moved to HSL notation in b45c536 at
|
|
64
|
+
// bit-identical values (#00000040 = 0.251, #00000010 = 0.063, #ffffff40,
|
|
65
|
+
// #ffffff10 likewise), and they follow that notation here — in the
|
|
66
|
+
// SPACE-separated hsl() form, `hsl(0 0% 0% / 0.251)`, not the guide's
|
|
67
|
+
// comma-separated `hsl(0, 0%, 0%, 0.251)`. The two are the same colour in
|
|
68
|
+
// CSS Color 4; the difference is forced by Tailwind's arbitrary-value
|
|
69
|
+
// syntax, where commas inside `shadow-[…]` are the shadow-LAYER separator
|
|
70
|
+
// and a comma-form hsl() would be parsed as several broken layers. That is
|
|
71
|
+
// why the previous version of this line used #RRGGBBAA hex at all.
|
|
72
|
+
//
|
|
73
|
+
// --box-primary is a real passthrough token as of this task (it was promoted
|
|
74
|
+
// from a per-surface local property to one neutral :root value in 4ec82cd,
|
|
75
|
+
// see scripts/token-map.js), so it is referenced by name rather than
|
|
76
|
+
// substituted — the previous `var(--color-jelinek-black)` stand-in would now
|
|
77
|
+
// render a 95%-light highlight in dark mode instead of the intended 50% grey.
|
|
78
|
+
const BASE =
|
|
79
|
+
'flex flex-nowrap gap-1.5 rounded-pill border border-border-subtle bg-transparent p-1.5 ' +
|
|
80
|
+
'overflow-x-auto [scroll-behavior:smooth] [scrollbar-width:none] ' +
|
|
81
|
+
'[-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden ' +
|
|
82
|
+
'shadow-[inset_0_0px_5px_hsl(0_0%_0%_/_0.251),inset_0_0px_5px_hsl(0_0%_0%_/_0.063)] ' +
|
|
83
|
+
'dark:shadow-[0_0_10px_hsl(0_0%_100%_/_0.251)_inset,inset_0_0px_50px_hsl(0_0%_100%_/_0.063),inset_0_2px_1px_-1px_var(--box-primary)]';
|
|
84
|
+
|
|
85
|
+
// jelinek.css:894-900 (shared segmented-switcher pill — selector list also
|
|
86
|
+
// covers .color-formats/.logo-tool__langs/.logo-tool__bgtabs): transparent
|
|
87
|
+
// background, no border, pill radius, 6px padding (p-1.5, same unit as the
|
|
88
|
+
// row gap), plus a two-layer inset shadow, above. Every appearance of tabs in
|
|
89
|
+
// the guide sits inside this pill — ui-old/komponenty.html:117 puts both
|
|
90
|
+
// classes (`class="tabs tabs--demo"`) on the very same element this component
|
|
91
|
+
// renders, and ui-old/barvy-v-akci.html:54 does the same for the division
|
|
92
|
+
// switcher. There is no bare-track tab style in the guide, so this pill IS
|
|
93
|
+
// Tabs' default appearance, not optional demo chrome. The shadow values are
|
|
94
|
+
// literals in the guide, not tokens, so they stay Tailwind arbitrary values
|
|
95
|
+
// here rather than named shadow-resting/interactive utilities.
|
|
96
|
+
//
|
|
97
|
+
// jelinek.css:255-258 (dark-mode counterpart, same selector list): a
|
|
98
|
+
// different, hand-written inset shadow (not a token) — applied through the
|
|
99
|
+
// kit's `dark:` variant (tokens.css:181).
|
|
100
|
+
//
|
|
101
|
+
// STILL NOT reproduced here: the width. That rule moved and became
|
|
102
|
+
// responsive in the merge — `.tabs--demo, .color-formats { width: 100%;
|
|
103
|
+
// margin-inline: auto }` with `@media (min-width: 640px) { width: 75% }`
|
|
104
|
+
// (jelinek.css:1067-1068) — but it is still page-layout centring for the
|
|
105
|
+
// guide's demo card rather than part of the switcher, so it stays out of the
|
|
106
|
+
// component on exactly the reasoning that kept the old flat `width: 75%`
|
|
107
|
+
// out. Callers apply it through the external `class` prop, and the showcase
|
|
108
|
+
// caller uses the mobile-first pair (see routes/komponenty/+page.svelte).
|
|
109
|
+
// Baking it in would be worse now than before: a component that forces
|
|
110
|
+
// itself to 75% of its parent cannot be used in a narrow column at all.
|
|
111
|
+
|
|
112
|
+
// ─── TASK 32 — THE SCROLL AFFORDANCE, AND WHY IT IS ARROWS ──────────────
|
|
113
|
+
//
|
|
114
|
+
// Task 31 shipped the scroller CSS without the thing that makes a hidden
|
|
115
|
+
// scrollbar usable, and said so: an overflowing row showed nothing at all.
|
|
116
|
+
// Two closable designs existed in the guide, and this component takes the
|
|
117
|
+
// one the guide applies to THIS element:
|
|
118
|
+
//
|
|
119
|
+
// - `.tabs` gets ARROW BUTTONS. assets/js/jelinek.js:477-521
|
|
120
|
+
// (`initTabScroll`) wraps every element matching `SCROLLERS` (:475 —
|
|
121
|
+
// `.tabs, .logo-tool__bgtabs, .tile-scroller`) in a `.tabs-scroll` div
|
|
122
|
+
// (jelinek.css:851) and injects `.tabs-scroll__prev/__next`, styled at
|
|
123
|
+
// jelinek.css:858-873.
|
|
124
|
+
// - `.chapter-nav__inner` gets GRADIENT EDGE FADES, and jelinek.js
|
|
125
|
+
// :465-474 goes out of its way to EXCLUDE it from that selector, in a
|
|
126
|
+
// comment that is explicit about why: "Dvě různá vodítka na jednom
|
|
127
|
+
// prvku … nenavrhl nikdo" (nobody designed two different affordances on
|
|
128
|
+
// one element), and "kdyby lišta měla mít šipky místo zastínění, vrať
|
|
129
|
+
// sem `.chapter-nav__inner` a smaž ::before/::after v CSS — obojí naráz
|
|
130
|
+
// ne".
|
|
131
|
+
//
|
|
132
|
+
// So the two treatments are a deliberate per-element choice, not a default
|
|
133
|
+
// and an exception, and copying ChapterNav's fades onto tabs would be a
|
|
134
|
+
// deviation from the guide rather than reuse of it. It would also not work:
|
|
135
|
+
// a fade is a `linear-gradient` from an OPAQUE surface colour, and
|
|
136
|
+
// ChapterNav can use `var(--color-surface)` because its bar is always the
|
|
137
|
+
// page background. `.tabs` is `background: transparent` sitting inside
|
|
138
|
+
// whatever surface a caller puts it on — a `<Demo variant="brown">`, a
|
|
139
|
+
// `<Panel variant="black">`, a photo behind `<Glass>` — so the same fade
|
|
140
|
+
// would paint a page-coloured smear over a coloured card. Arrows are
|
|
141
|
+
// self-contained (`bg-surface` pill + shadow) and carry no assumption about
|
|
142
|
+
// what is behind them.
|
|
143
|
+
//
|
|
144
|
+
// Written as real markup rather than injected on mount, because a component
|
|
145
|
+
// owns its markup — the same call PageHero makes about the guide's
|
|
146
|
+
// `<hr class="rule">`. That also fixes the guide's own ordering hazard: its
|
|
147
|
+
// arrows only exist after `initTabScroll()` runs, so a `.tabs` rendered
|
|
148
|
+
// later by an app's own JS never gets any.
|
|
149
|
+
//
|
|
150
|
+
// WHAT THIS DOES NOT REPRODUCE: jelinek.js:496-497's `try/catch` around
|
|
151
|
+
// `scrollBy({...})`, a fallback for embedded webviews that lack the object
|
|
152
|
+
// form. This kit's floor is Svelte 5 + `ResizeObserver`, both of which
|
|
153
|
+
// postdate `ScrollToOptions` everywhere, so the catch would be unreachable.
|
|
154
|
+
// Recorded as a decision, not an oversight.
|
|
155
|
+
//
|
|
156
|
+
// GEOMETRY NOTE. The guide pins its arrows to the WRAPPER's edges
|
|
157
|
+
// (`left: 2px` / `right: 2px`, jelinek.css:867-868), not the row's, and so
|
|
158
|
+
// does this. That is exact whenever the row fills its wrapper — which is
|
|
159
|
+
// every phone width, since the guide's own call-site width is `100%` below
|
|
160
|
+
// 640px and overflow is a narrow-screen condition. It is also why `class`
|
|
161
|
+
// keeps landing on the ROW rather than on the wrapper: the cn() contract and
|
|
162
|
+
// every existing caller depend on it. A caller that needs the WRAPPER to be
|
|
163
|
+
// a flex item with `flex: 1` (the guide does this for `.color-formats`)
|
|
164
|
+
// cannot express that through `class` today; no caller needs it yet.
|
|
165
|
+
const WRAP = 'relative min-w-0';
|
|
166
|
+
|
|
167
|
+
// jelinek.css:858-866 + :872-873, one to one: 30px circle, no border, no padding,
|
|
168
|
+
// centred content, surface fill + --text-muted1 glyph, --shadow-resting, and
|
|
169
|
+
// the two-property transition on the guide's own duration/easing tokens.
|
|
170
|
+
// Hover inverts to the neutral accent pair (--JELINEK-BLACK /
|
|
171
|
+
// --ON-JELINEK-BLACK), which flips itself in dark mode.
|
|
172
|
+
const ARROW =
|
|
173
|
+
'absolute top-1/2 flex h-[30px] w-[30px] -translate-y-1/2 items-center justify-center ' +
|
|
174
|
+
'rounded-pill border-0 p-0 bg-surface text-fg-muted1 shadow-resting cursor-pointer ' +
|
|
175
|
+
'transition-[background-color,color] duration-[var(--dur-base)] ease-[var(--ease)] ' +
|
|
176
|
+
'hover:bg-jelinek-black hover:text-on-jelinek-black';
|
|
177
|
+
|
|
178
|
+
// State comes from the scroller, never from a breakpoint: a two-segment row
|
|
179
|
+
// does not overflow at any width and correctly shows no arrow at all. The
|
|
180
|
+
// thresholds are the guide's own (jelinek.js:506-508 uses +2/-2 px slack, so
|
|
181
|
+
// sub-pixel layout never leaves a permanently-lit arrow that cannot move).
|
|
182
|
+
// Both start at 0, so server-rendered HTML contains no buttons and the
|
|
183
|
+
// hydrated page adds them only if the row really does overflow.
|
|
184
|
+
let scroller = $state<HTMLElement | null>(null);
|
|
185
|
+
let prevButton = $state<HTMLButtonElement | null>(null);
|
|
186
|
+
let nextButton = $state<HTMLButtonElement | null>(null);
|
|
187
|
+
let scrollLeft = $state(0);
|
|
188
|
+
let scrollMax = $state(0);
|
|
189
|
+
const showPrev = $derived(scrollMax > 2 && scrollLeft > 2);
|
|
190
|
+
const showNext = $derived(scrollMax > 2 && scrollLeft < scrollMax - 2);
|
|
191
|
+
|
|
192
|
+
$effect(() => {
|
|
193
|
+
const el = scroller;
|
|
194
|
+
if (!el) return;
|
|
195
|
+
function measure() {
|
|
196
|
+
if (!el) return;
|
|
197
|
+
scrollLeft = el.scrollLeft;
|
|
198
|
+
scrollMax = el.scrollWidth - el.clientWidth;
|
|
199
|
+
}
|
|
200
|
+
measure();
|
|
201
|
+
el.addEventListener('scroll', measure, { passive: true });
|
|
202
|
+
const ro = new ResizeObserver(measure);
|
|
203
|
+
ro.observe(el);
|
|
204
|
+
return () => {
|
|
205
|
+
el.removeEventListener('scroll', measure);
|
|
206
|
+
ro.disconnect();
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// jelinek.js:493-497 — 80 % of the visible width per press, smooth.
|
|
211
|
+
//
|
|
212
|
+
// THE FOCUS HAND-OFF is this component's own addition, and it is why `nudge`
|
|
213
|
+
// is not simply a `scrollBy`. The guide's arrows are `display: none` the
|
|
214
|
+
// moment their direction is exhausted, so a keyboard user who presses Enter
|
|
215
|
+
// on "next" until the row ends has the button pulled out from under them and
|
|
216
|
+
// focus falls to <body> — measured, on this very page: one Enter at 320px
|
|
217
|
+
// takes scrollLeft from 0 to its 73px maximum, "next" unmounts, and
|
|
218
|
+
// `document.activeElement` becomes BODY, i.e. the next Tab press restarts
|
|
219
|
+
// from the top of the document.
|
|
220
|
+
//
|
|
221
|
+
// It cannot be fixed inline in this handler. At the moment of the press the
|
|
222
|
+
// opposite arrow does not exist yet — at scrollLeft 0 there is no "prev"
|
|
223
|
+
// button to hand focus to — and it only mounts once a scroll event reports
|
|
224
|
+
// the new position, part-way through the smooth animation. So the intent is
|
|
225
|
+
// recorded here and the effect below performs the focus when the element
|
|
226
|
+
// actually appears.
|
|
227
|
+
let handoff = $state<'prev' | 'next' | null>(null);
|
|
228
|
+
|
|
229
|
+
function nudge(dir: 'prev' | 'next') {
|
|
230
|
+
const el = scroller;
|
|
231
|
+
if (!el) return;
|
|
232
|
+
const step = Math.round(el.clientWidth * 0.8) * (dir === 'prev' ? -1 : 1);
|
|
233
|
+
const target = Math.max(0, Math.min(el.scrollLeft + step, scrollMax));
|
|
234
|
+
const exhausts = dir === 'prev' ? target <= 2 : target >= scrollMax - 2;
|
|
235
|
+
// If this press exhausts the direction, the pressed button is about to
|
|
236
|
+
// unmount; claim the opposite one, which by definition becomes reachable
|
|
237
|
+
// in the same move. Otherwise cancel any stale claim.
|
|
238
|
+
handoff = exhausts ? (dir === 'prev' ? 'next' : 'prev') : null;
|
|
239
|
+
el.scrollBy({ left: step, behavior: 'smooth' });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
$effect(() => {
|
|
243
|
+
if (!handoff) return;
|
|
244
|
+
const target = handoff === 'prev' ? prevButton : nextButton;
|
|
245
|
+
if (!target) return; // not mounted yet; this effect re-runs when it is
|
|
246
|
+
target.focus();
|
|
247
|
+
handoff = null;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// jelinek.js:459-463 (`chevron()`), byte for byte: 16×16 in a 24-unit box, stroked in
|
|
251
|
+
// currentColor at 2.5 with round joins, so it inherits the button's hover
|
|
252
|
+
// colour change rather than needing its own rule.
|
|
253
|
+
const CHEVRON = { prev: 'M15 6l-6 6 6 6', next: 'M9 6l6 6-6 6' };
|
|
254
|
+
</script>
|
|
255
|
+
|
|
256
|
+
<div class={WRAP}>
|
|
257
|
+
<div bind:this={scroller} {...rest} role={rest.role ?? 'tablist'} class={cn(BASE, className)}>
|
|
258
|
+
{@render children()}
|
|
259
|
+
</div>
|
|
260
|
+
{#if showPrev}
|
|
261
|
+
<button
|
|
262
|
+
bind:this={prevButton}
|
|
263
|
+
type="button"
|
|
264
|
+
class={cn(ARROW, 'left-0.5')}
|
|
265
|
+
aria-label={prevLabel}
|
|
266
|
+
onclick={() => nudge('prev')}
|
|
267
|
+
>
|
|
268
|
+
<svg
|
|
269
|
+
width="16"
|
|
270
|
+
height="16"
|
|
271
|
+
viewBox="0 0 24 24"
|
|
272
|
+
fill="none"
|
|
273
|
+
stroke="currentColor"
|
|
274
|
+
stroke-width="2.5"
|
|
275
|
+
stroke-linecap="round"
|
|
276
|
+
stroke-linejoin="round"
|
|
277
|
+
aria-hidden="true"
|
|
278
|
+
>
|
|
279
|
+
<path d={CHEVRON.prev} />
|
|
280
|
+
</svg>
|
|
281
|
+
</button>
|
|
282
|
+
{/if}
|
|
283
|
+
{#if showNext}
|
|
284
|
+
<button
|
|
285
|
+
bind:this={nextButton}
|
|
286
|
+
type="button"
|
|
287
|
+
class={cn(ARROW, 'right-0.5')}
|
|
288
|
+
aria-label={nextLabel}
|
|
289
|
+
onclick={() => nudge('next')}
|
|
290
|
+
>
|
|
291
|
+
<svg
|
|
292
|
+
width="16"
|
|
293
|
+
height="16"
|
|
294
|
+
viewBox="0 0 24 24"
|
|
295
|
+
fill="none"
|
|
296
|
+
stroke="currentColor"
|
|
297
|
+
stroke-width="2.5"
|
|
298
|
+
stroke-linecap="round"
|
|
299
|
+
stroke-linejoin="round"
|
|
300
|
+
aria-hidden="true"
|
|
301
|
+
>
|
|
302
|
+
<path d={CHEVRON.next} />
|
|
303
|
+
</svg>
|
|
304
|
+
</button>
|
|
305
|
+
{/if}
|
|
306
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
interface Props {
|
|
4
|
+
value?: string;
|
|
5
|
+
/**
|
|
6
|
+
* Accessible names for the two scroll arrows. Verbatim from
|
|
7
|
+
* assets/js/jelinek.js:502-503, which passes exactly these strings to
|
|
8
|
+
* `arrow()` and sets them as the buttons' `aria-label`. Czech by default,
|
|
9
|
+
* like every other user-facing string this kit ships (see ChapterNav's
|
|
10
|
+
* `label`).
|
|
11
|
+
*/
|
|
12
|
+
prevLabel?: string;
|
|
13
|
+
nextLabel?: string;
|
|
14
|
+
class?: string;
|
|
15
|
+
children: Snippet;
|
|
16
|
+
}
|
|
17
|
+
type $$ComponentProps = Props & HTMLAttributes<HTMLDivElement>;
|
|
18
|
+
declare const Tabs: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
19
|
+
type Tabs = ReturnType<typeof Tabs>;
|
|
20
|
+
export default Tabs;
|
|
@@ -0,0 +1,71 @@
|
|
|
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 = 'solid' | 'soft' | 'fill' | 'instock';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
variant?: Variant;
|
|
10
|
+
/** Hue 0-360, for the soft and fill variants. */
|
|
11
|
+
hue?: number;
|
|
12
|
+
/** Saturation 0-100, for the soft and fill variants. */
|
|
13
|
+
saturation?: number;
|
|
14
|
+
class?: string;
|
|
15
|
+
children: Snippet;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
variant = 'solid',
|
|
20
|
+
hue,
|
|
21
|
+
saturation,
|
|
22
|
+
class: className,
|
|
23
|
+
children,
|
|
24
|
+
...rest
|
|
25
|
+
}: Props & HTMLAttributes<HTMLSpanElement> = $props();
|
|
26
|
+
|
|
27
|
+
// jelinek.css:581-584 (.tag): display:inline-block (not flex), 3px/12px
|
|
28
|
+
// padding (py-1 would be 4px, not 3px), font-weight 700 (not 500), and a
|
|
29
|
+
// fixed 0.78rem font-size that is NOT on the --step scale (its nearest
|
|
30
|
+
// rung, --step-down1, is 0.875rem since Task 25 — a flat value now, but
|
|
31
|
+
// still not this one) — kept as an arbitrary value.
|
|
32
|
+
const BASE = 'inline-block rounded-pill px-3 py-[3px] font-secondary text-[0.78rem] font-bold';
|
|
33
|
+
|
|
34
|
+
// `solid` has no CSS counterpart — bare `.tag` sets no background/color of
|
|
35
|
+
// its own in jelinek.css (real usages either add `.tag--instock` or an
|
|
36
|
+
// inline `style="background:..."`). This default is this component's own
|
|
37
|
+
// judgment call for a neutral resting tag, not a literal translation.
|
|
38
|
+
const VARIANT: Record<Variant, string> = {
|
|
39
|
+
solid: 'bg-surface-gray text-fg',
|
|
40
|
+
instock: 'bg-sem-success-strong text-fg-inverse',
|
|
41
|
+
// The soft and fill shades are computed from --tag-h / --tag-s, which are
|
|
42
|
+
// set inline. The lightness steps (90 / 54 / 24) and their light-vs-dark
|
|
43
|
+
// swap live in jelinek.css lines 592-596: dark mode re-lightens the
|
|
44
|
+
// *background* for soft but the *text* for fill — not the same property.
|
|
45
|
+
soft: 'bg-[hsl(var(--tag-h),var(--tag-s),90%)] text-[hsl(var(--tag-h),var(--tag-s),54%)] dark:bg-[hsl(var(--tag-h),var(--tag-s),24%)]',
|
|
46
|
+
fill: 'bg-[hsl(var(--tag-h),var(--tag-s),54%)] text-[hsl(var(--tag-h),var(--tag-s),90%)] dark:text-[hsl(var(--tag-h),var(--tag-s),24%)]'
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const tunable = $derived(variant === 'soft' || variant === 'fill');
|
|
50
|
+
|
|
51
|
+
const style = $derived(tunable ? `--tag-h: ${hue ?? 0}; --tag-s: ${saturation ?? 0}%;` : undefined);
|
|
52
|
+
|
|
53
|
+
// Composes rather than overwrites: the previous version set the bare
|
|
54
|
+
// `style` shorthand unconditionally after {...rest}, which is `undefined`
|
|
55
|
+
// for the solid/instock variants — erasing a consumer's own `style` to
|
|
56
|
+
// `null` on those variants instead of leaving it alone. Trailing
|
|
57
|
+
// semicolons are stripped from each piece before rejoining: `style`
|
|
58
|
+
// already ends in `;`, so a naive join produced a double `;;`, which
|
|
59
|
+
// `CSSStyleDeclaration.cssText`'s parser treats as a malformed
|
|
60
|
+
// declaration and silently drops everything after it.
|
|
61
|
+
const mergedStyle = $derived(
|
|
62
|
+
[style, rest.style]
|
|
63
|
+
.filter((s): s is string => Boolean(s))
|
|
64
|
+
.map((s) => s.trim().replace(/;+$/, ''))
|
|
65
|
+
.join('; ') || undefined
|
|
66
|
+
);
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<span {...rest} style={mergedStyle} class={cn(BASE, VARIANT[variant], className)}>
|
|
70
|
+
{@render children()}
|
|
71
|
+
</span>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
type Variant = 'solid' | 'soft' | 'fill' | 'instock';
|
|
4
|
+
interface Props {
|
|
5
|
+
variant?: Variant;
|
|
6
|
+
/** Hue 0-360, for the soft and fill variants. */
|
|
7
|
+
hue?: number;
|
|
8
|
+
/** Saturation 0-100, for the soft and fill variants. */
|
|
9
|
+
saturation?: number;
|
|
10
|
+
class?: string;
|
|
11
|
+
children: Snippet;
|
|
12
|
+
}
|
|
13
|
+
type $$ComponentProps = Props & HTMLAttributes<HTMLSpanElement>;
|
|
14
|
+
declare const Tag: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
15
|
+
type Tag = ReturnType<typeof Tag>;
|
|
16
|
+
export default Tag;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
+
import { cn } from '../utils/cn.js';
|
|
4
|
+
import Label from './Label.svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
value?: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
id?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Invalid state. Reproduces the guide's `.input.is-error`
|
|
12
|
+
* (jelinek.css:988-991) — new in the Task 31 merge — and sets
|
|
13
|
+
* `aria-invalid` alongside it, so the state is announced and not only
|
|
14
|
+
* coloured. See ERROR below.
|
|
15
|
+
*/
|
|
16
|
+
error?: boolean;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
value = $bindable(''),
|
|
22
|
+
label,
|
|
23
|
+
id,
|
|
24
|
+
error = false,
|
|
25
|
+
class: className,
|
|
26
|
+
...rest
|
|
27
|
+
}: Props & Omit<HTMLInputAttributes, 'value'> = $props();
|
|
28
|
+
|
|
29
|
+
// $props.id() gives a stable per-instance id, so a label can be associated
|
|
30
|
+
// without the consumer having to invent one.
|
|
31
|
+
const uid = $props.id();
|
|
32
|
+
const inputId = $derived(id ?? uid);
|
|
33
|
+
|
|
34
|
+
// jelinek.css:966-975 (`.input, .select`), corrected against the draft brief:
|
|
35
|
+
// - border is 1px solid var(--text-muted2) (border-fg-muted2), NOT
|
|
36
|
+
// border-border-subtle (that token is --border-subtle, a different,
|
|
37
|
+
// lighter grey used elsewhere, e.g. Card).
|
|
38
|
+
// - padding is 11px 14px, NOT the 16px/12px py-3 px-4 the brief used.
|
|
39
|
+
// - font-size is a fixed 0.98rem literal (not on the --step scale, where
|
|
40
|
+
// --step0 is a constant 1rem unrelated to this value) — kept as
|
|
41
|
+
// text-[0.98rem], not text-step0.
|
|
42
|
+
// width:100%, font-family:--font-secondary, background:--bg-white,
|
|
43
|
+
// color:--text-black and border-radius:--radius-button all check out as
|
|
44
|
+
// w-full/font-secondary/bg-surface/text-fg/rounded-button.
|
|
45
|
+
//
|
|
46
|
+
// TASK 31 — THE FOCUS RING IS TOKENISED, AND THAT FIXES A DARK-MODE BUG.
|
|
47
|
+
// jelinek.css:972-975 now reads `outline: var(--focus-width) solid
|
|
48
|
+
// var(--focus-color); outline-offset: 1px; border-color: var(--focus-color)`.
|
|
49
|
+
// --focus-color is NOT simply --JELINEK-BROWN: it is --JELINEK-BROWN in
|
|
50
|
+
// light and the lighter --JELINEK-BROWN-HIGHLIGHT in dark
|
|
51
|
+
// (jelinek.css:139, :229), precisely so the ring stays visible against the
|
|
52
|
+
// dark canvas. The old `focus:outline-jelinek-brown focus:border-jelinek-brown`
|
|
53
|
+
// therefore drew a too-dark ring in dark mode. Referenced as var() rather
|
|
54
|
+
// than translated to a named token because --focus-color/--focus-width are
|
|
55
|
+
// emitted into :root by gen-tokens but are not in the @theme inline map, so
|
|
56
|
+
// no `outline-focus-color` utility exists; the var() reference also keeps
|
|
57
|
+
// this tracking the token if the designer retunes it.
|
|
58
|
+
// The width needs the `[length:…]` hint — `outline-[var(--focus-width)]`
|
|
59
|
+
// alone is ambiguous and Tailwind reads it as a COLOUR (verified against
|
|
60
|
+
// compiled output).
|
|
61
|
+
// Note the guide uses `:focus`, not `:focus-visible`, for form fields, so a
|
|
62
|
+
// mouse click shows the ring too — kept as `focus:`. That is deliberate on
|
|
63
|
+
// the guide's part: base.css's new global ring is `:focus-visible`, and
|
|
64
|
+
// `.input:focus` (specificity 0,2,0) outranks it either way.
|
|
65
|
+
const BASE =
|
|
66
|
+
'w-full rounded-button border border-fg-muted2 bg-surface px-[14px] py-[11px] ' +
|
|
67
|
+
'font-secondary text-[0.98rem] text-fg placeholder:text-fg-muted2 ' +
|
|
68
|
+
'focus:outline-[length:var(--focus-width)] focus:outline-offset-1 ' +
|
|
69
|
+
'focus:outline-[var(--focus-color)] focus:border-[var(--focus-color)]';
|
|
70
|
+
|
|
71
|
+
// jelinek.css:987-991 — NEW in the merge. `.input.is-error` recolours the
|
|
72
|
+
// border to --sem-error-strong, and `.input.is-error:focus` recolours the
|
|
73
|
+
// focus ring to match while leaving its width and offset alone. That token
|
|
74
|
+
// IS in the @theme map (--color-sem-error-strong), so these are named
|
|
75
|
+
// utilities; it also lightens in dark mode (hsl(7,100%,45%) -> 62%) to stay
|
|
76
|
+
// legible, which the named utility follows for free.
|
|
77
|
+
// Merged AFTER BASE so it wins on source order within the same
|
|
78
|
+
// tailwind-merge groups, and BEFORE `className` so a consumer override still
|
|
79
|
+
// beats both — the kit's standing cn() contract.
|
|
80
|
+
//
|
|
81
|
+
// NOT reproduced: the guide's `.field-error` message element
|
|
82
|
+
// (jelinek.css:992-995). That is markup plus `aria-describedby` wiring, i.e.
|
|
83
|
+
// a prop-surface addition rather than a rule to copy, and `.field-error` is
|
|
84
|
+
// not a guarded class so the lock makes no claim about it. Noted in the Task
|
|
85
|
+
// 31 report as a candidate follow-up.
|
|
86
|
+
const ERROR =
|
|
87
|
+
'border-sem-error-strong focus:border-sem-error-strong focus:outline-sem-error-strong';
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
{#if label}
|
|
91
|
+
<Label for={inputId}>{label}</Label>
|
|
92
|
+
{/if}
|
|
93
|
+
<input
|
|
94
|
+
{...rest}
|
|
95
|
+
id={inputId}
|
|
96
|
+
type={rest.type ?? 'text'}
|
|
97
|
+
aria-invalid={error ? 'true' : rest['aria-invalid']}
|
|
98
|
+
bind:value
|
|
99
|
+
class={cn(BASE, error && ERROR, className)}
|
|
100
|
+
/>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
+
interface Props {
|
|
3
|
+
value?: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
id?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Invalid state. Reproduces the guide's `.input.is-error`
|
|
8
|
+
* (jelinek.css:988-991) — new in the Task 31 merge — and sets
|
|
9
|
+
* `aria-invalid` alongside it, so the state is announced and not only
|
|
10
|
+
* coloured. See ERROR below.
|
|
11
|
+
*/
|
|
12
|
+
error?: boolean;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
type $$ComponentProps = Props & Omit<HTMLInputAttributes, 'value'>;
|
|
16
|
+
declare const TextField: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
17
|
+
type TextField = ReturnType<typeof TextField>;
|
|
18
|
+
export default TextField;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
3
|
+
import { cn } from '../utils/cn.js';
|
|
4
|
+
|
|
5
|
+
type Theme = 'light' | 'dark';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
theme?: Theme;
|
|
9
|
+
class?: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
theme = $bindable(),
|
|
15
|
+
// Literal copy of partials/header.html:10's aria-label, not the
|
|
16
|
+
// brief's shorter invented "Tmavý režim".
|
|
17
|
+
label = 'Přepnout světlý/tmavý režim',
|
|
18
|
+
class: className,
|
|
19
|
+
...rest
|
|
20
|
+
}: Props & Omit<HTMLButtonAttributes, 'type'> = $props();
|
|
21
|
+
|
|
22
|
+
// Seed from whatever is already on <html> instead of writing blind. A
|
|
23
|
+
// consumer that persists the user's choice (localStorage, a cookie, an
|
|
24
|
+
// SSR-rendered attribute) sets `data-theme` on the document element
|
|
25
|
+
// before this component ever mounts; unconditionally writing `theme`'s
|
|
26
|
+
// default ('light') back onto it on mount clobbered that on every load.
|
|
27
|
+
// Falls back to prefers-color-scheme, then 'light', only when the
|
|
28
|
+
// consumer hasn't bound `theme` AND nothing is already on the document.
|
|
29
|
+
$effect(() => {
|
|
30
|
+
if (theme === undefined) {
|
|
31
|
+
const existing = document.documentElement.dataset.theme;
|
|
32
|
+
theme =
|
|
33
|
+
existing === 'dark' || existing === 'light'
|
|
34
|
+
? existing
|
|
35
|
+
: window.matchMedia?.('(prefers-color-scheme: dark)').matches
|
|
36
|
+
? 'dark'
|
|
37
|
+
: 'light';
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (document.documentElement.dataset.theme !== theme) {
|
|
41
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const dark = $derived(theme === 'dark');
|
|
46
|
+
|
|
47
|
+
// jelinek.css:536-541 (.theme-toggle): relative, no shrink/grow, 52px x
|
|
48
|
+
// 28px (w-[52px] — not on Tailwind's default scale, which has no 13
|
|
49
|
+
// (52px) stop, jumping 12→14; h-7 = 28px is exact), no padding,
|
|
50
|
+
// cursor-pointer, a 2px solid border in text-black (border-2 border-fg —
|
|
51
|
+
// the brief's `border border-border-subtle` was a much lighter 1px
|
|
52
|
+
// subtle-grey border, wrong on both width and colour), transparent
|
|
53
|
+
// background (the brief filled it with bg-surface-gray), rounded-pill.
|
|
54
|
+
const TRACK =
|
|
55
|
+
'relative h-7 w-[52px] flex-none cursor-pointer rounded-pill border-2 border-fg bg-transparent p-0';
|
|
56
|
+
|
|
57
|
+
// jelinek.css:542-550 (.theme-toggle__knob / .is-dark / __icon): 20x20px
|
|
58
|
+
// circle (h-5 w-5, exact), 2px inset (top-0.5 left-0.5, exact),
|
|
59
|
+
// background var(--bg-white) → bg-surface (the brief's bg-jelinek-black
|
|
60
|
+
// was a solid BLACK knob, the opposite of the guide's light one),
|
|
61
|
+
// shadow-resting, rounded-full (a literal 50%, not the pill radius
|
|
62
|
+
// token). `.is-dark .theme-toggle__knob { transform: translateX(24px) }`
|
|
63
|
+
// (:549) → translate-x-6 = 24px, NOT the brief's translate-x-5 (20px).
|
|
64
|
+
//
|
|
65
|
+
// Task 31 — the knob's slide is `transform var(--dur-slow) var(--ease)`
|
|
66
|
+
// (jelinek.css:547). This is the one transition in the merge whose DURATION
|
|
67
|
+
// genuinely moved, not just its spelling: it was a literal `0.18s`, and the
|
|
68
|
+
// designer folded it into the shared --dur-slow (0.2s) with the other
|
|
69
|
+
// large-surface animations. A bare `transition-transform` would have run at
|
|
70
|
+
// Tailwind's 150ms default on Tailwind's own easing curve — wrong on both
|
|
71
|
+
// counts now — so both are named explicitly. See Button.svelte's comment for
|
|
72
|
+
// why this is not solved once globally in extras.css.
|
|
73
|
+
const KNOB =
|
|
74
|
+
'absolute top-0.5 left-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-surface shadow-resting ' +
|
|
75
|
+
'transition-transform duration-[var(--dur-slow)] ease-[var(--ease)]';
|
|
76
|
+
|
|
77
|
+
// jelinek.css:550 only declares `.theme-toggle__icon`'s size/colour
|
|
78
|
+
// (13x13px, color:var(--text-black)) — the actual sun/moon markup lives
|
|
79
|
+
// in assets/js/jelinek.js:259-260 (SUN_ICON/MOON_ICON), not in the CSS.
|
|
80
|
+
// Reproduced here verbatim as native Svelte conditionals keyed off
|
|
81
|
+
// `dark` (reactive) instead of the guide's imperative innerHTML swap —
|
|
82
|
+
// this is the one addition in this task not literally sourced from
|
|
83
|
+
// jelinek.css, flagged in the Task 9 report since the CSS only implies
|
|
84
|
+
// an icon exists, it doesn't specify one.
|
|
85
|
+
const ICON = 'block h-[13px] w-[13px] text-fg';
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<button
|
|
89
|
+
{...rest}
|
|
90
|
+
type="button"
|
|
91
|
+
role={rest.role ?? 'switch'}
|
|
92
|
+
aria-checked={dark}
|
|
93
|
+
aria-label={label}
|
|
94
|
+
onclick={(event) => {
|
|
95
|
+
theme = dark ? 'light' : 'dark';
|
|
96
|
+
rest.onclick?.(event);
|
|
97
|
+
}}
|
|
98
|
+
class={cn(TRACK, className)}
|
|
99
|
+
>
|
|
100
|
+
<span class={cn(KNOB, dark && 'translate-x-6')}>
|
|
101
|
+
{#if dark}
|
|
102
|
+
<svg class={ICON} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
103
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
104
|
+
</svg>
|
|
105
|
+
{:else}
|
|
106
|
+
<svg
|
|
107
|
+
class={ICON}
|
|
108
|
+
viewBox="0 0 24 24"
|
|
109
|
+
fill="none"
|
|
110
|
+
stroke="currentColor"
|
|
111
|
+
stroke-width="2"
|
|
112
|
+
stroke-linecap="round"
|
|
113
|
+
aria-hidden="true"
|
|
114
|
+
>
|
|
115
|
+
<circle cx="12" cy="12" r="4" />
|
|
116
|
+
<path
|
|
117
|
+
d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"
|
|
118
|
+
/>
|
|
119
|
+
</svg>
|
|
120
|
+
{/if}
|
|
121
|
+
</span>
|
|
122
|
+
</button>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
2
|
+
type Theme = 'light' | 'dark';
|
|
3
|
+
interface Props {
|
|
4
|
+
theme?: Theme;
|
|
5
|
+
class?: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
}
|
|
8
|
+
type $$ComponentProps = Props & Omit<HTMLButtonAttributes, 'type'>;
|
|
9
|
+
declare const ThemeToggle: import("svelte").Component<$$ComponentProps, {}, "theme">;
|
|
10
|
+
type ThemeToggle = ReturnType<typeof ThemeToggle>;
|
|
11
|
+
export default ThemeToggle;
|