@jelinek/ui 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -10
- package/dist/components/ChapterNav.svelte +1 -1
- package/dist/components/Checkbox.svelte +13 -15
- package/dist/components/Checkbox.svelte.d.ts +1 -0
- package/dist/components/ContentLogo.svelte +18 -13
- package/dist/components/ContentLogo.svelte.d.ts +6 -5
- package/dist/components/Grid.svelte +9 -5
- package/dist/components/Icon.svelte +52 -0
- package/dist/components/Icon.svelte.d.ts +30 -0
- package/dist/components/InfoBar.svelte +254 -0
- package/dist/components/InfoBar.svelte.d.ts +46 -0
- package/dist/components/Label.svelte +12 -1
- package/dist/components/Label.svelte.d.ts +8 -0
- package/dist/components/PageHero.svelte +12 -9
- package/dist/components/Pager.svelte +5 -2
- package/dist/components/PhotoTile.svelte +4 -2
- package/dist/components/ProductCard.svelte +66 -44
- package/dist/components/ProductCard.svelte.d.ts +17 -4
- package/dist/components/Radio.svelte +12 -8
- package/dist/components/Radio.svelte.d.ts +1 -0
- package/dist/components/Select.svelte +70 -12
- package/dist/components/Select.svelte.d.ts +11 -0
- package/dist/components/SiteFooter.svelte +19 -0
- package/dist/components/SiteFooter.svelte.d.ts +8 -0
- package/dist/components/SiteHeader.svelte +70 -9
- package/dist/components/SiteHeader.svelte.d.ts +16 -0
- package/dist/components/Slider.svelte +6 -11
- package/dist/components/Switch.svelte +76 -0
- package/dist/components/Switch.svelte.d.ts +12 -0
- package/dist/components/Tab.svelte +29 -1
- package/dist/components/TextField.svelte +61 -10
- package/dist/components/TextField.svelte.d.ts +13 -0
- package/dist/components/Textarea.svelte +92 -0
- package/dist/components/Textarea.svelte.d.ts +23 -0
- package/dist/components/Tile.svelte +12 -11
- package/dist/components/Tile.svelte.d.ts +8 -2
- package/dist/components/blocks/CmsButton.svelte +36 -0
- package/dist/components/blocks/CmsButton.svelte.d.ts +12 -0
- package/dist/components/blocks/CmsColumns.svelte +20 -0
- package/dist/components/blocks/CmsColumns.svelte.d.ts +9 -0
- package/dist/components/blocks/CmsCover.svelte +71 -0
- package/dist/components/blocks/CmsCover.svelte.d.ts +17 -0
- package/dist/components/blocks/CmsHeading.svelte +50 -0
- package/dist/components/blocks/CmsHeading.svelte.d.ts +14 -0
- package/dist/components/blocks/CmsImage.svelte +43 -0
- package/dist/components/blocks/CmsImage.svelte.d.ts +13 -0
- package/dist/components/blocks/CmsMediaText.svelte +64 -0
- package/dist/components/blocks/CmsMediaText.svelte.d.ts +16 -0
- package/dist/components/blocks/CmsText.svelte +53 -0
- package/dist/components/blocks/CmsText.svelte.d.ts +10 -0
- package/dist/components/commerce/Breadcrumbs.svelte +22 -5
- package/dist/components/commerce/CartDrawer.svelte +7 -4
- package/dist/components/commerce/CartLineItem.svelte +1 -1
- package/dist/components/commerce/CheckoutProgress.svelte +1 -1
- package/dist/components/commerce/ConfigSection.svelte +2 -2
- package/dist/components/commerce/DimensionSlider.svelte +1 -1
- package/dist/components/commerce/Drawer.svelte +38 -24
- package/dist/components/commerce/Drawer.svelte.d.ts +0 -2
- package/dist/components/commerce/EmptyState.svelte +37 -0
- package/dist/components/commerce/EmptyState.svelte.d.ts +13 -0
- package/dist/components/commerce/FilterChip.svelte +35 -0
- package/dist/components/commerce/FilterChip.svelte.d.ts +10 -0
- package/dist/components/commerce/FreeShippingBar.svelte +1 -1
- package/dist/components/commerce/Gallery.svelte +4 -2
- package/dist/components/commerce/ListingToolbar.svelte +62 -0
- package/dist/components/commerce/ListingToolbar.svelte.d.ts +19 -0
- package/dist/components/commerce/Modal.svelte +31 -21
- package/dist/components/commerce/Modal.svelte.d.ts +0 -2
- package/dist/components/commerce/OrderSummary.svelte +1 -1
- package/dist/components/commerce/Pagination.svelte +115 -0
- package/dist/components/commerce/Pagination.svelte.d.ts +14 -0
- package/dist/components/commerce/PriceDisplay.svelte +3 -3
- package/dist/components/commerce/ProductHeader.svelte +2 -2
- package/dist/components/commerce/QuantityStepper.svelte +1 -1
- package/dist/components/commerce/RatingStars.svelte +1 -1
- package/dist/components/commerce/SearchField.svelte +92 -0
- package/dist/components/commerce/SearchField.svelte.d.ts +15 -0
- package/dist/components/commerce/SideToggle.svelte +1 -1
- package/dist/components/commerce/Skeleton.svelte +4 -1
- package/dist/components/commerce/StickyBar.svelte +4 -4
- package/dist/components/commerce/StockBadge.svelte +2 -2
- package/dist/components/commerce/Toaster.svelte +9 -5
- package/dist/components/commerce/TrustBadges.svelte +1 -1
- package/dist/components/commerce/overlay.d.ts +14 -0
- package/dist/components/commerce/overlay.js +49 -0
- package/dist/components/info-bar.d.ts +26 -0
- package/dist/components/info-bar.js +23 -0
- package/dist/icons/material-icons.generated.d.ts +4 -0
- package/dist/icons/material-icons.generated.js +118 -0
- package/dist/icons/material.d.ts +17 -0
- package/dist/icons/material.js +52 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/theme/base.css +22 -0
- package/dist/theme/extras.css +62 -0
- package/dist/theme/fonts.css +9 -42
- package/dist/theme/tokens.css +23 -9
- package/package.json +4 -2
|
@@ -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
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
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-
|
|
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}
|
|
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-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
|
6
|
-
import
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
/**
|
|
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
|
-
|
|
28
|
-
|
|
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<
|
|
49
|
+
}: Props & HTMLAttributes<HTMLElement> = $props();
|
|
33
50
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
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
|
-
<
|
|
72
|
+
<svelte:element this={href ? 'a' : 'div'} {...rest} {href} class={cn(CARD, href && LINKED, className)}>
|
|
44
73
|
{#if image}
|
|
45
|
-
<!--
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
<
|
|
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
|
|
78
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
/**
|
|
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<
|
|
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)
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
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-
|
|
55
|
+
class="accent-jelinek-black"
|
|
52
56
|
/>
|
|
53
57
|
{label}
|
|
54
58
|
</label>
|
|
@@ -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>;
|