@sarimarcus/content-sites-ui 0.33.0 → 0.34.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/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  All packages in the platform release in lockstep; entries are per release version.
4
4
 
5
+ ## 0.34.0 (2026-10-12)
6
+
7
+ - `ContentHero` joins `@sarimarcus/content-sites-ui/layout` (SLA-3041): full-bleed image hero with a themed gradient, title, description and the `badges`, `subtitle`, `info`, `actions` and `decoration` slots. A slot's wrapper is emitted only when the slot rendered content, so an adapter can forward every slot unconditionally. `themeGradients` overlays per-site gradients on the built-in map, `imageTitle` sets the image's title attribute, and colours use the `overlay` / `overlay-foreground` roles. Additive: no existing export changes.
8
+
5
9
  ## 0.33.0 (2026-10-12)
6
10
 
7
11
  - `./composed` exports the `TableOfContentsItem` type; no markup change (SLA-3043).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarimarcus/content-sites-ui",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
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.33.0"
52
+ "@sarimarcus/content-sites-core": "0.34.0"
53
53
  }
54
54
  }
@@ -0,0 +1,161 @@
1
+ ---
2
+ /**
3
+ * ContentHero — full-bleed image hero with a themed gradient, title, description and four optional slots
4
+ * (badges, subtitle, info, actions) plus an absolutely positioned `decoration` layer.
5
+ *
6
+ * A slot's wrapper is emitted only when the slot rendered something, so an adapter can always forward
7
+ * `<Fragment slot="x"><slot name="x" /></Fragment>` without checking whether its own caller filled it
8
+ * (Astro.slots.has() is true for a slot whose expression evaluated to false).
9
+ */
10
+ import { ResponsivePicture } from '../primitives/index.ts';
11
+
12
+ interface Props {
13
+ title: string;
14
+ description?: string;
15
+ image?: ImageMetadata;
16
+ imageFallback?: string;
17
+ imagePlaceholder?: string;
18
+ imageAlt?: string;
19
+ /** HTML title attribute of the hero image (tooltip / image SEO). */
20
+ imageTitle?: string;
21
+ /** Unknown names fall back to `dark` (`stone` and `nature` are such names on Granada). */
22
+ theme?: string;
23
+ /** Per-theme gradient classes layered over the built-in map, for a site whose theme looks differ. */
24
+ themeGradients?: Record<string, string>;
25
+ overlay?: 'themed' | 'editorial';
26
+ minHeight?: 'short' | 'medium' | 'tall';
27
+ class?: string;
28
+ }
29
+
30
+ const {
31
+ title,
32
+ description,
33
+ image,
34
+ imageFallback,
35
+ imagePlaceholder,
36
+ imageAlt = title,
37
+ imageTitle,
38
+ theme = 'dark',
39
+ themeGradients: themeOverrides,
40
+ overlay = 'themed',
41
+ minHeight = 'medium',
42
+ class: className,
43
+ } = Astro.props;
44
+
45
+ const baseGradients: Record<string, string> = {
46
+ dark: 'bg-gradient-to-t from-overlay via-overlay/60 to-transparent',
47
+ warm: 'bg-gradient-to-t from-overlay/90 via-primary/70 to-transparent',
48
+ azulejo: 'bg-gradient-to-b from-overlay/95 via-secondary/85 to-secondary-strong/90',
49
+ nebula: 'bg-gradient-to-b from-overlay/95 via-secondary/85 to-secondary-strong/90',
50
+ aurora: 'bg-gradient-to-b from-accent/90 via-primary/85 to-primary/95',
51
+ terracotta: 'bg-gradient-to-t from-primary-strong/90 via-primary/40 to-transparent',
52
+ gold: 'bg-gradient-to-t from-overlay/90 via-accent/60 to-transparent',
53
+ summer: 'bg-gradient-to-b from-orange-600/85 via-red-600/75 to-red-700/80',
54
+ autumn: 'bg-gradient-to-b from-amber-700/85 via-orange-700/80 to-orange-800/90',
55
+ winter: 'bg-gradient-to-b from-slate-700/85 via-blue-800/80 to-blue-900/90',
56
+ spring: 'bg-gradient-to-b from-green-600/85 via-emerald-600/80 to-emerald-700/90',
57
+ };
58
+ const themeGradients = { ...baseGradients, ...themeOverrides };
59
+
60
+ const heightClasses: Record<string, string> = {
61
+ short: 'min-h-[350px] sm:min-h-[400px] lg:min-h-[450px]',
62
+ medium: 'min-h-[450px] sm:min-h-[500px] lg:min-h-[550px]',
63
+ tall: 'min-h-[550px] sm:min-h-[600px] lg:min-h-[650px]',
64
+ };
65
+
66
+ const gradientClass = themeGradients[theme] || themeGradients.dark;
67
+ const heightClass = heightClasses[minHeight] || heightClasses.medium;
68
+
69
+ const slotHtml = async (name: string) => (Astro.slots.has(name) ? (await Astro.slots.render(name)).trim() : '');
70
+ const [badges, subtitle, info, actions, decoration] = await Promise.all(
71
+ ['badges', 'subtitle', 'info', 'actions', 'decoration'].map(slotHtml),
72
+ );
73
+ ---
74
+
75
+ <section class:list={['relative flex items-end overflow-hidden', heightClass, className]}>
76
+ <div class="absolute inset-0 overflow-hidden">
77
+ {image ? (
78
+ <ResponsivePicture
79
+ src={image}
80
+ alt={imageAlt}
81
+ title={imageTitle}
82
+ sizes="100vw"
83
+ quality={70}
84
+ class="w-full h-full object-cover object-center"
85
+ pictureAttributes={{
86
+ class: 'absolute inset-0'
87
+ }}
88
+ loading="eager"
89
+ fetchpriority="high"
90
+ />
91
+ ) : imageFallback ? (
92
+ <img
93
+ src={imageFallback}
94
+ alt={imageAlt}
95
+ width={1920}
96
+ height={1080}
97
+ class="absolute inset-0 w-full h-full object-cover object-center"
98
+ loading="eager"
99
+ fetchpriority="high"
100
+ onerror={imagePlaceholder ? `this.src='${imagePlaceholder}'` : undefined}
101
+ />
102
+ ) : imagePlaceholder ? (
103
+ <img
104
+ src={imagePlaceholder}
105
+ alt={imageAlt}
106
+ width={1920}
107
+ height={1080}
108
+ class="absolute inset-0 w-full h-full object-cover object-center"
109
+ loading="eager"
110
+ fetchpriority="high"
111
+ />
112
+ ) : null}
113
+ {overlay === 'editorial' ? (
114
+ <>
115
+ <div class="absolute inset-0 bg-overlay/10"></div>
116
+ <div class="absolute inset-0 bg-gradient-to-r from-overlay/80 via-overlay/30 to-transparent"></div>
117
+ <div class="absolute inset-0 bg-gradient-to-t from-overlay/55 via-transparent to-transparent"></div>
118
+ </>
119
+ ) : (
120
+ <div class:list={['absolute inset-0', gradientClass]}></div>
121
+ )}
122
+ </div>
123
+
124
+ {decoration && <Fragment set:html={decoration} />}
125
+
126
+ <div class="relative z-10 w-full pt-8 lg:pt-12 pb-8 lg:pb-16">
127
+ <div class="mx-auto max-w-shell px-4 sm:px-6 lg:px-8">
128
+ <div class="max-w-measure mx-auto drop-shadow-[0_1px_2px_rgba(0,0,0,0.75)]">
129
+ {badges && <div class="flex flex-wrap gap-2 mb-4" set:html={badges} />}
130
+
131
+ <h1
132
+ class:list={[
133
+ 'font-medium text-overlay-foreground leading-[1.08] mb-4 font-display',
134
+ overlay === 'editorial'
135
+ ? 'max-w-[22ch] text-[1.75rem] sm:max-w-[20ch] sm:text-4xl lg:max-w-[19ch] lg:text-5xl xl:text-6xl'
136
+ : 'max-w-[18ch] text-3xl sm:text-4xl lg:text-5xl xl:text-6xl',
137
+ ]}
138
+ >
139
+ {title}
140
+ </h1>
141
+
142
+ {subtitle && <div class="mb-4" set:html={subtitle} />}
143
+
144
+ {description && (
145
+ <p class:list={[
146
+ 'text-overlay-foreground/85 leading-relaxed max-w-deck',
147
+ overlay === 'editorial'
148
+ ? 'text-sm sm:text-lg lg:text-xl line-clamp-2 sm:line-clamp-none'
149
+ : 'text-lg sm:text-xl mb-8',
150
+ ]}>
151
+ {description}
152
+ </p>
153
+ )}
154
+
155
+ {info && <div class="flex flex-wrap gap-3" set:html={info} />}
156
+
157
+ {actions && <div class="mt-5 flex flex-wrap gap-3" set:html={actions} />}
158
+ </div>
159
+ </div>
160
+ </div>
161
+ </section>
@@ -1,5 +1,6 @@
1
1
  // Public surface of @sarimarcus/content-sites-ui/layout.
2
2
  export { default as Container } from './Container.astro';
3
+ export { default as ContentHero } from './ContentHero.astro';
3
4
  export { default as BreadcrumbBand } from './BreadcrumbBand.astro';
4
5
  export { default as ListingShell } from './ListingShell.astro';
5
6
  export { default as EntityDetailShell } from './EntityDetailShell.astro';