@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.
Files changed (98) hide show
  1. package/README.md +33 -10
  2. package/dist/components/ChapterNav.svelte +1 -1
  3. package/dist/components/Checkbox.svelte +13 -15
  4. package/dist/components/Checkbox.svelte.d.ts +1 -0
  5. package/dist/components/ContentLogo.svelte +18 -13
  6. package/dist/components/ContentLogo.svelte.d.ts +6 -5
  7. package/dist/components/Grid.svelte +9 -5
  8. package/dist/components/Icon.svelte +52 -0
  9. package/dist/components/Icon.svelte.d.ts +30 -0
  10. package/dist/components/InfoBar.svelte +254 -0
  11. package/dist/components/InfoBar.svelte.d.ts +46 -0
  12. package/dist/components/Label.svelte +12 -1
  13. package/dist/components/Label.svelte.d.ts +8 -0
  14. package/dist/components/PageHero.svelte +12 -9
  15. package/dist/components/Pager.svelte +5 -2
  16. package/dist/components/PhotoTile.svelte +4 -2
  17. package/dist/components/ProductCard.svelte +66 -44
  18. package/dist/components/ProductCard.svelte.d.ts +17 -4
  19. package/dist/components/Radio.svelte +12 -8
  20. package/dist/components/Radio.svelte.d.ts +1 -0
  21. package/dist/components/Select.svelte +70 -12
  22. package/dist/components/Select.svelte.d.ts +11 -0
  23. package/dist/components/SiteFooter.svelte +19 -0
  24. package/dist/components/SiteFooter.svelte.d.ts +8 -0
  25. package/dist/components/SiteHeader.svelte +70 -9
  26. package/dist/components/SiteHeader.svelte.d.ts +16 -0
  27. package/dist/components/Slider.svelte +6 -11
  28. package/dist/components/Switch.svelte +76 -0
  29. package/dist/components/Switch.svelte.d.ts +12 -0
  30. package/dist/components/Tab.svelte +29 -1
  31. package/dist/components/TextField.svelte +61 -10
  32. package/dist/components/TextField.svelte.d.ts +13 -0
  33. package/dist/components/Textarea.svelte +92 -0
  34. package/dist/components/Textarea.svelte.d.ts +23 -0
  35. package/dist/components/Tile.svelte +12 -11
  36. package/dist/components/Tile.svelte.d.ts +8 -2
  37. package/dist/components/blocks/CmsButton.svelte +36 -0
  38. package/dist/components/blocks/CmsButton.svelte.d.ts +12 -0
  39. package/dist/components/blocks/CmsColumns.svelte +20 -0
  40. package/dist/components/blocks/CmsColumns.svelte.d.ts +9 -0
  41. package/dist/components/blocks/CmsCover.svelte +71 -0
  42. package/dist/components/blocks/CmsCover.svelte.d.ts +17 -0
  43. package/dist/components/blocks/CmsHeading.svelte +50 -0
  44. package/dist/components/blocks/CmsHeading.svelte.d.ts +14 -0
  45. package/dist/components/blocks/CmsImage.svelte +43 -0
  46. package/dist/components/blocks/CmsImage.svelte.d.ts +13 -0
  47. package/dist/components/blocks/CmsMediaText.svelte +64 -0
  48. package/dist/components/blocks/CmsMediaText.svelte.d.ts +16 -0
  49. package/dist/components/blocks/CmsText.svelte +53 -0
  50. package/dist/components/blocks/CmsText.svelte.d.ts +10 -0
  51. package/dist/components/commerce/Breadcrumbs.svelte +22 -5
  52. package/dist/components/commerce/CartDrawer.svelte +7 -4
  53. package/dist/components/commerce/CartLineItem.svelte +1 -1
  54. package/dist/components/commerce/CheckoutProgress.svelte +1 -1
  55. package/dist/components/commerce/ConfigSection.svelte +2 -2
  56. package/dist/components/commerce/DimensionSlider.svelte +1 -1
  57. package/dist/components/commerce/Drawer.svelte +38 -24
  58. package/dist/components/commerce/Drawer.svelte.d.ts +0 -2
  59. package/dist/components/commerce/EmptyState.svelte +37 -0
  60. package/dist/components/commerce/EmptyState.svelte.d.ts +13 -0
  61. package/dist/components/commerce/FilterChip.svelte +35 -0
  62. package/dist/components/commerce/FilterChip.svelte.d.ts +10 -0
  63. package/dist/components/commerce/FreeShippingBar.svelte +1 -1
  64. package/dist/components/commerce/Gallery.svelte +4 -2
  65. package/dist/components/commerce/ListingToolbar.svelte +62 -0
  66. package/dist/components/commerce/ListingToolbar.svelte.d.ts +19 -0
  67. package/dist/components/commerce/Modal.svelte +31 -21
  68. package/dist/components/commerce/Modal.svelte.d.ts +0 -2
  69. package/dist/components/commerce/OrderSummary.svelte +1 -1
  70. package/dist/components/commerce/Pagination.svelte +115 -0
  71. package/dist/components/commerce/Pagination.svelte.d.ts +14 -0
  72. package/dist/components/commerce/PriceDisplay.svelte +3 -3
  73. package/dist/components/commerce/ProductHeader.svelte +2 -2
  74. package/dist/components/commerce/QuantityStepper.svelte +1 -1
  75. package/dist/components/commerce/RatingStars.svelte +1 -1
  76. package/dist/components/commerce/SearchField.svelte +92 -0
  77. package/dist/components/commerce/SearchField.svelte.d.ts +15 -0
  78. package/dist/components/commerce/SideToggle.svelte +1 -1
  79. package/dist/components/commerce/Skeleton.svelte +4 -1
  80. package/dist/components/commerce/StickyBar.svelte +4 -4
  81. package/dist/components/commerce/StockBadge.svelte +2 -2
  82. package/dist/components/commerce/Toaster.svelte +9 -5
  83. package/dist/components/commerce/TrustBadges.svelte +1 -1
  84. package/dist/components/commerce/overlay.d.ts +14 -0
  85. package/dist/components/commerce/overlay.js +49 -0
  86. package/dist/components/info-bar.d.ts +26 -0
  87. package/dist/components/info-bar.js +23 -0
  88. package/dist/icons/material-icons.generated.d.ts +4 -0
  89. package/dist/icons/material-icons.generated.js +118 -0
  90. package/dist/icons/material.d.ts +17 -0
  91. package/dist/icons/material.js +52 -0
  92. package/dist/index.d.ts +17 -0
  93. package/dist/index.js +17 -0
  94. package/dist/theme/base.css +22 -0
  95. package/dist/theme/extras.css +62 -0
  96. package/dist/theme/fonts.css +9 -42
  97. package/dist/theme/tokens.css +23 -9
  98. 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`, not `sticky`. His 74ef043 made
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
- 'absolute inset-x-0 top-0 z-50 p-0 [--logo-mark:46px] ' +
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-semibold tracking-[0.08em] uppercase text-fg-muted1';
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): the same
589
- // small uppercase Avenir treatment as the header subtitle.
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-semibold tracking-[0.08em] uppercase text-fg-muted1';
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} style={SUBTITLE_STYLE}>{group.title}</div>{/if}
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-white" />
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} style={SUBTITLE_STYLE}>Menu</span>
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. ui/komponenty.html's two sliders do carry an
32
- // inline `accent-color: var(--JELINEK-MAGENTA)`, but both are
33
- // mattress-width demos (90 cm, 120 cm) — a mattress-CONTEXT instance, not
34
- // a stylesheet rule for sliders in general. Treating that literal magenta
35
- // as the default would leak the mattress accent onto furniture-division
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-brown', className)}
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
- // (docs/chybejici-komponenty.md).
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
- aria-invalid={error ? 'true' : rest['aria-invalid']}
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, error && ERROR, className)}
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;