@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
@@ -61,7 +61,7 @@
61
61
  // while `display: none` holds; from 640px up the bar is `absolute`, which is
62
62
  // what makes it the containing block for the scroll-affordance fades below.
63
63
  const OUTER =
64
- 'hidden z-[49] px-6 ' +
64
+ 'hidden z-(--z-chapter-nav) px-6 ' +
65
65
  'min-[640px]:block min-[640px]:absolute min-[640px]:inset-x-0 ' +
66
66
  'min-[640px]:top-[calc(var(--header-h)-11px)]';
67
67
 
@@ -6,6 +6,7 @@
6
6
  checked?: boolean;
7
7
  label: string;
8
8
  id?: string;
9
+ disabled?: boolean;
9
10
  class?: string;
10
11
  }
11
12
 
@@ -13,6 +14,7 @@
13
14
  checked = $bindable(false),
14
15
  label,
15
16
  id,
17
+ disabled = false,
16
18
  class: className,
17
19
  ...rest
18
20
  }: Props & Omit<HTMLInputAttributes, 'checked' | 'type'> = $props();
@@ -31,22 +33,18 @@
31
33
  // without depending on an ambient .demo wrapper the consumer may not
32
34
  // have. gap-2.5 matches the 10px inline gap.
33
35
  //
34
- // ACCENT COLOUR — no CSS source of truth exists, so this is a judgement
35
- // call, flagged for adjudication (see task-7-report.md). The checkbox
36
- // square itself uses accent-jelinek-brown, NOT accent-jelinek-magenta:
37
- // magenta is reserved for the Zdravý spánek (mattress) division and the
38
- // guide is explicit brown/magenta never mix in one interface, so baking
39
- // magenta into every checkbox would leak the mattress accent into
40
- // furniture-division UIs. jelinek-brown is also what TextField/Select's
41
- // own focus ring already uses (jelinek.css:661), and the token layer
42
- // already re-themes --JELINEK-BROWN to magenta under
43
- // [data-division="mattress"] (tokens.css:161-166) — the same mechanism
44
- // Button relies on for its brand colours — so this accent still turns
45
- // magenta automatically on mattress-themed pages, without hardcoding it.
46
- const LABEL = 'flex cursor-pointer items-center gap-2.5 font-secondary text-step0 text-fg';
36
+ // ACCENT COLOUR — adjudicated by the owner (14. 8. 2026): forms are
37
+ // colour-neutral. Every emphasis in a form is BLACK (--JELINEK-BLACK,
38
+ // which flips to near-white in dark mode so it stays visible); the only
39
+ // colour a form carries is the error state. No division re-theme here —
40
+ // forms look the same in Nábytek and Zdravý spánek.
41
+ const LABEL = 'flex cursor-pointer items-center gap-2.5 font-secondary font-light text-step0 text-fg';
42
+ // Zakázaný stav mluví jako zakázané tlačítko (Button.svelte:117):
43
+ // utlumený text a cursor-not-allowed; políčko samotné tlumí prohlížeč.
44
+ const DISABLED = 'cursor-not-allowed text-fg-muted2';
47
45
  </script>
48
46
 
49
- <label for={inputId} class={cn(LABEL, className)}>
50
- <input {...rest} id={inputId} type="checkbox" bind:checked class="accent-jelinek-brown" />
47
+ <label for={inputId} class={cn(LABEL, disabled && DISABLED, className)}>
48
+ <input {...rest} id={inputId} type="checkbox" {disabled} bind:checked class="accent-jelinek-black" />
51
49
  {label}
52
50
  </label>
@@ -3,6 +3,7 @@ interface Props {
3
3
  checked?: boolean;
4
4
  label: string;
5
5
  id?: string;
6
+ disabled?: boolean;
6
7
  class?: string;
7
8
  }
8
9
  type $$ComponentProps = Props & Omit<HTMLInputAttributes, 'checked' | 'type'>;
@@ -3,13 +3,14 @@
3
3
  import { cn } from '../utils/cn.js';
4
4
  import { DEER_MARK_PATH } from './deer-mark-path.js';
5
5
 
6
- type Variant = 'brown' | 'magenta';
6
+ type Variant = 'brown';
7
7
 
8
8
  interface Props {
9
- /** jelinek.css:744-745. Left unset for the bare/default rule (mark
10
- * colour --text-white, jelinek.css:743) — NOT itself a "neutral"
11
- * variant name, since there is no third `.content-logo--*` modifier
12
- * in the guide to give it one. */
9
+ /** Jelen je jen HNĚDÝ, nebo BÍLÝ (brand → Znak, Užití s produktovými
10
+ * barvami; majitel, 21. 8. 2026): `brown` pro divizi Nábytek, bez
11
+ * varianty BÍLÝ — divize Zdravý spánek a neutrální užití (interní
12
+ * aplikace). Purpurová varianta NEEXISTUJE — dřívější `magenta` byla
13
+ * chyba proti manuálu a je odstraněná. */
13
14
  variant?: Variant;
14
15
  /**
15
16
  * Sets `--logo-mark` (default matches jelinek.css:743's own inline
@@ -76,14 +77,13 @@
76
77
  // actually invisible once the colour scoping below is right.
77
78
  const BASE = 'inline-flex items-center gap-[calc(var(--logo-mark)*107/400)] text-fg-inverse';
78
79
 
79
- // jelinek.css:744 (--brown, --JELINEK-BROWN, division-tracking) and :745
80
- // (--magenta, --JELINEK-MAGENTA, fixed regardless of division) — same
81
- // token pair and same division-tracking-vs-fixed distinction Eyebrow's
82
- // own VARIANT already documents for the identical reason (tokens.css's
83
- // mattress re-theme only ever reassigns --JELINEK-BROWN).
80
+ // jelinek.css .content-logo--brown: the brown deer. The division re-theme
81
+ // remaps --JELINEK-BROWN to magenta — for the deer that is a FORBIDDEN
82
+ // state, so under [data-division="mattress"] every lockup flips to WHITE
83
+ // instead; the rule lives in extras.css and hooks on the data-content-logo
84
+ // attribute below, mirroring jelinek.css's own division override.
84
85
  const VARIANT: Record<Variant, string> = {
85
- brown: 'text-jelinek-brown',
86
- magenta: 'text-jelinek-magenta'
86
+ brown: 'text-jelinek-brown'
87
87
  };
88
88
 
89
89
  // jelinek.css:746 (.content-logo__svg): width/height both var(--logo-mark),
@@ -142,7 +142,12 @@
142
142
  );
143
143
  </script>
144
144
 
145
- <div {...rest} style={mergedStyle} class={cn(BASE, variant && VARIANT[variant], className)}>
145
+ <div
146
+ {...rest}
147
+ style={mergedStyle}
148
+ data-content-logo=""
149
+ class={cn(BASE, variant && VARIANT[variant], className)}
150
+ >
146
151
  <svg class={SVG} viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
147
152
  <rect width="400" height="400" rx="25" ry="25" fill="#000" />
148
153
  <path d={DEER_MARK_PATH} fill="currentColor" />
@@ -1,10 +1,11 @@
1
1
  import type { HTMLAttributes } from 'svelte/elements';
2
- type Variant = 'brown' | 'magenta';
2
+ type Variant = 'brown';
3
3
  interface Props {
4
- /** jelinek.css:744-745. Left unset for the bare/default rule (mark
5
- * colour --text-white, jelinek.css:743) — NOT itself a "neutral"
6
- * variant name, since there is no third `.content-logo--*` modifier
7
- * in the guide to give it one. */
4
+ /** Jelen je jen HNĚDÝ, nebo BÍLÝ (brand → Znak, Užití s produktovými
5
+ * barvami; majitel, 21. 8. 2026): `brown` pro divizi Nábytek, bez
6
+ * varianty BÍLÝ — divize Zdravý spánek a neutrální užití (interní
7
+ * aplikace). Purpurová varianta NEEXISTUJE — dřívější `magenta` byla
8
+ * chyba proti manuálu a je odstraněná. */
8
9
  variant?: Variant;
9
10
  /**
10
11
  * Sets `--logo-mark` (default matches jelinek.css:743's own inline
@@ -49,10 +49,11 @@
49
49
  // Behaviour by width (the guide's, verbatim):
50
50
  // w < 640 -> 1 column, every cols value
51
51
  // 640 <= w < 1024 -> 2 columns, every cols value
52
- // w >= 1024 -> the cols value itself (2/3/4)
53
- // (.grid--3 and .grid--4 both collapse to 2 columns between 640 and 1024,
54
- // so cols=4 goes 4 -> 2 -> 1 and never passes through 3; cols=2 has no
55
- // 1024px rule at all, it is already 2-up from 640px.)
52
+ // w >= 1024 -> the cols value itself for 2/3; cols=4 steps
53
+ // 3 (>=1024) -> 4 (>=1280) -> 5 (>=1600), so the
54
+ // four-column grid climbs 1/2/3/4/5 with width
55
+ // (owner's request) instead of jumping 2 -> 4.
56
+ // (cols=2 has no 1024px rule at all, it is already 2-up from 640px.)
56
57
  //
57
58
  // Why Tailwind's native `min-*` variant family and NOT arbitrary *media*
58
59
  // variants (`[@media(min-width:1024px)]:...`), even though those can express
@@ -75,10 +76,13 @@
75
76
  // `min-[1024px]:grid-cols-${n}` template, so Tailwind's static scanner can
76
77
  // see each one literally — a template string is invisible to it and the
77
78
  // class would never be generated.
79
+ // cols={4} přidává od 1600 px pátý sloupec (jelinek.css .grid--4, majitel:
80
+ // širší web = víc obsahu do řady; trojka a dvojka zůstávají — jsou volené
81
+ // kvůli obsahu, ne kvůli místu).
78
82
  const COLS: Record<2 | 3 | 4, string> = {
79
83
  2: 'grid-cols-1 min-[640px]:grid-cols-2',
80
84
  3: 'grid-cols-1 min-[640px]:grid-cols-2 min-[1024px]:grid-cols-3',
81
- 4: 'grid-cols-1 min-[640px]:grid-cols-2 min-[1024px]:grid-cols-4'
85
+ 4: 'grid-cols-1 min-[640px]:grid-cols-2 min-[1024px]:grid-cols-3 min-[1280px]:grid-cols-4 min-[1600px]:grid-cols-5'
82
86
  };
83
87
  </script>
84
88
 
@@ -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
@@ -329,7 +328,7 @@
329
328
  two copies drifting apart. -->
330
329
  {#snippet content()}
331
330
  {#if crumbs.length > 0}
332
- <nav class={CRUMBS} style={CRUMBS_STYLE} aria-label="Drobečková navigace">
331
+ <nav class={CRUMBS} aria-label="Drobečková navigace">
333
332
  {#each crumbs as crumb, i (crumb.label)}
334
333
  {#if i > 0}<span class="text-fg-muted2" aria-hidden="true">/</span>{/if}
335
334
  {#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'>;