@brandlab-digital/site-next 3.14.0 → 3.15.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/dist/analytics.d.ts +1 -1
- package/dist/analytics.d.ts.map +1 -1
- package/dist/analytics.js +2 -0
- package/dist/analytics.js.map +1 -1
- package/dist/navigation/constants.d.ts +11 -2
- package/dist/navigation/constants.d.ts.map +1 -1
- package/dist/navigation/constants.js +11 -2
- package/dist/navigation/constants.js.map +1 -1
- package/dist/navigation/index.d.ts +1 -1
- package/dist/navigation/index.d.ts.map +1 -1
- package/dist/navigation/index.js +1 -1
- package/dist/navigation/index.js.map +1 -1
- package/dist/navigation/use-back-to-top.d.ts.map +1 -1
- package/dist/navigation/use-back-to-top.js +12 -4
- package/dist/navigation/use-back-to-top.js.map +1 -1
- package/dist/navigation/use-header-metrics.d.ts +1 -1
- package/dist/navigation/use-header-metrics.js +1 -1
- package/dist/navigation/use-header-metrics.js.map +1 -1
- package/dist/navigation/use-sticky-call-bar.d.ts +2 -1
- package/dist/navigation/use-sticky-call-bar.d.ts.map +1 -1
- package/dist/navigation/use-sticky-call-bar.js +11 -2
- package/dist/navigation/use-sticky-call-bar.js.map +1 -1
- package/dist/ui-kit/brandlab-skin.d.ts +26 -0
- package/dist/ui-kit/brandlab-skin.d.ts.map +1 -1
- package/dist/ui-kit/brandlab-skin.js.map +1 -1
- package/dist/ui-kit/prefetch.d.ts +19 -10
- package/dist/ui-kit/prefetch.d.ts.map +1 -1
- package/dist/ui-kit/prefetch.js +23 -10
- package/dist/ui-kit/prefetch.js.map +1 -1
- package/dist/ui-kit/primitives/index.d.ts +1 -1
- package/dist/ui-kit/primitives/index.d.ts.map +1 -1
- package/dist/ui-kit/primitives/index.js.map +1 -1
- package/dist/ui-kit/primitives/links.js +1 -1
- package/dist/ui-kit/primitives/links.js.map +1 -1
- package/dist/ui-kit/primitives/lists.d.ts +9 -1
- package/dist/ui-kit/primitives/lists.d.ts.map +1 -1
- package/dist/ui-kit/primitives/lists.js +1 -1
- package/dist/ui-kit/primitives/lists.js.map +1 -1
- package/dist/ui-kit/primitives/quote-button.d.ts +25 -2
- package/dist/ui-kit/primitives/quote-button.d.ts.map +1 -1
- package/dist/ui-kit/primitives/quote-button.js +25 -3
- package/dist/ui-kit/primitives/quote-button.js.map +1 -1
- package/dist/ui-kit/repeated-slots.d.ts +32 -0
- package/dist/ui-kit/repeated-slots.d.ts.map +1 -1
- package/dist/ui-kit/repeated-slots.js +16 -0
- package/dist/ui-kit/repeated-slots.js.map +1 -1
- package/dist/ui-kit/sections/area-map.d.ts +64 -0
- package/dist/ui-kit/sections/area-map.d.ts.map +1 -0
- package/dist/ui-kit/sections/area-map.js +142 -0
- package/dist/ui-kit/sections/area-map.js.map +1 -0
- package/dist/ui-kit/sections/before-after.d.ts +59 -0
- package/dist/ui-kit/sections/before-after.d.ts.map +1 -0
- package/dist/ui-kit/sections/before-after.js +42 -0
- package/dist/ui-kit/sections/before-after.js.map +1 -0
- package/dist/ui-kit/sections/council-rules.d.ts +64 -0
- package/dist/ui-kit/sections/council-rules.d.ts.map +1 -0
- package/dist/ui-kit/sections/council-rules.js +46 -0
- package/dist/ui-kit/sections/council-rules.js.map +1 -0
- package/dist/ui-kit/sections/coverage.d.ts +53 -0
- package/dist/ui-kit/sections/coverage.d.ts.map +1 -0
- package/dist/ui-kit/sections/coverage.js +57 -0
- package/dist/ui-kit/sections/coverage.js.map +1 -0
- package/dist/ui-kit/sections/emergency-band.d.ts +60 -0
- package/dist/ui-kit/sections/emergency-band.d.ts.map +1 -0
- package/dist/ui-kit/sections/emergency-band.js +43 -0
- package/dist/ui-kit/sections/emergency-band.js.map +1 -0
- package/dist/ui-kit/sections/gallery.d.ts +60 -0
- package/dist/ui-kit/sections/gallery.d.ts.map +1 -0
- package/dist/ui-kit/sections/gallery.js +39 -0
- package/dist/ui-kit/sections/gallery.js.map +1 -0
- package/dist/ui-kit/sections/hero.d.ts +5 -0
- package/dist/ui-kit/sections/hero.d.ts.map +1 -1
- package/dist/ui-kit/sections/hero.js +1 -1
- package/dist/ui-kit/sections/hero.js.map +1 -1
- package/dist/ui-kit/sections/index.d.ts +14 -1
- package/dist/ui-kit/sections/index.d.ts.map +1 -1
- package/dist/ui-kit/sections/index.js +14 -1
- package/dist/ui-kit/sections/index.js.map +1 -1
- package/dist/ui-kit/sections/licences.d.ts +72 -0
- package/dist/ui-kit/sections/licences.d.ts.map +1 -0
- package/dist/ui-kit/sections/licences.js +46 -0
- package/dist/ui-kit/sections/licences.js.map +1 -0
- package/dist/ui-kit/sections/local-facts.d.ts +78 -0
- package/dist/ui-kit/sections/local-facts.d.ts.map +1 -0
- package/dist/ui-kit/sections/local-facts.js +32 -0
- package/dist/ui-kit/sections/local-facts.js.map +1 -0
- package/dist/ui-kit/sections/nearby-areas.d.ts +50 -0
- package/dist/ui-kit/sections/nearby-areas.d.ts.map +1 -0
- package/dist/ui-kit/sections/nearby-areas.js +49 -0
- package/dist/ui-kit/sections/nearby-areas.js.map +1 -0
- package/dist/ui-kit/sections/nearby-jobs.d.ts +66 -0
- package/dist/ui-kit/sections/nearby-jobs.d.ts.map +1 -0
- package/dist/ui-kit/sections/nearby-jobs.js +49 -0
- package/dist/ui-kit/sections/nearby-jobs.js.map +1 -0
- package/dist/ui-kit/sections/projects.d.ts +79 -0
- package/dist/ui-kit/sections/projects.d.ts.map +1 -0
- package/dist/ui-kit/sections/projects.js +69 -0
- package/dist/ui-kit/sections/projects.js.map +1 -0
- package/dist/ui-kit/sections/quote-checklist.d.ts +43 -0
- package/dist/ui-kit/sections/quote-checklist.d.ts.map +1 -0
- package/dist/ui-kit/sections/quote-checklist.js +19 -0
- package/dist/ui-kit/sections/quote-checklist.js.map +1 -0
- package/dist/ui-kit/sections/related-services.d.ts +41 -0
- package/dist/ui-kit/sections/related-services.d.ts.map +1 -0
- package/dist/ui-kit/sections/related-services.js +52 -0
- package/dist/ui-kit/sections/related-services.js.map +1 -0
- package/dist/ui-kit/sections/reviews-band.d.ts +11 -0
- package/dist/ui-kit/sections/reviews-band.d.ts.map +1 -1
- package/dist/ui-kit/sections/reviews-band.js +11 -0
- package/dist/ui-kit/sections/reviews-band.js.map +1 -1
- package/dist/ui-kit/sections/services-grid.js +1 -1
- package/dist/ui-kit/sections/services-grid.js.map +1 -1
- package/dist/ui-kit/sections/work-photos.d.ts +15 -0
- package/dist/ui-kit/sections/work-photos.d.ts.map +1 -0
- package/dist/ui-kit/sections/work-photos.js +24 -0
- package/dist/ui-kit/sections/work-photos.js.map +1 -0
- package/dist/ui-kit/shell/back-to-top.js +1 -1
- package/dist/ui-kit/shell/back-to-top.js.map +1 -1
- package/dist/ui-kit/shell/current-link.d.ts +21 -5
- package/dist/ui-kit/shell/current-link.d.ts.map +1 -1
- package/dist/ui-kit/shell/current-link.js +16 -5
- package/dist/ui-kit/shell/current-link.js.map +1 -1
- package/dist/ui-kit/shell/drawer-client.js +1 -1
- package/dist/ui-kit/shell/drawer-client.js.map +1 -1
- package/dist/ui-kit/shell/intent-prefetch.d.ts.map +1 -1
- package/dist/ui-kit/shell/intent-prefetch.js +3 -2
- package/dist/ui-kit/shell/intent-prefetch.js.map +1 -1
- package/dist/ui-kit/shell/mobile-call-bar.d.ts +6 -1
- package/dist/ui-kit/shell/mobile-call-bar.d.ts.map +1 -1
- package/dist/ui-kit/shell/mobile-call-bar.js +6 -3
- package/dist/ui-kit/shell/mobile-call-bar.js.map +1 -1
- package/dist/ui-kit/shell/mobile-drawer.js +2 -2
- package/dist/ui-kit/shell/mobile-drawer.js.map +1 -1
- package/dist/ui-kit/shell/shell-quote.d.ts +10 -0
- package/dist/ui-kit/shell/shell-quote.d.ts.map +1 -0
- package/dist/ui-kit/shell/shell-quote.js +13 -0
- package/dist/ui-kit/shell/shell-quote.js.map +1 -0
- package/dist/ui-kit/shell/site-header.d.ts +1 -1
- package/dist/ui-kit/shell/site-header.js +4 -4
- package/dist/ui-kit/shell/site-header.js.map +1 -1
- package/dist/ui-kit/structure.css +410 -7
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"related-services.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/related-services.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * `RelatedServices` (S28, `related-services`, 3.15.0): the other services in this place (\"Other services in Castle\n * Hill\") or a service page's siblings. Server component.\n *\n * Each entry links to its service-in-suburb page where one exists, or to its section on the suburb page\n * (`#tree-pruning`); the href always comes from the site's data. At most 8 (K11). Variants:\n * - `list` (the default, lean): a `LinkList` of plain `<li><a>` entries, nothing prefetched\n * (`data-bl-links=\"other-services\"`, the `otherServicesInSuburb` role in `LINK_PREFETCH_POLICY`);\n * - `cards`: `ServiceCard`s with a summary and photo, each card's one link prefetching on intent (`contentCard`).\n */\nimport Link from 'next/link'\nimport type { ReactNode } from 'react'\nimport { SECTION_READY, type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order'\nimport { LINK_PREFETCH_LIMITS, LINK_PREFETCH_POLICY } from '../prefetch.js'\nimport { LinkList } from '../primitives/lists.js'\nimport { Section, SectionHeading, type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js'\nimport type { RepeatedSlotOf } from '../repeated-slots.js'\nimport { slot, type SkinEntry } from '../skin.js'\nimport type { UiLink } from '../types.js'\nimport { ServiceCard, defaultImageSizes, type ServiceCardData, type ServicesGridLayout } from './services-grid.js'\n\n/** The most services the section lists (K11; the in-content intent-prefetch limit). */\nexport const RELATED_SERVICES_LIMIT = LINK_PREFETCH_LIMITS.contentCard\n\nexport const RELATED_SERVICES_VARIANTS = ['list', 'cards'] as const\nexport type RelatedServicesVariant = (typeof RELATED_SERVICES_VARIANTS)[number]\nexport type RelatedServicesSlot = SectionSlot | SectionHeadingSlot | 'list' | 'more' | RepeatedSlotOf<'relatedServices'>\n/** `cards`: rendered once per service, styled from the skin stylesheet, never by class. */\nexport type RelatedServicesRepeatedSlot = RepeatedSlotOf<'relatedServices'>\nexport interface RelatedServicesLayout {\n /** Columns on wide screens: `list` 1 to 4 (default 3), `cards` 2 to 4 (default 3). */\n readonly columns?: 1 | 2 | 3 | 4\n /** `cards`: the photo's crop. Defaults to 4:3. */\n readonly ratio?: ServicesGridLayout['ratio']\n}\nexport type RelatedServicesSkin = SkinEntry<RelatedServicesSlot, RelatedServicesVariant, RelatedServicesLayout, RelatedServicesRepeatedSlot>\n\nexport interface RelatedServicesProps extends RelatedServicesSkin {\n /** The H2, for example \"Other services in Castle Hill\". */\n readonly title: ReactNode\n readonly eyebrow?: ReactNode\n readonly lead?: ReactNode\n /** The other services, in money order; `cards` uses their summaries and photos. */\n readonly services: readonly ServiceCardData[]\n /** A link after the list, for example the services hub. Never prefetched. */\n readonly more?: UiLink\n /** `cards`: the photo `sizes`. */\n readonly imageSizes?: string\n /** Defaults to `related-services-heading`. */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n}\n\n/** The guard for `SectionSequence`. */\nexport function relatedServicesReady({ services }: Pick<RelatedServicesProps, 'services'>): BrandlabSectionReadiness {\n return services.length > 0 ? SECTION_READY : { render: false, reason: 'no-other-services' }\n}\n\nfunction assertServices(services: readonly ServiceCardData[]) {\n if (services.length > RELATED_SERVICES_LIMIT) {\n throw new Error(`RelatedServices: ${services.length} services; the section lists at most ${RELATED_SERVICES_LIMIT} (K11). Link the services hub for the rest.`)\n }\n const ids = new Set<string>()\n const hrefs = new Set<string>()\n for (const service of services) {\n if (!service.id || ids.has(service.id)) throw new Error(`RelatedServices: every service needs its own id (\"${service.id}\" is empty or repeated).`)\n ids.add(service.id)\n if (!service.title?.trim() || !service.href?.trim()) throw new Error(`RelatedServices: \"${service.id}\" needs a title and a link.`)\n if (hrefs.has(service.href)) throw new Error(`RelatedServices: two services link to ${service.href}.`)\n hrefs.add(service.href)\n }\n}\n\nexport function RelatedServices({\n title,\n eyebrow,\n lead,\n services,\n more,\n imageSizes,\n headingId = 'related-services-heading',\n tone,\n variant = 'list',\n layout,\n classes,\n}: RelatedServicesProps) {\n if (services.length === 0) return null\n assertServices(services)\n const columns = layout?.columns ?? 3\n const ratio = layout?.ratio ?? '4:3'\n const cardColumns = (columns < 2 ? 2 : columns) as 2 | 3 | 4\n return (\n <Section\n id=\"related-services\"\n labelledBy={headingId}\n tone={tone}\n variant={variant}\n classes={classes}\n hooks={variant === 'cards' ? { 'data-columns': String(cardColumns), 'data-ratio': ratio } : undefined}\n >\n <SectionHeading id={headingId} title={title} eyebrow={eyebrow} lead={lead} classes={classes} />\n {variant === 'cards' ? (\n <ul role=\"list\" {...slot(classes, 'list')}>\n {services.map((service) => (\n <ServiceCard\n key={service.id}\n service={service}\n variant=\"cards\"\n imageSizes={imageSizes ?? defaultImageSizes('cards', cardColumns)}\n ratio={ratio}\n prefetch={LINK_PREFETCH_POLICY.contentCard}\n />\n ))}\n </ul>\n ) : (\n <LinkList\n links={services.map((service) => ({ label: service.title, href: service.href }))}\n kind=\"other-services\"\n prefetch={LINK_PREFETCH_POLICY.otherServicesInSuburb}\n layout={{ columns }}\n classes={{ root: classes?.list }}\n />\n )}\n {more ? (\n <p {...slot(classes, 'more')}>\n <Link href={more.href} prefetch={false}>\n {more.label}\n </Link>\n </p>\n ) : null}\n </Section>\n )\n}\n"]}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { type ReactNode } from 'react';
|
|
17
17
|
import { type BusinessProfileReview, type PreparedBusinessProfileReviews } from '@brandlab-digital/site-core/google-business-reviews';
|
|
18
|
+
import { type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order';
|
|
18
19
|
import { type SectionTone } from '../primitives/layout.js';
|
|
19
20
|
import type { RepeatedSlotOf } from '../repeated-slots.js';
|
|
20
21
|
import { type SkinEntry, type SlotClasses } from '../skin.js';
|
|
@@ -34,6 +35,16 @@ export type ReviewsBandRepeatedSlot = RepeatedSlotOf<'reviewsBand'>;
|
|
|
34
35
|
export type ReviewsBandSkin = SkinEntry<ReviewsBandSlot, ReviewsBandVariant, ReviewsBandLayout, ReviewsBandRepeatedSlot>;
|
|
35
36
|
type Classes = SlotClasses<Exclude<ReviewsBandSlot, ReviewsBandRepeatedSlot>> | undefined;
|
|
36
37
|
export declare const REVIEWS_BAND_HEADING_ID = "bl-reviews-heading";
|
|
38
|
+
/** The skip reason a band with nothing to show records in the section manifest: the feed has no reviews or rating. */
|
|
39
|
+
export declare const REVIEWS_BAND_SKIP_REASON = "no-feed-reviews";
|
|
40
|
+
/**
|
|
41
|
+
* The guard for `SectionSequence` (3.15.0), from the same prepared reviews the band renders: ready when the band
|
|
42
|
+
* renders (written reviews, or Google's rating alone as the summary fallback); otherwise `no-feed-reviews`, for a
|
|
43
|
+
* missing feed, an empty one, or the `summary` variant without a rating.
|
|
44
|
+
*/
|
|
45
|
+
export declare function reviewsBandReady(reviews: PreparedBusinessProfileReviews | null | undefined, { variant }?: {
|
|
46
|
+
readonly variant?: ReviewsBandVariant;
|
|
47
|
+
}): BrandlabSectionReadiness;
|
|
37
48
|
/**
|
|
38
49
|
* A long review split at a sentence end (or else a space) near `limit`, so the first part reads on its own. The two
|
|
39
50
|
* parts together are the review word for word. Short reviews, and reviews only a little over the limit, don't fold.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reviews-band.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/reviews-band.tsx"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"reviews-band.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/reviews-band.tsx"],"names":[],"mappings":"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"}
|
|
@@ -16,6 +16,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
16
16
|
*/
|
|
17
17
|
import { Children } from 'react';
|
|
18
18
|
import { businessProfileReviewMentions, } from '@brandlab-digital/site-core/google-business-reviews';
|
|
19
|
+
import { SECTION_READY } from '@brandlab-digital/site-core/section-order';
|
|
19
20
|
import { RatingStars, Section, SectionHeading } from '../primitives/layout.js';
|
|
20
21
|
import { repeatedSlot, slot } from '../skin.js';
|
|
21
22
|
import { hasGoogleRating, RatingChip } from './rating-chip.js';
|
|
@@ -34,6 +35,16 @@ function shownVariant(reviews, variant) {
|
|
|
34
35
|
}
|
|
35
36
|
const MONTH = new Intl.DateTimeFormat('en-AU', { month: 'long', year: 'numeric', timeZone: 'Australia/Sydney' });
|
|
36
37
|
const DAY = new Intl.DateTimeFormat('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit', timeZone: 'Australia/Sydney' });
|
|
38
|
+
/** The skip reason a band with nothing to show records in the section manifest: the feed has no reviews or rating. */
|
|
39
|
+
export const REVIEWS_BAND_SKIP_REASON = 'no-feed-reviews';
|
|
40
|
+
/**
|
|
41
|
+
* The guard for `SectionSequence` (3.15.0), from the same prepared reviews the band renders: ready when the band
|
|
42
|
+
* renders (written reviews, or Google's rating alone as the summary fallback); otherwise `no-feed-reviews`, for a
|
|
43
|
+
* missing feed, an empty one, or the `summary` variant without a rating.
|
|
44
|
+
*/
|
|
45
|
+
export function reviewsBandReady(reviews, { variant = 'band' } = {}) {
|
|
46
|
+
return shownVariant(reviews, variant) ? SECTION_READY : { render: false, reason: REVIEWS_BAND_SKIP_REASON };
|
|
47
|
+
}
|
|
37
48
|
/**
|
|
38
49
|
* A long review split at a sentence end (or else a space) near `limit`, so the first part reads on its own. The two
|
|
39
50
|
* parts together are the review word for word. Short reviews, and reviews only a little over the limit, don't fold.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reviews-band.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/reviews-band.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * S7 `ReviewsBand` (`reviews`): prominent, honest proof from the Business Profile feed. Server component, no client\n * JS. Google's own average and total sit beside the selected reviews with a link to the profile; the text is verbatim\n * and always in the HTML (a long review folds behind a native \"Read more\" disclosure); the band says how the reviews\n * were chosen (\"Selected five-star Google reviews\" or \"Recent Google reviews\"). No review or rating schema: a business\n * that controls its own reviews isn't eligible for review stars.\n *\n * Data comes only from `prepareBusinessProfileReviews` (site-core), which applies the policy, mention ordering, the\n * 3.7.0 exclude guard and the limit. No prepared reviews, or none and no rating, renders nothing at all (no heading).\n * With no written reviews but a rating, every variant renders as `summary`: the heading and the rating chip only.\n * Never place it on the contact page or inside another section.\n *\n * Sites that arrange their own layout use the parts (`ReviewsBand.Root`, `.Heading`, `.Summary`, `.List`, `.Item`):\n * the root keeps the section semantics, the empty rule and the disclosure line; the parts keep the data and markup.\n */\nimport { Children, type ReactNode } from 'react'\nimport {\n businessProfileReviewMentions,\n type BusinessProfileReview,\n type PreparedBusinessProfileReviews,\n} from '@brandlab-digital/site-core/google-business-reviews'\nimport { RatingStars, Section, SectionHeading, type SectionTone } from '../primitives/layout.js'\nimport type { RepeatedSlotOf } from '../repeated-slots.js'\nimport { repeatedSlot, slot, type SkinEntry, type SlotClasses } from '../skin.js'\nimport { hasGoogleRating, RatingChip } from './rating-chip.js'\n\nexport const REVIEWS_BAND_VARIANTS = ['band', 'feature', 'cards', 'wall', 'summary'] as const\nexport type ReviewsBandVariant = (typeof REVIEWS_BAND_VARIANTS)[number]\n\nexport type ReviewsBandSlot =\n | 'root'\n | 'inner'\n | 'top'\n | 'head'\n | 'eyebrow'\n | 'heading'\n | 'lead'\n | 'summary'\n | 'rating'\n | 'source'\n | 'list'\n | 'item'\n | 'stars'\n | 'quote'\n | 'more'\n | 'author'\n | 'avatar'\n | 'name'\n | 'date'\n | 'disclosure'\n | 'profileLink'\n\nexport interface ReviewsBandLayout {\n readonly columns?: 1 | 2 | 3\n readonly align?: 'start' | 'center'\n /** Reviewer photos beside the names (Google's own small photos, decorative). */\n readonly avatars?: boolean\n /** Characters shown before a long review folds behind \"Read more\" (default 320); `false` never folds. */\n readonly excerpt?: number | false\n}\n\n/** Rendered once per review: styled from the skin stylesheet, never by class (3.14.0). */\nexport type ReviewsBandRepeatedSlot = RepeatedSlotOf<'reviewsBand'>\nexport type ReviewsBandSkin = SkinEntry<ReviewsBandSlot, ReviewsBandVariant, ReviewsBandLayout, ReviewsBandRepeatedSlot>\n\ntype Classes = SlotClasses<Exclude<ReviewsBandSlot, ReviewsBandRepeatedSlot>> | undefined\n\nexport const REVIEWS_BAND_HEADING_ID = 'bl-reviews-heading'\nconst DEFAULT_EXCERPT = 320\n\n/**\n * The variant a band renders, or null when it has nothing true to show. With no written reviews every variant is\n * `summary`, and `summary` needs Google's rating: a heading over nothing never renders.\n */\nfunction shownVariant(reviews: PreparedBusinessProfileReviews | null | undefined, variant: ReviewsBandVariant): ReviewsBandVariant | null {\n if (!reviews) return null\n const shown = reviews.reviews.length === 0 ? 'summary' : variant\n return shown === 'summary' && !hasGoogleRating(reviews) ? null : shown\n}\n\nconst MONTH = new Intl.DateTimeFormat('en-AU', { month: 'long', year: 'numeric', timeZone: 'Australia/Sydney' })\nconst DAY = new Intl.DateTimeFormat('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit', timeZone: 'Australia/Sydney' })\n\n/**\n * A long review split at a sentence end (or else a space) near `limit`, so the first part reads on its own. The two\n * parts together are the review word for word. Short reviews, and reviews only a little over the limit, don't fold.\n */\nexport function splitReviewText(text: string, limit: number | false): readonly [string, string] | null {\n if (limit === false || text.length <= limit * 1.25) return null\n const window = text.slice(0, limit)\n const sentence = Math.max(...['. ', '! ', '? ', '\\n'].map((mark) => window.lastIndexOf(mark)))\n const space = window.lastIndexOf(' ')\n const cut = sentence >= limit * 0.4 ? sentence + 1 : space > 0 ? space : limit\n return [text.slice(0, cut).trimEnd(), text.slice(cut).trimStart()]\n}\n\n/** The `feature` variant leads with the longest review naming a mention word, or else the longest review. */\nfunction featureOrder(reviews: PreparedBusinessProfileReviews): BusinessProfileReview[] {\n const named = reviews.mention.length > 0 ? reviews.reviews.filter((review) => businessProfileReviewMentions(review, reviews.mention)) : []\n const pool = named.length > 0 ? named : reviews.reviews\n const lead = pool.reduce<BusinessProfileReview | undefined>((longest, review) => (!longest || review.text.length > longest.text.length ? review : longest), undefined)\n return lead ? [lead, ...reviews.reviews.filter((review) => review !== lead)] : []\n}\n\nexport interface ReviewsBandRootProps extends ReviewsBandSkin {\n readonly reviews: PreparedBusinessProfileReviews | null | undefined\n /** The id of the band's H2 (default `bl-reviews-heading`). */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n /** Signature art, rendered `aria-hidden`. */\n readonly decoration?: ReactNode\n readonly children: ReactNode\n}\n\n/**\n * The section: `data-bl-section=\"reviews\"` with `data-bl-source=\"feed\"` (the ready gate's mark for feed numbers),\n * the empty rule, and the locked disclosure line after the children.\n */\nexport function ReviewsBandRoot({\n reviews,\n headingId = REVIEWS_BAND_HEADING_ID,\n tone,\n decoration,\n variant = 'band',\n layout,\n classes,\n children,\n}: ReviewsBandRootProps) {\n const shown = shownVariant(reviews, variant)\n if (!shown || !reviews) return null\n return (\n <Section\n id=\"reviews\"\n labelledBy={headingId}\n tone={tone}\n variant={shown}\n classes={{ root: classes?.root, inner: classes?.inner }}\n decoration={decoration}\n hooks={{ 'data-bl-source': 'feed', 'data-columns': String(layout?.columns ?? 3), 'data-align': layout?.align ?? 'start' }}\n >\n {children}\n {shown === 'summary' ? null : (\n <p {...slot(classes, 'disclosure')}>\n {reviews.policy === 'five-star' ? 'Selected five-star Google reviews.' : 'Recent Google reviews.'}{' '}\n <a {...slot(classes, 'profileLink')} href={reviews.profileUrl} target=\"_blank\" rel=\"noopener noreferrer\">\n Read all reviews on Google\n </a>\n </p>\n )}\n </Section>\n )\n}\n\nexport interface ReviewsBandHeadingProps {\n /** The H2's id; the same `headingId` the root has (default `bl-reviews-heading`). */\n readonly id?: string\n /** The H2 text the site owns. */\n readonly children: string\n readonly eyebrow?: string\n readonly intro?: string\n readonly layout?: Pick<ReviewsBandLayout, 'align'>\n readonly classes?: Classes\n}\n\n/** The heading block: an optional eyebrow, the H2 and an optional intro. */\nexport function ReviewsBandHeading({ id = REVIEWS_BAND_HEADING_ID, children, eyebrow, intro, layout, classes }: ReviewsBandHeadingProps) {\n return (\n <SectionHeading\n id={id}\n title={children}\n eyebrow={eyebrow}\n lead={intro}\n layout={{ align: layout?.align ?? 'start' }}\n classes={{ head: classes?.head, eyebrow: classes?.eyebrow, heading: classes?.heading, lead: classes?.lead }}\n />\n )\n}\n\nexport interface ReviewsBandSummaryProps {\n readonly reviews: PreparedBusinessProfileReviews | null | undefined\n readonly classes?: Classes\n}\n\n/** Google's own average and total with the profile link; nothing when the feed has no rating. */\nexport function ReviewsBandSummary({ reviews, classes }: ReviewsBandSummaryProps) {\n if (!hasGoogleRating(reviews)) return null\n return (\n <div {...slot(classes, 'summary')}>\n <RatingChip reviews={reviews} classes={{ rating: classes?.rating, source: classes?.source }} />\n </div>\n )\n}\n\n/** The review list; nothing without items. */\nexport function ReviewsBandList({ classes, children }: { readonly classes?: Classes; readonly children: ReactNode }) {\n if (Children.toArray(children).length === 0) return null\n return <ul {...slot(classes, 'list')}>{children}</ul>\n}\n\nexport interface ReviewsBandItemProps {\n readonly review: BusinessProfileReview\n /** The `feature` variant's wide lead card. */\n readonly lead?: boolean\n readonly layout?: Pick<ReviewsBandLayout, 'avatars' | 'excerpt'>\n readonly classes?: Classes\n}\n\n/** One review, word for word: its stars, the quote and the reviewer with the month. */\nexport function ReviewsBandItem({ review, lead = false, layout, classes }: ReviewsBandItemProps) {\n const parts = splitReviewText(review.text, layout?.excerpt ?? DEFAULT_EXCERPT)\n const date = new Date(review.createTime)\n return (\n <li {...repeatedSlot('item')} data-lead={lead ? 'true' : undefined}>\n <figure>\n <span {...repeatedSlot('stars')}>\n <RatingStars rating={review.rating} label={`Rated ${review.rating} out of 5`} />\n </span>\n <blockquote {...repeatedSlot('quote')}>\n {parts ? (\n <>\n <p>{parts[0]}</p>\n <details {...repeatedSlot('more')}>\n <summary>Read more</summary>\n <p>{parts[1]}</p>\n </details>\n </>\n ) : (\n <p>{review.text}</p>\n )}\n </blockquote>\n <figcaption {...repeatedSlot('author')}>\n {layout?.avatars && review.authorPhotoUrl ? (\n <img\n {...repeatedSlot('avatar')}\n src={review.authorPhotoUrl}\n alt=\"\"\n width={48}\n height={48}\n loading=\"lazy\"\n decoding=\"async\"\n referrerPolicy=\"no-referrer\"\n />\n ) : null}\n <span {...repeatedSlot('name')}>{review.authorName}</span>{' '}\n <time {...repeatedSlot('date')} dateTime={DAY.format(date)}>\n {MONTH.format(date)}\n </time>\n </figcaption>\n </figure>\n </li>\n )\n}\n\nexport interface ReviewsBandProps extends ReviewsBandSkin {\n /** From `prepareBusinessProfileReviews`; null renders nothing at all. */\n readonly reviews: PreparedBusinessProfileReviews | null | undefined\n /** The H2 text the site owns. */\n readonly heading: string\n readonly eyebrow?: string\n readonly intro?: string\n /** The id of the band's H2 (default `bl-reviews-heading`). */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n /** Signature art, rendered `aria-hidden`. */\n readonly decoration?: ReactNode\n}\n\nfunction ReviewsBandSection({ reviews, heading, eyebrow, intro, headingId = REVIEWS_BAND_HEADING_ID, tone, decoration, variant = 'band', layout, classes }: ReviewsBandProps) {\n const shown = shownVariant(reviews, variant)\n if (!shown || !reviews) return null\n const items = shown === 'summary' ? [] : shown === 'feature' ? featureOrder(reviews) : reviews.reviews\n return (\n <ReviewsBandRoot reviews={reviews} headingId={headingId} tone={tone} decoration={decoration} variant={shown} layout={layout} classes={classes}>\n <div {...slot(classes, 'top')}>\n <ReviewsBandHeading id={headingId} eyebrow={eyebrow} intro={intro} layout={layout} classes={classes}>\n {heading}\n </ReviewsBandHeading>\n <ReviewsBandSummary reviews={reviews} classes={classes} />\n </div>\n <ReviewsBandList classes={classes}>\n {items.map((review, index) => (\n <ReviewsBandItem key={review.id} review={review} lead={shown === 'feature' && index === 0} layout={layout} classes={classes} />\n ))}\n </ReviewsBandList>\n </ReviewsBandRoot>\n )\n}\n\n/** The reviews band, with its parts for sites that arrange their own layout. */\nexport const ReviewsBand = Object.assign(ReviewsBandSection, {\n Root: ReviewsBandRoot,\n Heading: ReviewsBandHeading,\n Summary: ReviewsBandSummary,\n List: ReviewsBandList,\n Item: ReviewsBandItem,\n})\n"]}
|
|
1
|
+
{"version":3,"file":"reviews-band.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/reviews-band.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * S7 `ReviewsBand` (`reviews`): prominent, honest proof from the Business Profile feed. Server component, no client\n * JS. Google's own average and total sit beside the selected reviews with a link to the profile; the text is verbatim\n * and always in the HTML (a long review folds behind a native \"Read more\" disclosure); the band says how the reviews\n * were chosen (\"Selected five-star Google reviews\" or \"Recent Google reviews\"). No review or rating schema: a business\n * that controls its own reviews isn't eligible for review stars.\n *\n * Data comes only from `prepareBusinessProfileReviews` (site-core), which applies the policy, mention ordering, the\n * 3.7.0 exclude guard and the limit. No prepared reviews, or none and no rating, renders nothing at all (no heading).\n * With no written reviews but a rating, every variant renders as `summary`: the heading and the rating chip only.\n * Never place it on the contact page or inside another section.\n *\n * Sites that arrange their own layout use the parts (`ReviewsBand.Root`, `.Heading`, `.Summary`, `.List`, `.Item`):\n * the root keeps the section semantics, the empty rule and the disclosure line; the parts keep the data and markup.\n */\nimport { Children, type ReactNode } from 'react'\nimport {\n businessProfileReviewMentions,\n type BusinessProfileReview,\n type PreparedBusinessProfileReviews,\n} from '@brandlab-digital/site-core/google-business-reviews'\nimport { SECTION_READY, type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order'\nimport { RatingStars, Section, SectionHeading, type SectionTone } from '../primitives/layout.js'\nimport type { RepeatedSlotOf } from '../repeated-slots.js'\nimport { repeatedSlot, slot, type SkinEntry, type SlotClasses } from '../skin.js'\nimport { hasGoogleRating, RatingChip } from './rating-chip.js'\n\nexport const REVIEWS_BAND_VARIANTS = ['band', 'feature', 'cards', 'wall', 'summary'] as const\nexport type ReviewsBandVariant = (typeof REVIEWS_BAND_VARIANTS)[number]\n\nexport type ReviewsBandSlot =\n | 'root'\n | 'inner'\n | 'top'\n | 'head'\n | 'eyebrow'\n | 'heading'\n | 'lead'\n | 'summary'\n | 'rating'\n | 'source'\n | 'list'\n | 'item'\n | 'stars'\n | 'quote'\n | 'more'\n | 'author'\n | 'avatar'\n | 'name'\n | 'date'\n | 'disclosure'\n | 'profileLink'\n\nexport interface ReviewsBandLayout {\n readonly columns?: 1 | 2 | 3\n readonly align?: 'start' | 'center'\n /** Reviewer photos beside the names (Google's own small photos, decorative). */\n readonly avatars?: boolean\n /** Characters shown before a long review folds behind \"Read more\" (default 320); `false` never folds. */\n readonly excerpt?: number | false\n}\n\n/** Rendered once per review: styled from the skin stylesheet, never by class (3.14.0). */\nexport type ReviewsBandRepeatedSlot = RepeatedSlotOf<'reviewsBand'>\nexport type ReviewsBandSkin = SkinEntry<ReviewsBandSlot, ReviewsBandVariant, ReviewsBandLayout, ReviewsBandRepeatedSlot>\n\ntype Classes = SlotClasses<Exclude<ReviewsBandSlot, ReviewsBandRepeatedSlot>> | undefined\n\nexport const REVIEWS_BAND_HEADING_ID = 'bl-reviews-heading'\nconst DEFAULT_EXCERPT = 320\n\n/**\n * The variant a band renders, or null when it has nothing true to show. With no written reviews every variant is\n * `summary`, and `summary` needs Google's rating: a heading over nothing never renders.\n */\nfunction shownVariant(reviews: PreparedBusinessProfileReviews | null | undefined, variant: ReviewsBandVariant): ReviewsBandVariant | null {\n if (!reviews) return null\n const shown = reviews.reviews.length === 0 ? 'summary' : variant\n return shown === 'summary' && !hasGoogleRating(reviews) ? null : shown\n}\n\nconst MONTH = new Intl.DateTimeFormat('en-AU', { month: 'long', year: 'numeric', timeZone: 'Australia/Sydney' })\nconst DAY = new Intl.DateTimeFormat('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit', timeZone: 'Australia/Sydney' })\n\n/** The skip reason a band with nothing to show records in the section manifest: the feed has no reviews or rating. */\nexport const REVIEWS_BAND_SKIP_REASON = 'no-feed-reviews'\n\n/**\n * The guard for `SectionSequence` (3.15.0), from the same prepared reviews the band renders: ready when the band\n * renders (written reviews, or Google's rating alone as the summary fallback); otherwise `no-feed-reviews`, for a\n * missing feed, an empty one, or the `summary` variant without a rating.\n */\nexport function reviewsBandReady(\n reviews: PreparedBusinessProfileReviews | null | undefined,\n { variant = 'band' }: { readonly variant?: ReviewsBandVariant } = {},\n): BrandlabSectionReadiness {\n return shownVariant(reviews, variant) ? SECTION_READY : { render: false, reason: REVIEWS_BAND_SKIP_REASON }\n}\n\n/**\n * A long review split at a sentence end (or else a space) near `limit`, so the first part reads on its own. The two\n * parts together are the review word for word. Short reviews, and reviews only a little over the limit, don't fold.\n */\nexport function splitReviewText(text: string, limit: number | false): readonly [string, string] | null {\n if (limit === false || text.length <= limit * 1.25) return null\n const window = text.slice(0, limit)\n const sentence = Math.max(...['. ', '! ', '? ', '\\n'].map((mark) => window.lastIndexOf(mark)))\n const space = window.lastIndexOf(' ')\n const cut = sentence >= limit * 0.4 ? sentence + 1 : space > 0 ? space : limit\n return [text.slice(0, cut).trimEnd(), text.slice(cut).trimStart()]\n}\n\n/** The `feature` variant leads with the longest review naming a mention word, or else the longest review. */\nfunction featureOrder(reviews: PreparedBusinessProfileReviews): BusinessProfileReview[] {\n const named = reviews.mention.length > 0 ? reviews.reviews.filter((review) => businessProfileReviewMentions(review, reviews.mention)) : []\n const pool = named.length > 0 ? named : reviews.reviews\n const lead = pool.reduce<BusinessProfileReview | undefined>((longest, review) => (!longest || review.text.length > longest.text.length ? review : longest), undefined)\n return lead ? [lead, ...reviews.reviews.filter((review) => review !== lead)] : []\n}\n\nexport interface ReviewsBandRootProps extends ReviewsBandSkin {\n readonly reviews: PreparedBusinessProfileReviews | null | undefined\n /** The id of the band's H2 (default `bl-reviews-heading`). */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n /** Signature art, rendered `aria-hidden`. */\n readonly decoration?: ReactNode\n readonly children: ReactNode\n}\n\n/**\n * The section: `data-bl-section=\"reviews\"` with `data-bl-source=\"feed\"` (the ready gate's mark for feed numbers),\n * the empty rule, and the locked disclosure line after the children.\n */\nexport function ReviewsBandRoot({\n reviews,\n headingId = REVIEWS_BAND_HEADING_ID,\n tone,\n decoration,\n variant = 'band',\n layout,\n classes,\n children,\n}: ReviewsBandRootProps) {\n const shown = shownVariant(reviews, variant)\n if (!shown || !reviews) return null\n return (\n <Section\n id=\"reviews\"\n labelledBy={headingId}\n tone={tone}\n variant={shown}\n classes={{ root: classes?.root, inner: classes?.inner }}\n decoration={decoration}\n hooks={{ 'data-bl-source': 'feed', 'data-columns': String(layout?.columns ?? 3), 'data-align': layout?.align ?? 'start' }}\n >\n {children}\n {shown === 'summary' ? null : (\n <p {...slot(classes, 'disclosure')}>\n {reviews.policy === 'five-star' ? 'Selected five-star Google reviews.' : 'Recent Google reviews.'}{' '}\n <a {...slot(classes, 'profileLink')} href={reviews.profileUrl} target=\"_blank\" rel=\"noopener noreferrer\">\n Read all reviews on Google\n </a>\n </p>\n )}\n </Section>\n )\n}\n\nexport interface ReviewsBandHeadingProps {\n /** The H2's id; the same `headingId` the root has (default `bl-reviews-heading`). */\n readonly id?: string\n /** The H2 text the site owns. */\n readonly children: string\n readonly eyebrow?: string\n readonly intro?: string\n readonly layout?: Pick<ReviewsBandLayout, 'align'>\n readonly classes?: Classes\n}\n\n/** The heading block: an optional eyebrow, the H2 and an optional intro. */\nexport function ReviewsBandHeading({ id = REVIEWS_BAND_HEADING_ID, children, eyebrow, intro, layout, classes }: ReviewsBandHeadingProps) {\n return (\n <SectionHeading\n id={id}\n title={children}\n eyebrow={eyebrow}\n lead={intro}\n layout={{ align: layout?.align ?? 'start' }}\n classes={{ head: classes?.head, eyebrow: classes?.eyebrow, heading: classes?.heading, lead: classes?.lead }}\n />\n )\n}\n\nexport interface ReviewsBandSummaryProps {\n readonly reviews: PreparedBusinessProfileReviews | null | undefined\n readonly classes?: Classes\n}\n\n/** Google's own average and total with the profile link; nothing when the feed has no rating. */\nexport function ReviewsBandSummary({ reviews, classes }: ReviewsBandSummaryProps) {\n if (!hasGoogleRating(reviews)) return null\n return (\n <div {...slot(classes, 'summary')}>\n <RatingChip reviews={reviews} classes={{ rating: classes?.rating, source: classes?.source }} />\n </div>\n )\n}\n\n/** The review list; nothing without items. */\nexport function ReviewsBandList({ classes, children }: { readonly classes?: Classes; readonly children: ReactNode }) {\n if (Children.toArray(children).length === 0) return null\n return <ul {...slot(classes, 'list')}>{children}</ul>\n}\n\nexport interface ReviewsBandItemProps {\n readonly review: BusinessProfileReview\n /** The `feature` variant's wide lead card. */\n readonly lead?: boolean\n readonly layout?: Pick<ReviewsBandLayout, 'avatars' | 'excerpt'>\n readonly classes?: Classes\n}\n\n/** One review, word for word: its stars, the quote and the reviewer with the month. */\nexport function ReviewsBandItem({ review, lead = false, layout, classes }: ReviewsBandItemProps) {\n const parts = splitReviewText(review.text, layout?.excerpt ?? DEFAULT_EXCERPT)\n const date = new Date(review.createTime)\n return (\n <li {...repeatedSlot('item')} data-lead={lead ? 'true' : undefined}>\n <figure>\n <span {...repeatedSlot('stars')}>\n <RatingStars rating={review.rating} label={`Rated ${review.rating} out of 5`} />\n </span>\n <blockquote {...repeatedSlot('quote')}>\n {parts ? (\n <>\n <p>{parts[0]}</p>\n <details {...repeatedSlot('more')}>\n <summary>Read more</summary>\n <p>{parts[1]}</p>\n </details>\n </>\n ) : (\n <p>{review.text}</p>\n )}\n </blockquote>\n <figcaption {...repeatedSlot('author')}>\n {layout?.avatars && review.authorPhotoUrl ? (\n <img\n {...repeatedSlot('avatar')}\n src={review.authorPhotoUrl}\n alt=\"\"\n width={48}\n height={48}\n loading=\"lazy\"\n decoding=\"async\"\n referrerPolicy=\"no-referrer\"\n />\n ) : null}\n <span {...repeatedSlot('name')}>{review.authorName}</span>{' '}\n <time {...repeatedSlot('date')} dateTime={DAY.format(date)}>\n {MONTH.format(date)}\n </time>\n </figcaption>\n </figure>\n </li>\n )\n}\n\nexport interface ReviewsBandProps extends ReviewsBandSkin {\n /** From `prepareBusinessProfileReviews`; null renders nothing at all. */\n readonly reviews: PreparedBusinessProfileReviews | null | undefined\n /** The H2 text the site owns. */\n readonly heading: string\n readonly eyebrow?: string\n readonly intro?: string\n /** The id of the band's H2 (default `bl-reviews-heading`). */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n /** Signature art, rendered `aria-hidden`. */\n readonly decoration?: ReactNode\n}\n\nfunction ReviewsBandSection({ reviews, heading, eyebrow, intro, headingId = REVIEWS_BAND_HEADING_ID, tone, decoration, variant = 'band', layout, classes }: ReviewsBandProps) {\n const shown = shownVariant(reviews, variant)\n if (!shown || !reviews) return null\n const items = shown === 'summary' ? [] : shown === 'feature' ? featureOrder(reviews) : reviews.reviews\n return (\n <ReviewsBandRoot reviews={reviews} headingId={headingId} tone={tone} decoration={decoration} variant={shown} layout={layout} classes={classes}>\n <div {...slot(classes, 'top')}>\n <ReviewsBandHeading id={headingId} eyebrow={eyebrow} intro={intro} layout={layout} classes={classes}>\n {heading}\n </ReviewsBandHeading>\n <ReviewsBandSummary reviews={reviews} classes={classes} />\n </div>\n <ReviewsBandList classes={classes}>\n {items.map((review, index) => (\n <ReviewsBandItem key={review.id} review={review} lead={shown === 'feature' && index === 0} layout={layout} classes={classes} />\n ))}\n </ReviewsBandList>\n </ReviewsBandRoot>\n )\n}\n\n/** The reviews band, with its parts for sites that arrange their own layout. */\nexport const ReviewsBand = Object.assign(ReviewsBandSection, {\n Root: ReviewsBandRoot,\n Heading: ReviewsBandHeading,\n Summary: ReviewsBandSummary,\n List: ReviewsBandList,\n Item: ReviewsBandItem,\n})\n"]}
|
|
@@ -27,7 +27,7 @@ function ServiceLink({ href, prefetch, children }) {
|
|
|
27
27
|
if (href.startsWith('#')) {
|
|
28
28
|
return (_jsx("a", { href: href, ...repeatedSlot('link'), children: children }));
|
|
29
29
|
}
|
|
30
|
-
return (_jsx(Link, { href: href, ...linkPrefetchProps(prefetch), ...repeatedSlot('link'), children: children }));
|
|
30
|
+
return (_jsx(Link, { href: href, ...linkPrefetchProps(prefetch, href), ...repeatedSlot('link'), children: children }));
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
33
|
* One service as a list item of `ServicesGrid` (also the building block for `RelatedServices`). Its parts are the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"services-grid.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/services-grid.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * `ServicesGrid` (S4, `services`, 3.14.0) and `ServiceCard`: route the visitor to the right service. Server components.\n *\n * Variants:\n * - `rows` (the fleet reference, AA's `ServiceRows`): one full-width row per service, the photo beside the text and\n * alternating sides on wide screens, with an optional line of scope points and the service's own link;\n * - `cards`: a grid of cards, each headed by its linked title;\n * - `compact` (suburb and region pages): a lean list of every service offered there, each a single link to its\n * service-in-suburb page or to its anchored section on the same page (`#tree-pruning`). No photos and no summaries.\n *\n * Locked rules (S4): one link per card; no prices and no before-and-afters in cards; no image on two cards; at most 8\n * `rows` or `cards` (K11's card limit, and the most in-content links that prefetch on intent), so a page that offers\n * more services lists them `compact` or links its services hub. Card links prefetch on intent (`LINK_PREFETCH_POLICY`\n * `contentCard`); compact links, the hub link and same-page anchors never prefetch.\n */\nimport Link from 'next/link'\nimport type { ReactNode } from 'react'\nimport type { BrandlabMediaEntry } from '@brandlab-digital/site-core/media'\nimport { LINK_PREFETCH_LIMITS, LINK_PREFETCH_POLICY, linkPrefetchProps, type LinkPrefetch } from '../prefetch.js'\nimport { Media, type MediaLayout } from '../primitives/media.js'\nimport { Section, SectionHeading, type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js'\nimport type { RepeatedSlotOf } from '../repeated-slots.js'\nimport { repeatedSlot, slot, type SkinEntry } from '../skin.js'\nimport type { UiLink } from '../types.js'\n\nexport const SERVICES_GRID_VARIANTS = ['rows', 'cards', 'compact'] as const\nexport type ServicesGridVariant = (typeof SERVICES_GRID_VARIANTS)[number]\nexport type ServiceCardSlot = 'item' | 'media' | 'body' | 'title' | 'summary' | 'points' | 'point' | 'action' | 'link'\nexport type ServicesGridSlot = SectionSlot | SectionHeadingSlot | 'list' | 'more' | ServiceCardSlot\n/** Rendered once per service: styled from the skin stylesheet, never by class. */\nexport type ServicesGridRepeatedSlot = RepeatedSlotOf<'servicesGrid'>\nexport interface ServicesGridLayout {\n /** `cards`: columns on wide screens. Defaults to 3. */\n readonly columns?: 2 | 3 | 4\n /** `rows` and `cards`: the photo's crop. Defaults to 4:3. */\n readonly ratio?: NonNullable<MediaLayout['ratio']>\n /** `rows`: the photo's side on wide screens. Defaults to `alternate` (first photo at the start). */\n readonly mediaSide?: 'alternate' | 'start' | 'end'\n}\nexport type ServicesGridSkin = SkinEntry<ServicesGridSlot, ServicesGridVariant, ServicesGridLayout, ServicesGridRepeatedSlot>\n\n/** The most services `rows` and `cards` show (K11; the in-content intent-prefetch limit). */\nexport const SERVICES_GRID_CARD_LIMIT = LINK_PREFETCH_LIMITS.contentCard\n\n/** One service, in money order. */\nexport interface ServiceCardData {\n /** A stable slug, rendered as `data-item`. */\n readonly id: string\n /** The service's name, the card's H3 (`rows`, `cards`) or the link text (`compact`). */\n readonly title: string\n /**\n * Where the card goes: the service page, its service-in-suburb page or its anchored section on this page\n * (`#tree-pruning`). From the site's data, never built here.\n */\n readonly href: string\n /** One line on what the service is. `rows` and `cards`; never a price. */\n readonly summary?: ReactNode\n /** `rows`: a few scope points under the summary (verified service scope, never prices). */\n readonly points?: readonly string[]\n /** The service's own photo from the media manifest. `rows` and `cards`; no image on two cards. */\n readonly image?: BrandlabMediaEntry\n /**\n * `rows`: the link under the text, for example \"Tree removal in Castle Hill\". Left out, the title is the link, as on\n * `cards`. Name the service: never \"Learn more\".\n */\n readonly linkLabel?: string\n}\n\nexport interface ServiceCardProps {\n readonly service: ServiceCardData\n readonly variant: ServicesGridVariant\n /** `rows` and `cards`: the photo's `sizes`. */\n readonly imageSizes?: string\n readonly ratio?: ServicesGridLayout['ratio']\n /** `rows` and `cards` default to `intent`; `compact` is always off. */\n readonly prefetch?: LinkPrefetch\n}\n\nfunction ServiceLink({ href, prefetch, children }: { href: string; prefetch: LinkPrefetch; children: ReactNode }) {\n // A same-page section needs no route prefetch: a plain anchor.\n if (href.startsWith('#')) {\n return (\n <a href={href} {...repeatedSlot('link')}>\n {children}\n </a>\n )\n }\n return (\n <Link href={href} {...linkPrefetchProps(prefetch)} {...repeatedSlot('link')}>\n {children}\n </Link>\n )\n}\n\n/**\n * One service as a list item of `ServicesGrid` (also the building block for `RelatedServices`). Its parts are the\n * grid's repeated slots: the root sets no classes on them.\n */\nexport function ServiceCard({ service, variant, imageSizes, ratio = '4:3', prefetch }: ServiceCardProps) {\n if (variant === 'compact') {\n return (\n <li {...repeatedSlot('item')} data-item={service.id}>\n <ServiceLink href={service.href} prefetch={LINK_PREFETCH_POLICY.otherServicesInSuburb}>\n {service.title}\n </ServiceLink>\n </li>\n )\n }\n const linkPrefetch = prefetch ?? LINK_PREFETCH_POLICY.contentCard\n const separateLink = variant === 'rows' && service.linkLabel !== undefined && service.linkLabel.trim() !== ''\n return (\n <li {...repeatedSlot('item')} data-item={service.id}>\n <div {...repeatedSlot('body')}>\n <h3 {...repeatedSlot('title')}>\n {separateLink ? (\n service.title\n ) : (\n <ServiceLink href={service.href} prefetch={linkPrefetch}>\n {service.title}\n </ServiceLink>\n )}\n </h3>\n {service.summary ? <p {...repeatedSlot('summary')}>{service.summary}</p> : null}\n {variant === 'rows' && service.points && service.points.length > 0 ? (\n <ul role=\"list\" {...repeatedSlot('points')}>\n {service.points.map((point) => (\n <li key={point} {...repeatedSlot('point')}>\n {point}\n </li>\n ))}\n </ul>\n ) : null}\n {separateLink ? (\n <p {...repeatedSlot('action')}>\n <ServiceLink href={service.href} prefetch={linkPrefetch}>\n {service.linkLabel}\n </ServiceLink>\n </p>\n ) : null}\n </div>\n {service.image ? (\n <div {...repeatedSlot('media')}>\n <Media media={service.image} sizes={imageSizes ?? defaultImageSizes(variant, 3)} layout={{ ratio }} />\n </div>\n ) : null}\n </li>\n )\n}\n\n/** The photo `sizes` for each variant: half the container in rows, one column of the grid in cards. */\nexport function defaultImageSizes(variant: Exclude<ServicesGridVariant, 'compact'>, columns: 2 | 3 | 4): string {\n if (variant === 'rows') return '(min-width: 1024px) 50vw, 100vw'\n return `(min-width: 1024px) ${Math.round(100 / columns)}vw, (min-width: 640px) 50vw, 100vw`\n}\n\nexport interface ServicesGridProps extends ServicesGridSkin {\n /** The H2, for example \"Our tree services\" or \"Tree services in Castle Hill\". */\n readonly title: ReactNode\n readonly eyebrow?: ReactNode\n readonly lead?: ReactNode\n /** Every service the page offers, in money order. */\n readonly services: readonly ServiceCardData[]\n /** A link after the list, for example the services hub or \"Find your area\". Never prefetched. */\n readonly more?: UiLink\n /** `rows` and `cards`: override the photo `sizes` when the skin's grid differs from the defaults. */\n readonly imageSizes?: string\n /** Defaults to `services-heading`. */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n}\n\nfunction assertServices(services: readonly ServiceCardData[], variant: ServicesGridVariant): void {\n const ids = new Set<string>()\n const hrefs = new Set<string>()\n const images = new Map<string, string>()\n for (const service of services) {\n if (!service.id || ids.has(service.id)) throw new Error(`ServicesGrid: every service needs its own id (\"${service.id}\" is empty or repeated).`)\n ids.add(service.id)\n if (!service.title?.trim()) throw new Error(`ServicesGrid: the service \"${service.id}\" has no title.`)\n if (!service.href?.trim()) throw new Error(`ServicesGrid: the service \"${service.id}\" has no link.`)\n if (hrefs.has(service.href)) throw new Error(`ServicesGrid: two services link to ${service.href}; each card has its own page or section.`)\n hrefs.add(service.href)\n if (variant !== 'compact' && service.image) {\n const other = images.get(service.image.src)\n if (other) throw new Error(`ServicesGrid: \"${service.id}\" and \"${other}\" use the same image (${service.image.src}); no image is on two cards.`)\n images.set(service.image.src, service.id)\n }\n }\n if (variant !== 'compact' && services.length > SERVICES_GRID_CARD_LIMIT) {\n throw new Error(\n `ServicesGrid: ${services.length} services in ${variant}; ${variant} shows at most ${SERVICES_GRID_CARD_LIMIT} (K11). List every service with the compact variant, or show the money services and link the services hub.`,\n )\n }\n}\n\nexport function ServicesGrid({\n title,\n eyebrow,\n lead,\n services,\n more,\n imageSizes,\n headingId = 'services-heading',\n tone,\n variant = 'rows',\n layout,\n classes,\n}: ServicesGridProps) {\n if (services.length === 0) return null\n assertServices(services, variant)\n const columns = layout?.columns ?? 3\n const ratio = layout?.ratio ?? '4:3'\n const hooks =\n variant === 'compact'\n ? undefined\n : variant === 'cards'\n ? { 'data-columns': String(columns), 'data-ratio': ratio }\n : { 'data-media-side': layout?.mediaSide ?? 'alternate', 'data-ratio': ratio }\n const sizes = variant === 'compact' ? undefined : (imageSizes ?? defaultImageSizes(variant, columns))\n return (\n <Section id=\"services\" labelledBy={headingId} tone={tone} variant={variant} classes={classes} hooks={hooks}>\n <SectionHeading id={headingId} title={title} eyebrow={eyebrow} lead={lead} classes={classes} />\n <ul role=\"list\" {...slot(classes, 'list')}>\n {services.map((service) => (\n <ServiceCard key={service.id} service={service} variant={variant} imageSizes={sizes} ratio={ratio} />\n ))}\n </ul>\n {more ? (\n <p {...slot(classes, 'more')}>\n {more.href.startsWith('#') ? (\n <a href={more.href}>{more.label}</a>\n ) : (\n <Link href={more.href} prefetch={false}>\n {more.label}\n </Link>\n )}\n </p>\n ) : null}\n </Section>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"services-grid.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/services-grid.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * `ServicesGrid` (S4, `services`, 3.14.0) and `ServiceCard`: route the visitor to the right service. Server components.\n *\n * Variants:\n * - `rows` (the fleet reference, AA's `ServiceRows`): one full-width row per service, the photo beside the text and\n * alternating sides on wide screens, with an optional line of scope points and the service's own link;\n * - `cards`: a grid of cards, each headed by its linked title;\n * - `compact` (suburb and region pages): a lean list of every service offered there, each a single link to its\n * service-in-suburb page or to its anchored section on the same page (`#tree-pruning`). No photos and no summaries.\n *\n * Locked rules (S4): one link per card; no prices and no before-and-afters in cards; no image on two cards; at most 8\n * `rows` or `cards` (K11's card limit, and the most in-content links that prefetch on intent), so a page that offers\n * more services lists them `compact` or links its services hub. Card links prefetch on intent (`LINK_PREFETCH_POLICY`\n * `contentCard`); compact links, the hub link and same-page anchors never prefetch.\n */\nimport Link from 'next/link'\nimport type { ReactNode } from 'react'\nimport type { BrandlabMediaEntry } from '@brandlab-digital/site-core/media'\nimport { LINK_PREFETCH_LIMITS, LINK_PREFETCH_POLICY, linkPrefetchProps, type LinkPrefetch } from '../prefetch.js'\nimport { Media, type MediaLayout } from '../primitives/media.js'\nimport { Section, SectionHeading, type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js'\nimport type { RepeatedSlotOf } from '../repeated-slots.js'\nimport { repeatedSlot, slot, type SkinEntry } from '../skin.js'\nimport type { UiLink } from '../types.js'\n\nexport const SERVICES_GRID_VARIANTS = ['rows', 'cards', 'compact'] as const\nexport type ServicesGridVariant = (typeof SERVICES_GRID_VARIANTS)[number]\nexport type ServiceCardSlot = 'item' | 'media' | 'body' | 'title' | 'summary' | 'points' | 'point' | 'action' | 'link'\nexport type ServicesGridSlot = SectionSlot | SectionHeadingSlot | 'list' | 'more' | ServiceCardSlot\n/** Rendered once per service: styled from the skin stylesheet, never by class. */\nexport type ServicesGridRepeatedSlot = RepeatedSlotOf<'servicesGrid'>\nexport interface ServicesGridLayout {\n /** `cards`: columns on wide screens. Defaults to 3. */\n readonly columns?: 2 | 3 | 4\n /** `rows` and `cards`: the photo's crop. Defaults to 4:3. */\n readonly ratio?: NonNullable<MediaLayout['ratio']>\n /** `rows`: the photo's side on wide screens. Defaults to `alternate` (first photo at the start). */\n readonly mediaSide?: 'alternate' | 'start' | 'end'\n}\nexport type ServicesGridSkin = SkinEntry<ServicesGridSlot, ServicesGridVariant, ServicesGridLayout, ServicesGridRepeatedSlot>\n\n/** The most services `rows` and `cards` show (K11; the in-content intent-prefetch limit). */\nexport const SERVICES_GRID_CARD_LIMIT = LINK_PREFETCH_LIMITS.contentCard\n\n/** One service, in money order. */\nexport interface ServiceCardData {\n /** A stable slug, rendered as `data-item`. */\n readonly id: string\n /** The service's name, the card's H3 (`rows`, `cards`) or the link text (`compact`). */\n readonly title: string\n /**\n * Where the card goes: the service page, its service-in-suburb page or its anchored section on this page\n * (`#tree-pruning`). From the site's data, never built here.\n */\n readonly href: string\n /** One line on what the service is. `rows` and `cards`; never a price. */\n readonly summary?: ReactNode\n /** `rows`: a few scope points under the summary (verified service scope, never prices). */\n readonly points?: readonly string[]\n /** The service's own photo from the media manifest. `rows` and `cards`; no image on two cards. */\n readonly image?: BrandlabMediaEntry\n /**\n * `rows`: the link under the text, for example \"Tree removal in Castle Hill\". Left out, the title is the link, as on\n * `cards`. Name the service: never \"Learn more\".\n */\n readonly linkLabel?: string\n}\n\nexport interface ServiceCardProps {\n readonly service: ServiceCardData\n readonly variant: ServicesGridVariant\n /** `rows` and `cards`: the photo's `sizes`. */\n readonly imageSizes?: string\n readonly ratio?: ServicesGridLayout['ratio']\n /** `rows` and `cards` default to `intent`; `compact` is always off. */\n readonly prefetch?: LinkPrefetch\n}\n\nfunction ServiceLink({ href, prefetch, children }: { href: string; prefetch: LinkPrefetch; children: ReactNode }) {\n // A same-page section needs no route prefetch: a plain anchor.\n if (href.startsWith('#')) {\n return (\n <a href={href} {...repeatedSlot('link')}>\n {children}\n </a>\n )\n }\n return (\n <Link href={href} {...linkPrefetchProps(prefetch, href)} {...repeatedSlot('link')}>\n {children}\n </Link>\n )\n}\n\n/**\n * One service as a list item of `ServicesGrid` (also the building block for `RelatedServices`). Its parts are the\n * grid's repeated slots: the root sets no classes on them.\n */\nexport function ServiceCard({ service, variant, imageSizes, ratio = '4:3', prefetch }: ServiceCardProps) {\n if (variant === 'compact') {\n return (\n <li {...repeatedSlot('item')} data-item={service.id}>\n <ServiceLink href={service.href} prefetch={LINK_PREFETCH_POLICY.otherServicesInSuburb}>\n {service.title}\n </ServiceLink>\n </li>\n )\n }\n const linkPrefetch = prefetch ?? LINK_PREFETCH_POLICY.contentCard\n const separateLink = variant === 'rows' && service.linkLabel !== undefined && service.linkLabel.trim() !== ''\n return (\n <li {...repeatedSlot('item')} data-item={service.id}>\n <div {...repeatedSlot('body')}>\n <h3 {...repeatedSlot('title')}>\n {separateLink ? (\n service.title\n ) : (\n <ServiceLink href={service.href} prefetch={linkPrefetch}>\n {service.title}\n </ServiceLink>\n )}\n </h3>\n {service.summary ? <p {...repeatedSlot('summary')}>{service.summary}</p> : null}\n {variant === 'rows' && service.points && service.points.length > 0 ? (\n <ul role=\"list\" {...repeatedSlot('points')}>\n {service.points.map((point) => (\n <li key={point} {...repeatedSlot('point')}>\n {point}\n </li>\n ))}\n </ul>\n ) : null}\n {separateLink ? (\n <p {...repeatedSlot('action')}>\n <ServiceLink href={service.href} prefetch={linkPrefetch}>\n {service.linkLabel}\n </ServiceLink>\n </p>\n ) : null}\n </div>\n {service.image ? (\n <div {...repeatedSlot('media')}>\n <Media media={service.image} sizes={imageSizes ?? defaultImageSizes(variant, 3)} layout={{ ratio }} />\n </div>\n ) : null}\n </li>\n )\n}\n\n/** The photo `sizes` for each variant: half the container in rows, one column of the grid in cards. */\nexport function defaultImageSizes(variant: Exclude<ServicesGridVariant, 'compact'>, columns: 2 | 3 | 4): string {\n if (variant === 'rows') return '(min-width: 1024px) 50vw, 100vw'\n return `(min-width: 1024px) ${Math.round(100 / columns)}vw, (min-width: 640px) 50vw, 100vw`\n}\n\nexport interface ServicesGridProps extends ServicesGridSkin {\n /** The H2, for example \"Our tree services\" or \"Tree services in Castle Hill\". */\n readonly title: ReactNode\n readonly eyebrow?: ReactNode\n readonly lead?: ReactNode\n /** Every service the page offers, in money order. */\n readonly services: readonly ServiceCardData[]\n /** A link after the list, for example the services hub or \"Find your area\". Never prefetched. */\n readonly more?: UiLink\n /** `rows` and `cards`: override the photo `sizes` when the skin's grid differs from the defaults. */\n readonly imageSizes?: string\n /** Defaults to `services-heading`. */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n}\n\nfunction assertServices(services: readonly ServiceCardData[], variant: ServicesGridVariant): void {\n const ids = new Set<string>()\n const hrefs = new Set<string>()\n const images = new Map<string, string>()\n for (const service of services) {\n if (!service.id || ids.has(service.id)) throw new Error(`ServicesGrid: every service needs its own id (\"${service.id}\" is empty or repeated).`)\n ids.add(service.id)\n if (!service.title?.trim()) throw new Error(`ServicesGrid: the service \"${service.id}\" has no title.`)\n if (!service.href?.trim()) throw new Error(`ServicesGrid: the service \"${service.id}\" has no link.`)\n if (hrefs.has(service.href)) throw new Error(`ServicesGrid: two services link to ${service.href}; each card has its own page or section.`)\n hrefs.add(service.href)\n if (variant !== 'compact' && service.image) {\n const other = images.get(service.image.src)\n if (other) throw new Error(`ServicesGrid: \"${service.id}\" and \"${other}\" use the same image (${service.image.src}); no image is on two cards.`)\n images.set(service.image.src, service.id)\n }\n }\n if (variant !== 'compact' && services.length > SERVICES_GRID_CARD_LIMIT) {\n throw new Error(\n `ServicesGrid: ${services.length} services in ${variant}; ${variant} shows at most ${SERVICES_GRID_CARD_LIMIT} (K11). List every service with the compact variant, or show the money services and link the services hub.`,\n )\n }\n}\n\nexport function ServicesGrid({\n title,\n eyebrow,\n lead,\n services,\n more,\n imageSizes,\n headingId = 'services-heading',\n tone,\n variant = 'rows',\n layout,\n classes,\n}: ServicesGridProps) {\n if (services.length === 0) return null\n assertServices(services, variant)\n const columns = layout?.columns ?? 3\n const ratio = layout?.ratio ?? '4:3'\n const hooks =\n variant === 'compact'\n ? undefined\n : variant === 'cards'\n ? { 'data-columns': String(columns), 'data-ratio': ratio }\n : { 'data-media-side': layout?.mediaSide ?? 'alternate', 'data-ratio': ratio }\n const sizes = variant === 'compact' ? undefined : (imageSizes ?? defaultImageSizes(variant, columns))\n return (\n <Section id=\"services\" labelledBy={headingId} tone={tone} variant={variant} classes={classes} hooks={hooks}>\n <SectionHeading id={headingId} title={title} eyebrow={eyebrow} lead={lead} classes={classes} />\n <ul role=\"list\" {...slot(classes, 'list')}>\n {services.map((service) => (\n <ServiceCard key={service.id} service={service} variant={variant} imageSizes={sizes} ratio={ratio} />\n ))}\n </ul>\n {more ? (\n <p {...slot(classes, 'more')}>\n {more.href.startsWith('#') ? (\n <a href={more.href}>{more.label}</a>\n ) : (\n <Link href={more.href} prefetch={false}>\n {more.label}\n </Link>\n )}\n </p>\n ) : null}\n </Section>\n )\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules the "our work" sections share (3.15.0): `Projects`, `Gallery` and `BeforeAfter` show the client's real work
|
|
3
|
+
* only (v2 §11 imagery rule 4, F13). A stock or AI image never stands for a job, so these sections refuse one and fail
|
|
4
|
+
* the build, the same way the hero refuses an AI photo; when there are no genuine photos the section is skipped.
|
|
5
|
+
*/
|
|
6
|
+
import type { MediaFrameSizing, UiMedia } from '../primitives/media-frame.js';
|
|
7
|
+
/** Throws unless the image is a genuine, described photo of the work. */
|
|
8
|
+
export declare function assertGenuineWork(component: string, media: UiMedia, where: string): void;
|
|
9
|
+
/** Fluid `sizes` for one column of a grid that is one column on phones, two from 640 px and `columns` from 1024 px. */
|
|
10
|
+
export declare function gridImageSizes(columns: number): string;
|
|
11
|
+
/** `MediaFrame`'s sizing from a section's `imageSizes`: a number is a fixed CSS width, a string a fluid `vw` size. */
|
|
12
|
+
export declare function frameSizing(sizes: number | string): MediaFrameSizing;
|
|
13
|
+
/** An internal page path: starts with one `/`, no spaces. */
|
|
14
|
+
export declare function isInternalPath(href: string): boolean;
|
|
15
|
+
//# sourceMappingURL=work-photos.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"work-photos.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/work-photos.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAA;AAE7E,yEAAyE;AACzE,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CASxF;AAED,uHAAuH;AACvH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGtD;AAED,sHAAsH;AACtH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,gBAAgB,CAEpE;AAED,6DAA6D;AAC7D,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Throws unless the image is a genuine, described photo of the work. */
|
|
2
|
+
export function assertGenuineWork(component, media, where) {
|
|
3
|
+
if (media.provenance !== 'genuine') {
|
|
4
|
+
throw new Error(`${component}: ${where} (${media.src}) is ${media.provenance}. Projects, the gallery and before-and-after pairs show genuine photos of the work only; use the image elsewhere as supporting media.`);
|
|
5
|
+
}
|
|
6
|
+
if (media.decorative || !media.alt.trim()) {
|
|
7
|
+
throw new Error(`${component}: ${where} (${media.src}) has no alt text; a photo of the work says what it shows.`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** Fluid `sizes` for one column of a grid that is one column on phones, two from 640 px and `columns` from 1024 px. */
|
|
11
|
+
export function gridImageSizes(columns) {
|
|
12
|
+
if (columns <= 2)
|
|
13
|
+
return '(min-width: 640px) 50vw, 100vw';
|
|
14
|
+
return `(min-width: 1024px) ${Math.round(100 / columns)}vw, (min-width: 640px) 50vw, 100vw`;
|
|
15
|
+
}
|
|
16
|
+
/** `MediaFrame`'s sizing from a section's `imageSizes`: a number is a fixed CSS width, a string a fluid `vw` size. */
|
|
17
|
+
export function frameSizing(sizes) {
|
|
18
|
+
return typeof sizes === 'number' ? { displayWidth: sizes } : { sizes };
|
|
19
|
+
}
|
|
20
|
+
/** An internal page path: starts with one `/`, no spaces. */
|
|
21
|
+
export function isInternalPath(href) {
|
|
22
|
+
return /^\/(?!\/)\S*$/.test(href);
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=work-photos.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"work-photos.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/work-photos.ts"],"names":[],"mappings":"AAOA,yEAAyE;AACzE,MAAM,UAAU,iBAAiB,CAAC,SAAiB,EAAE,KAAc,EAAE,KAAa;IAChF,IAAI,KAAK,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CACb,GAAG,SAAS,KAAK,KAAK,KAAK,KAAK,CAAC,GAAG,QAAQ,KAAK,CAAC,UAAU,uIAAuI,CACpM,CAAA;IACH,CAAC;IACD,IAAI,KAAK,CAAC,UAAU,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,GAAG,SAAS,KAAK,KAAK,KAAK,KAAK,CAAC,GAAG,4DAA4D,CAAC,CAAA;IACnH,CAAC;AACH,CAAC;AAED,uHAAuH;AACvH,MAAM,UAAU,cAAc,CAAC,OAAe;IAC5C,IAAI,OAAO,IAAI,CAAC;QAAE,OAAO,gCAAgC,CAAA;IACzD,OAAO,uBAAuB,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,OAAO,CAAC,oCAAoC,CAAA;AAC7F,CAAC;AAED,sHAAsH;AACtH,MAAM,UAAU,WAAW,CAAC,KAAsB;IAChD,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAA;AACxE,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACnC,CAAC","sourcesContent":["/**\n * The rules the \"our work\" sections share (3.15.0): `Projects`, `Gallery` and `BeforeAfter` show the client's real work\n * only (v2 §11 imagery rule 4, F13). A stock or AI image never stands for a job, so these sections refuse one and fail\n * the build, the same way the hero refuses an AI photo; when there are no genuine photos the section is skipped.\n */\nimport type { MediaFrameSizing, UiMedia } from '../primitives/media-frame.js'\n\n/** Throws unless the image is a genuine, described photo of the work. */\nexport function assertGenuineWork(component: string, media: UiMedia, where: string): void {\n if (media.provenance !== 'genuine') {\n throw new Error(\n `${component}: ${where} (${media.src}) is ${media.provenance}. Projects, the gallery and before-and-after pairs show genuine photos of the work only; use the image elsewhere as supporting media.`,\n )\n }\n if (media.decorative || !media.alt.trim()) {\n throw new Error(`${component}: ${where} (${media.src}) has no alt text; a photo of the work says what it shows.`)\n }\n}\n\n/** Fluid `sizes` for one column of a grid that is one column on phones, two from 640 px and `columns` from 1024 px. */\nexport function gridImageSizes(columns: number): string {\n if (columns <= 2) return '(min-width: 640px) 50vw, 100vw'\n return `(min-width: 1024px) ${Math.round(100 / columns)}vw, (min-width: 640px) 50vw, 100vw`\n}\n\n/** `MediaFrame`'s sizing from a section's `imageSizes`: a number is a fixed CSS width, a string a fluid `vw` size. */\nexport function frameSizing(sizes: number | string): MediaFrameSizing {\n return typeof sizes === 'number' ? { displayWidth: sizes } : { sizes }\n}\n\n/** An internal page path: starts with one `/`, no spaces. */\nexport function isInternalPath(href: string): boolean {\n return /^\\/(?!\\/)\\S*$/.test(href)\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* F7: back-to-top (server component). A 44 px `<button>` named "Back to top" that appears after one and a half
|
|
4
|
-
* screens,
|
|
4
|
+
* screens, stays hidden while the phone call bar shows (3.15.0; it never covers the bar or the content above it), scrolls smoothly unless the visitor prefers reduced motion and moves
|
|
5
5
|
* focus to `#main`. Never a footer text link. Render it once, after the page's `<main>`.
|
|
6
6
|
*/
|
|
7
7
|
import { Icon } from '../icons.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"back-to-top.js","sourceRoot":"","sources":["../../../src/ui-kit/shell/back-to-top.tsx"],"names":[],"mappings":";AAAA;;;;GAIG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAClC,OAAO,EAAE,IAAI,EAAkB,MAAM,YAAY,CAAA;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAGzD,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAU,CAAA;AAWxE,MAAM,UAAU,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,EAAkB;IAC3D,OAAO,CACL,KAAC,eAAe,IAAC,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,YAC5G,kBAAU,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,YAC7B,KAAC,IAAI,IAAC,IAAI,EAAC,UAAU,GAAG,GACnB,GACS,CACnB,CAAA;AACH,CAAC","sourcesContent":["/**\n * F7: back-to-top (server component). A 44 px `<button>` named \"Back to top\" that appears after one and a half\n * screens,
|
|
1
|
+
{"version":3,"file":"back-to-top.js","sourceRoot":"","sources":["../../../src/ui-kit/shell/back-to-top.tsx"],"names":[],"mappings":";AAAA;;;;GAIG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAClC,OAAO,EAAE,IAAI,EAAkB,MAAM,YAAY,CAAA;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAGzD,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAU,CAAA;AAWxE,MAAM,UAAU,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,EAAkB;IAC3D,OAAO,CACL,KAAC,eAAe,IAAC,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,YAC5G,kBAAU,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,YAC7B,KAAC,IAAI,IAAC,IAAI,EAAC,UAAU,GAAG,GACnB,GACS,CACnB,CAAA;AACH,CAAC","sourcesContent":["/**\n * F7: back-to-top (server component). A 44 px `<button>` named \"Back to top\" that appears after one and a half\n * screens, stays hidden while the phone call bar shows (3.15.0; it never covers the bar or the content above it), scrolls smoothly unless the visitor prefers reduced motion and moves\n * focus to `#main`. Never a footer text link. Render it once, after the page's `<main>`.\n */\nimport { Icon } from '../icons.js'\nimport { slot, type SkinEntry } from '../skin.js'\nimport { BackToTopButton } from './back-to-top-client.js'\n\nexport type BackToTopSlot = 'root' | 'icon'\nexport const BACK_TO_TOP_OFFSETS = ['small', 'medium', 'large'] as const\nexport interface BackToTopLayout {\n /** The bottom corner it sits in (default `end`). */\n readonly corner?: 'end' | 'start'\n /** Its distance from the side and from the bottom (or the call bar): 0.75, 1.25 or 2 rem (default `medium`). */\n readonly offset?: (typeof BACK_TO_TOP_OFFSETS)[number]\n}\nexport type BackToTopSkin = SkinEntry<BackToTopSlot, never, BackToTopLayout>\n\nexport type BackToTopProps = BackToTopSkin\n\nexport function BackToTop({ layout, classes }: BackToTopProps) {\n return (\n <BackToTopButton corner={layout?.corner ?? 'end'} offset={layout?.offset ?? 'medium'} className={classes?.root}>\n <span {...slot(classes, 'icon')}>\n <Icon name=\"arrow-up\" />\n </span>\n </BackToTopButton>\n )\n}\n"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
+
import { type LinkPrefetch } from '../prefetch.js';
|
|
2
3
|
/** True when `href` is the current page. External links are never current. */
|
|
3
4
|
export declare function isCurrentPage(pathname: string, href: string): boolean;
|
|
4
5
|
/** True when the page is one of `paths` or below it (the home path matches only itself). */
|
|
@@ -8,16 +9,18 @@ export declare function CurrentLink({ href, slot, hub, prefetch, className, chil
|
|
|
8
9
|
readonly slot: string;
|
|
9
10
|
readonly hub?: boolean;
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* The link's prefetch role (`LINK_PREFETCH_POLICY`): `intent` for the top-level menu links, false for links in panels
|
|
13
|
+
* and the drawer (hidden panels count as in view and would prefetch every menu page). The current page is never
|
|
14
|
+
* prefetched, whatever this says.
|
|
13
15
|
*/
|
|
14
|
-
readonly prefetch?:
|
|
16
|
+
readonly prefetch?: LinkPrefetch | undefined;
|
|
15
17
|
readonly className?: string | undefined;
|
|
16
18
|
readonly children: ReactNode;
|
|
17
19
|
}): import("react/jsx-runtime").JSX.Element;
|
|
18
20
|
/**
|
|
19
|
-
* The header logo's home link:
|
|
20
|
-
* prefetched. No `aria-current`: the logo is named by the
|
|
21
|
+
* The header logo's home link. It never prefetches (3.15.0): the logo is in view on every page, so on landing it
|
|
22
|
+
* prefetched home from every page, and home itself on the home page. No `aria-current`: the logo is named by the
|
|
23
|
+
* brand, not as a menu item.
|
|
21
24
|
*/
|
|
22
25
|
export declare function LogoLink({ href, label, slot, className, children, }: {
|
|
23
26
|
readonly href: string;
|
|
@@ -26,4 +29,17 @@ export declare function LogoLink({ href, label, slot, className, children, }: {
|
|
|
26
29
|
readonly className?: string | undefined;
|
|
27
30
|
readonly children: ReactNode;
|
|
28
31
|
}): import("react/jsx-runtime").JSX.Element;
|
|
32
|
+
/**
|
|
33
|
+
* A shell action's link (the quote button in the header, drawer and call bar): its role's prefetch, except that it
|
|
34
|
+
* never prefetches the page it is on (`/contact#quote` on `/contact`) or a place on the same page. The anchor's
|
|
35
|
+
* hooks and content come from the server component that renders it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function PageLink({ href, prefetch, hooks, children, }: {
|
|
38
|
+
readonly href: string;
|
|
39
|
+
readonly prefetch?: LinkPrefetch | undefined;
|
|
40
|
+
readonly hooks: {
|
|
41
|
+
readonly className?: string | undefined;
|
|
42
|
+
} & Readonly<Record<`data-${string}`, string | undefined>>;
|
|
43
|
+
readonly children: ReactNode;
|
|
44
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
29
45
|
//# sourceMappingURL=current-link.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"current-link.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/shell/current-link.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"current-link.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/shell/current-link.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,EAAqC,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAOrF,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,WAG3D;AAED,4FAA4F;AAC5F,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,WAO1E;AAED,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,IAAI,EACJ,GAAW,EACX,QAAQ,EACR,SAAS,EACT,QAAQ,GACT,EAAE;IACD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAA;IACtB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,GAAG,SAAS,CAAA;IAC5C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B,2CAeA;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,EACvB,IAAI,EACJ,KAAK,EACL,IAAI,EACJ,SAAS,EACT,QAAQ,GACT,EAAE;IACD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B,2CAMA;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,EACvB,IAAI,EACJ,QAAQ,EACR,KAAK,EACL,QAAQ,GACT,EAAE;IACD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,GAAG,SAAS,CAAA;IAC5C,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,QAAQ,MAAM,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;IACpH,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B,2CAQA"}
|
|
@@ -3,10 +3,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
/**
|
|
4
4
|
* A menu link that marks itself `aria-current="page"` on its own page. A client leaf only because the root layout
|
|
5
5
|
* can't read the route: it receives one href and its server-rendered label, and renders in the server HTML with the
|
|
6
|
-
* right state.
|
|
6
|
+
* right state. The current page is never prefetched (W2), so a link to it renders `prefetch={false}` whatever its role.
|
|
7
7
|
*/
|
|
8
8
|
import Link from 'next/link';
|
|
9
9
|
import { usePathname } from 'next/navigation';
|
|
10
|
+
import { isSamePageHref, linkPrefetchProps } from '../prefetch.js';
|
|
10
11
|
function normalisePath(path) {
|
|
11
12
|
const bare = path.split(/[?#]/, 1)[0] ?? '';
|
|
12
13
|
return bare.replace(/\/+$/, '') || '/';
|
|
@@ -30,14 +31,24 @@ export function isCurrentSection(pathname, paths) {
|
|
|
30
31
|
export function CurrentLink({ href, slot, hub = false, prefetch, className, children, }) {
|
|
31
32
|
const pathname = usePathname();
|
|
32
33
|
const current = isCurrentPage(pathname, href);
|
|
33
|
-
return (_jsx(Link, { href: href,
|
|
34
|
+
return (_jsx(Link, { href: href, ...(current ? { prefetch: false } : linkPrefetchProps(prefetch, href)), "data-slot": slot, "data-hub": hub ? 'true' : undefined, "aria-current": current ? 'page' : undefined, className: className, children: children }));
|
|
34
35
|
}
|
|
35
36
|
/**
|
|
36
|
-
* The header logo's home link:
|
|
37
|
-
* prefetched. No `aria-current`: the logo is named by the
|
|
37
|
+
* The header logo's home link. It never prefetches (3.15.0): the logo is in view on every page, so on landing it
|
|
38
|
+
* prefetched home from every page, and home itself on the home page. No `aria-current`: the logo is named by the
|
|
39
|
+
* brand, not as a menu item.
|
|
38
40
|
*/
|
|
39
41
|
export function LogoLink({ href, label, slot, className, children, }) {
|
|
42
|
+
return (_jsx(Link, { href: href, prefetch: false, "aria-label": label, "data-slot": slot, className: className, children: children }));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A shell action's link (the quote button in the header, drawer and call bar): its role's prefetch, except that it
|
|
46
|
+
* never prefetches the page it is on (`/contact#quote` on `/contact`) or a place on the same page. The anchor's
|
|
47
|
+
* hooks and content come from the server component that renders it.
|
|
48
|
+
*/
|
|
49
|
+
export function PageLink({ href, prefetch, hooks, children, }) {
|
|
40
50
|
const pathname = usePathname();
|
|
41
|
-
|
|
51
|
+
const current = isSamePageHref(href) || isCurrentPage(pathname, href);
|
|
52
|
+
return (_jsx(Link, { href: href, ...(current ? { prefetch: false } : linkPrefetchProps(prefetch)), ...hooks, children: children }));
|
|
42
53
|
}
|
|
43
54
|
//# sourceMappingURL=current-link.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"current-link.js","sourceRoot":"","sources":["../../../src/ui-kit/shell/current-link.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ;;;;GAIG;AACH,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"current-link.js","sourceRoot":"","sources":["../../../src/ui-kit/shell/current-link.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\n/**\n * A menu link that marks itself `aria-current=\"page\"` on its own page. A client leaf only because the root layout\n * can't read the route: it receives one href and its server-rendered label, and renders in the server HTML with the\n * right state. The current page is never prefetched (W2), so a link to it renders `prefetch={false}` whatever its role.\n */\nimport Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport type { ReactNode } from 'react'\nimport { isSamePageHref, linkPrefetchProps, type LinkPrefetch } from '../prefetch.js'\n\nfunction normalisePath(path: string) {\n const bare = path.split(/[?#]/, 1)[0] ?? ''\n return bare.replace(/\\/+$/, '') || '/'\n}\n\n/** True when `href` is the current page. External links are never current. */\nexport function isCurrentPage(pathname: string, href: string) {\n if (!href.startsWith('/') || href.startsWith('//')) return false\n return normalisePath(pathname) === normalisePath(href)\n}\n\n/** True when the page is one of `paths` or below it (the home path matches only itself). */\nexport function isCurrentSection(pathname: string, paths: readonly string[]) {\n const current = normalisePath(pathname)\n return paths.some((path) => {\n if (!path.startsWith('/') || path.startsWith('//')) return false\n const section = normalisePath(path)\n return current === section || (section !== '/' && current.startsWith(`${section}/`))\n })\n}\n\nexport function CurrentLink({\n href,\n slot,\n hub = false,\n prefetch,\n className,\n children,\n}: {\n readonly href: string\n readonly slot: string\n readonly hub?: boolean\n /**\n * The link's prefetch role (`LINK_PREFETCH_POLICY`): `intent` for the top-level menu links, false for links in panels\n * and the drawer (hidden panels count as in view and would prefetch every menu page). The current page is never\n * prefetched, whatever this says.\n */\n readonly prefetch?: LinkPrefetch | undefined\n readonly className?: string | undefined\n readonly children: ReactNode\n}) {\n const pathname = usePathname()\n const current = isCurrentPage(pathname, href)\n return (\n <Link\n href={href}\n {...(current ? { prefetch: false as const } : linkPrefetchProps(prefetch, href))}\n data-slot={slot}\n data-hub={hub ? 'true' : undefined}\n aria-current={current ? 'page' : undefined}\n className={className}\n >\n {children}\n </Link>\n )\n}\n\n/**\n * The header logo's home link. It never prefetches (3.15.0): the logo is in view on every page, so on landing it\n * prefetched home from every page, and home itself on the home page. No `aria-current`: the logo is named by the\n * brand, not as a menu item.\n */\nexport function LogoLink({\n href,\n label,\n slot,\n className,\n children,\n}: {\n readonly href: string\n readonly label: string\n readonly slot: string\n readonly className?: string | undefined\n readonly children: ReactNode\n}) {\n return (\n <Link href={href} prefetch={false} aria-label={label} data-slot={slot} className={className}>\n {children}\n </Link>\n )\n}\n\n/**\n * A shell action's link (the quote button in the header, drawer and call bar): its role's prefetch, except that it\n * never prefetches the page it is on (`/contact#quote` on `/contact`) or a place on the same page. The anchor's\n * hooks and content come from the server component that renders it.\n */\nexport function PageLink({\n href,\n prefetch,\n hooks,\n children,\n}: {\n readonly href: string\n readonly prefetch?: LinkPrefetch | undefined\n readonly hooks: { readonly className?: string | undefined } & Readonly<Record<`data-${string}`, string | undefined>>\n readonly children: ReactNode\n}) {\n const pathname = usePathname()\n const current = isSamePageHref(href) || isCurrentPage(pathname, href)\n return (\n <Link href={href} {...(current ? { prefetch: false as const } : linkPrefetchProps(prefetch))} {...hooks}>\n {children}\n </Link>\n )\n}\n"]}
|
|
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
4
4
|
* The drawer's client leaf: the Menu button and the modal `<dialog>` (top layer, the page inert, focus kept inside).
|
|
5
5
|
* The menu, the actions and the social links are server-rendered children. The drawer closes on Escape, the close
|
|
6
6
|
* button, a tap on the backdrop, a link click, a route change and any change of the header's mode (crossing 1024 or
|
|
7
|
-
*
|
|
7
|
+
* 1200 px), so the page is never left inert. Groups are `<details name>` (one open at a time, no script needed); the
|
|
8
8
|
* toggle listener below keeps that rule in browsers without exclusive `<details>`.
|
|
9
9
|
*/
|
|
10
10
|
import { usePathname } from 'next/navigation';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drawer-client.js","sourceRoot":"","sources":["../../../src/ui-kit/shell/drawer-client.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\n/**\n * The drawer's client leaf: the Menu button and the modal `<dialog>` (top layer, the page inert, focus kept inside).\n * The menu, the actions and the social links are server-rendered children. The drawer closes on Escape, the close\n * button, a tap on the backdrop, a link click, a route change and any change of the header's mode (crossing 1024 or\n *
|
|
1
|
+
{"version":3,"file":"drawer-client.js","sourceRoot":"","sources":["../../../src/ui-kit/shell/drawer-client.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\n/**\n * The drawer's client leaf: the Menu button and the modal `<dialog>` (top layer, the page inert, focus kept inside).\n * The menu, the actions and the social links are server-rendered children. The drawer closes on Escape, the close\n * button, a tap on the backdrop, a link click, a route change and any change of the header's mode (crossing 1024 or\n * 1200 px), so the page is never left inert. Groups are `<details name>` (one open at a time, no script needed); the\n * toggle listener below keeps that rule in browsers without exclusive `<details>`.\n */\nimport { usePathname } from 'next/navigation'\nimport { useCallback, useContext, useEffect, useRef, useState, type ReactNode } from 'react'\nimport { HeaderModeContext } from './shell-context.js'\n\nexport interface DrawerControllerClasses {\n readonly root?: string | undefined\n readonly menuButton?: string | undefined\n readonly menuLabel?: string | undefined\n readonly panel?: string | undefined\n readonly top?: string | undefined\n readonly close?: string | undefined\n readonly title?: string | undefined\n readonly logo?: string | undefined\n}\n\nexport function DrawerController({\n id,\n variant,\n side,\n classes,\n logo,\n menuIcon,\n closeIcon,\n children,\n}: {\n readonly id: string\n readonly variant: string\n readonly side: string\n readonly classes: DrawerControllerClasses\n readonly logo?: ReactNode\n readonly menuIcon: ReactNode\n readonly closeIcon: ReactNode\n readonly children: ReactNode\n}) {\n const pathname = usePathname()\n const mode = useContext(HeaderModeContext)\n const dialogRef = useRef<HTMLDialogElement>(null)\n const buttonRef = useRef<HTMLButtonElement>(null)\n const closeRef = useRef<HTMLButtonElement>(null)\n const [open, setOpen] = useState(false)\n const titleId = `${id}-title`\n\n const close = useCallback(() => {\n const dialog = dialogRef.current\n if (dialog?.open) dialog.close()\n }, [])\n\n const show = () => {\n const dialog = dialogRef.current\n if (!dialog || dialog.open) return\n for (const group of dialog.querySelectorAll<HTMLDetailsElement>('details[open]')) group.open = false\n dialog.showModal()\n // The close button comes first in focus order and takes focus when the drawer opens.\n closeRef.current?.focus()\n setOpen(true)\n }\n\n // A route change or a mode change (breakpoint crossing) resets the drawer.\n useEffect(() => {\n close()\n }, [pathname, mode, close])\n\n useEffect(() => {\n const dialog = dialogRef.current\n if (!dialog) return\n const onToggle = (event: Event) => {\n const opened = event.target\n if (!(opened instanceof HTMLDetailsElement) || !opened.open) return\n for (const other of dialog.querySelectorAll<HTMLDetailsElement>('details[open]')) if (other !== opened) other.open = false\n }\n dialog.addEventListener('toggle', onToggle, true)\n return () => dialog.removeEventListener('toggle', onToggle, true)\n }, [])\n\n return (\n <>\n <button\n ref={buttonRef}\n type=\"button\"\n data-slot=\"menuButton\"\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n aria-controls={id}\n className={classes.menuButton}\n onClick={show}\n >\n {menuIcon}\n <span data-slot=\"menuLabel\" className={classes.menuLabel}>\n Menu\n </span>\n </button>\n <dialog\n ref={dialogRef}\n id={id}\n data-bl=\"drawer\"\n data-variant={variant}\n data-side={side}\n aria-labelledby={titleId}\n className={classes.root}\n {...({ closedby: 'any' } as Record<string, string>)}\n onClose={() => {\n setOpen(false)\n // Safari doesn't focus a clicked button, so the dialog may have nothing to return focus to.\n const active = document.activeElement\n if (!active || active === document.body || dialogRef.current?.contains(active)) buttonRef.current?.focus()\n }}\n onClick={(event) => {\n // A tap on the backdrop (where closedby=\"any\" isn't supported yet) or on any link closes the drawer.\n if (event.target === dialogRef.current) close()\n else if ((event.target as Element).closest('a[href]')) close()\n }}\n >\n <div data-slot=\"panel\" className={classes.panel}>\n <div data-slot=\"top\" className={classes.top}>\n <button ref={closeRef} type=\"button\" data-slot=\"close\" aria-label=\"Close menu\" className={classes.close} onClick={close}>\n {closeIcon}\n </button>\n <p id={titleId} data-slot=\"title\" className={classes.title}>\n Menu\n </p>\n {logo ? (\n <span data-slot=\"logo\" aria-hidden=\"true\" className={classes.logo}>\n {logo}\n </span>\n ) : null}\n </div>\n {children}\n </div>\n </dialog>\n </>\n )\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"intent-prefetch.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/shell/intent-prefetch.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"intent-prefetch.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/shell/intent-prefetch.tsx"],"names":[],"mappings":"AA6BA,sGAAsG;AACtG,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAIvE;AAOD,wBAAgB,cAAc,IAAI,IAAI,CAsBrC"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
/**
|
|
3
|
-
* `IntentPrefetch`: prefetches an in-content card link when a visitor shows
|
|
4
|
-
* Links with `prefetch="intent"` (ButtonLink, TextLink, QuoteButton
|
|
3
|
+
* `IntentPrefetch`: prefetches a top-level header link (from 3.15.0) or an in-content card link when a visitor shows
|
|
4
|
+
* intent, not when it scrolls into view. Links with `prefetch="intent"` (ButtonLink, TextLink, QuoteButton and the
|
|
5
|
+
* SiteHeader's top-level links) render `prefetch={false}` and
|
|
5
6
|
* `data-prefetch="intent"`; this island holds one document listener for pointer entry, focus and touch, and calls
|
|
6
7
|
* `router.prefetch(href)` for the one link the visitor points at, focuses or touches, once per page address. A touch
|
|
7
8
|
* prefetches only the touched link, never the list around it; the current page and links to other sites are never
|