@sarimarcus/content-sites-ui 0.31.0 → 0.31.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/CHANGELOG.md +9 -0
- package/package.json +2 -2
- package/src/composed/EntityCard.astro +67 -27
- package/src/primitives/ArrowGlyph.astro +20 -0
- package/src/primitives/ArrowLabel.astro +3 -13
- package/src/primitives/Icon.astro +6 -2
- package/src/primitives/LinkArrow.astro +2 -12
- package/src/primitives/RatingStars.astro +30 -0
- package/src/primitives/index.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All packages in the platform release in lockstep; entries are per release version.
|
|
4
4
|
|
|
5
|
+
## 0.31.2 (2026-10-08)
|
|
6
|
+
|
|
7
|
+
- `./composed`: `EntityCard` takes `eager` (default 3), `priority`, `mediaClass`, `sizes`, `fallbackWidth`/`fallbackHeight`, `tone` (`primary` | `accent`), `footer` (default `true`) and `footerJustify` (`between` | `end`); `fallbackSrc` is optional, and without `image` or `fallbackSrc` the new `fallback` slot renders (never an `<img>` without `src`). Default output is unchanged apart from the image's new `motion-reduce:group-hover:scale-100`, which cancels the hover zoom under `prefers-reduced-motion` on every EntityCard (Hotel, Activity, Lodge, Tour and the tourism cards moved onto it) (SLA-3019).
|
|
8
|
+
- `./primitives`: `Icon` takes `strokeWidth` (default 2; a non-default value reaches Lucide's `stroke-width`, default output unchanged). New `RatingStars` (`value` fills `Math.round(value)` of 5, or `count` filled stars; `ariaLabel`, `sizeClass` default `w-3.5 h-3.5`, `class`). `ArrowLabel` and `LinkArrow` share one arrow svg: LinkArrow's output is byte-identical, ArrowLabel's loses the space between its two `<path>`s (whitespace only, every ArrowLabel page on all three sites) (SLA-3019).
|
|
9
|
+
|
|
10
|
+
## 0.31.1 (2026-10-08)
|
|
11
|
+
|
|
12
|
+
- No changes; lockstep release with tourism (SLA-3018).
|
|
13
|
+
|
|
5
14
|
## 0.31.0 (2026-10-08)
|
|
6
15
|
|
|
7
16
|
- `./editorial` adds `AlsoExplore` (`AlsoExploreItem`) and `VisitTiming` (`VisitTimingData`; props `title`, `id`, `class` default `mt-12`, `labelClass` default `text-[0.8125rem]`), moved from the sites (SLA-3017).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sarimarcus/content-sites-ui",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.2",
|
|
4
4
|
"description": "Universal Astro UI components, layout shell and design tokens shared by the content sites.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,6 +49,6 @@
|
|
|
49
49
|
"@lucide/astro": "^1.23.0",
|
|
50
50
|
"astro": "^7.0.6",
|
|
51
51
|
"tailwindcss": "^4.1.18",
|
|
52
|
-
"@sarimarcus/content-sites-core": "0.31.
|
|
52
|
+
"@sarimarcus/content-sites-core": "0.31.2"
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
3
|
* EntityCard — the shared frame of an entity teaser card: a whole-card link with a focus ring, an image block
|
|
4
|
-
* (optimized Picture, or a fallback
|
|
4
|
+
* (optimized Picture, or a fallback), a content area and a footer row. Everything that varies between card types
|
|
5
5
|
* comes in through slots, so the frame owns no copy and no data shape.
|
|
6
6
|
*
|
|
7
7
|
* Variants:
|
|
8
|
-
* media — fixed-height image, the name heading sits in the body (hotel, lodge).
|
|
9
|
-
* poster — 3:2 image, the
|
|
8
|
+
* media — fixed-height image, the name heading sits in the body (hotel, lodge, restaurant, bar).
|
|
9
|
+
* poster — 3:2 image, the content area grows to the card's height (activity, tour, monument, dish, walk).
|
|
10
|
+
*
|
|
11
|
+
* Options (each default reproduces the original frame):
|
|
12
|
+
* eager / priority — the first `eager` cards (default 3) load eagerly; fetchpriority=high on index 0 or with
|
|
13
|
+
* `priority`, otherwise omitted.
|
|
14
|
+
* mediaClass — the image block's size utilities (media 'h-44 sm:h-52', poster 'aspect-[3/2]').
|
|
15
|
+
* sizes — the picture's sizes attribute.
|
|
16
|
+
* fallbackWidth/Height — the fallback img's intrinsic size (media 320×192, poster 400×224).
|
|
17
|
+
* tone — 'primary' or 'accent': the article border and the focus ring.
|
|
18
|
+
* footer — false drops the footer row (a card whose CTA ends its body).
|
|
19
|
+
* footerJustify — 'between' (meta start, CTA end) or 'end' (CTA only).
|
|
20
|
+
* Without `image`, `fallbackSrc` renders an <img>; without that too, the `fallback` slot renders (nothing if absent).
|
|
21
|
+
* Under prefers-reduced-motion the hover zoom is cancelled.
|
|
10
22
|
*
|
|
11
23
|
* Slots:
|
|
12
24
|
* overlay — inside the image block, after the image: corner badges, gradient scrims, an overlaid heading.
|
|
25
|
+
* fallback — the image block's content when there is neither `image` nor `fallbackSrc` (a placeholder icon).
|
|
13
26
|
* (default) — the body above the footer.
|
|
14
27
|
* footer-meta — start of the footer row (location, closing time…).
|
|
15
28
|
* footer-cta — end of the footer row.
|
|
@@ -22,12 +35,21 @@ interface Props {
|
|
|
22
35
|
variant?: 'media' | 'poster';
|
|
23
36
|
image?: ImageMetadata;
|
|
24
37
|
/** Shown when `image` is absent. */
|
|
25
|
-
fallbackSrc
|
|
38
|
+
fallbackSrc?: string;
|
|
26
39
|
imageAlt: string;
|
|
27
40
|
/** @deprecated Ignored: images are WebP only (SLA-2960). Removed in the next minor. */
|
|
28
41
|
formats?: Array<'avif' | 'webp' | 'png' | 'jpeg' | 'jpg' | 'svg'>;
|
|
29
|
-
/** Position in the list: the first
|
|
42
|
+
/** Position in the list: the first `eager` load eagerly, the first one at high priority. */
|
|
30
43
|
index?: number;
|
|
44
|
+
eager?: number;
|
|
45
|
+
priority?: boolean;
|
|
46
|
+
mediaClass?: string;
|
|
47
|
+
sizes?: string;
|
|
48
|
+
fallbackWidth?: string | number;
|
|
49
|
+
fallbackHeight?: string | number;
|
|
50
|
+
tone?: 'primary' | 'accent';
|
|
51
|
+
footer?: boolean;
|
|
52
|
+
footerJustify?: 'between' | 'end';
|
|
31
53
|
class?: string;
|
|
32
54
|
}
|
|
33
55
|
|
|
@@ -38,49 +60,65 @@ const {
|
|
|
38
60
|
fallbackSrc,
|
|
39
61
|
imageAlt,
|
|
40
62
|
index = 999,
|
|
63
|
+
eager = 3,
|
|
64
|
+
priority = false,
|
|
65
|
+
sizes = '(max-width: 639px) 100vw, (max-width: 1023px) 50vw, 33vw',
|
|
66
|
+
tone = 'primary',
|
|
67
|
+
footer = true,
|
|
68
|
+
footerJustify = 'between',
|
|
41
69
|
class: className,
|
|
42
70
|
} = Astro.props;
|
|
43
71
|
|
|
44
72
|
const poster = variant === 'poster';
|
|
73
|
+
const {
|
|
74
|
+
mediaClass: media = poster ? 'aspect-[3/2]' : 'h-44 sm:h-52',
|
|
75
|
+
fallbackWidth = poster ? '400' : '320',
|
|
76
|
+
fallbackHeight = poster ? '224' : '192',
|
|
77
|
+
} = Astro.props;
|
|
78
|
+
const ring = tone === 'accent' ? 'focus-visible:ring-accent' : 'focus-visible:ring-primary';
|
|
79
|
+
const border = tone === 'accent' ? 'border-accent/20' : 'border-border';
|
|
45
80
|
const linkClass = poster
|
|
46
|
-
?
|
|
47
|
-
:
|
|
48
|
-
const mediaClass =
|
|
49
|
-
? 'relative aspect-[3/2] overflow-hidden img-placeholder shrink-0'
|
|
50
|
-
: 'relative h-44 sm:h-52 overflow-hidden img-placeholder shrink-0';
|
|
51
|
-
const fallbackWidth = poster ? '400' : '320';
|
|
52
|
-
const fallbackHeight = poster ? '224' : '192';
|
|
81
|
+
? `flex flex-col h-full focus-visible:ring-2 ${ring} focus-visible:ring-offset-2 rounded-xl`
|
|
82
|
+
: `flex flex-col flex-1 focus-visible:ring-2 ${ring} focus-visible:ring-offset-2 rounded-xl`;
|
|
83
|
+
const mediaClass = `relative ${media} overflow-hidden img-placeholder shrink-0`;
|
|
53
84
|
const contentClass = poster ? 'p-4 sm:p-5 flex flex-col flex-1' : 'flex-1 p-4 sm:p-5 flex flex-col';
|
|
85
|
+
const justify = footerJustify === 'end' ? 'justify-end' : 'justify-between';
|
|
54
86
|
const footerClass = poster
|
|
55
|
-
?
|
|
56
|
-
:
|
|
87
|
+
? `flex items-center ${justify} pt-3 border-t border-border mt-auto`
|
|
88
|
+
: `flex items-center ${justify} pt-3 border-t border-border`;
|
|
89
|
+
// motion-reduce:group-hover:scale-100 has group-hover:scale-105's specificity and compiles after it, so it wins.
|
|
90
|
+
const imgClass = 'w-full h-full object-cover transition-transform duration-500 group-hover:scale-105 motion-reduce:group-hover:scale-100';
|
|
91
|
+
const loading = index < eager ? 'eager' : 'lazy';
|
|
92
|
+
const fetchpriority = priority || index === 0 ? 'high' : undefined;
|
|
57
93
|
---
|
|
58
94
|
|
|
59
|
-
<article class:list={[
|
|
95
|
+
<article class:list={[`group relative bg-card rounded-xl overflow-hidden card-lift border ${border} flex flex-col h-full`, className]}>
|
|
60
96
|
<a href={href} class={linkClass}>
|
|
61
97
|
<div class={mediaClass}>
|
|
62
98
|
{image ? (
|
|
63
99
|
<ResponsivePicture
|
|
64
100
|
src={image}
|
|
65
101
|
alt={imageAlt}
|
|
66
|
-
sizes=
|
|
102
|
+
sizes={sizes}
|
|
67
103
|
pictureAttributes={{ class: 'block w-full h-full' }}
|
|
68
|
-
class=
|
|
69
|
-
loading={
|
|
70
|
-
fetchpriority={
|
|
104
|
+
class={imgClass}
|
|
105
|
+
loading={loading}
|
|
106
|
+
fetchpriority={fetchpriority}
|
|
71
107
|
decoding="async"
|
|
72
108
|
/>
|
|
73
|
-
) : (
|
|
109
|
+
) : fallbackSrc ? (
|
|
74
110
|
<img
|
|
75
111
|
src={fallbackSrc}
|
|
76
112
|
alt={imageAlt}
|
|
77
113
|
width={fallbackWidth}
|
|
78
114
|
height={fallbackHeight}
|
|
79
|
-
class=
|
|
80
|
-
loading={
|
|
81
|
-
fetchpriority={
|
|
115
|
+
class={imgClass}
|
|
116
|
+
loading={loading}
|
|
117
|
+
fetchpriority={fetchpriority}
|
|
82
118
|
decoding="async"
|
|
83
119
|
/>
|
|
120
|
+
) : (
|
|
121
|
+
<slot name="fallback" />
|
|
84
122
|
)}
|
|
85
123
|
|
|
86
124
|
<slot name="overlay" />
|
|
@@ -95,11 +133,13 @@ const footerClass = poster
|
|
|
95
133
|
</div>
|
|
96
134
|
)}
|
|
97
135
|
|
|
98
|
-
|
|
99
|
-
<
|
|
136
|
+
{footer && (
|
|
137
|
+
<div class={footerClass}>
|
|
138
|
+
<slot name="footer-meta" />
|
|
100
139
|
|
|
101
|
-
|
|
102
|
-
|
|
140
|
+
<slot name="footer-cta" />
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
103
143
|
</div>
|
|
104
144
|
</a>
|
|
105
145
|
</article>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** The right arrow of ArrowLabel and LinkArrow. `class` is emitted verbatim. */
|
|
3
|
+
interface Props {
|
|
4
|
+
class: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const { class: className } = Astro.props;
|
|
8
|
+
---
|
|
9
|
+
<svg
|
|
10
|
+
class={className}
|
|
11
|
+
viewBox="0 0 24 24"
|
|
12
|
+
fill="none"
|
|
13
|
+
stroke="currentColor"
|
|
14
|
+
stroke-width="2"
|
|
15
|
+
stroke-linecap="round"
|
|
16
|
+
stroke-linejoin="round"
|
|
17
|
+
aria-hidden="true"
|
|
18
|
+
>
|
|
19
|
+
<path d="M5 12h14" /><path d="m12 5 7 7-7 7" />
|
|
20
|
+
</svg>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
+
import ArrowGlyph from './ArrowGlyph.astro';
|
|
3
|
+
|
|
2
4
|
interface Props {
|
|
3
5
|
class?: string;
|
|
4
6
|
arrowClass?: string;
|
|
@@ -8,17 +10,5 @@ const { class: className = '', arrowClass = 'h-4 w-4' } = Astro.props;
|
|
|
8
10
|
---
|
|
9
11
|
<span class:list={['inline-flex items-center gap-2', className]}>
|
|
10
12
|
<slot />
|
|
11
|
-
<
|
|
12
|
-
class:list={[arrowClass, 'shrink-0 transition-transform group-hover:translate-x-1']}
|
|
13
|
-
viewBox="0 0 24 24"
|
|
14
|
-
fill="none"
|
|
15
|
-
stroke="currentColor"
|
|
16
|
-
stroke-width="2"
|
|
17
|
-
stroke-linecap="round"
|
|
18
|
-
stroke-linejoin="round"
|
|
19
|
-
aria-hidden="true"
|
|
20
|
-
>
|
|
21
|
-
<path d="M5 12h14" />
|
|
22
|
-
<path d="m12 5 7 7-7 7" />
|
|
23
|
-
</svg>
|
|
13
|
+
<ArrowGlyph class={`${arrowClass} shrink-0 transition-transform group-hover:translate-x-1`} />
|
|
24
14
|
</span>
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* class — Tailwind classes for sizing/color (default: "w-4 h-4")
|
|
9
9
|
* size — explicit px size (optional — prefer Tailwind sizing classes)
|
|
10
10
|
* filled — render as filled shape instead of outline (e.g. solid stars)
|
|
11
|
+
* strokeWidth — outline weight (default 2). @lucide/astro reads `stroke-width` and emits `strokeWidth` as a literal
|
|
12
|
+
* attribute, which stays for byte-identical output.
|
|
11
13
|
* style — inline style passthrough (e.g. a breadcrumb fading its separators by index)
|
|
12
14
|
*/
|
|
13
15
|
|
|
@@ -19,9 +21,10 @@ interface Props {
|
|
|
19
21
|
size?: number;
|
|
20
22
|
filled?: boolean;
|
|
21
23
|
style?: string;
|
|
24
|
+
strokeWidth?: number;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
const { name, class: className = 'w-4 h-4', size, filled = false, style } = Astro.props;
|
|
27
|
+
const { name, class: className = 'w-4 h-4', size, filled = false, style, strokeWidth = 2 } = Astro.props;
|
|
25
28
|
|
|
26
29
|
// 'map-pin' → 'MapPin', 'arrow-right' → 'ArrowRight'
|
|
27
30
|
const pascalName = name
|
|
@@ -41,7 +44,8 @@ const resolvedClass = filled ? `${className} fill-current [stroke:none]` : class
|
|
|
41
44
|
class={resolvedClass}
|
|
42
45
|
{...(size !== undefined ? { size } : {})}
|
|
43
46
|
{...(style ? { style } : {})}
|
|
44
|
-
strokeWidth={
|
|
47
|
+
strokeWidth={strokeWidth}
|
|
48
|
+
{...(strokeWidth !== 2 ? { 'stroke-width': strokeWidth } : {})}
|
|
45
49
|
aria-hidden="true"
|
|
46
50
|
/>
|
|
47
51
|
)}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { newTab } from './newTab';
|
|
3
|
+
import ArrowGlyph from './ArrowGlyph.astro';
|
|
3
4
|
interface Props {
|
|
4
5
|
href: string;
|
|
5
6
|
size?: 'sm' | 'base';
|
|
@@ -34,16 +35,5 @@ const colorClass = {
|
|
|
34
35
|
]}
|
|
35
36
|
>
|
|
36
37
|
<slot />
|
|
37
|
-
<
|
|
38
|
-
class="w-4 h-4 transition-transform group-hover:translate-x-1"
|
|
39
|
-
viewBox="0 0 24 24"
|
|
40
|
-
fill="none"
|
|
41
|
-
stroke="currentColor"
|
|
42
|
-
stroke-width="2"
|
|
43
|
-
stroke-linecap="round"
|
|
44
|
-
stroke-linejoin="round"
|
|
45
|
-
aria-hidden="true"
|
|
46
|
-
>
|
|
47
|
-
<path d="M5 12h14" /><path d="m12 5 7 7-7 7" />
|
|
48
|
-
</svg>
|
|
38
|
+
<ArrowGlyph class="w-4 h-4 transition-transform group-hover:translate-x-1" />
|
|
49
39
|
</a>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* RatingStars — a row of filled stars with one accessible label. `value` fills Math.round(value) of 5 stars and mutes
|
|
4
|
+
* the rest; `count` draws that many filled stars (a hotel's star class).
|
|
5
|
+
*/
|
|
6
|
+
interface Props {
|
|
7
|
+
value?: number;
|
|
8
|
+
count?: number;
|
|
9
|
+
ariaLabel: string;
|
|
10
|
+
/** Size utilities of each star. */
|
|
11
|
+
sizeClass?: string;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { value, count, ariaLabel, sizeClass = 'w-3.5 h-3.5', class: className } = Astro.props;
|
|
16
|
+
const total = count ?? 5;
|
|
17
|
+
const filled = count ?? Math.round(value ?? 0);
|
|
18
|
+
const stars = Array.from({ length: total }, (_, i) => i + 1);
|
|
19
|
+
---
|
|
20
|
+
<div class:list={['flex items-center gap-0.5', className]} role="img" aria-label={ariaLabel}>
|
|
21
|
+
{stars.map((star) => (
|
|
22
|
+
<svg
|
|
23
|
+
class={`${sizeClass} ${star <= filled ? 'text-accent fill-current' : 'text-muted fill-current'}`}
|
|
24
|
+
aria-hidden="true"
|
|
25
|
+
viewBox="0 0 24 24"
|
|
26
|
+
>
|
|
27
|
+
<path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" />
|
|
28
|
+
</svg>
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
package/src/primitives/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { default as Icon } from './Icon.astro';
|
|
|
3
3
|
export { default as Badge } from './Badge.astro';
|
|
4
4
|
export { default as Chip } from './Chip.astro';
|
|
5
5
|
export { default as ArrowLabel } from './ArrowLabel.astro';
|
|
6
|
+
export { default as RatingStars } from './RatingStars.astro';
|
|
6
7
|
export { default as SectionDivider } from './SectionDivider.astro';
|
|
7
8
|
export { default as LinkArrow } from './LinkArrow.astro';
|
|
8
9
|
export { default as Button } from './Button.astro';
|