@jelinek/ui 0.9.0 → 0.9.1

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 (85) hide show
  1. package/dist/components/ChapterNav.svelte +1 -1
  2. package/dist/components/Checkbox.svelte +13 -15
  3. package/dist/components/Checkbox.svelte.d.ts +1 -0
  4. package/dist/components/ContentLogo.svelte +18 -13
  5. package/dist/components/ContentLogo.svelte.d.ts +6 -5
  6. package/dist/components/Grid.svelte +9 -5
  7. package/dist/components/Label.svelte +12 -1
  8. package/dist/components/Label.svelte.d.ts +8 -0
  9. package/dist/components/PageHero.svelte +6 -7
  10. package/dist/components/Pager.svelte +5 -2
  11. package/dist/components/PhotoTile.svelte +4 -2
  12. package/dist/components/ProductCard.svelte +66 -44
  13. package/dist/components/ProductCard.svelte.d.ts +17 -4
  14. package/dist/components/Radio.svelte +12 -8
  15. package/dist/components/Radio.svelte.d.ts +1 -0
  16. package/dist/components/Select.svelte +70 -12
  17. package/dist/components/Select.svelte.d.ts +11 -0
  18. package/dist/components/SiteFooter.svelte +19 -0
  19. package/dist/components/SiteFooter.svelte.d.ts +8 -0
  20. package/dist/components/SiteHeader.svelte +11 -9
  21. package/dist/components/Slider.svelte +6 -11
  22. package/dist/components/Switch.svelte +76 -0
  23. package/dist/components/Switch.svelte.d.ts +12 -0
  24. package/dist/components/TextField.svelte +61 -10
  25. package/dist/components/TextField.svelte.d.ts +13 -0
  26. package/dist/components/Textarea.svelte +92 -0
  27. package/dist/components/Textarea.svelte.d.ts +23 -0
  28. package/dist/components/Tile.svelte +12 -11
  29. package/dist/components/Tile.svelte.d.ts +8 -2
  30. package/dist/components/blocks/CmsButton.svelte +36 -0
  31. package/dist/components/blocks/CmsButton.svelte.d.ts +12 -0
  32. package/dist/components/blocks/CmsColumns.svelte +20 -0
  33. package/dist/components/blocks/CmsColumns.svelte.d.ts +9 -0
  34. package/dist/components/blocks/CmsCover.svelte +71 -0
  35. package/dist/components/blocks/CmsCover.svelte.d.ts +17 -0
  36. package/dist/components/blocks/CmsHeading.svelte +50 -0
  37. package/dist/components/blocks/CmsHeading.svelte.d.ts +14 -0
  38. package/dist/components/blocks/CmsImage.svelte +43 -0
  39. package/dist/components/blocks/CmsImage.svelte.d.ts +13 -0
  40. package/dist/components/blocks/CmsMediaText.svelte +64 -0
  41. package/dist/components/blocks/CmsMediaText.svelte.d.ts +16 -0
  42. package/dist/components/blocks/CmsText.svelte +53 -0
  43. package/dist/components/blocks/CmsText.svelte.d.ts +10 -0
  44. package/dist/components/commerce/Breadcrumbs.svelte +22 -5
  45. package/dist/components/commerce/CartDrawer.svelte +7 -4
  46. package/dist/components/commerce/CartLineItem.svelte +1 -1
  47. package/dist/components/commerce/CheckoutProgress.svelte +1 -1
  48. package/dist/components/commerce/ConfigSection.svelte +2 -2
  49. package/dist/components/commerce/DimensionSlider.svelte +1 -1
  50. package/dist/components/commerce/Drawer.svelte +38 -24
  51. package/dist/components/commerce/Drawer.svelte.d.ts +0 -2
  52. package/dist/components/commerce/EmptyState.svelte +37 -0
  53. package/dist/components/commerce/EmptyState.svelte.d.ts +13 -0
  54. package/dist/components/commerce/FilterChip.svelte +35 -0
  55. package/dist/components/commerce/FilterChip.svelte.d.ts +10 -0
  56. package/dist/components/commerce/FreeShippingBar.svelte +1 -1
  57. package/dist/components/commerce/Gallery.svelte +4 -2
  58. package/dist/components/commerce/ListingToolbar.svelte +62 -0
  59. package/dist/components/commerce/ListingToolbar.svelte.d.ts +19 -0
  60. package/dist/components/commerce/Modal.svelte +31 -21
  61. package/dist/components/commerce/Modal.svelte.d.ts +0 -2
  62. package/dist/components/commerce/OrderSummary.svelte +1 -1
  63. package/dist/components/commerce/Pagination.svelte +115 -0
  64. package/dist/components/commerce/Pagination.svelte.d.ts +14 -0
  65. package/dist/components/commerce/PriceDisplay.svelte +3 -3
  66. package/dist/components/commerce/ProductHeader.svelte +2 -2
  67. package/dist/components/commerce/QuantityStepper.svelte +1 -1
  68. package/dist/components/commerce/RatingStars.svelte +1 -1
  69. package/dist/components/commerce/SearchField.svelte +92 -0
  70. package/dist/components/commerce/SearchField.svelte.d.ts +15 -0
  71. package/dist/components/commerce/SideToggle.svelte +1 -1
  72. package/dist/components/commerce/Skeleton.svelte +4 -1
  73. package/dist/components/commerce/StickyBar.svelte +4 -4
  74. package/dist/components/commerce/StockBadge.svelte +2 -2
  75. package/dist/components/commerce/Toaster.svelte +9 -5
  76. package/dist/components/commerce/TrustBadges.svelte +1 -1
  77. package/dist/components/commerce/overlay.d.ts +14 -0
  78. package/dist/components/commerce/overlay.js +49 -0
  79. package/dist/index.d.ts +14 -0
  80. package/dist/index.js +14 -0
  81. package/dist/theme/base.css +22 -0
  82. package/dist/theme/extras.css +44 -0
  83. package/dist/theme/fonts.css +6 -42
  84. package/dist/theme/tokens.css +16 -9
  85. package/package.json +1 -1
@@ -15,6 +15,17 @@
15
15
  * the guide styles both in one selector list.
16
16
  */
17
17
  error?: boolean;
18
+ /**
19
+ * Chybová hláška pod polem — stejné chování jako u TextFieldu: zapíná
20
+ * chybový stav, váže se přes `aria-describedby` a `aria-live`,
21
+ * má přednost před `hint`.
22
+ */
23
+ errorText?: string;
24
+ /** Nápověda pod polem, navázaná přes `aria-describedby`. */
25
+ hint?: string;
26
+ /** Povinné pole: hvězdička u popisku + nativní `required`. */
27
+ required?: boolean;
28
+ disabled?: boolean;
18
29
  class?: string;
19
30
  children: Snippet;
20
31
  }
@@ -24,6 +35,10 @@
24
35
  label,
25
36
  id,
26
37
  error = false,
38
+ errorText,
39
+ hint,
40
+ required = false,
41
+ disabled = false,
27
42
  class: className,
28
43
  children,
29
44
  ...rest
@@ -31,6 +46,14 @@
31
46
 
32
47
  const uid = $props.id();
33
48
  const selectId = $derived(id ?? uid);
49
+ const hintId = $derived(`${selectId}-hint`);
50
+ const errorId = $derived(`${selectId}-error`);
51
+ const isError = $derived(error || !!errorText);
52
+ const describedBy = $derived(
53
+ [errorText ? errorId : null, !errorText && hint ? hintId : null, rest['aria-describedby']]
54
+ .filter(Boolean)
55
+ .join(' ') || undefined
56
+ );
34
57
 
35
58
  // jelinek.css:966-975 (`.input, .select`) — same rule, and same
36
59
  // corrections against the draft brief, as TextField: border-fg-muted2
@@ -41,25 +64,60 @@
41
64
  // (jelinek.css:987-991) is new. See TextField.svelte for the full note on
42
65
  // both; the guide styles `.input` and `.select` in one selector list, so the
43
66
  // two components stay deliberately identical here.
67
+ // Vlastní šipka místo nativní: Chrome na macOS kreslí u stylovaného
68
+ // selectu svůj chevron nalepený na pravý kraj, bez ohledu na padding.
69
+ // `appearance-none` ho vypne a chevron kreslí SVG ve wrapperu níž,
70
+ // posazené na stejných 14 px od kraje jako text. `pr-9` dělá šipce místo.
71
+ // `leading-normal` drží výšku ve shodě s TextFieldem — viz komentář tam.
44
72
  const BASE =
45
- 'w-full rounded-button border border-fg-muted2 bg-surface px-[14px] py-[11px] ' +
46
- 'font-secondary text-[0.98rem] text-fg ' +
73
+ 'w-full appearance-none rounded-button border border-fg-muted2 bg-surface px-[14px] py-[11px] pr-9 ' +
74
+ 'font-secondary font-light text-[0.98rem] leading-normal text-fg [&_option]:font-light ' +
47
75
  'focus:outline-[length:var(--focus-width)] focus:outline-offset-1 ' +
48
76
  'focus:outline-[var(--focus-color)] focus:border-[var(--focus-color)]';
49
77
 
50
78
  const ERROR =
51
79
  'border-sem-error-strong focus:border-sem-error-strong focus:outline-sem-error-strong';
80
+
81
+ // Stejná řeč jako TextField.svelte — zakázaný select podle zakázaného
82
+ // tlačítka (Button.svelte:117), hlášky pod polem beze změny vzhledu.
83
+ const DISABLED = 'cursor-not-allowed border-border-subtle bg-surface-gray text-fg-muted2';
84
+ const MSG_ERROR = 'mt-1.5 font-secondary text-step-down1 font-medium text-sem-error-strong';
85
+ const MSG_HINT = 'mt-1.5 font-secondary text-step-down1 font-light text-fg-muted1';
52
86
  </script>
53
87
 
54
88
  {#if label}
55
- <Label for={selectId}>{label}</Label>
89
+ <Label for={selectId} {required}>{label}</Label>
90
+ {/if}
91
+ <span class="relative block">
92
+ <select
93
+ {...rest}
94
+ id={selectId}
95
+ {required}
96
+ {disabled}
97
+ aria-invalid={isError ? 'true' : rest['aria-invalid']}
98
+ aria-describedby={describedBy}
99
+ bind:value
100
+ class={cn(BASE, isError && ERROR, disabled && DISABLED, className)}
101
+ >
102
+ {@render children()}
103
+ </select>
104
+ <svg
105
+ class="pointer-events-none absolute top-1/2 right-[14px] size-3 -translate-y-1/2 {disabled
106
+ ? 'text-fg-muted2'
107
+ : 'text-fg'}"
108
+ viewBox="0 0 12 12"
109
+ fill="none"
110
+ stroke="currentColor"
111
+ stroke-width="1.5"
112
+ stroke-linecap="round"
113
+ stroke-linejoin="round"
114
+ aria-hidden="true"
115
+ >
116
+ <path d="m2.5 4.25 3.5 3.5 3.5-3.5" />
117
+ </svg>
118
+ </span>
119
+ {#if errorText}
120
+ <p id={errorId} aria-live="polite" class={MSG_ERROR}>{errorText}</p>
121
+ {:else if hint}
122
+ <p id={hintId} class={MSG_HINT}>{hint}</p>
56
123
  {/if}
57
- <select
58
- {...rest}
59
- id={selectId}
60
- aria-invalid={error ? 'true' : rest['aria-invalid']}
61
- bind:value
62
- class={cn(BASE, error && ERROR, className)}
63
- >
64
- {@render children()}
65
- </select>
@@ -11,6 +11,17 @@ interface Props {
11
11
  * the guide styles both in one selector list.
12
12
  */
13
13
  error?: boolean;
14
+ /**
15
+ * Chybová hláška pod polem — stejné chování jako u TextFieldu: zapíná
16
+ * chybový stav, váže se přes `aria-describedby` a `aria-live`,
17
+ * má přednost před `hint`.
18
+ */
19
+ errorText?: string;
20
+ /** Nápověda pod polem, navázaná přes `aria-describedby`. */
21
+ hint?: string;
22
+ /** Povinné pole: hvězdička u popisku + nativní `required`. */
23
+ required?: boolean;
24
+ disabled?: boolean;
14
25
  class?: string;
15
26
  children: Snippet;
16
27
  }
@@ -27,6 +27,14 @@
27
27
  * Without it (or without `stamp`) the stamp stays a plain line.
28
28
  */
29
29
  changelog?: string;
30
+ /**
31
+ * Task 49 — controls in the footer's brand row, after the stamp (the
32
+ * theme toggle moved here from the header, the owner's call —
33
+ * partials/footer.html carries the same markup). The wrapper remaps the
34
+ * fg/surface tokens to their inverse: the footer is ALWAYS black, so a
35
+ * control drawn in text colour would vanish in light mode.
36
+ */
37
+ actions?: Snippet;
30
38
  class?: string;
31
39
  }
32
40
 
@@ -38,10 +46,18 @@
38
46
  stamp,
39
47
  changelog,
40
48
  children,
49
+ actions,
41
50
  class: className,
42
51
  ...rest
43
52
  }: Props & HTMLAttributes<HTMLElement> = $props();
44
53
 
54
+ // Mirrors jelinek.css's `.site-footer .theme-toggle` overrides: light
55
+ // track/knob in both themes, dark icon on the light knob.
56
+ const ACTIONS =
57
+ 'flex flex-none items-center ' +
58
+ '[--color-fg:var(--color-fg-inverse)] [--color-surface:var(--color-fg-inverse)] ' +
59
+ '[&_svg]:text-surface-black';
60
+
45
61
  let dialogEl = $state<HTMLDialogElement | null>(null);
46
62
 
47
63
  // Mirrors the guide's .changelog dialog (jelinek.css) — a card over the
@@ -179,6 +195,9 @@
179
195
  the guide. -->
180
196
  <div class="ml-auto font-serif text-[0.85rem] text-fg-muted2">{stamp}</div>
181
197
  {/if}
198
+ {#if actions}
199
+ <div class={cn(ACTIONS, !stamp && 'ml-auto')}>{@render actions()}</div>
200
+ {/if}
182
201
  </div>
183
202
  {#if stamp && changelog}
184
203
  <dialog
@@ -24,6 +24,14 @@ interface Props {
24
24
  * Without it (or without `stamp`) the stamp stays a plain line.
25
25
  */
26
26
  changelog?: string;
27
+ /**
28
+ * Task 49 — controls in the footer's brand row, after the stamp (the
29
+ * theme toggle moved here from the header, the owner's call —
30
+ * partials/footer.html carries the same markup). The wrapper remaps the
31
+ * fg/surface tokens to their inverse: the footer is ALWAYS black, so a
32
+ * control drawn in text colour would vanish in light mode.
33
+ */
34
+ actions?: Snippet;
27
35
  class?: string;
28
36
  }
29
37
  type $$ComponentProps = Props & HTMLAttributes<HTMLElement>;
@@ -276,7 +276,7 @@
276
276
  // Three of his commits land here (36354b9, 6750ee8, 74ef043, a430f9c) and
277
277
  // they supersede this component's previous phone treatment wholesale:
278
278
  //
279
- // POSITION. `absolute inset-x-0 top-0 z-50`, not `sticky`. His 74ef043 made
279
+ // POSITION. `absolute inset-x-0 top-0` at --z-header (50), not `sticky`. His 74ef043 made
280
280
  // this change deliberately and says why: with the bar in flow (or sticky at
281
281
  // the top) the hero's grey band started BELOW it, leaving a visible seam in
282
282
  // the page background right above the bar. Absolute takes the bar out of
@@ -340,7 +340,7 @@
340
340
  // consumer's own layout can do that). PageHero and the showcase's
341
341
  // `+layout.svelte` model exactly that.
342
342
  const OUTER =
343
- 'absolute inset-x-0 top-0 z-50 p-0 [--logo-mark:46px] ' +
343
+ 'absolute inset-x-0 top-0 z-(--z-header) p-0 [--logo-mark:46px] ' +
344
344
  'min-[640px]:fixed min-[640px]:px-6 min-[640px]:py-3';
345
345
  // Task 43 — the glass card no longer IS the flex row: the row (logo,
346
346
  // lockup, burger, nav) moved into its own .site-header__bar-equivalent
@@ -537,7 +537,7 @@
537
537
  // Drawer head: small uppercase "Menu" + a 44px close cross. The drawer
538
538
  // covers the bar (and the burger with it), so it closes itself.
539
539
  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';
540
+ const DRAWER_LABEL = 'font-secondary text-[0.8rem] font-bold tracking-[0.08em] uppercase text-fg-muted1';
541
541
  const DRAWER_CLOSE =
542
542
  'inline-flex h-11 w-11 flex-none cursor-pointer items-center justify-center ' +
543
543
  'rounded-button border-0 bg-transparent p-0 text-fg';
@@ -585,10 +585,10 @@
585
585
  'transition-[padding-top] duration-[var(--dur-base)] ease-[var(--ease)]';
586
586
  const MEGA_COLS_OPEN = 'pt-4';
587
587
  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.
588
+ // Column heading (shared by the mega-menu and the accordion): GT Walsheim
589
+ // Bold, small uppercase — Avenir patří jen podnázvu loga (majitel, 21. 8. 2026).
590
590
  const HEADING =
591
- 'px-4 pt-2.5 pb-0.5 text-[0.72rem] font-semibold tracking-[0.08em] uppercase text-fg-muted1';
591
+ 'px-4 pt-2.5 pb-0.5 font-secondary text-[0.72rem] font-bold tracking-[0.08em] uppercase text-fg-muted1';
592
592
 
593
593
  const NAV_ROW = 'hidden ml-auto items-center gap-2 min-[640px]:flex';
594
594
 
@@ -684,7 +684,7 @@
684
684
  <!-- One definition of a submenu column's links, rendered into both the
685
685
  mega-menu columns (≥640px) and the drawer accordion (phone). -->
686
686
  {#snippet subLinks(group: NavGroup)}
687
- {#if group.title}<div class={HEADING} style={SUBTITLE_STYLE}>{group.title}</div>{/if}
687
+ {#if group.title}<div class={HEADING}>{group.title}</div>{/if}
688
688
  {#each group.items as chapter (chapter.href)}
689
689
  <Button
690
690
  href={chapter.href}
@@ -726,8 +726,10 @@
726
726
  xmlns="http://www.w3.org/2000/svg"
727
727
  aria-hidden="true"
728
728
  >
729
+ <!-- Majitel 21. 8. 2026: jelen v hlavičce je HNĚDÝ (barevná
730
+ varianta znaku), ne bílý — zrcadlí partials/header.html. -->
729
731
  <rect width="400" height="400" class="fill-jelinek-black" />
730
- <path d={DEER_MARK_PATH} class="fill-jelinek-white" />
732
+ <path d={DEER_MARK_PATH} class="fill-jelinek-brown" />
731
733
  </svg>
732
734
  {/if}
733
735
  </a>
@@ -842,7 +844,7 @@
842
844
  640px up, so only one of the two navs is ever in the a11y tree. -->
843
845
  <nav id={panelId} bind:this={linksEl} class={drawer} aria-label={navLabel}>
844
846
  <div class={DRAWER_HEAD}>
845
- <span class={DRAWER_LABEL} style={SUBTITLE_STYLE}>Menu</span>
847
+ <span class={DRAWER_LABEL}>Menu</span>
846
848
  <button type="button" class={DRAWER_CLOSE} aria-label={menuLabelClose} onclick={closeMenu}>
847
849
  <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
850
  </button>
@@ -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;
@@ -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;