@sarimarcus/content-sites-ui 0.15.1 → 0.17.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,21 @@
2
2
 
3
3
  All packages in the platform release in lockstep; entries are per release version.
4
4
 
5
+ ## 0.17.0 (2026-10-01)
6
+
7
+ - New `DocumentShell` (`./layout`): the whole HTML document a site page renders into: description, robots (`noindex, follow` when noindex), canonical from the page path, RSS and favicons, Open Graph / article / Twitter (handle optional), title, non-critical stylesheets, the Emerald loader, the skip link, navigation and footer wrapped out of the Pagefind index, `#main-content`, and consent-gated analytics. Slots `head-start`, `fonts`, `head`, `body-start`, `nav`, `footer`. It imports no stylesheet and emits no bundled script, so a site's thin Layout keeps its bundle names (SLA-2741).
8
+ - New `ConsentAnalytics` (`./layout`): the inline GTM + Consent Mode v2 + Silktide bootstrap (consent default before the gtm.js event in one synchronous step; `window.openCookiePreferences`), production only, plus the `<noscript>` GTM iframe. `layout/resources.ts` declares the hosts it loads (SLA-2741).
9
+ - New `EmeraldLoader` (`./affiliate`): the Travelpayouts Emerald script from the site's trs; `emeraldResources` declares its host (SLA-2741).
10
+ - New entry points `./analytics` (`setupAnalytics`, `track`, `pageTypeOf`, delegated, FAQ and scroll-depth tracking) and `./travelpayouts-styler` (`setupTravelpayoutsStyler`, palette, fonts and copy as options), moved from the sites. FAQ tracking now fires: it tested `dataset.faqTrack`, which is empty on the bare `data-faq-track` attribute ui `FAQ` renders (SLA-2741).
11
+
12
+ ## 0.16.0 (2026-10-01)
13
+
14
+ - New `./navigation`: `SiteHeader` (fixed header, labelled main `<nav>`, logo/desktop/actions/mobile slots, mobile toggle, mobile panel after the header with `data-pagefind-ignore`, `surfaceClass`, Ctrl/Cmd+K → `searchPath?focus=1`), `NavDropdown` (click, hover with a 150 ms close delay, ArrowDown/ArrowUp/Escape, outside click, focus leaving; a click on a hover-opened dropdown keeps it open), `MobileNavSection` (single-open accordion, `aria-controls`), `NavMenuLink` / `MobileNavLink` (panel rows by `NavTone`), `SiteFooter` (sections, `<nav aria-label="Legal">`, cookie-preferences button through `data-cookie-preferences`, no inline handler), `isActivePath`, and the `SiteIdentity` / `NavLink` / `NavGroup` / `FooterSection` / `NavTone` contracts. One bundled controller, idempotent on `astro:page-load`, any number of headers per page; the toggle and chevron icons now switch state (SLA-2735).
15
+ - New `./search`: `appendResults` (Pagefind result cards from the site's type taxonomy; the excerpt sanitiser's `DOMParser` is created on first use, so the entry point is safe to import during SSR) and `SearchSuggestions` (SLA-2735).
16
+ - New `./search.css` and `./consent.css`: the search page and Silktide consent styles on theme roles, tuned per site by optional `--search-*` / `--consent-*` custom properties. Folds in the consent modal type hierarchy and toggle contrast (Dark Skies 5d13f8a), the SLA-2290 mobile banner compaction, and `.search-filters[hidden]` (SLA-2735).
17
+ - `./layout`: `AsyncStylesheet` (non-render-blocking sheet with a `<noscript>` fallback) (SLA-2735).
18
+ - `theme.css`: `inverse`, `inverse-foreground`, `inverse-muted` roles for the footer surface; sites must set them (SLA-2735).
19
+
5
20
  ## 0.15.1 (2026-10-01)
6
21
 
7
22
  - `selectActivities` accepts activities without an `id` (the sites' activity type has it optional); 0.15.0 failed site type checks (SLA-2736).
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # @sarimarcus/content-sites-ui
2
2
 
3
3
  Universal Astro UI shared by every content site. Named entry points, one per folder: `primitives`,
4
- `composed`, `editorial`, `affiliate`, `seo`, `layout`, `navigation` and `types`. For example,
4
+ `composed`, `editorial`, `affiliate`, `seo`, `layout`, `navigation`, `search` and `types`, plus the script
5
+ entry points `analytics` and `travelpayouts-styler`. For example,
5
6
  `import { Container } from '@sarimarcus/content-sites-ui/layout'`. `src/internal` is never exported,
6
7
  and `src/styles` holds the Tailwind entry.
7
8
 
@@ -48,6 +49,78 @@ import { Icon, Badge, Chip, ArrowLabel, SectionDivider, LinkArrow, Button, Callo
48
49
  Chip colour only through theme roles, including `-tint`, `-emphasis` and the `badge-on-image` / `chip-on-image`
49
50
  roles, which each site sets to its own values.
50
51
 
52
+ ## Navigation and search
53
+
54
+ ```ts
55
+ import { SiteHeader, NavDropdown, MobileNavSection, SiteFooter, type NavLink, type FooterSection } from '@sarimarcus/content-sites-ui/navigation';
56
+ import { SearchSuggestions, appendResults } from '@sarimarcus/content-sites-ui/search';
57
+ ```
58
+
59
+ Shells for site chrome. The site composes every link, panel, logo and label; the shells own the landmarks, the ARIA
60
+ state and one bundled controller (idempotent on `astro:page-load`, any number of headers per page).
61
+
62
+ - `SiteHeader`: the fixed `<header>` with a labelled `<nav>`, slots `logo`, `desktop`, `actions`, `mobile-actions`
63
+ and `mobile`, the mobile-menu toggle, and the mobile panel rendered after the header (with `data-pagefind-ignore`).
64
+ `searchPath` turns on Ctrl/Cmd+K → `<searchPath>?focus=1`. `surfaceClass` sets the bar's and the panel's
65
+ background. Ids default to `main-nav`, `mobile-menu`, `mobile-menu-btn`.
66
+ - `NavDropdown`: trigger plus panel (default slot). Click, hover with a 150 ms close delay, ArrowDown/ArrowUp/Escape,
67
+ outside click and focus leaving. `tone` tints the default trigger; `buttonClass` replaces it.
68
+ - `MobileNavSection`: one section of the mobile panel's single-open accordion (its button's `aria-controls` names
69
+ its content).
70
+ - `NavMenuLink` / `MobileNavLink`: one row of a dropdown panel / of the mobile panel, from a `NavLink`, tinted by a
71
+ `NavTone`. `active` marks the row `aria-current="page"`; `emphasis` paints hub links in the tone.
72
+ - `isActivePath(currentPath, href)`: the page or a page below it, on a path-segment boundary.
73
+ - `SiteFooter`: brand slot, one column per `FooterSection`, a `bottom` slot, the copyright line and a
74
+ `<nav aria-label="Legal">` ending with the cookie-preferences button, which calls `window.openCookiePreferences()`
75
+ (the site's consent loader defines it). Defaults paint the `inverse` roles; class props restyle each part.
76
+ - `appendResults` renders Pagefind result cards from the site's type taxonomy; `SearchSuggestions` renders the
77
+ empty-state chips. The search page, its copy and its index stay in the site.
78
+
79
+ ## Document shell
80
+
81
+ ```astro
82
+ ---
83
+ import { DocumentShell } from '@sarimarcus/content-sites-ui/layout';
84
+ ---
85
+ <DocumentShell title={title} description={description} siteName={site.siteName} siteUrl={site.url} themeColor="#FAFAF8"
86
+ gtmId={site.analytics.gtmId} consentPrompt="…" consentTypes={consentTypes} skipLinkLabel="Skip to main content">
87
+ <Fragment slot="fonts">…</Fragment>
88
+ <Navigation slot="nav" />
89
+ <slot />
90
+ <Footer slot="footer" />
91
+ </DocumentShell>
92
+ ```
93
+
94
+ `DocumentShell` renders the whole document: description, robots (`noindex, follow` when `noindex`), canonical (from
95
+ the page path unless `canonicalUrl`), RSS and favicons, Open Graph / article / Twitter (`twitterHandle` optional),
96
+ title, non-critical `stylesheets`, the Emerald loader (`travelpayoutsTrs`), the skip link, the navigation and footer
97
+ wrapped in `data-pagefind-ignore`, `#main-content` and `ConsentAnalytics`. Slots: `head-start`, `fonts`, `head`,
98
+ `body-start`, `nav`, `footer`, default. It imports no stylesheet and emits no bundled script, so the site's thin
99
+ Layout keeps both (and with them its bundle names).
100
+
101
+ `ConsentAnalytics` is inline on purpose: the Consent Mode default must reach the dataLayer before the `gtm.js` event
102
+ in one synchronous step. It defines `window.openCookiePreferences` for the footer button. `./analytics`
103
+ (`setupAnalytics`, `track`, `pageTypeOf`) and `./travelpayouts-styler` (`setupTravelpayoutsStyler`) are their own
104
+ entry points so a client `<script>` imports no Astro component. The hosts the shell loads are declared in
105
+ `layout/resources.ts`; `scripts/validate-csp-coverage.mjs` checks every site's CSP against them.
106
+
107
+ ## Search and consent styles
108
+
109
+ `@sarimarcus/content-sites-ui/search.css` styles the search page markup (`appendResults`, `SearchSuggestions` and
110
+ the page's form, filters and states); `@sarimarcus/content-sites-ui/consent.css` styles the Silktide consent banner
111
+ and modal. Both read the theme roles, and optional `--search-*` / `--consent-*` custom properties, listed at the top
112
+ of each file, tune what differs per site. Load them as non-critical sheets with `AsyncStylesheet` (ui/layout):
113
+
114
+ ```astro
115
+ ---
116
+ import searchCssUrl from '@sarimarcus/content-sites-ui/search.css?url';
117
+ import { AsyncStylesheet } from '@sarimarcus/content-sites-ui/layout';
118
+ ---
119
+ <AsyncStylesheet href={searchCssUrl} />
120
+ ```
121
+
122
+ A site's own additions to a sheet load after it.
123
+
51
124
  ## Tailwind
52
125
 
53
126
  Tailwind v4 does not scan `node_modules`, so a site must import the package's source entry next to Tailwind:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarimarcus/content-sites-ui",
3
- "version": "0.15.1",
3
+ "version": "0.17.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",
@@ -30,18 +30,23 @@
30
30
  "./composed": "./src/composed/index.ts",
31
31
  "./editorial": "./src/editorial/index.ts",
32
32
  "./affiliate": "./src/affiliate/index.ts",
33
+ "./travelpayouts-styler": "./src/affiliate/travelpayoutsStyler.ts",
33
34
  "./seo": "./src/seo/index.ts",
34
35
  "./layout": "./src/layout/index.ts",
36
+ "./analytics": "./src/layout/analytics.ts",
35
37
  "./navigation": "./src/navigation/index.ts",
38
+ "./search": "./src/search/index.ts",
36
39
  "./types": "./src/types/index.ts",
37
40
  "./tailwind.css": "./src/styles/tailwind.css",
38
41
  "./widths.css": "./src/styles/widths.css",
39
- "./theme.css": "./src/styles/theme.css"
42
+ "./theme.css": "./src/styles/theme.css",
43
+ "./consent.css": "./src/styles/consent.css",
44
+ "./search.css": "./src/styles/search.css"
40
45
  },
41
46
  "peerDependencies": {
42
47
  "@lucide/astro": "^1.23.0",
43
48
  "astro": "^7.0.6",
44
49
  "tailwindcss": "^4.1.18",
45
- "@sarimarcus/content-sites-core": "0.15.1"
50
+ "@sarimarcus/content-sites-core": "0.17.0"
46
51
  }
47
52
  }
@@ -0,0 +1,40 @@
1
+ ---
2
+ /**
3
+ * The Travelpayouts Emerald script that turns the site's affiliate links into tracked ones. Deferred off the critical
4
+ * path: it loads on the first interaction, or once the page is idle after load. `trs` is the site's Travelpayouts id.
5
+ */
6
+ import { EMERALD_ORIGIN } from './resources.ts';
7
+
8
+ interface Props {
9
+ trs: string;
10
+ }
11
+
12
+ const { trs } = Astro.props;
13
+ const emeraldOrigin = EMERALD_ORIGIN;
14
+ ---
15
+
16
+ <script is:inline define:vars={{ trs, emeraldOrigin }}>
17
+ (function () {
18
+ if (!/^\d+$/.test(trs)) return;
19
+ var loaded = false;
20
+ function loadAffiliate() {
21
+ if (loaded) return;
22
+ loaded = true;
23
+ var script = document.createElement('script');
24
+ script.async = 1;
25
+ script.src = emeraldOrigin + '/' + btoa(trs) + '.js?t=' + trs;
26
+ document.head.appendChild(script);
27
+ }
28
+ var opts = { once: true, passive: true, capture: true };
29
+ ['pointerdown', 'keydown', 'touchstart', 'scroll'].forEach(function (evt) {
30
+ window.addEventListener(evt, loadAffiliate, opts);
31
+ });
32
+ window.addEventListener('load', function () {
33
+ if ('requestIdleCallback' in window) {
34
+ requestIdleCallback(loadAffiliate, { timeout: 5000 });
35
+ } else {
36
+ setTimeout(loadAffiliate, 4000);
37
+ }
38
+ });
39
+ })();
40
+ </script>
@@ -11,7 +11,10 @@ export { default as AffiliateCardSection } from './AffiliateCardSection.astro';
11
11
  export { default as AffiliateCarousel } from './AffiliateCarousel.astro';
12
12
  export { default as AffiliateComparisonTable } from './AffiliateComparisonTable.astro';
13
13
  export { default as HotelAffiliateCTA } from './HotelAffiliateCTA.astro';
14
- export { externalResources } from './resources';
14
+ export { externalResources, emeraldResources, EMERALD_ORIGIN } from './resources';
15
15
  export type { WidgetKind, CspDirective, WidgetResources } from './resources';
16
16
  export type { AffiliateCardLabels, AffiliateCardItem, HotelCtaLabels, ComparisonRow, ComparisonTableLabels } from './types';
17
17
  export { selectActivities, type SelectableActivity, type SelectOptions } from './select';
18
+ export { default as EmeraldLoader } from './EmeraldLoader.astro';
19
+ export { setupTravelpayoutsStyler } from './travelpayoutsStyler.ts';
20
+ export type { TravelpayoutsStylerOptions, TravelpayoutsStylerColors } from './travelpayoutsStyler.ts';
@@ -28,3 +28,10 @@ export const externalResources: Record<WidgetKind, WidgetResources> = {
28
28
  },
29
29
  };
30
30
 
31
+
32
+ /** The Travelpayouts Emerald loader every page carries (EmeraldLoader): https://emrldtp.com/<base64 trs>.js?t=<trs>. */
33
+ export const EMERALD_ORIGIN = 'https://emrldtp.com';
34
+
35
+ export const emeraldResources: Partial<Record<CspDirective, readonly string[]>> = {
36
+ 'script-src': [EMERALD_ORIGIN],
37
+ };