ferst-core 0.7.8 → 0.7.9
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.
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { inlineMarkdown } from '../lib/inline-markdown';
|
|
2
3
|
/**
|
|
3
4
|
* CalendarEmbed — a consent-gated external calendar embed (e.g. Google Calendar).
|
|
4
5
|
* Nothing loads from the provider until the visitor clicks "Load calendar", so no
|
|
@@ -38,7 +39,7 @@ const safe = isSafeEmbedUrl(finalEmbedUrl);
|
|
|
38
39
|
|
|
39
40
|
<section class="b-calendar">
|
|
40
41
|
{title && <h2 class="b-calendar__title">{title}</h2>}
|
|
41
|
-
{intro && <p class="b-calendar__intro"
|
|
42
|
+
{intro && <p class="b-calendar__intro" set:html={inlineMarkdown(intro)} />}
|
|
42
43
|
|
|
43
44
|
{!finalEmbedUrl ? (
|
|
44
45
|
<p class="b-calendar__note">The calendar isn't set up yet.</p>
|
|
@@ -23,11 +23,13 @@ interface Props {
|
|
|
23
23
|
image?: { src: string; alt?: string };
|
|
24
24
|
media?: { image?: string; images?: string[]; video?: string; poster?: string; overlay?: number; link?: string; linkLabel?: string };
|
|
25
25
|
layout?: 'centered' | 'split' | 'media';
|
|
26
|
-
size?: 'standard' | 'tall' | 'screen';
|
|
26
|
+
size?: 'compact' | 'standard' | 'tall' | 'screen';
|
|
27
27
|
parallax?: boolean;
|
|
28
28
|
background?: 'none' | 'mesh';
|
|
29
|
+
fullBleed?: boolean;
|
|
30
|
+
focus?: string;
|
|
29
31
|
}
|
|
30
|
-
const { eyebrow, title, subtitle, cta, image, media, layout = 'centered', size = 'standard', parallax = false, background = 'none' } = Astro.props;
|
|
32
|
+
const { eyebrow, title, subtitle, cta, image, media, layout = 'centered', size = 'standard', parallax = false, background = 'none', fullBleed = false, focus } = Astro.props;
|
|
31
33
|
// A YouTube / Vimeo link plays as an embedded background (after consent); anything else
|
|
32
34
|
// in `video` is a file for a native <video>.
|
|
33
35
|
const embed = media?.video ? backgroundEmbed(media.video) : null;
|
|
@@ -55,7 +57,8 @@ const bgImage = isMedia && stillSrc ? `--hero-image: url('${stillSrc}')` : '';
|
|
|
55
57
|
data-motion={motion}
|
|
56
58
|
data-linked={bgLink ? 'true' : undefined}
|
|
57
59
|
data-parallax={isMedia && parallax ? 'true' : undefined}
|
|
58
|
-
|
|
60
|
+
data-full-bleed={isMedia && fullBleed ? 'true' : undefined}
|
|
61
|
+
style={isMedia ? `--hero-overlay: ${overlay}; ${bgImage}${focus ? `; --hero-focus: ${focus}` : ''}` : undefined}
|
|
59
62
|
>
|
|
60
63
|
{isMedia && (
|
|
61
64
|
<div class="b-hero__bg" aria-hidden="true">
|
|
@@ -307,6 +310,21 @@ const bgImage = isMedia && stillSrc ? `--hero-image: url('${stillSrc}')` : '';
|
|
|
307
310
|
overflow: hidden;
|
|
308
311
|
isolation: isolate;
|
|
309
312
|
}
|
|
313
|
+
/* A narrow band for a short, wide header strip: close to the strip's own shape, so it
|
|
314
|
+
isn't cropped to its middle and blown up. Less padding and a smaller title to fit. */
|
|
315
|
+
.b-hero[data-size='compact'] {
|
|
316
|
+
min-height: clamp(8rem, 18vw, 16rem);
|
|
317
|
+
padding-block: clamp(1.25rem, 3vw, 2.25rem);
|
|
318
|
+
}
|
|
319
|
+
.b-hero[data-size='compact'] :global(h1) {
|
|
320
|
+
font-size: clamp(1.6rem, 3.4vw, 2.6rem);
|
|
321
|
+
}
|
|
322
|
+
/* Edge to edge across the screen, like a full-bleed banner. */
|
|
323
|
+
.b-hero[data-full-bleed='true'] {
|
|
324
|
+
width: 100vw;
|
|
325
|
+
margin-inline: calc(50% - 50vw);
|
|
326
|
+
border-radius: 0;
|
|
327
|
+
}
|
|
310
328
|
.b-hero[data-size='tall'] {
|
|
311
329
|
min-height: min(85vh, 52rem);
|
|
312
330
|
}
|
|
@@ -330,7 +348,7 @@ const bgImage = isMedia && stillSrc ? `--hero-image: url('${stillSrc}')` : '';
|
|
|
330
348
|
z-index: -1;
|
|
331
349
|
background-image: var(--hero-image, none);
|
|
332
350
|
background-size: cover;
|
|
333
|
-
background-position: center;
|
|
351
|
+
background-position: var(--hero-focus, center);
|
|
334
352
|
background-color: var(--bg-section);
|
|
335
353
|
}
|
|
336
354
|
/* Parallax: give the background room to drift without uncovering an edge. */
|
|
@@ -342,6 +360,7 @@ const bgImage = isMedia && stillSrc ? `--hero-image: url('${stillSrc}')` : '';
|
|
|
342
360
|
width: 100%;
|
|
343
361
|
height: 100%;
|
|
344
362
|
object-fit: cover;
|
|
363
|
+
object-position: var(--hero-focus, center);
|
|
345
364
|
}
|
|
346
365
|
/* An embedded background player, sized to COVER the hero like object-fit (16:9 kept,
|
|
347
366
|
overflow cropped). Never interactive; fades in once it has started. */
|
|
@@ -374,7 +393,7 @@ const bgImage = isMedia && stillSrc ? `--hero-image: url('${stillSrc}')` : '';
|
|
|
374
393
|
position: absolute;
|
|
375
394
|
inset: 0;
|
|
376
395
|
background-size: cover;
|
|
377
|
-
background-position: center;
|
|
396
|
+
background-position: var(--hero-focus, center);
|
|
378
397
|
opacity: 0;
|
|
379
398
|
transition: opacity 1.6s ease-in-out;
|
|
380
399
|
}
|
package/content/blocks.ts
CHANGED
|
@@ -146,9 +146,19 @@ export const heroBlock = z.object({
|
|
|
146
146
|
})
|
|
147
147
|
.optional(),
|
|
148
148
|
layout: z.enum(['centered', 'split', 'media']).default('centered'),
|
|
149
|
-
/** How much of the screen a `media` hero takes: `
|
|
150
|
-
*
|
|
151
|
-
|
|
149
|
+
/** How much of the screen a `media` hero takes: `compact` (a narrow band for a short,
|
|
150
|
+
* wide header strip, e.g. 1000 × 175), `standard` (a band), `tall`, or `screen` — the
|
|
151
|
+
* full first screen, edge to edge (e.g. a video opener). */
|
|
152
|
+
size: z.enum(['compact', 'standard', 'tall', 'screen']).default('standard'),
|
|
153
|
+
/** A `media` hero runs edge to edge across the screen, like a full-bleed banner
|
|
154
|
+
* (`screen` always does). */
|
|
155
|
+
fullBleed: z.boolean().default(false),
|
|
156
|
+
/** Which part of the background image shows when it's cropped: one or two positions,
|
|
157
|
+
* keywords or percentages (e.g. `center 30%`, `top`, `left 20%`). Default `center`. */
|
|
158
|
+
focus: z
|
|
159
|
+
.string()
|
|
160
|
+
.regex(/^(?:left|center|right|top|bottom|\d{1,3}%)(?: (?:left|center|right|top|bottom|\d{1,3}%))?$/, 'focus is one or two of left / center / right / top / bottom / a percentage, e.g. "center 30%"')
|
|
161
|
+
.optional(),
|
|
152
162
|
/** A `media` hero's background drifts more slowly than the page as it scrolls (any
|
|
153
163
|
* background: image, slides or video). Off on touch devices and under reduced motion. */
|
|
154
164
|
parallax: z.boolean().default(false),
|
package/layouts/Base.astro
CHANGED
|
@@ -431,6 +431,7 @@ const metaDescription = description ?? identity.description;
|
|
|
431
431
|
/* A full-bleed opener (a full-screen hero, a full-bleed banner) as the page's first
|
|
432
432
|
block sits flush under the header, not below the main's top padding. */
|
|
433
433
|
.site-main:has(> .b-hero[data-size='screen']:first-child),
|
|
434
|
+
.site-main:has(> .b-hero[data-full-bleed='true']:first-child),
|
|
434
435
|
.site-main:has(> .b-banner[data-full-bleed='true']:first-child) {
|
|
435
436
|
padding-block-start: 0;
|
|
436
437
|
}
|
package/lib/inline-markdown.ts
CHANGED
|
@@ -69,3 +69,16 @@ export function inlineMarkdown(text: string | undefined | null): string {
|
|
|
69
69
|
out += inlineFormat(escapeHtml(text.slice(last)));
|
|
70
70
|
return out;
|
|
71
71
|
}
|
|
72
|
+
|
|
73
|
+
/** The words of formatted copy with the formatting gone (a link becomes its label): for
|
|
74
|
+
* places that must be plain text, like a page's meta description. */
|
|
75
|
+
export function plainText(text: string | undefined | null): string {
|
|
76
|
+
if (!text) return '';
|
|
77
|
+
return text
|
|
78
|
+
.replace(LINK_RE, '$1')
|
|
79
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
80
|
+
.replace(/(^|[^*\w])\*(?![\s*])([^*]*?[^\s*])\*(?!\*)/g, '$1$2')
|
|
81
|
+
.replace(/(^|[^\w])_(?![\s_])([^_]*?[^\s_])_(?!\w)/g, '$1$2')
|
|
82
|
+
.replace(/\s*\n\s*/g, ' ')
|
|
83
|
+
.trim();
|
|
84
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ferst-core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ferst Core \u2014 the shared, brand-agnostic client-site system: Astro components, layouts, content schemas, the layered config resolver, and a neutral styling architecture that every client overrides.",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { plainText } from '../../lib/inline-markdown';
|
|
2
3
|
/**
|
|
3
4
|
* The calendar page (`/calendar`) — INJECTED by the ferst() integration. Consent-
|
|
4
5
|
* gated by CalendarEmbed (nothing loads from Google until the visitor opts in).
|
|
@@ -12,7 +13,7 @@ import { getCalendarSettings } from '../../lib/calendar';
|
|
|
12
13
|
const cal = await getCalendarSettings();
|
|
13
14
|
---
|
|
14
15
|
|
|
15
|
-
<Base title={cal.title} description={cal.intro || undefined}>
|
|
16
|
+
<Base title={cal.title} description={plainText(cal.intro) || undefined}>
|
|
16
17
|
<CalendarEmbed
|
|
17
18
|
title={cal.title}
|
|
18
19
|
intro={cal.intro}
|