@sarimarcus/content-sites-ui 0.30.0 → 0.31.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 +15 -0
- package/package.json +2 -2
- package/src/editorial/AlsoExplore.astro +37 -0
- package/src/editorial/NotFoundPage.astro +4 -2
- package/src/editorial/StaticProsePage.astro +4 -2
- package/src/editorial/VisitTiming.astro +73 -0
- package/src/editorial/index.ts +4 -0
- package/src/layout/ListingShell.astro +16 -6
- package/src/search/searchPage.ts +15 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All packages in the platform release in lockstep; entries are per release version.
|
|
4
4
|
|
|
5
|
+
## 0.31.0 (2026-10-08)
|
|
6
|
+
|
|
7
|
+
- `./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).
|
|
8
|
+
|
|
9
|
+
### Breaking
|
|
10
|
+
|
|
11
|
+
- The sites' `components/ui/AlsoExplore.astro` and `VisitTiming.astro` now live in ui; sites run `rewrite-imports.mjs --delete-local`, then `scripts/platform/migrations/core-markdown.mjs` and `scripts/platform/migrations/promoted-site-files.mjs` (Dark Skies' two `<VisitTiming>` calls get `labelClass="text-sm"`) (SLA-3017).
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## 0.30.1 (2026-10-07)
|
|
15
|
+
|
|
16
|
+
- `./editorial`: `StaticProsePage` takes `proseClass`, the variant beside `prose` (default `prose-stone`) and `NotFoundPage` takes `searchBoxClass` (default: the current search-link classes), so a dark theme can pass its own prose variant and input tokens; default output is unchanged (SLA-2989).
|
|
17
|
+
- `./layout`: `ListingShell` takes `contained` (default `true`) and `pagefindIgnore` (default `true`); `false` drops the Container inside `<main>` for a main of full-bleed bands, and keeps a listing's `data-pagefind-body` bands indexed. `mainClass={null}` renders a class-less `<main>`. Default output is unchanged (SLA-2989).
|
|
18
|
+
- `./search`: with `?focus`, `initSearchPage` re-claims input focus every 300 ms for 3 s, stopping at the first pointer or key press, instead of once at 1.7 s and only from `<body>`; the deferred cookie banner moves focus to its own button, which the single reclaim missed. This reaches every site (SLA-2989).
|
|
19
|
+
|
|
5
20
|
## 0.30.0 (2026-10-07)
|
|
6
21
|
|
|
7
22
|
- `./affiliate`: `HotelAffiliateCTA` and `AffiliateComparisonTable` links carry `data-track-affiliate-provider` and `data-track-affiliate-placement` so booking clicks are attributed; both take `provider`, `ComparisonRow` takes a per-row one, and without either the provider is read from the link's host (Booking.com links only; any other host is left unattributed) (SLA-2985).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sarimarcus/content-sites-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.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.
|
|
52
|
+
"@sarimarcus/content-sites-core": "0.31.0"
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** "Also explore": up to four sibling links under a hairline, at the foot of a hub or listing page. */
|
|
3
|
+
import Icon from '../primitives/Icon.astro';
|
|
4
|
+
|
|
5
|
+
export interface AlsoExploreItem {
|
|
6
|
+
href: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
eyebrow?: string;
|
|
12
|
+
items: AlsoExploreItem[];
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { eyebrow = 'Also explore', items, class: className } = Astro.props;
|
|
17
|
+
|
|
18
|
+
if (import.meta.env.DEV && items.length > 4) {
|
|
19
|
+
console.warn(`AlsoExplore: received ${items.length} items, capping at 4.`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const displayItems = items.slice(0, 4);
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
{displayItems.length > 0 && (
|
|
26
|
+
<nav class:list={['mt-12 pt-8 border-t border-border', className]} aria-label={eyebrow}>
|
|
27
|
+
<p class="text-xs font-semibold uppercase tracking-widest text-muted-foreground mb-3">{eyebrow}</p>
|
|
28
|
+
<div class="flex flex-wrap gap-x-6 gap-y-2">
|
|
29
|
+
{displayItems.map((item) => (
|
|
30
|
+
<a href={item.href} class="inline-flex items-center gap-1.5 text-sm text-foreground hover:text-primary transition-colors">
|
|
31
|
+
{item.label}
|
|
32
|
+
<Icon name="arrow-right" class="w-3 h-3" />
|
|
33
|
+
</a>
|
|
34
|
+
))}
|
|
35
|
+
</div>
|
|
36
|
+
</nav>
|
|
37
|
+
)}
|
|
@@ -20,9 +20,11 @@ interface Props {
|
|
|
20
20
|
/** Accessible name of the section links. */
|
|
21
21
|
exploreAriaLabel?: string;
|
|
22
22
|
links: { href: string; label: string }[];
|
|
23
|
+
/** Classes of the search link; a dark theme passes its own border and background tokens. */
|
|
24
|
+
searchBoxClass?: string;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
const { tagline, lede, homeHref, homeLabel, searchHref, searchLabel, searchAriaLabel, exploreLabel, exploreAriaLabel = 'Explore sections', links } = Astro.props;
|
|
27
|
+
const { tagline, lede, homeHref, homeLabel, searchHref, searchLabel, searchAriaLabel, exploreLabel, exploreAriaLabel = 'Explore sections', links, searchBoxClass = 'group flex items-center gap-3 w-full max-w-sm mx-auto px-4 py-3 rounded-xl border border-foreground/20 bg-white/60 backdrop-blur-sm text-left hover:border-primary/40 hover:bg-white/80 transition-all no-underline' } = Astro.props;
|
|
26
28
|
---
|
|
27
29
|
|
|
28
30
|
<main class="relative min-h-[80vh] flex items-center justify-center overflow-hidden bg-muted">
|
|
@@ -74,7 +76,7 @@ const { tagline, lede, homeHref, homeLabel, searchHref, searchLabel, searchAriaL
|
|
|
74
76
|
<a
|
|
75
77
|
href={searchHref}
|
|
76
78
|
data-search-trigger
|
|
77
|
-
class=
|
|
79
|
+
class={searchBoxClass}
|
|
78
80
|
aria-label={searchAriaLabel}
|
|
79
81
|
>
|
|
80
82
|
<Icon name="search" class="w-4 h-4 text-muted-foreground shrink-0" />
|
|
@@ -18,9 +18,11 @@ interface Props {
|
|
|
18
18
|
eyebrow?: string;
|
|
19
19
|
/** Closing box under the prose: a short line and secondary buttons. */
|
|
20
20
|
cta?: { text: string; links: CtaLink[] };
|
|
21
|
+
/** Prose variant beside the base `prose` class; a dark theme passes its own. */
|
|
22
|
+
proseClass?: string;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
const { heading, eyebrow, cta } = Astro.props;
|
|
25
|
+
const { heading, eyebrow, cta, proseClass = 'prose-stone' } = Astro.props;
|
|
24
26
|
---
|
|
25
27
|
|
|
26
28
|
<main class="py-16 lg:py-24">
|
|
@@ -28,7 +30,7 @@ const { heading, eyebrow, cta } = Astro.props;
|
|
|
28
30
|
{eyebrow && <p class="text-sm font-semibold uppercase tracking-[0.18em] text-primary mb-3">{eyebrow}</p>}
|
|
29
31
|
<h1 class="text-3xl sm:text-4xl font-medium font-display mb-8">{heading}</h1>
|
|
30
32
|
|
|
31
|
-
<div class=
|
|
33
|
+
<div class:list={['prose', proseClass, 'max-w-none']}>
|
|
32
34
|
<slot />
|
|
33
35
|
</div>
|
|
34
36
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* "When to Visit": best season, quietest hours and visit duration, each shown only when set. `class` replaces the
|
|
4
|
+
* default top margin (`mt-12`); `labelClass` sets the slot label size.
|
|
5
|
+
*/
|
|
6
|
+
import Icon from '../primitives/Icon.astro';
|
|
7
|
+
|
|
8
|
+
export interface VisitTimingData {
|
|
9
|
+
bestSeason?: string;
|
|
10
|
+
quietestHours?: string;
|
|
11
|
+
visitDuration?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
visitTiming?: VisitTimingData;
|
|
16
|
+
/** Heading text — override to avoid colliding with nearby "When to…" headings. */
|
|
17
|
+
title?: string;
|
|
18
|
+
/** Section id — override when a prose section on the page could slugify to the same value. */
|
|
19
|
+
id?: string;
|
|
20
|
+
class?: string;
|
|
21
|
+
labelClass?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
visitTiming,
|
|
26
|
+
title = 'When to Visit',
|
|
27
|
+
id = 'when-to-visit',
|
|
28
|
+
class: className = 'mt-12',
|
|
29
|
+
labelClass = 'text-[0.8125rem]',
|
|
30
|
+
} = Astro.props;
|
|
31
|
+
|
|
32
|
+
const slots = visitTiming
|
|
33
|
+
? [
|
|
34
|
+
visitTiming.bestSeason
|
|
35
|
+
? { icon: 'calendar', label: 'Best season', value: visitTiming.bestSeason }
|
|
36
|
+
: null,
|
|
37
|
+
visitTiming.quietestHours
|
|
38
|
+
? { icon: 'clock', label: 'Quietest hours', value: visitTiming.quietestHours }
|
|
39
|
+
: null,
|
|
40
|
+
visitTiming.visitDuration
|
|
41
|
+
? { icon: 'hourglass', label: 'Visit duration', value: visitTiming.visitDuration }
|
|
42
|
+
: null,
|
|
43
|
+
].filter((s): s is { icon: string; label: string; value: string } => s !== null)
|
|
44
|
+
: [];
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
{slots.length > 0 && (
|
|
48
|
+
<section
|
|
49
|
+
id={id}
|
|
50
|
+
class:list={['scroll-mt-28', className]}
|
|
51
|
+
data-guide-section
|
|
52
|
+
>
|
|
53
|
+
<h2 class="text-2xl font-display font-semibold text-foreground mb-6 leading-tight">{title}</h2>
|
|
54
|
+
<div
|
|
55
|
+
class:list={[
|
|
56
|
+
'bg-primary/5 border border-primary/15 rounded-xl p-6 grid gap-6',
|
|
57
|
+
slots.length === 1 && 'grid-cols-1',
|
|
58
|
+
slots.length === 2 && 'grid-cols-1 sm:grid-cols-2',
|
|
59
|
+
slots.length === 3 && 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3',
|
|
60
|
+
]}
|
|
61
|
+
>
|
|
62
|
+
{slots.map((slot) => (
|
|
63
|
+
<div class="min-w-0">
|
|
64
|
+
<p class:list={['flex items-center gap-1.5', labelClass, 'uppercase tracking-wider text-muted-foreground mb-1.5 leading-none']}>
|
|
65
|
+
<Icon name={slot.icon} class="w-3.5 h-3.5 text-primary/60 shrink-0" />
|
|
66
|
+
{slot.label}
|
|
67
|
+
</p>
|
|
68
|
+
<p class="text-base font-medium text-foreground leading-snug tabular-nums">{slot.value}</p>
|
|
69
|
+
</div>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
</section>
|
|
73
|
+
)}
|
package/src/editorial/index.ts
CHANGED
|
@@ -20,6 +20,10 @@ export type { AuthorSummary, ArticleSummary, KeyFact, SourceItem, ArticleCategor
|
|
|
20
20
|
export { default as InsiderTips } from './InsiderTips.astro';
|
|
21
21
|
export type { InsiderTip, InsiderTipAuthor, InsiderTipKinds, InsiderTipTone } from './types';
|
|
22
22
|
export { default as AuthorStrip } from './AuthorStrip.astro';
|
|
23
|
+
export { default as AlsoExplore } from './AlsoExplore.astro';
|
|
24
|
+
export { default as VisitTiming } from './VisitTiming.astro';
|
|
25
|
+
export type { AlsoExploreItem } from './AlsoExplore.astro';
|
|
26
|
+
export type { VisitTimingData } from './VisitTiming.astro';
|
|
23
27
|
export { default as AnswerBox } from './interludes/AnswerBox.astro';
|
|
24
28
|
export { default as ComparisonTable } from './interludes/ComparisonTable.astro';
|
|
25
29
|
export { default as EvidenceContext } from './interludes/EvidenceContext.astro';
|
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
3
|
* ListingShell — the frame of a listing hub: breadcrumb band, hero, optional full-width bands, <main> with a
|
|
4
|
-
* Container, and trailing bands, all inside one Pagefind-
|
|
4
|
+
* Container, and trailing bands, all inside one wrapper that is Pagefind-ignored by default (most listings are not
|
|
5
|
+
* indexed; `pagefindIgnore={false}` keeps a listing's own `data-pagefind-body` bands). The route keeps
|
|
5
6
|
* Layout and passes StructuredData to Layout directly, so its JSON-LD lands in <head>; everything else comes in by slot.
|
|
6
7
|
*/
|
|
7
8
|
import BreadcrumbBand from './BreadcrumbBand.astro';
|
|
8
9
|
import Container from './Container.astro';
|
|
9
10
|
|
|
10
11
|
interface Props {
|
|
11
|
-
|
|
12
|
+
/** `null` renders a class-less <main>. */
|
|
13
|
+
mainClass?: string | null;
|
|
14
|
+
/** `false` drops the Container inside <main>, for a main made of full-bleed bands that carry their own. */
|
|
15
|
+
contained?: boolean;
|
|
16
|
+
/** `false` keeps the page indexable, for a listing whose bands carry `data-pagefind-body` prose. */
|
|
17
|
+
pagefindIgnore?: boolean;
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
const { mainClass = 'py-12 lg:py-20' } = Astro.props;
|
|
20
|
+
const { mainClass = 'py-12 lg:py-20', contained = true, pagefindIgnore = true } = Astro.props;
|
|
15
21
|
---
|
|
16
22
|
|
|
17
|
-
<div data-pagefind-ignore>
|
|
23
|
+
<div data-pagefind-ignore={pagefindIgnore ? '' : undefined}>
|
|
18
24
|
<BreadcrumbBand ignore={false}>
|
|
19
25
|
<slot name="breadcrumb" />
|
|
20
26
|
</BreadcrumbBand>
|
|
@@ -23,9 +29,13 @@ const { mainClass = 'py-12 lg:py-20' } = Astro.props;
|
|
|
23
29
|
<slot name="before-main" />
|
|
24
30
|
|
|
25
31
|
<main class={mainClass}>
|
|
26
|
-
|
|
32
|
+
{contained ? (
|
|
33
|
+
<Container>
|
|
34
|
+
<slot />
|
|
35
|
+
</Container>
|
|
36
|
+
) : (
|
|
27
37
|
<slot />
|
|
28
|
-
|
|
38
|
+
)}
|
|
29
39
|
</main>
|
|
30
40
|
|
|
31
41
|
<slot name="after-main" />
|
package/src/search/searchPage.ts
CHANGED
|
@@ -591,12 +591,22 @@ export function initSearchPage<T extends string>(config: SearchPageConfig<T>): v
|
|
|
591
591
|
}
|
|
592
592
|
};
|
|
593
593
|
requestAnimationFrame(claimFocus);
|
|
594
|
-
//
|
|
595
|
-
//
|
|
594
|
+
// The deferred cookie banner can steal focus (to its own button, not <body>) up to ~2s after load. Re-claim it
|
|
595
|
+
// every 300 ms for 3 s, and stop as soon as the user interacts.
|
|
596
596
|
if (initial.focus) {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
597
|
+
let tries = 0;
|
|
598
|
+
const stop = () => {
|
|
599
|
+
window.clearInterval(reclaim);
|
|
600
|
+
document.removeEventListener('pointerdown', stop, true);
|
|
601
|
+
document.removeEventListener('keydown', stop, true);
|
|
602
|
+
};
|
|
603
|
+
const reclaim = window.setInterval(() => {
|
|
604
|
+
tries += 1;
|
|
605
|
+
if (tries > 10) stop();
|
|
606
|
+
else claimFocus();
|
|
607
|
+
}, 300);
|
|
608
|
+
document.addEventListener('pointerdown', stop, true);
|
|
609
|
+
document.addEventListener('keydown', stop, true);
|
|
600
610
|
}
|
|
601
611
|
}
|
|
602
612
|
}
|