@jelinek/ui 0.4.0 → 0.5.0
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/dist/components/OptionTile.svelte +201 -0
- package/dist/components/OptionTile.svelte.d.ts +57 -0
- package/dist/components/PhotoTile.svelte +109 -0
- package/dist/components/PhotoTile.svelte.d.ts +31 -0
- package/dist/components/SiteFooter.svelte +21 -0
- package/dist/components/SiteFooter.svelte.d.ts +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
4
|
+
import { cn } from '../utils/cn.js';
|
|
5
|
+
|
|
6
|
+
type Color = 'brown' | 'magenta';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
label: string;
|
|
10
|
+
selected?: boolean;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
/** Short text shown over the disabled tile explaining WHY it cannot be
|
|
13
|
+
* picked ("Nelze kombinovat s výškou Komfort"). A disabled option with
|
|
14
|
+
* no reason reads as broken, not constrained. */
|
|
15
|
+
disabledNote?: string;
|
|
16
|
+
/** Accent of the selected border and the meter fill — same division
|
|
17
|
+
* pair as Tile. The utilities resolve through --JELINEK-BROWN /
|
|
18
|
+
* --JELINEK-MAGENTA, so an app remapping those per division section
|
|
19
|
+
* re-colours selected tiles without touching this prop. */
|
|
20
|
+
color?: Color;
|
|
21
|
+
/** Photo header (a fabric, a wood finish). Aspect 4:3, top corners
|
|
22
|
+
* follow the tile radius. */
|
|
23
|
+
image?: string;
|
|
24
|
+
/** Empty by default — the label below already names the option. */
|
|
25
|
+
imageAlt?: string;
|
|
26
|
+
/** CSS colour for a swatch-strip header (a lacquer tone, a weight
|
|
27
|
+
* category band) — the no-photograph alternative to `image`. */
|
|
28
|
+
swatch?: string;
|
|
29
|
+
/** Text centred on the swatch strip (matrace-fe's weight label). */
|
|
30
|
+
swatchLabel?: string;
|
|
31
|
+
/** PRE-FORMATTED price delta ("+1 670 Kč"). A string on purpose: money
|
|
32
|
+
* formatting is the app's business (locale, currency, sign), not this
|
|
33
|
+
* kit's — passing a number here would force one formatter on every
|
|
34
|
+
* consumer. Rendered in the accent colour beside the label; wraps to
|
|
35
|
+
* its own right-aligned line on narrow tiles instead of overflowing. */
|
|
36
|
+
price?: string;
|
|
37
|
+
/** Pre-formatted crossed-out price above `price` (a running discount). */
|
|
38
|
+
oldPrice?: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
/** A small labelled level meter (matrace-fe's "Tuhost"): `value` of
|
|
41
|
+
* `max` bars filled with the accent colour. */
|
|
42
|
+
meter?: { label: string; value: number; max?: number };
|
|
43
|
+
/** A Tag ("Novinka", "Volitelné"). Over an image header it floats
|
|
44
|
+
* top-left on the photo; on a tile with no media it renders at the
|
|
45
|
+
* top of the body, above the label row. */
|
|
46
|
+
badge?: Snippet;
|
|
47
|
+
onselect?: () => void;
|
|
48
|
+
/** Extra content at the body's end. Anything interactive inside must
|
|
49
|
+
* carry `data-stop-card-select` so its clicks don't also select the
|
|
50
|
+
* tile. */
|
|
51
|
+
children?: Snippet;
|
|
52
|
+
class?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let {
|
|
56
|
+
label,
|
|
57
|
+
selected = false,
|
|
58
|
+
disabled = false,
|
|
59
|
+
disabledNote,
|
|
60
|
+
color = 'brown',
|
|
61
|
+
image,
|
|
62
|
+
imageAlt = '',
|
|
63
|
+
swatch,
|
|
64
|
+
swatchLabel,
|
|
65
|
+
price,
|
|
66
|
+
oldPrice,
|
|
67
|
+
description,
|
|
68
|
+
meter,
|
|
69
|
+
badge,
|
|
70
|
+
onselect,
|
|
71
|
+
children,
|
|
72
|
+
class: className,
|
|
73
|
+
...rest
|
|
74
|
+
}: Props & Omit<HTMLButtonAttributes, 'disabled'> = $props();
|
|
75
|
+
|
|
76
|
+
function handleClick(event: MouseEvent) {
|
|
77
|
+
if (disabled) return;
|
|
78
|
+
if ((event.target as HTMLElement).closest('[data-stop-card-select]')) return;
|
|
79
|
+
onselect?.();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// A selectable option in a configurator — Tile's form-control sibling,
|
|
83
|
+
// first built twice over (matrace-fe's OptionCard, then the e-shop's) and
|
|
84
|
+
// promoted here so the third app doesn't roll a fourth copy. The guide has
|
|
85
|
+
// no .option-tile rule; the geometry is Tile's (rounded-card, the
|
|
86
|
+
// border-subtle resting frame), the selected state is the division-accent
|
|
87
|
+
// border the e-shop shipped, and — unlike Tile — there is NO hover lift:
|
|
88
|
+
// this is a form control, and its feedback is the border, not motion.
|
|
89
|
+
//
|
|
90
|
+
// A real <button aria-pressed>, not a clickable <div>: keyboard and screen
|
|
91
|
+
// readers get the option for free (matrace-fe's div needed two a11y-lint
|
|
92
|
+
// waivers; this needs none).
|
|
93
|
+
const SELECTED: Record<Color, string> = {
|
|
94
|
+
brown: 'border-jelinek-brown shadow-resting',
|
|
95
|
+
magenta: 'border-jelinek-magenta shadow-resting'
|
|
96
|
+
};
|
|
97
|
+
const HOVER: Record<Color, string> = {
|
|
98
|
+
brown: 'hover:border-jelinek-brown-highlight',
|
|
99
|
+
magenta: 'hover:border-jelinek-magenta-highlight'
|
|
100
|
+
};
|
|
101
|
+
const METER_FILL: Record<Color, string> = {
|
|
102
|
+
brown: 'bg-jelinek-brown',
|
|
103
|
+
magenta: 'bg-jelinek-magenta'
|
|
104
|
+
};
|
|
105
|
+
const PRICE_TEXT: Record<Color, string> = {
|
|
106
|
+
brown: 'text-jelinek-brown',
|
|
107
|
+
magenta: 'text-jelinek-magenta'
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const BASE =
|
|
111
|
+
'w-full cursor-pointer rounded-card border-2 bg-surface text-left ' +
|
|
112
|
+
'font-secondary font-light text-fg ' +
|
|
113
|
+
'transition-[border-color,box-shadow] duration-[var(--dur-base)] ease-[var(--ease)]';
|
|
114
|
+
|
|
115
|
+
const classes = $derived(
|
|
116
|
+
cn(
|
|
117
|
+
BASE,
|
|
118
|
+
selected ? SELECTED[color] : cn('border-border-subtle', HOVER[color]),
|
|
119
|
+
disabled && 'pointer-events-none opacity-60',
|
|
120
|
+
className
|
|
121
|
+
)
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const meterMax = $derived(meter?.max ?? 5);
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<div class="relative">
|
|
128
|
+
<button type="button" onclick={handleClick} aria-pressed={selected} {disabled} {...rest} class={classes}>
|
|
129
|
+
{#if image}
|
|
130
|
+
<div class="relative">
|
|
131
|
+
<img
|
|
132
|
+
src={image}
|
|
133
|
+
alt={imageAlt}
|
|
134
|
+
loading="lazy"
|
|
135
|
+
class="aspect-[4/3] w-full rounded-t-[calc(var(--radius-card)-2px)] object-cover"
|
|
136
|
+
/>
|
|
137
|
+
{#if badge}<div class="absolute top-2 left-2">{@render badge()}</div>{/if}
|
|
138
|
+
</div>
|
|
139
|
+
{:else if swatch}
|
|
140
|
+
<div
|
|
141
|
+
class="flex aspect-[4/1] w-full items-center justify-center rounded-t-[calc(var(--radius-card)-2px)]"
|
|
142
|
+
style="background:{swatch}"
|
|
143
|
+
>
|
|
144
|
+
{#if swatchLabel}
|
|
145
|
+
<!-- Literal white + a soft shadow: the swatch is an arbitrary
|
|
146
|
+
consumer colour, so no theme token can promise contrast. -->
|
|
147
|
+
<span class="font-semibold text-white [text-shadow:0_1px_2px_rgba(0,0,0,0.35)]"
|
|
148
|
+
>{swatchLabel}</span
|
|
149
|
+
>
|
|
150
|
+
{/if}
|
|
151
|
+
</div>
|
|
152
|
+
{/if}
|
|
153
|
+
|
|
154
|
+
<div class="flex flex-col gap-1 p-4">
|
|
155
|
+
{#if badge && !image && !swatch}<div>{@render badge()}</div>{/if}
|
|
156
|
+
<!-- flex-wrap + ml-auto: the price is one unbreakable chunk (nbsp
|
|
157
|
+
grouping); on a narrow tile it drops to its own right-aligned
|
|
158
|
+
line instead of overflowing the border. -->
|
|
159
|
+
<div class="flex flex-wrap items-baseline justify-between gap-x-2">
|
|
160
|
+
<span class="font-bold">{label}</span>
|
|
161
|
+
{#if price || oldPrice}
|
|
162
|
+
<span class="ml-auto flex flex-col items-end">
|
|
163
|
+
{#if oldPrice}<span class="text-step-down2 text-fg-muted2 line-through"
|
|
164
|
+
>{oldPrice}</span
|
|
165
|
+
>{/if}
|
|
166
|
+
{#if price}<span
|
|
167
|
+
class={cn('text-step-down1 font-medium whitespace-nowrap', PRICE_TEXT[color])}
|
|
168
|
+
>{price}</span
|
|
169
|
+
>{/if}
|
|
170
|
+
</span>
|
|
171
|
+
{/if}
|
|
172
|
+
</div>
|
|
173
|
+
{#if description}<p class="text-step-down1 text-fg-muted1 m-0">{description}</p>{/if}
|
|
174
|
+
{#if meter}
|
|
175
|
+
<div class="mt-1 flex items-center gap-2">
|
|
176
|
+
<span class="text-step-down2 text-fg-muted2 font-medium tracking-wide uppercase"
|
|
177
|
+
>{meter.label}</span
|
|
178
|
+
>
|
|
179
|
+
<div class="flex gap-[3px]" role="img" aria-label={`${meter.label}: ${meter.value} z ${meterMax}`}>
|
|
180
|
+
{#each Array(meterMax) as _, i (i)}
|
|
181
|
+
<div
|
|
182
|
+
class={cn(
|
|
183
|
+
'h-1 w-3 rounded-[2px]',
|
|
184
|
+
i < meter.value ? METER_FILL[color] : 'bg-border-subtle'
|
|
185
|
+
)}
|
|
186
|
+
></div>
|
|
187
|
+
{/each}
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
{/if}
|
|
191
|
+
{#if children}<div class="mt-1">{@render children()}</div>{/if}
|
|
192
|
+
</div>
|
|
193
|
+
</button>
|
|
194
|
+
{#if disabled && disabledNote}
|
|
195
|
+
<div
|
|
196
|
+
class="rounded-card text-step-down1 absolute inset-0 grid place-items-center bg-[var(--glass-fill)] p-4 text-center font-medium backdrop-blur-sm"
|
|
197
|
+
>
|
|
198
|
+
{disabledNote}
|
|
199
|
+
</div>
|
|
200
|
+
{/if}
|
|
201
|
+
</div>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
3
|
+
type Color = 'brown' | 'magenta';
|
|
4
|
+
interface Props {
|
|
5
|
+
label: string;
|
|
6
|
+
selected?: boolean;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
/** Short text shown over the disabled tile explaining WHY it cannot be
|
|
9
|
+
* picked ("Nelze kombinovat s výškou Komfort"). A disabled option with
|
|
10
|
+
* no reason reads as broken, not constrained. */
|
|
11
|
+
disabledNote?: string;
|
|
12
|
+
/** Accent of the selected border and the meter fill — same division
|
|
13
|
+
* pair as Tile. The utilities resolve through --JELINEK-BROWN /
|
|
14
|
+
* --JELINEK-MAGENTA, so an app remapping those per division section
|
|
15
|
+
* re-colours selected tiles without touching this prop. */
|
|
16
|
+
color?: Color;
|
|
17
|
+
/** Photo header (a fabric, a wood finish). Aspect 4:3, top corners
|
|
18
|
+
* follow the tile radius. */
|
|
19
|
+
image?: string;
|
|
20
|
+
/** Empty by default — the label below already names the option. */
|
|
21
|
+
imageAlt?: string;
|
|
22
|
+
/** CSS colour for a swatch-strip header (a lacquer tone, a weight
|
|
23
|
+
* category band) — the no-photograph alternative to `image`. */
|
|
24
|
+
swatch?: string;
|
|
25
|
+
/** Text centred on the swatch strip (matrace-fe's weight label). */
|
|
26
|
+
swatchLabel?: string;
|
|
27
|
+
/** PRE-FORMATTED price delta ("+1 670 Kč"). A string on purpose: money
|
|
28
|
+
* formatting is the app's business (locale, currency, sign), not this
|
|
29
|
+
* kit's — passing a number here would force one formatter on every
|
|
30
|
+
* consumer. Rendered in the accent colour beside the label; wraps to
|
|
31
|
+
* its own right-aligned line on narrow tiles instead of overflowing. */
|
|
32
|
+
price?: string;
|
|
33
|
+
/** Pre-formatted crossed-out price above `price` (a running discount). */
|
|
34
|
+
oldPrice?: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
/** A small labelled level meter (matrace-fe's "Tuhost"): `value` of
|
|
37
|
+
* `max` bars filled with the accent colour. */
|
|
38
|
+
meter?: {
|
|
39
|
+
label: string;
|
|
40
|
+
value: number;
|
|
41
|
+
max?: number;
|
|
42
|
+
};
|
|
43
|
+
/** A Tag ("Novinka", "Volitelné"). Over an image header it floats
|
|
44
|
+
* top-left on the photo; on a tile with no media it renders at the
|
|
45
|
+
* top of the body, above the label row. */
|
|
46
|
+
badge?: Snippet;
|
|
47
|
+
onselect?: () => void;
|
|
48
|
+
/** Extra content at the body's end. Anything interactive inside must
|
|
49
|
+
* carry `data-stop-card-select` so its clicks don't also select the
|
|
50
|
+
* tile. */
|
|
51
|
+
children?: Snippet;
|
|
52
|
+
class?: string;
|
|
53
|
+
}
|
|
54
|
+
type $$ComponentProps = Props & Omit<HTMLButtonAttributes, 'disabled'>;
|
|
55
|
+
declare const OptionTile: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
56
|
+
type OptionTile = ReturnType<typeof OptionTile>;
|
|
57
|
+
export default OptionTile;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAnchorAttributes, HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import { cn } from '../utils/cn.js';
|
|
5
|
+
|
|
6
|
+
type Level = 'h2' | 'h3';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
/** With `href` the tile is a link; without it an inert block — same
|
|
10
|
+
* contract as Tile, and the same reasoning: an unlinked tile has
|
|
11
|
+
* nowhere to go, so it should not be keyboard-reachable either. */
|
|
12
|
+
href?: string;
|
|
13
|
+
/** The photograph filling the tile. */
|
|
14
|
+
image: string;
|
|
15
|
+
/** Empty by default: the visible title already names the destination,
|
|
16
|
+
* so the image is decorative unless a consumer says otherwise. */
|
|
17
|
+
imageAlt?: string;
|
|
18
|
+
title: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
/** Heading tag for the title. `h3` matches Tile's own convention; an
|
|
21
|
+
* app using photo tiles as top-level section links (the e-shop's two
|
|
22
|
+
* division tiles) passes `h2`. */
|
|
23
|
+
level?: Level;
|
|
24
|
+
/** Passed straight to the <img>. Photo tiles usually sit below the
|
|
25
|
+
* fold, so lazy is the right default; a consumer rendering one above
|
|
26
|
+
* the fold passes `eager`. */
|
|
27
|
+
loading?: 'lazy' | 'eager';
|
|
28
|
+
class?: string;
|
|
29
|
+
/** Extra content under the description — a Tag, a small CTA line. */
|
|
30
|
+
children?: Snippet;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let {
|
|
34
|
+
href,
|
|
35
|
+
image,
|
|
36
|
+
imageAlt = '',
|
|
37
|
+
title,
|
|
38
|
+
description,
|
|
39
|
+
level = 'h3',
|
|
40
|
+
loading = 'lazy',
|
|
41
|
+
class: className,
|
|
42
|
+
children,
|
|
43
|
+
...rest
|
|
44
|
+
}: Props & Omit<HTMLAnchorAttributes & HTMLAttributes<HTMLDivElement>, 'href' | 'title'> =
|
|
45
|
+
$props();
|
|
46
|
+
|
|
47
|
+
// A photo tile is Tile's full-bleed photographic sibling — first built in
|
|
48
|
+
// the e-shop storefront (its home-page division tiles), promoted here so
|
|
49
|
+
// consumers stop hand-rolling it. The guide itself has no .photo-tile
|
|
50
|
+
// rule; every value below is either a shared token the guide's own
|
|
51
|
+
// surfaces use, or is forced by the photograph:
|
|
52
|
+
//
|
|
53
|
+
// - Geometry/motion are Tile's: rounded-section (a full-bleed photo card
|
|
54
|
+
// is section-scale, not card-scale), shadow-resting → shadow-interactive
|
|
55
|
+
// with the 2px hover lift, both on the guide's own
|
|
56
|
+
// box-shadow/transform transition pair (jelinek.css:924 — see Tile for
|
|
57
|
+
// why transition-all is wrong here).
|
|
58
|
+
// - `no-underline`: base.css underlines every anchor on hover; on a tile
|
|
59
|
+
// that underline bleeds onto the caption text (the exact defect the
|
|
60
|
+
// e-shop shipped with).
|
|
61
|
+
// - The scrim and the text are LITERAL black/white, not theme tokens, on
|
|
62
|
+
// purpose: they sit on a photograph, which does not re-theme.
|
|
63
|
+
// --JELINEK-WHITE inverts to hsl(0,0%,0%) in dark mode (tokens.css:96)
|
|
64
|
+
// — white-on-scrim text routed through it would go invisible on its own
|
|
65
|
+
// photo, the same trap SiteFooter documents for its logo.
|
|
66
|
+
const BASE =
|
|
67
|
+
'group relative flex flex-col justify-end overflow-hidden aspect-[4/3] ' +
|
|
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 ' +
|
|
71
|
+
'dark:shadow-[var(--shadow-resting),inset_0_2px_1px_-1px_var(--box-primary)] ' +
|
|
72
|
+
'dark:hover:shadow-[var(--shadow-interactive),inset_0_2px_1px_-1px_var(--box-primary)]';
|
|
73
|
+
|
|
74
|
+
// The photo zooms slowly under the pointer while the tile itself lifts on
|
|
75
|
+
// --dur-base — two speeds on purpose: the lift is feedback ("this is a
|
|
76
|
+
// control"), the zoom is atmosphere. 700ms is deliberate and NOT a --dur
|
|
77
|
+
// token: the scale is 8× the size of the 2px lift, and at --dur-slow
|
|
78
|
+
// (0.2s) it reads as a twitch, not a drift.
|
|
79
|
+
const IMG =
|
|
80
|
+
'absolute inset-0 h-full w-full object-cover ' +
|
|
81
|
+
'transition-transform duration-700 ease-[var(--ease)] group-hover:scale-105';
|
|
82
|
+
|
|
83
|
+
// Bottom-weighted scrim so white text passes contrast over any photo —
|
|
84
|
+
// strongest where the text sits, gone by mid-frame so the photo stays a
|
|
85
|
+
// photo.
|
|
86
|
+
const SCRIM = 'absolute inset-0 bg-gradient-to-t from-black/70 via-black/10 to-transparent';
|
|
87
|
+
|
|
88
|
+
const TEXT = 'relative p-6 sm:p-8';
|
|
89
|
+
const TITLE = 'font-primary text-step3 mb-1 font-bold text-white';
|
|
90
|
+
const DESC = 'text-step0 m-0 max-w-xs text-white/90';
|
|
91
|
+
|
|
92
|
+
const classes = $derived(cn(BASE, href && 'cursor-pointer', className));
|
|
93
|
+
</script>
|
|
94
|
+
|
|
95
|
+
{#snippet content()}
|
|
96
|
+
<img src={image} alt={imageAlt} {loading} class={IMG} />
|
|
97
|
+
<div class={SCRIM}></div>
|
|
98
|
+
<div class={TEXT}>
|
|
99
|
+
<svelte:element this={level} class={TITLE}>{title}</svelte:element>
|
|
100
|
+
{#if description}<p class={DESC}>{description}</p>{/if}
|
|
101
|
+
{#if children}{@render children()}{/if}
|
|
102
|
+
</div>
|
|
103
|
+
{/snippet}
|
|
104
|
+
|
|
105
|
+
{#if href}
|
|
106
|
+
<a {...rest} {href} class={classes}>{@render content()}</a>
|
|
107
|
+
{:else}
|
|
108
|
+
<div {...rest} class={classes}>{@render content()}</div>
|
|
109
|
+
{/if}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAnchorAttributes, HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
type Level = 'h2' | 'h3';
|
|
4
|
+
interface Props {
|
|
5
|
+
/** With `href` the tile is a link; without it an inert block — same
|
|
6
|
+
* contract as Tile, and the same reasoning: an unlinked tile has
|
|
7
|
+
* nowhere to go, so it should not be keyboard-reachable either. */
|
|
8
|
+
href?: string;
|
|
9
|
+
/** The photograph filling the tile. */
|
|
10
|
+
image: string;
|
|
11
|
+
/** Empty by default: the visible title already names the destination,
|
|
12
|
+
* so the image is decorative unless a consumer says otherwise. */
|
|
13
|
+
imageAlt?: string;
|
|
14
|
+
title: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
/** Heading tag for the title. `h3` matches Tile's own convention; an
|
|
17
|
+
* app using photo tiles as top-level section links (the e-shop's two
|
|
18
|
+
* division tiles) passes `h2`. */
|
|
19
|
+
level?: Level;
|
|
20
|
+
/** Passed straight to the <img>. Photo tiles usually sit below the
|
|
21
|
+
* fold, so lazy is the right default; a consumer rendering one above
|
|
22
|
+
* the fold passes `eager`. */
|
|
23
|
+
loading?: 'lazy' | 'eager';
|
|
24
|
+
class?: string;
|
|
25
|
+
/** Extra content under the description — a Tag, a small CTA line. */
|
|
26
|
+
children?: Snippet;
|
|
27
|
+
}
|
|
28
|
+
type $$ComponentProps = Props & Omit<HTMLAnchorAttributes & HTMLAttributes<HTMLDivElement>, 'href' | 'title'>;
|
|
29
|
+
declare const PhotoTile: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
30
|
+
type PhotoTile = ReturnType<typeof PhotoTile>;
|
|
31
|
+
export default PhotoTile;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
2
3
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
4
|
import { cn } from '../utils/cn.js';
|
|
4
5
|
|
|
@@ -8,6 +9,16 @@
|
|
|
8
9
|
slogan?: string;
|
|
9
10
|
meta?: string;
|
|
10
11
|
stamp?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Consumer content rendered inside the footer's black chrome, ABOVE the
|
|
14
|
+
* brand row (an e-shop's link columns, a newsletter box). The guide's
|
|
15
|
+
* own footer is the brand row alone, so this stays unset there and the
|
|
16
|
+
* markup is unchanged; an app footer is usually more than one strip,
|
|
17
|
+
* and without this slot it would have to stack its own black block on
|
|
18
|
+
* top of this one — two rounded-top black boxes with a seam between
|
|
19
|
+
* them. Same consumer-seam reasoning as SiteHeader's `barActions`.
|
|
20
|
+
*/
|
|
21
|
+
children?: Snippet;
|
|
11
22
|
/**
|
|
12
23
|
* Task 45 — HTML of the changelog shown in a floating <dialog> when the
|
|
13
24
|
* reader clicks the deploy stamp. The showcase passes the build-time
|
|
@@ -26,6 +37,7 @@
|
|
|
26
37
|
meta,
|
|
27
38
|
stamp,
|
|
28
39
|
changelog,
|
|
40
|
+
children,
|
|
29
41
|
class: className,
|
|
30
42
|
...rest
|
|
31
43
|
}: Props & HTMLAttributes<HTMLElement> = $props();
|
|
@@ -84,6 +96,15 @@
|
|
|
84
96
|
</script>
|
|
85
97
|
|
|
86
98
|
<footer {...rest} class={cn(BASE, className)}>
|
|
99
|
+
{#if children}
|
|
100
|
+
<!-- Consumer strip(s) above the brand row — see the `children` prop doc.
|
|
101
|
+
The divider separates it from the brand row the same way an app
|
|
102
|
+
would otherwise draw its own seam; the brand row below keeps its
|
|
103
|
+
guide geometry untouched. -->
|
|
104
|
+
<div class="border-b border-white/15">
|
|
105
|
+
{@render children()}
|
|
106
|
+
</div>
|
|
107
|
+
{/if}
|
|
87
108
|
<div class="mx-auto flex max-w-page flex-wrap items-center gap-6 px-6 py-10">
|
|
88
109
|
{#if logo}
|
|
89
110
|
<!-- jelinek.css:767 — 52px square, 12px radius. Neither value is on
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
1
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
3
|
interface Props {
|
|
3
4
|
logo?: string;
|
|
@@ -5,6 +6,16 @@ interface Props {
|
|
|
5
6
|
slogan?: string;
|
|
6
7
|
meta?: string;
|
|
7
8
|
stamp?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Consumer content rendered inside the footer's black chrome, ABOVE the
|
|
11
|
+
* brand row (an e-shop's link columns, a newsletter box). The guide's
|
|
12
|
+
* own footer is the brand row alone, so this stays unset there and the
|
|
13
|
+
* markup is unchanged; an app footer is usually more than one strip,
|
|
14
|
+
* and without this slot it would have to stack its own black block on
|
|
15
|
+
* top of this one — two rounded-top black boxes with a seam between
|
|
16
|
+
* them. Same consumer-seam reasoning as SiteHeader's `barActions`.
|
|
17
|
+
*/
|
|
18
|
+
children?: Snippet;
|
|
8
19
|
/**
|
|
9
20
|
* Task 45 — HTML of the changelog shown in a floating <dialog> when the
|
|
10
21
|
* reader clicks the deploy stamp. The showcase passes the build-time
|
package/dist/index.d.ts
CHANGED
|
@@ -15,9 +15,11 @@ export { default as Glass } from './components/Glass.svelte';
|
|
|
15
15
|
export { default as Grid } from './components/Grid.svelte';
|
|
16
16
|
export { default as Label } from './components/Label.svelte';
|
|
17
17
|
export { isActive, type NavItem, type NavGroup } from './components/nav.js';
|
|
18
|
+
export { default as OptionTile } from './components/OptionTile.svelte';
|
|
18
19
|
export { default as PageHero } from './components/PageHero.svelte';
|
|
19
20
|
export { default as Pager } from './components/Pager.svelte';
|
|
20
21
|
export { default as Panel } from './components/Panel.svelte';
|
|
22
|
+
export { default as PhotoTile } from './components/PhotoTile.svelte';
|
|
21
23
|
export { default as ProductCard } from './components/ProductCard.svelte';
|
|
22
24
|
export { default as Radio } from './components/Radio.svelte';
|
|
23
25
|
export { RADIO_CONTEXT, type RadioContext } from './components/radio-context.js';
|
package/dist/index.js
CHANGED
|
@@ -15,9 +15,11 @@ export { default as Glass } from './components/Glass.svelte';
|
|
|
15
15
|
export { default as Grid } from './components/Grid.svelte';
|
|
16
16
|
export { default as Label } from './components/Label.svelte';
|
|
17
17
|
export { isActive } from './components/nav.js';
|
|
18
|
+
export { default as OptionTile } from './components/OptionTile.svelte';
|
|
18
19
|
export { default as PageHero } from './components/PageHero.svelte';
|
|
19
20
|
export { default as Pager } from './components/Pager.svelte';
|
|
20
21
|
export { default as Panel } from './components/Panel.svelte';
|
|
22
|
+
export { default as PhotoTile } from './components/PhotoTile.svelte';
|
|
21
23
|
export { default as ProductCard } from './components/ProductCard.svelte';
|
|
22
24
|
export { default as Radio } from './components/Radio.svelte';
|
|
23
25
|
export { RADIO_CONTEXT } from './components/radio-context.js';
|