@jelinek/ui 0.9.0 → 0.9.2
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 +33 -10
- package/dist/components/ChapterNav.svelte +1 -1
- package/dist/components/Checkbox.svelte +13 -15
- package/dist/components/Checkbox.svelte.d.ts +1 -0
- package/dist/components/ContentLogo.svelte +18 -13
- package/dist/components/ContentLogo.svelte.d.ts +6 -5
- package/dist/components/Grid.svelte +9 -5
- package/dist/components/Icon.svelte +52 -0
- package/dist/components/Icon.svelte.d.ts +30 -0
- package/dist/components/InfoBar.svelte +254 -0
- package/dist/components/InfoBar.svelte.d.ts +46 -0
- package/dist/components/Label.svelte +12 -1
- package/dist/components/Label.svelte.d.ts +8 -0
- package/dist/components/PageHero.svelte +12 -9
- package/dist/components/Pager.svelte +5 -2
- package/dist/components/PhotoTile.svelte +4 -2
- package/dist/components/ProductCard.svelte +66 -44
- package/dist/components/ProductCard.svelte.d.ts +17 -4
- package/dist/components/Radio.svelte +12 -8
- package/dist/components/Radio.svelte.d.ts +1 -0
- package/dist/components/Select.svelte +70 -12
- package/dist/components/Select.svelte.d.ts +11 -0
- package/dist/components/SiteFooter.svelte +19 -0
- package/dist/components/SiteFooter.svelte.d.ts +8 -0
- package/dist/components/SiteHeader.svelte +70 -9
- package/dist/components/SiteHeader.svelte.d.ts +16 -0
- package/dist/components/Slider.svelte +6 -11
- package/dist/components/Switch.svelte +76 -0
- package/dist/components/Switch.svelte.d.ts +12 -0
- package/dist/components/Tab.svelte +29 -1
- package/dist/components/TextField.svelte +61 -10
- package/dist/components/TextField.svelte.d.ts +13 -0
- package/dist/components/Textarea.svelte +92 -0
- package/dist/components/Textarea.svelte.d.ts +23 -0
- package/dist/components/Tile.svelte +12 -11
- package/dist/components/Tile.svelte.d.ts +8 -2
- package/dist/components/blocks/CmsButton.svelte +36 -0
- package/dist/components/blocks/CmsButton.svelte.d.ts +12 -0
- package/dist/components/blocks/CmsColumns.svelte +20 -0
- package/dist/components/blocks/CmsColumns.svelte.d.ts +9 -0
- package/dist/components/blocks/CmsCover.svelte +71 -0
- package/dist/components/blocks/CmsCover.svelte.d.ts +17 -0
- package/dist/components/blocks/CmsHeading.svelte +50 -0
- package/dist/components/blocks/CmsHeading.svelte.d.ts +14 -0
- package/dist/components/blocks/CmsImage.svelte +43 -0
- package/dist/components/blocks/CmsImage.svelte.d.ts +13 -0
- package/dist/components/blocks/CmsMediaText.svelte +64 -0
- package/dist/components/blocks/CmsMediaText.svelte.d.ts +16 -0
- package/dist/components/blocks/CmsText.svelte +53 -0
- package/dist/components/blocks/CmsText.svelte.d.ts +10 -0
- package/dist/components/commerce/Breadcrumbs.svelte +22 -5
- package/dist/components/commerce/CartDrawer.svelte +7 -4
- package/dist/components/commerce/CartLineItem.svelte +1 -1
- package/dist/components/commerce/CheckoutProgress.svelte +1 -1
- package/dist/components/commerce/ConfigSection.svelte +2 -2
- package/dist/components/commerce/DimensionSlider.svelte +1 -1
- package/dist/components/commerce/Drawer.svelte +38 -24
- package/dist/components/commerce/Drawer.svelte.d.ts +0 -2
- package/dist/components/commerce/EmptyState.svelte +37 -0
- package/dist/components/commerce/EmptyState.svelte.d.ts +13 -0
- package/dist/components/commerce/FilterChip.svelte +35 -0
- package/dist/components/commerce/FilterChip.svelte.d.ts +10 -0
- package/dist/components/commerce/FreeShippingBar.svelte +1 -1
- package/dist/components/commerce/Gallery.svelte +4 -2
- package/dist/components/commerce/ListingToolbar.svelte +62 -0
- package/dist/components/commerce/ListingToolbar.svelte.d.ts +19 -0
- package/dist/components/commerce/Modal.svelte +31 -21
- package/dist/components/commerce/Modal.svelte.d.ts +0 -2
- package/dist/components/commerce/OrderSummary.svelte +1 -1
- package/dist/components/commerce/Pagination.svelte +115 -0
- package/dist/components/commerce/Pagination.svelte.d.ts +14 -0
- package/dist/components/commerce/PriceDisplay.svelte +3 -3
- package/dist/components/commerce/ProductHeader.svelte +2 -2
- package/dist/components/commerce/QuantityStepper.svelte +1 -1
- package/dist/components/commerce/RatingStars.svelte +1 -1
- package/dist/components/commerce/SearchField.svelte +92 -0
- package/dist/components/commerce/SearchField.svelte.d.ts +15 -0
- package/dist/components/commerce/SideToggle.svelte +1 -1
- package/dist/components/commerce/Skeleton.svelte +4 -1
- package/dist/components/commerce/StickyBar.svelte +4 -4
- package/dist/components/commerce/StockBadge.svelte +2 -2
- package/dist/components/commerce/Toaster.svelte +9 -5
- package/dist/components/commerce/TrustBadges.svelte +1 -1
- package/dist/components/commerce/overlay.d.ts +14 -0
- package/dist/components/commerce/overlay.js +49 -0
- package/dist/components/info-bar.d.ts +26 -0
- package/dist/components/info-bar.js +23 -0
- package/dist/icons/material-icons.generated.d.ts +4 -0
- package/dist/icons/material-icons.generated.js +118 -0
- package/dist/icons/material.d.ts +17 -0
- package/dist/icons/material.js +52 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/theme/base.css +22 -0
- package/dist/theme/extras.css +62 -0
- package/dist/theme/fonts.css +9 -42
- package/dist/theme/tokens.css +23 -9
- package/package.json +4 -2
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
4
|
import { cn } from '../utils/cn.js';
|
|
5
5
|
import Button from './Button.svelte';
|
|
6
|
+
import Icon from './Icon.svelte';
|
|
6
7
|
import { createChapterNavDock } from '../utils/chapter-nav-dock.svelte.js';
|
|
7
8
|
import { DEER_MARK_PATH } from './deer-mark-path.js';
|
|
8
9
|
import { isActive, type NavGroup, type NavItem } from './nav.js';
|
|
9
10
|
|
|
10
11
|
interface Props {
|
|
12
|
+
/** Cíl odkazu „Přeskočit na obsah“ — id hlavního obsahu stránky. */
|
|
13
|
+
skipHref?: string;
|
|
11
14
|
/**
|
|
12
15
|
* Escape hatch: a snippet rendering custom mark content in place of the
|
|
13
16
|
* bundled, re-theming deer mark (e.g. a division-specific logo). Most
|
|
@@ -21,6 +24,20 @@
|
|
|
21
24
|
* app is not rooted at `/`, e.g. mounted under a base path.
|
|
22
25
|
*/
|
|
23
26
|
logoHref?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Cesta zpět na firemní rozcestník. Když je zadaná, vykreslí se jako
|
|
29
|
+
* PRVNÍ položka lišty, vlevo před znakem, ikona domečku (`home` ze sady
|
|
30
|
+
* ikon) bez textu — na každé šířce, telefon včetně. Rozhodnutí majitele
|
|
31
|
+
* 15. 9. 2026: firemní aplikace ze šablon tak nepotřebují tlačítko
|
|
32
|
+
* „Zpět na rozcestník“ v `actions`, které vedle odkazů menu vypadalo
|
|
33
|
+
* jinak velké. Bez této vlastnosti se hlavička nemění.
|
|
34
|
+
*/
|
|
35
|
+
homeHref?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Přístupný název ikony domečku (`aria-label` i `title`). Ikona nemá
|
|
38
|
+
* viditelný text, takže tohle je jediné, co přečte čtečka.
|
|
39
|
+
*/
|
|
40
|
+
homeLabel?: string;
|
|
24
41
|
subtitle?: string;
|
|
25
42
|
items?: NavItem[];
|
|
26
43
|
active?: string;
|
|
@@ -98,9 +115,12 @@
|
|
|
98
115
|
}
|
|
99
116
|
|
|
100
117
|
let {
|
|
118
|
+
skipHref = '#hlavni-obsah',
|
|
101
119
|
logo,
|
|
102
120
|
logoAlt = 'JELÍNEK',
|
|
103
121
|
logoHref = '/',
|
|
122
|
+
homeHref,
|
|
123
|
+
homeLabel = 'Zpět na rozcestník',
|
|
104
124
|
subtitle,
|
|
105
125
|
items = [],
|
|
106
126
|
active,
|
|
@@ -276,7 +296,7 @@
|
|
|
276
296
|
// Three of his commits land here (36354b9, 6750ee8, 74ef043, a430f9c) and
|
|
277
297
|
// they supersede this component's previous phone treatment wholesale:
|
|
278
298
|
//
|
|
279
|
-
// POSITION. `absolute inset-x-0 top-0 z-50
|
|
299
|
+
// POSITION. `absolute inset-x-0 top-0` at --z-header (50), not `sticky`. His 74ef043 made
|
|
280
300
|
// this change deliberately and says why: with the bar in flow (or sticky at
|
|
281
301
|
// the top) the hero's grey band started BELOW it, leaving a visible seam in
|
|
282
302
|
// the page background right above the bar. Absolute takes the bar out of
|
|
@@ -339,8 +359,14 @@
|
|
|
339
359
|
// (`--header-h`/`--subnav-h` are exported passthrough tokens precisely so a
|
|
340
360
|
// consumer's own layout can do that). PageHero and the showcase's
|
|
341
361
|
// `+layout.svelte` model exactly that.
|
|
362
|
+
// .skip-link z jelinek.css: mimo obraz, dokud nemá fokus z klávesnice.
|
|
363
|
+
const SKIP =
|
|
364
|
+
'absolute top-3 left-3 z-[60] rounded-button bg-jelinek-black px-4 py-2.5 font-primary font-bold text-on-jelinek-black no-underline ' +
|
|
365
|
+
'-translate-y-[200%] opacity-0 focus-visible:translate-y-0 focus-visible:opacity-100';
|
|
342
366
|
const OUTER =
|
|
343
|
-
|
|
367
|
+
// top = --header-offset: InfoBar (lišta nad menu) ji nastaví na svou výšku
|
|
368
|
+
// a při rolování stahuje k nule; bez lišty je proměnná prázdná → 0.
|
|
369
|
+
'absolute inset-x-0 top-[var(--header-offset,0px)] z-(--z-header) p-0 [--logo-mark:46px] ' +
|
|
344
370
|
'min-[640px]:fixed min-[640px]:px-6 min-[640px]:py-3';
|
|
345
371
|
// Task 43 — the glass card no longer IS the flex row: the row (logo,
|
|
346
372
|
// lockup, burger, nav) moved into its own .site-header__bar-equivalent
|
|
@@ -377,6 +403,23 @@
|
|
|
377
403
|
// (leading-none), border-radius:radius-button, overflow hidden — the
|
|
378
404
|
// mark itself is a square var(--logo-mark) box.
|
|
379
405
|
const LOGO_LINK = 'inline-flex flex-none leading-none rounded-button overflow-hidden';
|
|
406
|
+
|
|
407
|
+
// HOME LINK (`homeHref`). Not guide markup — partials/header.html has no
|
|
408
|
+
// way back to a portal, because the guide IS the rozcestník; this is a
|
|
409
|
+
// consumer seam for company apps built from the templates. Rendered through
|
|
410
|
+
// this kit's own <Button variant="ghost" color="black">, like NAV_LINK, so
|
|
411
|
+
// the hover colour, transition and flat shadow are the button system's and
|
|
412
|
+
// cannot drift. The override only reshapes it into the BURGER's 44px square
|
|
413
|
+
// (`h-11 w-11 p-0`, cn() merges it last so it beats Button's padding); the
|
|
414
|
+
// focus ring is the global :focus-visible rule every link gets. The icon is
|
|
415
|
+
// 28px (owner, 15. 9. 2026: at 24px the weight-300 glyph read too light
|
|
416
|
+
// next to the burger); the hit target stays 44px. The 28px box goes through
|
|
417
|
+
// Icon's `size` (an inline style), not `w-7 h-7`: the `.icon` rule in
|
|
418
|
+
// extras.css is unlayered and would beat a layered utility. The SVG itself
|
|
419
|
+
// is sized here too, because `.icon > svg` filling its box also lives in
|
|
420
|
+
// extras.css, which is optional — without it the SVG would keep its 20px
|
|
421
|
+
// attributes inside the 28px span.
|
|
422
|
+
const HOME_LINK = 'h-11 w-11 flex-none gap-0 p-0 [&_svg]:block [&_svg]:h-7 [&_svg]:w-7';
|
|
380
423
|
const MARK = 'block h-[var(--logo-mark)] w-[var(--logo-mark)]';
|
|
381
424
|
|
|
382
425
|
// jelinek.css:447 (.site-header__title): font-primary (GT Walsheim),
|
|
@@ -537,7 +580,7 @@
|
|
|
537
580
|
// Drawer head: small uppercase "Menu" + a 44px close cross. The drawer
|
|
538
581
|
// covers the bar (and the burger with it), so it closes itself.
|
|
539
582
|
const DRAWER_HEAD = 'flex items-center justify-between pb-2 pl-4 min-[640px]:hidden';
|
|
540
|
-
const DRAWER_LABEL = 'text-[0.8rem] font-
|
|
583
|
+
const DRAWER_LABEL = 'font-secondary text-[0.8rem] font-bold tracking-[0.08em] uppercase text-fg-muted1';
|
|
541
584
|
const DRAWER_CLOSE =
|
|
542
585
|
'inline-flex h-11 w-11 flex-none cursor-pointer items-center justify-center ' +
|
|
543
586
|
'rounded-button border-0 bg-transparent p-0 text-fg';
|
|
@@ -585,10 +628,10 @@
|
|
|
585
628
|
'transition-[padding-top] duration-[var(--dur-base)] ease-[var(--ease)]';
|
|
586
629
|
const MEGA_COLS_OPEN = 'pt-4';
|
|
587
630
|
const MEGA_COL = 'flex flex-col gap-0.5';
|
|
588
|
-
// Column heading (shared by the mega-menu and the accordion):
|
|
589
|
-
// small uppercase Avenir
|
|
631
|
+
// Column heading (shared by the mega-menu and the accordion): GT Walsheim
|
|
632
|
+
// Bold, small uppercase — Avenir patří jen podnázvu loga (majitel, 21. 8. 2026).
|
|
590
633
|
const HEADING =
|
|
591
|
-
'px-4 pt-2.5 pb-0.5 text-[0.72rem] font-
|
|
634
|
+
'px-4 pt-2.5 pb-0.5 font-secondary text-[0.72rem] font-bold tracking-[0.08em] uppercase text-fg-muted1';
|
|
592
635
|
|
|
593
636
|
const NAV_ROW = 'hidden ml-auto items-center gap-2 min-[640px]:flex';
|
|
594
637
|
|
|
@@ -684,7 +727,7 @@
|
|
|
684
727
|
<!-- One definition of a submenu column's links, rendered into both the
|
|
685
728
|
mega-menu columns (≥640px) and the drawer accordion (phone). -->
|
|
686
729
|
{#snippet subLinks(group: NavGroup)}
|
|
687
|
-
{#if group.title}<div class={HEADING}
|
|
730
|
+
{#if group.title}<div class={HEADING}>{group.title}</div>{/if}
|
|
688
731
|
{#each group.items as chapter (chapter.href)}
|
|
689
732
|
<Button
|
|
690
733
|
href={chapter.href}
|
|
@@ -700,8 +743,24 @@
|
|
|
700
743
|
{/snippet}
|
|
701
744
|
|
|
702
745
|
<header {...rest} bind:this={headerEl} class={cn(OUTER, className)}>
|
|
746
|
+
<!-- Přeskočit na obsah (/ui/pristupnost): první prvek v pořadí Tabu, vidět jen
|
|
747
|
+
se zaostřením z klávesnice. Cíl `#hlavni-obsah` = <main id="hlavni-obsah"> layoutu; stejný
|
|
748
|
+
odkaz má partials/header.html (.skip-link v jelinek.css). -->
|
|
749
|
+
<a href={skipHref} class={SKIP}>Přeskočit na obsah</a>
|
|
703
750
|
<div class={inner}>
|
|
704
751
|
<div class={BAR_ROW}>
|
|
752
|
+
{#if homeHref}
|
|
753
|
+
<Button
|
|
754
|
+
href={homeHref}
|
|
755
|
+
variant="ghost"
|
|
756
|
+
color="black"
|
|
757
|
+
aria-label={homeLabel}
|
|
758
|
+
title={homeLabel}
|
|
759
|
+
class={HOME_LINK}
|
|
760
|
+
>
|
|
761
|
+
<Icon name="home" size={28} />
|
|
762
|
+
</Button>
|
|
763
|
+
{/if}
|
|
705
764
|
<a href={logoHref} aria-label={logoAlt} class={LOGO_LINK}>
|
|
706
765
|
{#if logo}
|
|
707
766
|
{@render logo()}
|
|
@@ -726,8 +785,10 @@
|
|
|
726
785
|
xmlns="http://www.w3.org/2000/svg"
|
|
727
786
|
aria-hidden="true"
|
|
728
787
|
>
|
|
788
|
+
<!-- Majitel 21. 8. 2026: jelen v hlavičce je HNĚDÝ (barevná
|
|
789
|
+
varianta znaku), ne bílý — zrcadlí partials/header.html. -->
|
|
729
790
|
<rect width="400" height="400" class="fill-jelinek-black" />
|
|
730
|
-
<path d={DEER_MARK_PATH} class="fill-jelinek-
|
|
791
|
+
<path d={DEER_MARK_PATH} class="fill-jelinek-brown" />
|
|
731
792
|
</svg>
|
|
732
793
|
{/if}
|
|
733
794
|
</a>
|
|
@@ -842,7 +903,7 @@
|
|
|
842
903
|
640px up, so only one of the two navs is ever in the a11y tree. -->
|
|
843
904
|
<nav id={panelId} bind:this={linksEl} class={drawer} aria-label={navLabel}>
|
|
844
905
|
<div class={DRAWER_HEAD}>
|
|
845
|
-
<span class={DRAWER_LABEL}
|
|
906
|
+
<span class={DRAWER_LABEL}>Menu</span>
|
|
846
907
|
<button type="button" class={DRAWER_CLOSE} aria-label={menuLabelClose} onclick={closeMenu}>
|
|
847
908
|
<svg class="h-[18px] w-[18px]" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 3l10 10M13 3L3 13" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" /></svg>
|
|
848
909
|
</button>
|
|
@@ -2,6 +2,8 @@ import type { Snippet } from 'svelte';
|
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
3
|
import { type NavItem } from './nav.js';
|
|
4
4
|
interface Props {
|
|
5
|
+
/** Cíl odkazu „Přeskočit na obsah“ — id hlavního obsahu stránky. */
|
|
6
|
+
skipHref?: string;
|
|
5
7
|
/**
|
|
6
8
|
* Escape hatch: a snippet rendering custom mark content in place of the
|
|
7
9
|
* bundled, re-theming deer mark (e.g. a division-specific logo). Most
|
|
@@ -15,6 +17,20 @@ interface Props {
|
|
|
15
17
|
* app is not rooted at `/`, e.g. mounted under a base path.
|
|
16
18
|
*/
|
|
17
19
|
logoHref?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Cesta zpět na firemní rozcestník. Když je zadaná, vykreslí se jako
|
|
22
|
+
* PRVNÍ položka lišty, vlevo před znakem, ikona domečku (`home` ze sady
|
|
23
|
+
* ikon) bez textu — na každé šířce, telefon včetně. Rozhodnutí majitele
|
|
24
|
+
* 15. 9. 2026: firemní aplikace ze šablon tak nepotřebují tlačítko
|
|
25
|
+
* „Zpět na rozcestník“ v `actions`, které vedle odkazů menu vypadalo
|
|
26
|
+
* jinak velké. Bez této vlastnosti se hlavička nemění.
|
|
27
|
+
*/
|
|
28
|
+
homeHref?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Přístupný název ikony domečku (`aria-label` i `title`). Ikona nemá
|
|
31
|
+
* viditelný text, takže tohle je jediné, co přečte čtečka.
|
|
32
|
+
*/
|
|
33
|
+
homeLabel?: string;
|
|
18
34
|
subtitle?: string;
|
|
19
35
|
items?: NavItem[];
|
|
20
36
|
active?: string;
|
|
@@ -28,16 +28,11 @@
|
|
|
28
28
|
const inputId = $derived(id ?? uid);
|
|
29
29
|
|
|
30
30
|
// Same reasoning as Checkbox.svelte/Radio.svelte: jelinek.css has NO rule
|
|
31
|
-
// at all for range inputs
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// pages, mixing brown and magenta in one interface exactly as the guide
|
|
37
|
-
// prohibits. Using accent-jelinek-brown instead still reproduces the
|
|
38
|
-
// guide's magenta slider exactly on mattress-division pages, because
|
|
39
|
-
// tokens.css:161-166 re-themes --JELINEK-BROWN to --JELINEK-MAGENTA
|
|
40
|
-
// there — the same division re-theme Checkbox and Radio rely on.
|
|
31
|
+
// at all for range inputs (the archive's magenta sliders were
|
|
32
|
+
// mattress-CONTEXT demos, not a stylesheet rule). Accent adjudicated by
|
|
33
|
+
// the owner (14. 8. 2026): forms are colour-neutral — every emphasis is
|
|
34
|
+
// BLACK (--JELINEK-BLACK, flips to near-white in dark mode), no division
|
|
35
|
+
// re-theme; the only colour a form carries is the error state.
|
|
41
36
|
</script>
|
|
42
37
|
|
|
43
38
|
{#if label}
|
|
@@ -51,5 +46,5 @@
|
|
|
51
46
|
{max}
|
|
52
47
|
{step}
|
|
53
48
|
bind:value
|
|
54
|
-
class={cn('w-full accent-jelinek-
|
|
49
|
+
class={cn('w-full accent-jelinek-black', className)}
|
|
55
50
|
/>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
+
import { cn } from '../utils/cn.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
checked?: boolean;
|
|
7
|
+
label: string;
|
|
8
|
+
id?: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
checked = $bindable(false),
|
|
15
|
+
label,
|
|
16
|
+
id,
|
|
17
|
+
disabled = false,
|
|
18
|
+
class: className,
|
|
19
|
+
...rest
|
|
20
|
+
}: Props & Omit<HTMLInputAttributes, 'checked' | 'type'> = $props();
|
|
21
|
+
|
|
22
|
+
const uid = $props.id();
|
|
23
|
+
const inputId = $derived(id ?? uid);
|
|
24
|
+
|
|
25
|
+
// Přepínač zap/vyp — zásobník práce, priorita 1. Uvnitř je nativní
|
|
26
|
+
// checkbox s role="switch" (sr-only), takže klávesnice i čtečky fungují
|
|
27
|
+
// zadarmo; dráha a palec jsou jen kresba nad ním.
|
|
28
|
+
//
|
|
29
|
+
// Geometrie záměrně opakuje přepínač tmavého režimu (ThemeToggle /
|
|
30
|
+
// jelinek.css `.theme-toggle`): dráha 44×24, palec 20 s 2px odsazením —
|
|
31
|
+
// jeden tvar přepínače v celém webu. Barvy: zapnuto = černá
|
|
32
|
+
// (--JELINEK-BLACK, v tmavém režimu se sama překlopí na bílou), vypnuto
|
|
33
|
+
// = šedá --text-muted2. Formuláře jsou podle rozhodnutí majitele
|
|
34
|
+
// (14. 8. 2026) barevně neutrální — žádné přebarvování podle divize,
|
|
35
|
+
// barvu nese jen chyba. Pohyb: tokeny --dur-base/--ease.
|
|
36
|
+
const LABEL = 'flex items-center gap-2.5 font-secondary font-light text-step0 text-fg';
|
|
37
|
+
const DISABLED_LABEL = 'cursor-not-allowed text-fg-muted2';
|
|
38
|
+
|
|
39
|
+
const TRACK =
|
|
40
|
+
'relative inline-flex h-6 w-11 shrink-0 rounded-full ' +
|
|
41
|
+
'transition-colors duration-[var(--dur-base)] ease-[var(--ease)] ' +
|
|
42
|
+
// Fokus z klávesnice kreslí prstenec z fokus tokenů na dráze —
|
|
43
|
+
// vlastní input je sr-only, prstenec na něm by nebyl vidět.
|
|
44
|
+
'peer-focus-visible:outline peer-focus-visible:outline-[length:var(--focus-width)] ' +
|
|
45
|
+
'peer-focus-visible:outline-offset-1 peer-focus-visible:outline-[var(--focus-color)]';
|
|
46
|
+
|
|
47
|
+
const KNOB =
|
|
48
|
+
'absolute top-0.5 left-0.5 size-5 rounded-full border border-border-subtle bg-surface ' +
|
|
49
|
+
'transition-transform duration-[var(--dur-base)] ease-[var(--ease)]';
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<label
|
|
53
|
+
for={inputId}
|
|
54
|
+
class={cn(LABEL, disabled ? DISABLED_LABEL : 'cursor-pointer', className)}
|
|
55
|
+
>
|
|
56
|
+
<input
|
|
57
|
+
{...rest}
|
|
58
|
+
id={inputId}
|
|
59
|
+
type="checkbox"
|
|
60
|
+
role="switch"
|
|
61
|
+
{disabled}
|
|
62
|
+
bind:checked
|
|
63
|
+
class="peer sr-only"
|
|
64
|
+
/>
|
|
65
|
+
<span
|
|
66
|
+
aria-hidden="true"
|
|
67
|
+
class={cn(
|
|
68
|
+
TRACK,
|
|
69
|
+
checked ? 'bg-jelinek-black' : 'bg-fg-muted2',
|
|
70
|
+
disabled && 'bg-border-subtle'
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
<span class={cn(KNOB, checked && 'translate-x-5')}></span>
|
|
74
|
+
</span>
|
|
75
|
+
{label}
|
|
76
|
+
</label>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
+
interface Props {
|
|
3
|
+
checked?: boolean;
|
|
4
|
+
label: string;
|
|
5
|
+
id?: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
type $$ComponentProps = Props & Omit<HTMLInputAttributes, 'checked' | 'type'>;
|
|
10
|
+
declare const Switch: import("svelte").Component<$$ComponentProps, {}, "checked">;
|
|
11
|
+
type Switch = ReturnType<typeof Switch>;
|
|
12
|
+
export default Switch;
|
|
@@ -22,6 +22,29 @@
|
|
|
22
22
|
|
|
23
23
|
const selected = $derived(tabs.current() === value);
|
|
24
24
|
|
|
25
|
+
// Klávesnice (/ui/pristupnost, audit 4. 9. 2026): šipky ←/→ přepínají mezi
|
|
26
|
+
// taby, Home/End skočí na první/poslední; vybraný tab se zároveň aktivuje
|
|
27
|
+
// (automatická aktivace, vzor WAI-ARIA Tabs). Fokus drží jen vybraný tab
|
|
28
|
+
// (roving tabindex), takže Tab klávesa opustí celý přepínač jedním stiskem.
|
|
29
|
+
function onKey(event: KeyboardEvent) {
|
|
30
|
+
const keys = ['ArrowRight', 'ArrowLeft', 'Home', 'End'];
|
|
31
|
+
if (!keys.includes(event.key)) return;
|
|
32
|
+
const self = event.currentTarget as HTMLElement;
|
|
33
|
+
const list = self.closest('[role="tablist"]') ?? self.parentElement;
|
|
34
|
+
if (!list) return;
|
|
35
|
+
const all = [...list.querySelectorAll<HTMLElement>('[role="tab"]')];
|
|
36
|
+
const i = all.indexOf(self);
|
|
37
|
+
if (i === -1) return;
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
const next =
|
|
40
|
+
event.key === 'Home' ? 0
|
|
41
|
+
: event.key === 'End' ? all.length - 1
|
|
42
|
+
: event.key === 'ArrowRight' ? (i + 1) % all.length
|
|
43
|
+
: (i - 1 + all.length) % all.length;
|
|
44
|
+
all[next].focus();
|
|
45
|
+
all[next].click();
|
|
46
|
+
}
|
|
47
|
+
|
|
25
48
|
// jelinek.css:877-884 (.tab, shared with .logo-tool__bgtab/.logo-tool__lang):
|
|
26
49
|
// text-align:center + white-space:nowrap — both absent from the original
|
|
27
50
|
// draft. font-family is
|
|
@@ -71,7 +94,7 @@
|
|
|
71
94
|
// (.btn--ghost.btn--black) after the muted grey washed out over the
|
|
72
95
|
// logo-tool photo. Marked provisional in jelinek.css: the pill/button
|
|
73
96
|
// relationship gets its final definition with the button-system design
|
|
74
|
-
// (
|
|
97
|
+
// (plán práce, audit.html).
|
|
75
98
|
</script>
|
|
76
99
|
|
|
77
100
|
<button
|
|
@@ -79,6 +102,11 @@
|
|
|
79
102
|
type={rest.type ?? 'button'}
|
|
80
103
|
role={rest.role ?? 'tab'}
|
|
81
104
|
aria-selected={selected}
|
|
105
|
+
tabindex={selected ? 0 : -1}
|
|
106
|
+
onkeydown={(event) => {
|
|
107
|
+
onKey(event);
|
|
108
|
+
rest.onkeydown?.(event);
|
|
109
|
+
}}
|
|
82
110
|
onclick={(event) => {
|
|
83
111
|
tabs.select(value);
|
|
84
112
|
rest.onclick?.(event);
|
|
@@ -14,6 +14,19 @@
|
|
|
14
14
|
* coloured. See ERROR below.
|
|
15
15
|
*/
|
|
16
16
|
error?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Chybová hláška pod polem. Zapíná chybový stav sama (není třeba
|
|
19
|
+
* posílat i `error`) a naváže se na pole přes `aria-describedby`,
|
|
20
|
+
* takže ji čtečky obrazovky přečtou k poli; `aria-live` ji ohlásí,
|
|
21
|
+
* i když se objeví až po odeslání. Má přednost před `hint`.
|
|
22
|
+
*/
|
|
23
|
+
errorText?: string;
|
|
24
|
+
/** Nápověda pod polem — např. formát hodnoty. Navázaná přes `aria-describedby`. */
|
|
25
|
+
hint?: string;
|
|
26
|
+
/** Povinné pole: hvězdička u popisku + nativní `required` na poli. */
|
|
27
|
+
required?: boolean;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
readonly?: boolean;
|
|
17
30
|
class?: string;
|
|
18
31
|
}
|
|
19
32
|
|
|
@@ -22,6 +35,11 @@
|
|
|
22
35
|
label,
|
|
23
36
|
id,
|
|
24
37
|
error = false,
|
|
38
|
+
errorText,
|
|
39
|
+
hint,
|
|
40
|
+
required = false,
|
|
41
|
+
disabled = false,
|
|
42
|
+
readonly = false,
|
|
25
43
|
class: className,
|
|
26
44
|
...rest
|
|
27
45
|
}: Props & Omit<HTMLInputAttributes, 'value'> = $props();
|
|
@@ -30,6 +48,17 @@
|
|
|
30
48
|
// without the consumer having to invent one.
|
|
31
49
|
const uid = $props.id();
|
|
32
50
|
const inputId = $derived(id ?? uid);
|
|
51
|
+
const hintId = $derived(`${inputId}-hint`);
|
|
52
|
+
const errorId = $derived(`${inputId}-error`);
|
|
53
|
+
const isError = $derived(error || !!errorText);
|
|
54
|
+
// The message under the field is not just visually adjacent — it is wired
|
|
55
|
+
// to the input, so assistive tech reads it as part of the field. A
|
|
56
|
+
// consumer-supplied aria-describedby is kept, appended after ours.
|
|
57
|
+
const describedBy = $derived(
|
|
58
|
+
[errorText ? errorId : null, !errorText && hint ? hintId : null, rest['aria-describedby']]
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.join(' ') || undefined
|
|
61
|
+
);
|
|
33
62
|
|
|
34
63
|
// jelinek.css:966-975 (`.input, .select`), corrected against the draft brief:
|
|
35
64
|
// - border is 1px solid var(--text-muted2) (border-fg-muted2), NOT
|
|
@@ -62,9 +91,13 @@
|
|
|
62
91
|
// mouse click shows the ring too — kept as `focus:`. That is deliberate on
|
|
63
92
|
// the guide's part: base.css's new global ring is `:focus-visible`, and
|
|
64
93
|
// `.input:focus` (specificity 0,2,0) outranks it either way.
|
|
94
|
+
// `leading-normal`: input jinak dědí řádkování 1.6 z body, zatímco select
|
|
95
|
+
// má od prohlížeče `normal` — pole pak byla o ~5 px vyšší než selecty
|
|
96
|
+
// vedle nich (nejnápadnější u type="date"). Explicitní `normal` na obou
|
|
97
|
+
// drží formulářový řádek v jedné výšce.
|
|
65
98
|
const BASE =
|
|
66
99
|
'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 ' +
|
|
100
|
+
'font-secondary font-light text-[0.98rem] leading-normal text-fg placeholder:text-fg-muted2 ' +
|
|
68
101
|
'focus:outline-[length:var(--focus-width)] focus:outline-offset-1 ' +
|
|
69
102
|
'focus:outline-[var(--focus-color)] focus:border-[var(--focus-color)]';
|
|
70
103
|
|
|
@@ -77,24 +110,42 @@
|
|
|
77
110
|
// Merged AFTER BASE so it wins on source order within the same
|
|
78
111
|
// tailwind-merge groups, and BEFORE `className` so a consumer override still
|
|
79
112
|
// 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
113
|
const ERROR =
|
|
87
114
|
'border-sem-error-strong focus:border-sem-error-strong focus:outline-sem-error-strong';
|
|
115
|
+
|
|
116
|
+
// Zakázané pole mluví stejnou řečí jako zakázané tlačítko
|
|
117
|
+
// (Button.svelte:117): šedá výplň, utlumený text, cursor-not-allowed.
|
|
118
|
+
// Jen pro čtení si nechává plný text (obsah platí, jen se nemění) a
|
|
119
|
+
// odlišuje se šedou výplní.
|
|
120
|
+
const DISABLED =
|
|
121
|
+
'cursor-not-allowed border-border-subtle bg-surface-gray text-fg-muted2 ' +
|
|
122
|
+
'placeholder:text-fg-muted2';
|
|
123
|
+
const READONLY = 'bg-surface-gray';
|
|
124
|
+
|
|
125
|
+
// Stejný vzhled, jaký sekce Formulářové prvky používala pro ručně psanou
|
|
126
|
+
// hlášku — teď ho nese komponenta, aby hláška byla vždy i programově
|
|
127
|
+
// navázaná na pole.
|
|
128
|
+
const MSG_ERROR = 'mt-1.5 font-secondary text-step-down1 font-medium text-sem-error-strong';
|
|
129
|
+
const MSG_HINT = 'mt-1.5 font-secondary text-step-down1 font-light text-fg-muted1';
|
|
88
130
|
</script>
|
|
89
131
|
|
|
90
132
|
{#if label}
|
|
91
|
-
<Label for={inputId}>{label}</Label>
|
|
133
|
+
<Label for={inputId} {required}>{label}</Label>
|
|
92
134
|
{/if}
|
|
93
135
|
<input
|
|
94
136
|
{...rest}
|
|
95
137
|
id={inputId}
|
|
96
138
|
type={rest.type ?? 'text'}
|
|
97
|
-
|
|
139
|
+
{required}
|
|
140
|
+
{disabled}
|
|
141
|
+
{readonly}
|
|
142
|
+
aria-invalid={isError ? 'true' : rest['aria-invalid']}
|
|
143
|
+
aria-describedby={describedBy}
|
|
98
144
|
bind:value
|
|
99
|
-
class={cn(BASE,
|
|
145
|
+
class={cn(BASE, isError && ERROR, readonly && !disabled && READONLY, disabled && DISABLED, className)}
|
|
100
146
|
/>
|
|
147
|
+
{#if errorText}
|
|
148
|
+
<p id={errorId} aria-live="polite" class={MSG_ERROR}>{errorText}</p>
|
|
149
|
+
{:else if hint}
|
|
150
|
+
<p id={hintId} class={MSG_HINT}>{hint}</p>
|
|
151
|
+
{/if}
|
|
@@ -10,6 +10,19 @@ interface Props {
|
|
|
10
10
|
* coloured. See ERROR below.
|
|
11
11
|
*/
|
|
12
12
|
error?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Chybová hláška pod polem. Zapíná chybový stav sama (není třeba
|
|
15
|
+
* posílat i `error`) a naváže se na pole přes `aria-describedby`,
|
|
16
|
+
* takže ji čtečky obrazovky přečtou k poli; `aria-live` ji ohlásí,
|
|
17
|
+
* i když se objeví až po odeslání. Má přednost před `hint`.
|
|
18
|
+
*/
|
|
19
|
+
errorText?: string;
|
|
20
|
+
/** Nápověda pod polem — např. formát hodnoty. Navázaná přes `aria-describedby`. */
|
|
21
|
+
hint?: string;
|
|
22
|
+
/** Povinné pole: hvězdička u popisku + nativní `required` na poli. */
|
|
23
|
+
required?: boolean;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
readonly?: boolean;
|
|
13
26
|
class?: string;
|
|
14
27
|
}
|
|
15
28
|
type $$ComponentProps = Props & Omit<HTMLInputAttributes, 'value'>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLTextareaAttributes } 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
|
+
/** Počet řádků naprázdno; výšku jde myší táhnout jen svisle (resize-y). */
|
|
11
|
+
rows?: number;
|
|
12
|
+
/** Chybový stav — stejné pravidlo `.input.is-error` jako TextField. */
|
|
13
|
+
error?: boolean;
|
|
14
|
+
/** Chybová hláška pod polem — stejné chování jako u TextFieldu. */
|
|
15
|
+
errorText?: string;
|
|
16
|
+
/** Nápověda pod polem, navázaná přes `aria-describedby`. */
|
|
17
|
+
hint?: string;
|
|
18
|
+
/** Povinné pole: hvězdička u popisku + nativní `required`. */
|
|
19
|
+
required?: boolean;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
readonly?: boolean;
|
|
22
|
+
class?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
value = $bindable(''),
|
|
27
|
+
label,
|
|
28
|
+
id,
|
|
29
|
+
rows = 4,
|
|
30
|
+
error = false,
|
|
31
|
+
errorText,
|
|
32
|
+
hint,
|
|
33
|
+
required = false,
|
|
34
|
+
disabled = false,
|
|
35
|
+
readonly = false,
|
|
36
|
+
class: className,
|
|
37
|
+
...rest
|
|
38
|
+
}: Props & Omit<HTMLTextareaAttributes, 'value'> = $props();
|
|
39
|
+
|
|
40
|
+
const uid = $props.id();
|
|
41
|
+
const inputId = $derived(id ?? uid);
|
|
42
|
+
const hintId = $derived(`${inputId}-hint`);
|
|
43
|
+
const errorId = $derived(`${inputId}-error`);
|
|
44
|
+
const isError = $derived(error || !!errorText);
|
|
45
|
+
const describedBy = $derived(
|
|
46
|
+
[errorText ? errorId : null, !errorText && hint ? hintId : null, rest['aria-describedby']]
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.join(' ') || undefined
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Víceřádkové pole je TextField s <textarea> uvnitř — záměrně stejné
|
|
52
|
+
// třídy (jelinek.css:966-975 styluje `.input` bez ohledu na element),
|
|
53
|
+
// stejný chybový stav i stejné hlášky. Jediné navíc: resize-y, aby šla
|
|
54
|
+
// výška táhnout, ale pole nikdy nerozbilo layout do šířky.
|
|
55
|
+
const BASE =
|
|
56
|
+
'w-full resize-y rounded-button border border-fg-muted2 bg-surface px-[14px] py-[11px] ' +
|
|
57
|
+
'font-secondary font-light text-[0.98rem] text-fg placeholder:text-fg-muted2 ' +
|
|
58
|
+
'focus:outline-[length:var(--focus-width)] focus:outline-offset-1 ' +
|
|
59
|
+
'focus:outline-[var(--focus-color)] focus:border-[var(--focus-color)]';
|
|
60
|
+
|
|
61
|
+
const ERROR =
|
|
62
|
+
'border-sem-error-strong focus:border-sem-error-strong focus:outline-sem-error-strong';
|
|
63
|
+
|
|
64
|
+
const DISABLED =
|
|
65
|
+
'cursor-not-allowed resize-none border-border-subtle bg-surface-gray text-fg-muted2 ' +
|
|
66
|
+
'placeholder:text-fg-muted2';
|
|
67
|
+
const READONLY = 'bg-surface-gray';
|
|
68
|
+
|
|
69
|
+
const MSG_ERROR = 'mt-1.5 font-secondary text-step-down1 font-medium text-sem-error-strong';
|
|
70
|
+
const MSG_HINT = 'mt-1.5 font-secondary text-step-down1 font-light text-fg-muted1';
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
{#if label}
|
|
74
|
+
<Label for={inputId} {required}>{label}</Label>
|
|
75
|
+
{/if}
|
|
76
|
+
<textarea
|
|
77
|
+
{...rest}
|
|
78
|
+
id={inputId}
|
|
79
|
+
{rows}
|
|
80
|
+
{required}
|
|
81
|
+
{disabled}
|
|
82
|
+
{readonly}
|
|
83
|
+
aria-invalid={isError ? 'true' : rest['aria-invalid']}
|
|
84
|
+
aria-describedby={describedBy}
|
|
85
|
+
bind:value
|
|
86
|
+
class={cn(BASE, isError && ERROR, readonly && !disabled && READONLY, disabled && DISABLED, className)}
|
|
87
|
+
></textarea>
|
|
88
|
+
{#if errorText}
|
|
89
|
+
<p id={errorId} aria-live="polite" class={MSG_ERROR}>{errorText}</p>
|
|
90
|
+
{:else if hint}
|
|
91
|
+
<p id={hintId} class={MSG_HINT}>{hint}</p>
|
|
92
|
+
{/if}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { HTMLTextareaAttributes } from 'svelte/elements';
|
|
2
|
+
interface Props {
|
|
3
|
+
value?: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
id?: string;
|
|
6
|
+
/** Počet řádků naprázdno; výšku jde myší táhnout jen svisle (resize-y). */
|
|
7
|
+
rows?: number;
|
|
8
|
+
/** Chybový stav — stejné pravidlo `.input.is-error` jako TextField. */
|
|
9
|
+
error?: boolean;
|
|
10
|
+
/** Chybová hláška pod polem — stejné chování jako u TextFieldu. */
|
|
11
|
+
errorText?: string;
|
|
12
|
+
/** Nápověda pod polem, navázaná přes `aria-describedby`. */
|
|
13
|
+
hint?: string;
|
|
14
|
+
/** Povinné pole: hvězdička u popisku + nativní `required`. */
|
|
15
|
+
required?: boolean;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
readonly?: boolean;
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
type $$ComponentProps = Props & Omit<HTMLTextareaAttributes, 'value'>;
|
|
21
|
+
declare const Textarea: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
22
|
+
type Textarea = ReturnType<typeof Textarea>;
|
|
23
|
+
export default Textarea;
|