@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
@@ -5,12 +5,21 @@
5
5
 
6
6
  interface Props {
7
7
  for?: string;
8
+ /**
9
+ * Hvězdička povinného pole za popiskem. Jen vizuální značka
10
+ * (aria-hidden) — povinnost samotnou hlásí nativní `required` na poli,
11
+ * které ji zapíná. Černá (--JELINEK-BLACK, v tmavém režimu se sama
12
+ * překlopí na bílou): formuláře jsou podle rozhodnutí majitele
13
+ * (14. 8. 2026) barevně neutrální — barvu nese jen chyba.
14
+ */
15
+ required?: boolean;
8
16
  class?: string;
9
17
  children: Snippet;
10
18
  }
11
19
 
12
20
  let {
13
21
  for: forId,
22
+ required = false,
14
23
  class: className,
15
24
  children,
16
25
  ...rest
@@ -33,5 +42,7 @@
33
42
  for={forId}
34
43
  class={cn('mb-1.5 block font-secondary text-[0.88rem] font-medium', className)}
35
44
  >
36
- {@render children()}
45
+ {@render children()}{#if required}<span aria-hidden="true" class="text-jelinek-black">
46
+ *</span
47
+ >{/if}
37
48
  </label>
@@ -2,6 +2,14 @@ import type { Snippet } from 'svelte';
2
2
  import type { HTMLLabelAttributes } from 'svelte/elements';
3
3
  interface Props {
4
4
  for?: string;
5
+ /**
6
+ * Hvězdička povinného pole za popiskem. Jen vizuální značka
7
+ * (aria-hidden) — povinnost samotnou hlásí nativní `required` na poli,
8
+ * které ji zapíná. Černá (--JELINEK-BLACK, v tmavém režimu se sama
9
+ * překlopí na bílou): formuláře jsou podle rozhodnutí majitele
10
+ * (14. 8. 2026) barevně neutrální — barvu nese jen chyba.
11
+ */
12
+ required?: boolean;
5
13
  class?: string;
6
14
  children: Snippet;
7
15
  }
@@ -82,15 +82,14 @@
82
82
  ...rest
83
83
  }: Props & HTMLAttributes<HTMLElement> = $props();
84
84
 
85
- // jelinek.css .breadcrumbs — a small Avenir UI row: muted, 600 weight,
86
- // step-down2, links darken on hover, current chapter reads in full text
87
- // colour. SUBTITLE_STYLE (below) carries the Avenir stack inline, same as
88
- // the header's subtitle does.
85
+ // jelinek.css .breadcrumbs — a small muted UI row: step-down2, links darken
86
+ // on hover, current chapter reads in full text colour.
87
+ // GT Walsheim Medium — Avenir do drobečků nepatří (majitel, 21. 8. 2026):
88
+ // pravidla znají Avenir jen u podnázvu loga.
89
89
  const CRUMBS =
90
- 'mb-4 flex flex-wrap items-center gap-2 text-step-down2 font-semibold tracking-[0.02em] text-fg-muted1';
90
+ 'mb-4 flex flex-wrap items-center gap-2 font-secondary text-step-down2 font-medium tracking-[0.02em] text-fg-muted1';
91
91
  const CRUMB_LINK =
92
92
  'text-fg-muted1 no-underline transition-[color] duration-[var(--dur-base)] ease-[var(--ease)] hover:text-fg hover:no-underline';
93
- const CRUMBS_STYLE = "font-family: 'Avenir Next LT Pro', 'Avenir Next', Arial, sans-serif;";
94
93
 
95
94
  // `hasContext` must run synchronously during this component's own
96
95
  // initialization (Svelte's lifecycle rule for context APIs) — captured
@@ -199,10 +198,14 @@
199
198
  // different reason: the chapter bar is `display: none` below 640px (its
200
199
  // entries move into the header's burger panel), so there is no strip to
201
200
  // reserve until the bar exists.
201
+ //
202
+ // --info-bar-h (majitel, 4. 9. 2026): informační lišta nad menu leží PŘES hero
203
+ // jako hlavička a hlásí svou výšku na <html>; bez lišty je proměnná prázdná → 0.
204
+ // Pozadí hero tak běží pod lištou i pod menu, žádný bílý pruh nad hlavičkou.
202
205
  const paddingTop = $derived(
203
206
  hasChapterNav
204
- ? 'pt-[calc(var(--header-h)_+_28px)] min-[640px]:pt-[calc(var(--header-h)_+_var(--subnav-h)_+_20px)]'
205
- : 'pt-[calc(var(--header-h)_+_28px)]'
207
+ ? 'pt-[calc(var(--header-h)_+_var(--info-bar-h,0px)_+_28px)] min-[640px]:pt-[calc(var(--header-h)_+_var(--info-bar-h,0px)_+_var(--subnav-h)_+_20px)]'
208
+ : 'pt-[calc(var(--header-h)_+_var(--info-bar-h,0px)_+_28px)]'
206
209
  );
207
210
 
208
211
  // The heading-underline mechanism. jelinek.css draws this via
@@ -329,7 +332,7 @@
329
332
  two copies drifting apart. -->
330
333
  {#snippet content()}
331
334
  {#if crumbs.length > 0}
332
- <nav class={CRUMBS} style={CRUMBS_STYLE} aria-label="Drobečková navigace">
335
+ <nav class={CRUMBS} aria-label="Drobečková navigace">
333
336
  {#each crumbs as crumb, i (crumb.label)}
334
337
  {#if i > 0}<span class="text-fg-muted2" aria-hidden="true">/</span>{/if}
335
338
  {#if crumb.href}
@@ -62,15 +62,18 @@
62
62
  // a 0 basis can always be squeezed into the line). jelinek.css:1116 is
63
63
  // `flex: 1 1 200px`; the 200px basis is what makes ROOT's `flex-wrap`
64
64
  // actually fire. Changing one without the other is a no-op.
65
+ // font-light, not font-normal: jelinek.css's `.tile, .pager a` moved to
66
+ // weight 300 (typografická revize 21. 8. 2026 — Walsheim má jen
67
+ // Bold/Medium/Light); the bold lives on the inner <strong>.
65
68
  const LINK =
66
69
  'max-w-[340px] flex-[1_1_200px] rounded-card border border-border-subtle px-5 py-4 font-secondary ' +
67
- 'font-normal text-fg no-underline shadow-resting hover:shadow-interactive ' +
70
+ 'font-light text-fg no-underline shadow-resting hover:shadow-interactive ' +
68
71
  'transition-shadow duration-[var(--dur-base)] ease-[var(--ease)] ' +
69
72
  'dark:border-t-[var(--border-highlight-white)]';
70
73
 
71
74
  // jelinek.css:1123 (.pager a span): block, 0.78rem (not on the --step
72
75
  // scale), text-muted1.
73
- const SUBLABEL = 'block text-[0.78rem] text-fg-muted1';
76
+ const SUBLABEL = 'block text-[0.78rem] font-light text-fg-muted1';
74
77
  </script>
75
78
 
76
79
  <nav {...rest} class={cn(ROOT, className)}>
@@ -66,8 +66,10 @@
66
66
  const BASE =
67
67
  'group relative flex flex-col justify-end overflow-hidden aspect-[4/3] ' +
68
68
  'rounded-section shadow-resting no-underline ' +
69
- 'transition-[box-shadow,transform] duration-[var(--dur-base)] ease-[var(--ease)] ' +
70
- 'hover:-translate-y-0.5 hover:shadow-interactive ' +
69
+ // Bez vyskočení na hover (majitel 14. 8. 2026, stejně jako Tile) —
70
+ // zvýrazňuje jen stín.
71
+ 'transition-[box-shadow] duration-[var(--dur-base)] ease-[var(--ease)] ' +
72
+ 'hover:shadow-interactive ' +
71
73
  'dark:shadow-[var(--shadow-resting),inset_0_2px_1px_-1px_var(--box-primary)] ' +
72
74
  'dark:hover:shadow-[var(--shadow-interactive),inset_0_2px_1px_-1px_var(--box-primary)]';
73
75
 
@@ -2,18 +2,31 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
4
  import { cn } from '../utils/cn.js';
5
- import Button from './Button.svelte';
6
- import Card from './Card.svelte';
5
+ import PriceDisplay from './commerce/PriceDisplay.svelte';
6
+ import StockBadge from './commerce/StockBadge.svelte';
7
7
 
8
8
  interface Props {
9
9
  title: string;
10
+ /** Krátký popis pod názvem — kolekce, materiál, shrnutí konfigurace. */
10
11
  description?: string;
11
12
  image?: string;
12
13
  imageAlt?: string;
14
+ /** Odkaz na detail — celá karta je jeden odkaz, žádné tlačítko uvnitř. */
13
15
  href?: string;
14
- ctaLabel?: string;
15
- color?: 'brown' | 'magenta' | 'black';
16
- /** Optional badge rendered above the title, e.g. <Tag variant="instock">. */
16
+ /** Cena v celých korunách (formátuje `formatPrice`). */
17
+ price?: number;
18
+ /** Přeškrtnutý původ — vykreslí se jen, když je vyšší než `price`. */
19
+ originalPrice?: number;
20
+ /** Kvalifikátor „od“ před cenou (konfigurovatelné produkty). */
21
+ priceFrom?: boolean;
22
+ /** Dostupnost pod cenou (kompaktní StockBadge). */
23
+ stock?: 'in-stock' | 'on-order' | 'sold-out' | 'lead';
24
+ /** Přepis textu dostupnosti — u stavu `lead` konkrétní lhůta. */
25
+ stockLabel?: string;
26
+ /** Produktová fotka na bílém pozadí (packshot): čtverec bez ořezu.
27
+ Výchozí je ambientní fotka v ořezu 4:3. */
28
+ packshot?: boolean;
29
+ /** Štítek nad názvem, např. <Tag variant="instock">. */
17
30
  tag?: Snippet;
18
31
  class?: string;
19
32
  }
@@ -24,58 +37,67 @@
24
37
  image,
25
38
  imageAlt = '',
26
39
  href,
27
- ctaLabel = 'Detail produktu',
28
- color = 'brown',
40
+ price,
41
+ originalPrice,
42
+ priceFrom = false,
43
+ stock,
44
+ stockLabel,
45
+ packshot = false,
29
46
  tag,
30
47
  class: className,
31
48
  ...rest
32
- }: Props & HTMLAttributes<HTMLDivElement> = $props();
49
+ }: Props & HTMLAttributes<HTMLElement> = $props();
33
50
 
34
- // No `.product-card` class exists in jelinek.css — this is a composition
35
- // of Card + Button + (via the `tag` snippet) Tag, not a standalone CSS
36
- // rule. Its structure and spacing are taken from the guide's own worked
37
- // example, ui/komponenty.html:253-281, reproduced value-for-value where
38
- // that markup gives one (18px inner padding, 10px gap under the tag,
39
- // 8px/4px heading margin), and by ordinary judgment where it doesn't
40
- // (there's no other source of truth to check those calls against).
51
+ // DEFINITIVNÍ PODOBA (audit P2, 21. 8. 2026) — do té doby bylo tohle
52
+ // provizorium složené z Card + Button. Předloha je karta výpisu z dema
53
+ // kategorie (demo/ortopedicke-lamelove-matrace), kterou schválil majitel:
54
+ //
55
+ // - CELÁ KARTA JE JEDEN ODKAZ. Ve výpisu se kliká na produkt, ne na
56
+ // tlačítko v něm — tlačítko uvnitř odkazu je past pro čtečky i palce.
57
+ // Bez `href` se vykreslí jako <div> (čistě prezentační užití).
58
+ // - Neutrální zvýraznění (pravidlo formulářů): klidová hrana
59
+ // border-subtle, na najetí tenká černá + zvednutý stín, ŽÁDNÝ posun.
60
+ // Barva divize na kartě není — nese ji obsah stránky, ne rámeček.
61
+ // - Cena a dostupnost jsou existující atomy PriceDisplay a StockBadge,
62
+ // karta si je nekreslí po svém.
63
+ // - Fotky: ambientní záběry ořez 4:3 (jako PhotoTile), packshoty na bílém
64
+ // pozadí čtverec bez ořezu (`packshot`) — matrace nesmí přijít o rohy.
65
+ const CARD =
66
+ 'block overflow-hidden rounded-card border border-border-subtle bg-surface ' +
67
+ 'no-underline shadow-resting transition-[border-color,box-shadow] ' +
68
+ 'duration-[var(--dur-base)] ease-[var(--ease)]';
69
+ const LINKED = 'hover:border-jelinek-black hover:shadow-interactive';
41
70
  </script>
42
71
 
43
- <Card {...rest} padded={false} class={cn('overflow-hidden', className)}>
72
+ <svelte:element this={href ? 'a' : 'div'} {...rest} {href} class={cn(CARD, href && LINKED, className)}>
44
73
  {#if image}
45
- <!-- jelinek.css:510 (.photo): rounded-image object-cover w-full. The
46
- demo (ui/komponenty.html:255) overrides the radius to 0 inline and
47
- adds aspect-ratio:4/3: the image sits flush against the card's own
48
- rounded top corners, and `overflow-hidden` on the card above does
49
- the rounding instead — a rounded image here would double up as a
50
- visible seam inside the rounded card. `rounded-none` is written
51
- explicitly, not just omitted, to keep that override visible. -->
52
- <img src={image} alt={imageAlt} class="aspect-[4/3] w-full rounded-none object-cover" />
74
+ <!-- rounded-none: fotka lícuje s kulatými rohy karty, ořez dělá
75
+ overflow-hidden nahoře — viz stejná poznámka v PhotoTile. -->
76
+ {#if packshot}
77
+ <img src={image} alt={imageAlt} class="aspect-square w-full rounded-none bg-surface object-contain" />
78
+ {:else}
79
+ <img src={image} alt={imageAlt} class="aspect-[4/3] w-full rounded-none object-cover" />
80
+ {/if}
53
81
  {/if}
54
82
  <div class="p-[18px]">
55
83
  {#if tag}
56
84
  <div class="mb-2.5">{@render tag()}</div>
57
85
  {/if}
58
- <!-- jelinek.css:652 (`.card h3`) already reaches this <h3> through
59
- Card's own [&_h3]:text-step1 [&_h3]:leading-[1.3] descendant rule
60
- (Card.svelte) — repeating text-step1/leading here would duplicate
61
- Card, not correct it. text-fg is still needed: Card sets no text
62
- color of its own (the static site's .card h3 inherits its color
63
- from body), so without it this heading would fall back to
64
- whichever default the consuming app happens to have. mt-2/mb-1
65
- reproduce the demo's inline `margin:8px 0 4px` exactly — no CSS
66
- rule sets this margin (it's specific to that one demo instance),
67
- but both values already sit on Tailwind's default spacing scale. -->
68
- <h3 class="mt-2 mb-1 text-fg">{title}</h3>
86
+ <h3 class="mt-0 mb-1 font-primary text-step1 leading-[1.3] font-bold text-fg">{title}</h3>
69
87
  {#if description}
70
- <!-- jelinek.css:306 (.small) + shared .muted (jelinek.css:303): a
71
- fixed --step-down1 size at line-height 1.1 (not on Tailwind's
72
- leading scale, kept arbitrary) and the muted1 text color.
73
- font-weight 300 already comes from Card's own
74
- font-secondary/font-light, inherited here — not re-stated. -->
75
- <p class="mb-4 text-step-down1 leading-[1.1] text-fg-muted1">{description}</p>
88
+ <p class="mb-0 font-secondary font-light text-step-down1 leading-[1.3] text-fg-muted1">
89
+ {description}
90
+ </p>
91
+ {/if}
92
+ {#if price !== undefined}
93
+ <div class="mt-2.5 text-fg">
94
+ <PriceDisplay {price} {originalPrice} from={priceFrom} size="sm" />
95
+ </div>
76
96
  {/if}
77
- {#if href}
78
- <Button {href} {color} fullWidth>{ctaLabel}</Button>
97
+ {#if stock}
98
+ <div class="mt-1.5">
99
+ <StockBadge state={stock} label={stockLabel} compact />
100
+ </div>
79
101
  {/if}
80
102
  </div>
81
- </Card>
103
+ </svelte:element>
@@ -2,17 +2,30 @@ import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
3
  interface Props {
4
4
  title: string;
5
+ /** Krátký popis pod názvem — kolekce, materiál, shrnutí konfigurace. */
5
6
  description?: string;
6
7
  image?: string;
7
8
  imageAlt?: string;
9
+ /** Odkaz na detail — celá karta je jeden odkaz, žádné tlačítko uvnitř. */
8
10
  href?: string;
9
- ctaLabel?: string;
10
- color?: 'brown' | 'magenta' | 'black';
11
- /** Optional badge rendered above the title, e.g. <Tag variant="instock">. */
11
+ /** Cena v celých korunách (formátuje `formatPrice`). */
12
+ price?: number;
13
+ /** Přeškrtnutý původ — vykreslí se jen, když je vyšší než `price`. */
14
+ originalPrice?: number;
15
+ /** Kvalifikátor „od“ před cenou (konfigurovatelné produkty). */
16
+ priceFrom?: boolean;
17
+ /** Dostupnost pod cenou (kompaktní StockBadge). */
18
+ stock?: 'in-stock' | 'on-order' | 'sold-out' | 'lead';
19
+ /** Přepis textu dostupnosti — u stavu `lead` konkrétní lhůta. */
20
+ stockLabel?: string;
21
+ /** Produktová fotka na bílém pozadí (packshot): čtverec bez ořezu.
22
+ Výchozí je ambientní fotka v ořezu 4:3. */
23
+ packshot?: boolean;
24
+ /** Štítek nad názvem, např. <Tag variant="instock">. */
12
25
  tag?: Snippet;
13
26
  class?: string;
14
27
  }
15
- type $$ComponentProps = Props & HTMLAttributes<HTMLDivElement>;
28
+ type $$ComponentProps = Props & HTMLAttributes<HTMLElement>;
16
29
  declare const ProductCard: import("svelte").Component<$$ComponentProps, {}, "">;
17
30
  type ProductCard = ReturnType<typeof ProductCard>;
18
31
  export default ProductCard;
@@ -8,6 +8,7 @@
8
8
  value: string;
9
9
  label: string;
10
10
  id?: string;
11
+ disabled?: boolean;
11
12
  class?: string;
12
13
  }
13
14
 
@@ -15,6 +16,7 @@
15
16
  value,
16
17
  label,
17
18
  id,
19
+ disabled = false,
18
20
  class: className,
19
21
  ...rest
20
22
  }: Props & Omit<HTMLInputAttributes, 'value' | 'type' | 'name'> = $props();
@@ -28,15 +30,16 @@
28
30
 
29
31
  // Same reasoning as Checkbox.svelte: no CSS rule exists for radio inputs
30
32
  // or their label; font-secondary/text-fg reproduces what the guide's own
31
- // `.demo` wrapper applies ambiently (jelinek.css:322-324), and
32
- // accent-jelinek-brown (not accent-jelinek-magenta) keeps the mattress
33
- // accent out of furniture UIs by default while still following the
34
- // division automatically via the --JELINEK-BROWN re-theme
35
- // (tokens.css:161-166). Flagged for adjudication.
36
- const LABEL = 'flex cursor-pointer items-center gap-2.5 font-secondary text-step0 text-fg';
33
+ // `.demo` wrapper applies ambiently (jelinek.css:322-324). Accent is
34
+ // BLACK per the owner's 14. 8. 2026 call — forms are colour-neutral,
35
+ // only the error state carries colour; --JELINEK-BLACK flips to
36
+ // near-white in dark mode.
37
+ const LABEL = 'flex cursor-pointer items-center gap-2.5 font-secondary font-light text-step0 text-fg';
38
+ // Stejná řeč jako Checkbox: utlumený text + cursor-not-allowed.
39
+ const DISABLED = 'cursor-not-allowed text-fg-muted2';
37
40
  </script>
38
41
 
39
- <label for={inputId} class={cn(LABEL, className)}>
42
+ <label for={inputId} class={cn(LABEL, disabled && DISABLED, className)}>
40
43
  <input
41
44
  {...rest}
42
45
  id={inputId}
@@ -44,11 +47,12 @@
44
47
  name={group.name}
45
48
  {value}
46
49
  {checked}
50
+ {disabled}
47
51
  onchange={(event) => {
48
52
  group.select(value);
49
53
  rest.onchange?.(event);
50
54
  }}
51
- class="accent-jelinek-brown"
55
+ class="accent-jelinek-black"
52
56
  />
53
57
  {label}
54
58
  </label>
@@ -3,6 +3,7 @@ interface Props {
3
3
  value: string;
4
4
  label: string;
5
5
  id?: string;
6
+ disabled?: boolean;
6
7
  class?: string;
7
8
  }
8
9
  type $$ComponentProps = Props & Omit<HTMLInputAttributes, 'value' | 'type' | 'name'>;
@@ -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>;