@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,49 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* `NearbyAreas` (S18, `nearby-areas`, 3.15.0): links to the real neighbours of a suburb page, or the same service in
|
|
4
|
+
* the neighbouring suburbs on a service-in-suburb page. Server component.
|
|
5
|
+
*
|
|
6
|
+
* Locked (v1 §4.4, L3): the nearest served suburbs by real distance between registry centres, nearest first, never
|
|
7
|
+
* shuffled or alphabetical; `nearbyAreaLinks()` builds them with site-core's `nearestSuburbsWithin`. Distances rank the
|
|
8
|
+
* links and are never printed (L10). The list is lean (cost item 1f): `LinkList` entries are plain `<li><a>` with no
|
|
9
|
+
* class, hook or icon, so each link stays under 120 bytes, and nothing in it prefetches (`data-bl-links="nearby"`, W2).
|
|
10
|
+
* 4 to 6 links suit most pages; 12 is the most it renders.
|
|
11
|
+
*/
|
|
12
|
+
import Link from 'next/link';
|
|
13
|
+
import { nearestSuburbsWithin } from '@brandlab-digital/site-core/locations';
|
|
14
|
+
import { SECTION_READY } from '@brandlab-digital/site-core/section-order';
|
|
15
|
+
import { LinkList } from '../primitives/lists.js';
|
|
16
|
+
import { Section, SectionHeading } from '../primitives/layout.js';
|
|
17
|
+
import { slot } from '../skin.js';
|
|
18
|
+
/** The most neighbours the section lists. */
|
|
19
|
+
export const NEARBY_AREAS_LIMIT = 12;
|
|
20
|
+
/** The neighbours `nearbyAreaLinks` returns by default (v1 §4.4: 4 to 6). */
|
|
21
|
+
export const NEARBY_AREAS_DEFAULT = 6;
|
|
22
|
+
export const NEARBY_AREAS_VARIANTS = ['list', 'chips'];
|
|
23
|
+
/**
|
|
24
|
+
* The nearest served suburbs to `origin` as links, nearest first by real distance (`nearestSuburbsWithin`). `link`
|
|
25
|
+
* gives a suburb's page (or `undefined` to leave it out, for a suburb without that page).
|
|
26
|
+
*/
|
|
27
|
+
export function nearbyAreaLinks(origin, served, options) {
|
|
28
|
+
const limit = options.limit ?? NEARBY_AREAS_DEFAULT;
|
|
29
|
+
if (limit > NEARBY_AREAS_LIMIT)
|
|
30
|
+
throw new Error(`nearbyAreaLinks: at most ${NEARBY_AREAS_LIMIT} neighbours.`);
|
|
31
|
+
const candidates = served.filter((suburb) => options.link(suburb) !== undefined);
|
|
32
|
+
return nearestSuburbsWithin(origin, candidates, { centreOf: options.centreOf, limit, ...(options.maxKm !== undefined ? { maxKm: options.maxKm } : {}) }).map((entry) => options.link(entry.slug));
|
|
33
|
+
}
|
|
34
|
+
/** The guard for `SectionSequence`. */
|
|
35
|
+
export function nearbyAreasReady({ areas }) {
|
|
36
|
+
return areas.length > 0 ? SECTION_READY : { render: false, reason: 'no-nearby-areas' };
|
|
37
|
+
}
|
|
38
|
+
export function NearbyAreas({ title, eyebrow, lead, areas, regionLink, more, headingId = 'nearby-areas-heading', tone, variant = 'list', layout, classes }) {
|
|
39
|
+
if (areas.length === 0)
|
|
40
|
+
return null;
|
|
41
|
+
if (areas.length > NEARBY_AREAS_LIMIT)
|
|
42
|
+
throw new Error(`NearbyAreas: ${areas.length} neighbours; the section lists at most ${NEARBY_AREAS_LIMIT}.`);
|
|
43
|
+
if (new Set(areas.map((area) => area.href)).size !== areas.length)
|
|
44
|
+
throw new Error('NearbyAreas: a neighbour is listed twice.');
|
|
45
|
+
if (regionLink && areas.some((area) => area.href === regionLink.href))
|
|
46
|
+
throw new Error('NearbyAreas: the region link repeats a neighbour.');
|
|
47
|
+
return (_jsxs(Section, { id: "nearby-areas", labelledBy: headingId, tone: tone, variant: variant, classes: classes, children: [_jsx(SectionHeading, { id: headingId, title: title, eyebrow: eyebrow, lead: lead, classes: classes }), _jsx(LinkList, { links: areas, kind: "nearby", layout: layout, classes: { root: classes?.list } }), regionLink ? (_jsx("p", { ...slot(classes, 'region'), children: _jsx(Link, { href: regionLink.href, prefetch: false, children: regionLink.label }) })) : null, more ? (_jsx("p", { ...slot(classes, 'more'), children: _jsx(Link, { href: more.href, prefetch: false, children: more.label }) })) : null] }));
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=nearby-areas.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nearby-areas.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/nearby-areas.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * `NearbyAreas` (S18, `nearby-areas`, 3.15.0): links to the real neighbours of a suburb page, or the same service in\n * the neighbouring suburbs on a service-in-suburb page. Server component.\n *\n * Locked (v1 §4.4, L3): the nearest served suburbs by real distance between registry centres, nearest first, never\n * shuffled or alphabetical; `nearbyAreaLinks()` builds them with site-core's `nearestSuburbsWithin`. Distances rank the\n * links and are never printed (L10). The list is lean (cost item 1f): `LinkList` entries are plain `<li><a>` with no\n * class, hook or icon, so each link stays under 120 bytes, and nothing in it prefetches (`data-bl-links=\"nearby\"`, W2).\n * 4 to 6 links suit most pages; 12 is the most it renders.\n */\nimport Link from 'next/link'\nimport type { ReactNode } from 'react'\nimport { nearestSuburbsWithin, type SuburbCentreLookup } from '@brandlab-digital/site-core/locations'\nimport { SECTION_READY, type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order'\nimport { LinkList, type LinkListLayout } from '../primitives/lists.js'\nimport { Section, SectionHeading, type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js'\nimport { slot, type SkinEntry } from '../skin.js'\nimport type { UiLink } from '../types.js'\n\n/** The most neighbours the section lists. */\nexport const NEARBY_AREAS_LIMIT = 12\n/** The neighbours `nearbyAreaLinks` returns by default (v1 §4.4: 4 to 6). */\nexport const NEARBY_AREAS_DEFAULT = 6\n\nexport const NEARBY_AREAS_VARIANTS = ['list', 'chips'] as const\nexport type NearbyAreasVariant = (typeof NEARBY_AREAS_VARIANTS)[number]\nexport type NearbyAreasSlot = SectionSlot | SectionHeadingSlot | 'list' | 'region' | 'more'\nexport type NearbyAreasLayout = LinkListLayout\nexport type NearbyAreasSkin = SkinEntry<NearbyAreasSlot, NearbyAreasVariant, NearbyAreasLayout>\n\nexport interface NearbyAreasProps extends NearbyAreasSkin {\n /** The H2, for example \"Tree removal near Castle Hill\". */\n readonly title: ReactNode\n readonly eyebrow?: ReactNode\n readonly lead?: ReactNode\n /** The neighbours, nearest first (`nearbyAreaLinks`). */\n readonly areas: readonly UiLink[]\n /**\n * The page of the region these suburbs belong to, for example `{ label: 'All of the Hills District', href:\n * '/tree-services/hills-district' }` (the label is site copy). It sits after the ranked list, outside it, so it is\n * never ranked by distance, and it is never prefetched.\n */\n readonly regionLink?: UiLink\n /** A link after the list (and the region link), for example the areas hub. Never prefetched. */\n readonly more?: UiLink\n /** Defaults to `nearby-areas-heading`. */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n}\n\n/**\n * The nearest served suburbs to `origin` as links, nearest first by real distance (`nearestSuburbsWithin`). `link`\n * gives a suburb's page (or `undefined` to leave it out, for a suburb without that page).\n */\nexport function nearbyAreaLinks(\n origin: string,\n served: readonly string[],\n options: { readonly centreOf: SuburbCentreLookup; readonly link: (suburb: string) => UiLink | undefined; readonly limit?: number; readonly maxKm?: number },\n): UiLink[] {\n const limit = options.limit ?? NEARBY_AREAS_DEFAULT\n if (limit > NEARBY_AREAS_LIMIT) throw new Error(`nearbyAreaLinks: at most ${NEARBY_AREAS_LIMIT} neighbours.`)\n const candidates = served.filter((suburb) => options.link(suburb) !== undefined)\n return nearestSuburbsWithin(origin, candidates, { centreOf: options.centreOf, limit, ...(options.maxKm !== undefined ? { maxKm: options.maxKm } : {}) }).map(\n (entry) => options.link(entry.slug)!,\n )\n}\n\n/** The guard for `SectionSequence`. */\nexport function nearbyAreasReady({ areas }: Pick<NearbyAreasProps, 'areas'>): BrandlabSectionReadiness {\n return areas.length > 0 ? SECTION_READY : { render: false, reason: 'no-nearby-areas' }\n}\n\nexport function NearbyAreas({ title, eyebrow, lead, areas, regionLink, more, headingId = 'nearby-areas-heading', tone, variant = 'list', layout, classes }: NearbyAreasProps) {\n if (areas.length === 0) return null\n if (areas.length > NEARBY_AREAS_LIMIT) throw new Error(`NearbyAreas: ${areas.length} neighbours; the section lists at most ${NEARBY_AREAS_LIMIT}.`)\n if (new Set(areas.map((area) => area.href)).size !== areas.length) throw new Error('NearbyAreas: a neighbour is listed twice.')\n if (regionLink && areas.some((area) => area.href === regionLink.href)) throw new Error('NearbyAreas: the region link repeats a neighbour.')\n return (\n <Section id=\"nearby-areas\" labelledBy={headingId} tone={tone} variant={variant} classes={classes}>\n <SectionHeading id={headingId} title={title} eyebrow={eyebrow} lead={lead} classes={classes} />\n <LinkList links={areas} kind=\"nearby\" layout={layout} classes={{ root: classes?.list }} />\n {regionLink ? (\n <p {...slot(classes, 'region')}>\n <Link href={regionLink.href} prefetch={false}>\n {regionLink.label}\n </Link>\n </p>\n ) : null}\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"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `NearbyJobs` (S17, `nearby-jobs`, 3.15.0): proof of real work near this place. Server component.
|
|
3
|
+
*
|
|
4
|
+
* The cards come from the job log (site-core `./jobs`): `nearbyJobs(origin, jobs, { centreOf, service })` ranks the
|
|
5
|
+
* page suburb's own jobs first, then others by real distance within 10 km, at most three. Each card's heading is
|
|
6
|
+
* `jobCardLabel`: "Tree removal in Castle Hill" for the page's own suburb, "Tree removal nearby: Ryde" for any other
|
|
7
|
+
* (L4); only a job whose month is known says "Recent" (or `labelPrefix`) and ends ", {Month Year}". Distances rank the cards and are never
|
|
8
|
+
* printed (L10). A card shows the job's first photo through `Media`, and only a genuine photo of that job: a stock or AI
|
|
9
|
+
* image never stands for the client's work. With no job close enough the section renders nothing and its guard says
|
|
10
|
+
* so (`no-jobs-nearby`, D2). A card links to the job's project page when the site has one (prefetch on intent).
|
|
11
|
+
* Card photos are always lazy. `pagePhotos` names the photos the page already shows (the hero's): a card then takes the
|
|
12
|
+
* job's next genuine photo, or none, so the hero's priority photo is never repeated below as a lazy copy (3.15.0; on
|
|
13
|
+
* AA's suburb pages the repeat made Next report the hero, the LCP image, as lazy and downloaded it twice).
|
|
14
|
+
*/
|
|
15
|
+
import type { ReactNode } from 'react';
|
|
16
|
+
import { type BrandlabJob, type BrandlabNearbyJob } from '@brandlab-digital/site-core/jobs';
|
|
17
|
+
import type { BrandlabMediaManifest } from '@brandlab-digital/site-core/media';
|
|
18
|
+
import { type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order';
|
|
19
|
+
import { type MediaLayout } from '../primitives/media.js';
|
|
20
|
+
import { type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js';
|
|
21
|
+
import type { RepeatedSlotOf } from '../repeated-slots.js';
|
|
22
|
+
import { type SkinEntry } from '../skin.js';
|
|
23
|
+
export declare const NEARBY_JOBS_VARIANTS: readonly ["grid", "rail"];
|
|
24
|
+
export type NearbyJobsVariant = (typeof NEARBY_JOBS_VARIANTS)[number];
|
|
25
|
+
export type NearbyJobsSlot = SectionSlot | SectionHeadingSlot | 'list' | RepeatedSlotOf<'nearbyJobs'>;
|
|
26
|
+
/** Rendered once per job: styled from the skin stylesheet, never by class. */
|
|
27
|
+
export type NearbyJobsRepeatedSlot = RepeatedSlotOf<'nearbyJobs'>;
|
|
28
|
+
export interface NearbyJobsLayout {
|
|
29
|
+
/** `grid`: columns on wide screens. Defaults to 3. */
|
|
30
|
+
readonly columns?: 2 | 3;
|
|
31
|
+
/** The photo's crop. Defaults to 4:3. */
|
|
32
|
+
readonly ratio?: NonNullable<MediaLayout['ratio']>;
|
|
33
|
+
}
|
|
34
|
+
export type NearbyJobsSkin = SkinEntry<NearbyJobsSlot, NearbyJobsVariant, NearbyJobsLayout, NearbyJobsRepeatedSlot>;
|
|
35
|
+
export interface NearbyJobsProps extends NearbyJobsSkin {
|
|
36
|
+
/** The H2, for example "Recent tree work near Castle Hill". */
|
|
37
|
+
readonly title: ReactNode;
|
|
38
|
+
readonly eyebrow?: ReactNode;
|
|
39
|
+
readonly lead?: ReactNode;
|
|
40
|
+
/** The ranked cards from `nearbyJobs()`, nearest first; at most three. */
|
|
41
|
+
readonly jobs: readonly BrandlabNearbyJob[];
|
|
42
|
+
/** The page's suburb slug: its own jobs read "in {Suburb}", every other "nearby: {Suburb}". */
|
|
43
|
+
readonly pageSuburb: string;
|
|
44
|
+
/** The service's name from the site's catalogue, for example `(slug) => services[slug].name`. */
|
|
45
|
+
readonly serviceName: (service: string) => string;
|
|
46
|
+
/** The suburb's name from the registry. */
|
|
47
|
+
readonly suburbName: (suburb: string) => string;
|
|
48
|
+
/** Replaces "Recent" on a dated card ("Completed tree removal …"); `''` for none. An undated card never has one. */
|
|
49
|
+
readonly labelPrefix?: string;
|
|
50
|
+
/** The media manifest, for the jobs' photos. Left out, the cards show no photo. */
|
|
51
|
+
readonly media?: BrandlabMediaManifest;
|
|
52
|
+
/** Photos the page already shows, such as the hero's: media ids or `src` paths. A card never repeats one. */
|
|
53
|
+
readonly pagePhotos?: readonly string[];
|
|
54
|
+
/** The job's project page, when the site has one. */
|
|
55
|
+
readonly jobHref?: (job: BrandlabJob) => string | undefined;
|
|
56
|
+
/** The photo `sizes`. Defaults to one column of the grid. */
|
|
57
|
+
readonly imageSizes?: string;
|
|
58
|
+
/** Defaults to `nearby-jobs-heading`. */
|
|
59
|
+
readonly headingId?: string;
|
|
60
|
+
/** Set by `SectionSequence`. */
|
|
61
|
+
readonly tone?: SectionTone;
|
|
62
|
+
}
|
|
63
|
+
/** The guard for `SectionSequence`: no card means no section (D2). */
|
|
64
|
+
export declare function nearbyJobsReady({ jobs }: Pick<NearbyJobsProps, 'jobs'>): BrandlabSectionReadiness;
|
|
65
|
+
export declare function NearbyJobs({ title, eyebrow, lead, jobs, pageSuburb, serviceName, suburbName, labelPrefix, media, pagePhotos, jobHref, imageSizes, headingId, tone, variant, layout, classes, }: NearbyJobsProps): import("react/jsx-runtime").JSX.Element | null;
|
|
66
|
+
//# sourceMappingURL=nearby-jobs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nearby-jobs.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/nearby-jobs.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { JOB_CARD_LIMIT, jobCardLabel, jobPhotos } from '@brandlab-digital/site-core/jobs';
|
|
3
|
+
import { SECTION_READY } from '@brandlab-digital/site-core/section-order';
|
|
4
|
+
import { LINK_PREFETCH_POLICY } from '../prefetch.js';
|
|
5
|
+
import { Anchor } from '../primitives/links.js';
|
|
6
|
+
import { Media } from '../primitives/media.js';
|
|
7
|
+
import { Section, SectionHeading } from '../primitives/layout.js';
|
|
8
|
+
import { repeatedSlot, slot } from '../skin.js';
|
|
9
|
+
export const NEARBY_JOBS_VARIANTS = ['grid', 'rail'];
|
|
10
|
+
/** The guard for `SectionSequence`: no card means no section (D2). */
|
|
11
|
+
export function nearbyJobsReady({ jobs }) {
|
|
12
|
+
return jobs.length > 0 ? SECTION_READY : { render: false, reason: 'no-jobs-nearby' };
|
|
13
|
+
}
|
|
14
|
+
function photoOf(job, media, shown) {
|
|
15
|
+
if (!media)
|
|
16
|
+
return undefined;
|
|
17
|
+
const photo = jobPhotos(job, media).find((entry) => !shown.has(entry.id) && !shown.has(entry.src));
|
|
18
|
+
if (!photo)
|
|
19
|
+
return undefined;
|
|
20
|
+
if (photo.provenance !== 'genuine') {
|
|
21
|
+
throw new Error(`NearbyJobs: "${photo.id}" is ${photo.provenance}; only a genuine photo of the job shows on its card.`);
|
|
22
|
+
}
|
|
23
|
+
if (photo.job !== undefined && photo.job !== job.id)
|
|
24
|
+
throw new Error(`NearbyJobs: "${photo.id}" is a photo of ${photo.job}, not ${job.id}.`);
|
|
25
|
+
return photo;
|
|
26
|
+
}
|
|
27
|
+
export function NearbyJobs({ title, eyebrow, lead, jobs, pageSuburb, serviceName, suburbName, labelPrefix, media, pagePhotos, jobHref, imageSizes, headingId = 'nearby-jobs-heading', tone, variant = 'grid', layout, classes, }) {
|
|
28
|
+
if (jobs.length === 0)
|
|
29
|
+
return null;
|
|
30
|
+
if (jobs.length > JOB_CARD_LIMIT)
|
|
31
|
+
throw new Error(`NearbyJobs: ${jobs.length} jobs; a page shows at most ${JOB_CARD_LIMIT} job cards.`);
|
|
32
|
+
const ids = new Set();
|
|
33
|
+
for (const { job } of jobs) {
|
|
34
|
+
if (ids.has(job.id))
|
|
35
|
+
throw new Error(`NearbyJobs: "${job.id}" is listed twice.`);
|
|
36
|
+
ids.add(job.id);
|
|
37
|
+
}
|
|
38
|
+
const shown = new Set(pagePhotos);
|
|
39
|
+
const columns = layout?.columns ?? 3;
|
|
40
|
+
const ratio = layout?.ratio ?? '4:3';
|
|
41
|
+
const sizes = imageSizes ?? `(min-width: 1024px) ${Math.round(100 / columns)}vw, (min-width: 640px) 50vw, 100vw`;
|
|
42
|
+
return (_jsxs(Section, { id: "nearby-jobs", labelledBy: headingId, tone: tone, variant: variant, classes: classes, hooks: { 'data-columns': String(columns), 'data-ratio': ratio }, children: [_jsx(SectionHeading, { id: headingId, title: title, eyebrow: eyebrow, lead: lead, classes: classes }), _jsx("ul", { role: "list", ...slot(classes, 'list'), children: jobs.map(({ job }) => {
|
|
43
|
+
const label = jobCardLabel(job, { service: serviceName(job.service), suburbName: suburbName(job.suburb), pageSuburb, ...(labelPrefix === undefined ? {} : { prefix: labelPrefix }) });
|
|
44
|
+
const href = jobHref?.(job);
|
|
45
|
+
const photo = photoOf(job, media, shown);
|
|
46
|
+
return (_jsxs("li", { ...repeatedSlot('item'), "data-item": job.id, "data-suburb": job.suburb, children: [_jsxs("div", { ...repeatedSlot('body'), children: [_jsx("h3", { ...repeatedSlot('title'), children: href ? (_jsx(Anchor, { href: href, prefetch: LINK_PREFETCH_POLICY.contentCard, hooks: repeatedSlot('link'), children: label })) : (label) }), _jsx("p", { ...repeatedSlot('outcome'), children: job.outcome })] }), photo ? (_jsx("div", { ...repeatedSlot('media'), children: _jsx(Media, { media: photo, sizes: sizes, layout: { ratio } }) })) : null] }, job.id));
|
|
47
|
+
}) })] }));
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=nearby-jobs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nearby-jobs.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/nearby-jobs.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * `NearbyJobs` (S17, `nearby-jobs`, 3.15.0): proof of real work near this place. Server component.\n *\n * The cards come from the job log (site-core `./jobs`): `nearbyJobs(origin, jobs, { centreOf, service })` ranks the\n * page suburb's own jobs first, then others by real distance within 10 km, at most three. Each card's heading is\n * `jobCardLabel`: \"Tree removal in Castle Hill\" for the page's own suburb, \"Tree removal nearby: Ryde\" for any other\n * (L4); only a job whose month is known says \"Recent\" (or `labelPrefix`) and ends \", {Month Year}\". Distances rank the cards and are never\n * printed (L10). A card shows the job's first photo through `Media`, and only a genuine photo of that job: a stock or AI\n * image never stands for the client's work. With no job close enough the section renders nothing and its guard says\n * so (`no-jobs-nearby`, D2). A card links to the job's project page when the site has one (prefetch on intent).\n * Card photos are always lazy. `pagePhotos` names the photos the page already shows (the hero's): a card then takes the\n * job's next genuine photo, or none, so the hero's priority photo is never repeated below as a lazy copy (3.15.0; on\n * AA's suburb pages the repeat made Next report the hero, the LCP image, as lazy and downloaded it twice).\n */\nimport type { ReactNode } from 'react'\nimport { JOB_CARD_LIMIT, jobCardLabel, jobPhotos, type BrandlabJob, type BrandlabNearbyJob } from '@brandlab-digital/site-core/jobs'\nimport type { BrandlabMediaManifest } from '@brandlab-digital/site-core/media'\nimport { SECTION_READY, type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order'\nimport { LINK_PREFETCH_POLICY } from '../prefetch.js'\nimport { Anchor } from '../primitives/links.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'\n\nexport const NEARBY_JOBS_VARIANTS = ['grid', 'rail'] as const\nexport type NearbyJobsVariant = (typeof NEARBY_JOBS_VARIANTS)[number]\nexport type NearbyJobsSlot = SectionSlot | SectionHeadingSlot | 'list' | RepeatedSlotOf<'nearbyJobs'>\n/** Rendered once per job: styled from the skin stylesheet, never by class. */\nexport type NearbyJobsRepeatedSlot = RepeatedSlotOf<'nearbyJobs'>\nexport interface NearbyJobsLayout {\n /** `grid`: columns on wide screens. Defaults to 3. */\n readonly columns?: 2 | 3\n /** The photo's crop. Defaults to 4:3. */\n readonly ratio?: NonNullable<MediaLayout['ratio']>\n}\nexport type NearbyJobsSkin = SkinEntry<NearbyJobsSlot, NearbyJobsVariant, NearbyJobsLayout, NearbyJobsRepeatedSlot>\n\nexport interface NearbyJobsProps extends NearbyJobsSkin {\n /** The H2, for example \"Recent tree work near Castle Hill\". */\n readonly title: ReactNode\n readonly eyebrow?: ReactNode\n readonly lead?: ReactNode\n /** The ranked cards from `nearbyJobs()`, nearest first; at most three. */\n readonly jobs: readonly BrandlabNearbyJob[]\n /** The page's suburb slug: its own jobs read \"in {Suburb}\", every other \"nearby: {Suburb}\". */\n readonly pageSuburb: string\n /** The service's name from the site's catalogue, for example `(slug) => services[slug].name`. */\n readonly serviceName: (service: string) => string\n /** The suburb's name from the registry. */\n readonly suburbName: (suburb: string) => string\n /** Replaces \"Recent\" on a dated card (\"Completed tree removal …\"); `''` for none. An undated card never has one. */\n readonly labelPrefix?: string\n /** The media manifest, for the jobs' photos. Left out, the cards show no photo. */\n readonly media?: BrandlabMediaManifest\n /** Photos the page already shows, such as the hero's: media ids or `src` paths. A card never repeats one. */\n readonly pagePhotos?: readonly string[]\n /** The job's project page, when the site has one. */\n readonly jobHref?: (job: BrandlabJob) => string | undefined\n /** The photo `sizes`. Defaults to one column of the grid. */\n readonly imageSizes?: string\n /** Defaults to `nearby-jobs-heading`. */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n}\n\n/** The guard for `SectionSequence`: no card means no section (D2). */\nexport function nearbyJobsReady({ jobs }: Pick<NearbyJobsProps, 'jobs'>): BrandlabSectionReadiness {\n return jobs.length > 0 ? SECTION_READY : { render: false, reason: 'no-jobs-nearby' }\n}\n\nfunction photoOf(job: BrandlabJob, media: BrandlabMediaManifest | undefined, shown: ReadonlySet<string>) {\n if (!media) return undefined\n const photo = jobPhotos(job, media).find((entry) => !shown.has(entry.id) && !shown.has(entry.src))\n if (!photo) return undefined\n if (photo.provenance !== 'genuine') {\n throw new Error(`NearbyJobs: \"${photo.id}\" is ${photo.provenance}; only a genuine photo of the job shows on its card.`)\n }\n if (photo.job !== undefined && photo.job !== job.id) throw new Error(`NearbyJobs: \"${photo.id}\" is a photo of ${photo.job}, not ${job.id}.`)\n return photo\n}\n\nexport function NearbyJobs({\n title,\n eyebrow,\n lead,\n jobs,\n pageSuburb,\n serviceName,\n suburbName,\n labelPrefix,\n media,\n pagePhotos,\n jobHref,\n imageSizes,\n headingId = 'nearby-jobs-heading',\n tone,\n variant = 'grid',\n layout,\n classes,\n}: NearbyJobsProps) {\n if (jobs.length === 0) return null\n if (jobs.length > JOB_CARD_LIMIT) throw new Error(`NearbyJobs: ${jobs.length} jobs; a page shows at most ${JOB_CARD_LIMIT} job cards.`)\n const ids = new Set<string>()\n for (const { job } of jobs) {\n if (ids.has(job.id)) throw new Error(`NearbyJobs: \"${job.id}\" is listed twice.`)\n ids.add(job.id)\n }\n const shown = new Set(pagePhotos)\n const columns = layout?.columns ?? 3\n const ratio = layout?.ratio ?? '4:3'\n const sizes = imageSizes ?? `(min-width: 1024px) ${Math.round(100 / columns)}vw, (min-width: 640px) 50vw, 100vw`\n return (\n <Section id=\"nearby-jobs\" labelledBy={headingId} tone={tone} variant={variant} classes={classes} hooks={{ 'data-columns': String(columns), 'data-ratio': ratio }}>\n <SectionHeading id={headingId} title={title} eyebrow={eyebrow} lead={lead} classes={classes} />\n <ul role=\"list\" {...slot(classes, 'list')}>\n {jobs.map(({ job }) => {\n const label = jobCardLabel(job, { service: serviceName(job.service), suburbName: suburbName(job.suburb), pageSuburb, ...(labelPrefix === undefined ? {} : { prefix: labelPrefix }) })\n const href = jobHref?.(job)\n const photo = photoOf(job, media, shown)\n return (\n <li key={job.id} {...repeatedSlot('item')} data-item={job.id} data-suburb={job.suburb}>\n <div {...repeatedSlot('body')}>\n <h3 {...repeatedSlot('title')}>\n {href ? (\n <Anchor href={href} prefetch={LINK_PREFETCH_POLICY.contentCard} hooks={repeatedSlot('link')}>\n {label}\n </Anchor>\n ) : (\n label\n )}\n </h3>\n <p {...repeatedSlot('outcome')}>{job.outcome}</p>\n </div>\n {photo ? (\n <div {...repeatedSlot('media')}>\n <Media media={photo} sizes={sizes} layout={{ ratio }} />\n </div>\n ) : null}\n </li>\n )\n })}\n </ul>\n </Section>\n )\n}\n"]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Projects` (`projects`, 3.15.0): completed jobs as cards, each linking its project page (`/projects/{slug}`). Server
|
|
3
|
+
* component. AA's `ProjectGrid`, re-implemented on the `Card` primitive.
|
|
4
|
+
*
|
|
5
|
+
* Locked:
|
|
6
|
+
* - genuine photos only (v2 §11 imagery rule 4): a stock or AI image on a project card fails the build, and a project
|
|
7
|
+
* without a photo shows as a card without one;
|
|
8
|
+
* - at most 8 cards (K11; the in-content intent-prefetch limit); each card is one link, its title, which prefetches on
|
|
9
|
+
* intent. The hub link after the list never prefetches;
|
|
10
|
+
* - each card links its own project page, a child of the site's projects hub (`/projects/{slug}`, `/our-work/{slug}`);
|
|
11
|
+
* - no default visible copy: the heading, titles, summaries and the suburb-and-month line all come from the site.
|
|
12
|
+
*
|
|
13
|
+
* `projectCardFromJob` builds a card from a site-core job log row (`./jobs`): the job's first genuine photo and its
|
|
14
|
+
* outcome line; the title, the link and the meta line stay the site's.
|
|
15
|
+
*/
|
|
16
|
+
import type { ReactNode } from 'react';
|
|
17
|
+
import type { BrandlabJob } from '@brandlab-digital/site-core/jobs';
|
|
18
|
+
import type { BrandlabMediaManifest, BrandlabMediaRatio } from '@brandlab-digital/site-core/media';
|
|
19
|
+
import { type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order';
|
|
20
|
+
import { type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js';
|
|
21
|
+
import { type UiMedia } from '../primitives/media-frame.js';
|
|
22
|
+
import { type SkinEntry } from '../skin.js';
|
|
23
|
+
import type { UiLink } from '../types.js';
|
|
24
|
+
/** The cards' parts are the `Card` primitive's (`[data-bl="card"]`), styled from the skin stylesheet. */
|
|
25
|
+
export type ProjectsSlot = SectionSlot | SectionHeadingSlot | 'list' | 'more';
|
|
26
|
+
export interface ProjectsLayout {
|
|
27
|
+
/** Columns on wide screens. Defaults to 3. */
|
|
28
|
+
readonly columns?: 2 | 3 | 4;
|
|
29
|
+
/** The photos' crop. Defaults to 4:3. */
|
|
30
|
+
readonly ratio?: BrandlabMediaRatio;
|
|
31
|
+
}
|
|
32
|
+
export type ProjectsSkin = SkinEntry<ProjectsSlot, never, ProjectsLayout>;
|
|
33
|
+
/** The most project cards a section shows (K11; the in-content intent-prefetch limit). */
|
|
34
|
+
export declare const PROJECTS_CARD_LIMIT: 8;
|
|
35
|
+
/** One completed job as a card. */
|
|
36
|
+
export interface ProjectCardData {
|
|
37
|
+
/** A stable slug (the job id), rendered as `data-item`. */
|
|
38
|
+
readonly id: string;
|
|
39
|
+
/** The card's H3 and its link's whole name, for example "Gum removal over a pool, Epping". */
|
|
40
|
+
readonly title: string;
|
|
41
|
+
/** The project page, for example `/projects/epping-gum-removal`. From the site's data. */
|
|
42
|
+
readonly href: string;
|
|
43
|
+
/** What was done, in a line. */
|
|
44
|
+
readonly summary?: ReactNode;
|
|
45
|
+
/** A short line under the summary, for example the suburb and month. */
|
|
46
|
+
readonly meta?: ReactNode;
|
|
47
|
+
/** A genuine photo of this job from the media manifest. */
|
|
48
|
+
readonly image?: UiMedia;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A card from the job log: the job's first photo (`jobPhotos`, genuine ones only) and its outcome as the summary. The
|
|
52
|
+
* site gives the title, the project page and the meta line in its own words.
|
|
53
|
+
*/
|
|
54
|
+
export declare function projectCardFromJob(job: BrandlabJob, options: {
|
|
55
|
+
readonly media: BrandlabMediaManifest;
|
|
56
|
+
readonly title: string;
|
|
57
|
+
readonly href: string;
|
|
58
|
+
readonly meta?: ReactNode;
|
|
59
|
+
}): ProjectCardData;
|
|
60
|
+
export interface ProjectsProps extends ProjectsSkin {
|
|
61
|
+
/** The H2, for example "Recent tree removal jobs". */
|
|
62
|
+
readonly title: ReactNode;
|
|
63
|
+
readonly eyebrow?: ReactNode;
|
|
64
|
+
readonly lead?: ReactNode;
|
|
65
|
+
/** Up to 8 projects, newest or nearest first. */
|
|
66
|
+
readonly projects: readonly ProjectCardData[];
|
|
67
|
+
/** A link after the cards, for example the projects hub. Never prefetched. */
|
|
68
|
+
readonly more?: UiLink;
|
|
69
|
+
/** The photos' `sizes`: a fluid `vw` string, or a number for a fixed CSS width. Defaults to one grid column. */
|
|
70
|
+
readonly imageSizes?: string | number;
|
|
71
|
+
/** Defaults to `projects-heading`. */
|
|
72
|
+
readonly headingId?: string;
|
|
73
|
+
/** Set by `SectionSequence`. */
|
|
74
|
+
readonly tone?: SectionTone;
|
|
75
|
+
}
|
|
76
|
+
/** The section's guard for `SectionSequence`: skipped when no project is recorded. */
|
|
77
|
+
export declare function projectsReady({ projects }: Pick<ProjectsProps, 'projects'>): BrandlabSectionReadiness;
|
|
78
|
+
export declare function Projects({ title, eyebrow, lead, projects, more, imageSizes, headingId, tone, layout, classes, }: ProjectsProps): import("react/jsx-runtime").JSX.Element | null;
|
|
79
|
+
//# sourceMappingURL=projects.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/projects.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAEnE,OAAO,KAAK,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAA;AAClG,OAAO,EAAiB,KAAK,wBAAwB,EAAE,MAAM,2CAA2C,CAAA;AAExG,OAAO,EAA2B,KAAK,kBAAkB,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAE9H,OAAO,EAAc,KAAK,OAAO,EAAE,MAAM,8BAA8B,CAAA;AAEvE,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,yGAAyG;AACzG,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,kBAAkB,GAAG,MAAM,GAAG,MAAM,CAAA;AAC7E,MAAM,WAAW,cAAc;IAC7B,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC5B,yCAAyC;IACzC,QAAQ,CAAC,KAAK,CAAC,EAAE,kBAAkB,CAAA;CACpC;AACD,MAAM,MAAM,YAAY,GAAG,SAAS,CAAC,YAAY,EAAE,KAAK,EAAE,cAAc,CAAC,CAAA;AAEzE,0FAA0F;AAC1F,eAAO,MAAM,mBAAmB,GAAmC,CAAA;AAEnE,mCAAmC;AACnC,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,8FAA8F;IAC9F,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,0FAA0F;IAC1F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,gCAAgC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAA;IAC5B,wEAAwE;IACxE,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAA;IACzB,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CACzB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,WAAW,EAChB,OAAO,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,qBAAqB,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAA;CAAE,GAC3H,eAAe,CAUjB;AAED,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,sDAAsD;IACtD,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAA;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAA;IACzB,iDAAiD;IACjD,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAA;IAC7C,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,gHAAgH;IAChH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACrC,sCAAsC;IACtC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,gCAAgC;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAC5B;AAED,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC,GAAG,wBAAwB,CAErG;AA2BD,wBAAgB,QAAQ,CAAC,EACvB,KAAK,EACL,OAAO,EACP,IAAI,EACJ,QAAQ,EACR,IAAI,EACJ,UAAU,EACV,SAA8B,EAC9B,IAAI,EACJ,MAAM,EACN,OAAO,GACR,EAAE,aAAa,kDAiCf"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jobPhotos } from '@brandlab-digital/site-core/jobs';
|
|
3
|
+
import { SECTION_READY } from '@brandlab-digital/site-core/section-order';
|
|
4
|
+
import { Card } from '../primitives/card.js';
|
|
5
|
+
import { Section, SectionHeading } from '../primitives/layout.js';
|
|
6
|
+
import { Anchor } from '../primitives/links.js';
|
|
7
|
+
import { MediaFrame } from '../primitives/media-frame.js';
|
|
8
|
+
import { LINK_PREFETCH_LIMITS, LINK_PREFETCH_POLICY } from '../prefetch.js';
|
|
9
|
+
import { slot } from '../skin.js';
|
|
10
|
+
import { assertGenuineWork, frameSizing, gridImageSizes, isInternalPath } from './work-photos.js';
|
|
11
|
+
/** The most project cards a section shows (K11; the in-content intent-prefetch limit). */
|
|
12
|
+
export const PROJECTS_CARD_LIMIT = LINK_PREFETCH_LIMITS.contentCard;
|
|
13
|
+
/**
|
|
14
|
+
* A card from the job log: the job's first photo (`jobPhotos`, genuine ones only) and its outcome as the summary. The
|
|
15
|
+
* site gives the title, the project page and the meta line in its own words.
|
|
16
|
+
*/
|
|
17
|
+
export function projectCardFromJob(job, options) {
|
|
18
|
+
const photo = jobPhotos(job, options.media).find((entry) => entry.provenance === 'genuine');
|
|
19
|
+
return {
|
|
20
|
+
id: job.id,
|
|
21
|
+
title: options.title,
|
|
22
|
+
href: options.href,
|
|
23
|
+
summary: job.outcome,
|
|
24
|
+
...(options.meta !== undefined ? { meta: options.meta } : {}),
|
|
25
|
+
...(photo ? { image: photo } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** The section's guard for `SectionSequence`: skipped when no project is recorded. */
|
|
29
|
+
export function projectsReady({ projects }) {
|
|
30
|
+
return projects.length > 0 ? SECTION_READY : { render: false, reason: 'no-projects' };
|
|
31
|
+
}
|
|
32
|
+
function assertProjects(projects) {
|
|
33
|
+
if (projects.length > PROJECTS_CARD_LIMIT) {
|
|
34
|
+
throw new Error(`Projects: ${projects.length} projects; a section shows at most ${PROJECTS_CARD_LIMIT} (K11). Link the projects hub for the rest.`);
|
|
35
|
+
}
|
|
36
|
+
const ids = new Set();
|
|
37
|
+
const hrefs = new Set();
|
|
38
|
+
const images = new Map();
|
|
39
|
+
for (const project of projects) {
|
|
40
|
+
if (!project.id || ids.has(project.id))
|
|
41
|
+
throw new Error(`Projects: every project needs its own id ("${project.id}" is empty or repeated).`);
|
|
42
|
+
ids.add(project.id);
|
|
43
|
+
if (!project.title?.trim())
|
|
44
|
+
throw new Error(`Projects: the project "${project.id}" has no title.`);
|
|
45
|
+
if (!isInternalPath(project.href) || project.href.split('/').filter(Boolean).length < 2) {
|
|
46
|
+
throw new Error(`Projects: "${project.id}" links ${project.href}; a project card links its own page under the projects hub, such as /projects/${project.id}.`);
|
|
47
|
+
}
|
|
48
|
+
if (hrefs.has(project.href))
|
|
49
|
+
throw new Error(`Projects: two projects link to ${project.href}.`);
|
|
50
|
+
hrefs.add(project.href);
|
|
51
|
+
if (project.image) {
|
|
52
|
+
assertGenuineWork('Projects', project.image, `the photo of "${project.id}"`);
|
|
53
|
+
const other = images.get(project.image.src);
|
|
54
|
+
if (other)
|
|
55
|
+
throw new Error(`Projects: "${project.id}" and "${other}" use the same photo (${project.image.src}).`);
|
|
56
|
+
images.set(project.image.src, project.id);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function Projects({ title, eyebrow, lead, projects, more, imageSizes, headingId = 'projects-heading', tone, layout, classes, }) {
|
|
61
|
+
if (!projectsReady({ projects }).render)
|
|
62
|
+
return null;
|
|
63
|
+
assertProjects(projects);
|
|
64
|
+
const columns = layout?.columns ?? 3;
|
|
65
|
+
const ratio = layout?.ratio ?? '4:3';
|
|
66
|
+
const sizing = frameSizing(imageSizes ?? gridImageSizes(columns));
|
|
67
|
+
return (_jsxs(Section, { id: "projects", labelledBy: headingId, tone: tone, classes: classes, hooks: { 'data-columns': String(columns), 'data-ratio': ratio }, children: [_jsx(SectionHeading, { id: headingId, title: title, eyebrow: eyebrow, lead: lead, classes: classes }), _jsx("ul", { role: "list", ...slot(classes, 'list'), children: projects.map((project) => (_jsx(Card, { as: "li", title: project.title, href: project.href, prefetch: LINK_PREFETCH_POLICY.contentCard, media: project.image ? _jsx(MediaFrame, { media: project.image, ratio: ratio, ...sizing }) : undefined, meta: project.meta, children: project.summary }, project.id))) }), more ? (_jsx("p", { ...slot(classes, 'more'), children: _jsx(Anchor, { href: more.href, prefetch: false, hooks: {}, children: more.label }) })) : null] }));
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=projects.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projects.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/projects.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * `Projects` (`projects`, 3.15.0): completed jobs as cards, each linking its project page (`/projects/{slug}`). Server\n * component. AA's `ProjectGrid`, re-implemented on the `Card` primitive.\n *\n * Locked:\n * - genuine photos only (v2 §11 imagery rule 4): a stock or AI image on a project card fails the build, and a project\n * without a photo shows as a card without one;\n * - at most 8 cards (K11; the in-content intent-prefetch limit); each card is one link, its title, which prefetches on\n * intent. The hub link after the list never prefetches;\n * - each card links its own project page, a child of the site's projects hub (`/projects/{slug}`, `/our-work/{slug}`);\n * - no default visible copy: the heading, titles, summaries and the suburb-and-month line all come from the site.\n *\n * `projectCardFromJob` builds a card from a site-core job log row (`./jobs`): the job's first genuine photo and its\n * outcome line; the title, the link and the meta line stay the site's.\n */\nimport type { ReactNode } from 'react'\nimport type { BrandlabJob } from '@brandlab-digital/site-core/jobs'\nimport { jobPhotos } from '@brandlab-digital/site-core/jobs'\nimport type { BrandlabMediaManifest, BrandlabMediaRatio } from '@brandlab-digital/site-core/media'\nimport { SECTION_READY, type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order'\nimport { Card } from '../primitives/card.js'\nimport { Section, SectionHeading, type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js'\nimport { Anchor } from '../primitives/links.js'\nimport { MediaFrame, type UiMedia } from '../primitives/media-frame.js'\nimport { LINK_PREFETCH_LIMITS, LINK_PREFETCH_POLICY } from '../prefetch.js'\nimport { slot, type SkinEntry } from '../skin.js'\nimport type { UiLink } from '../types.js'\nimport { assertGenuineWork, frameSizing, gridImageSizes, isInternalPath } from './work-photos.js'\n\n/** The cards' parts are the `Card` primitive's (`[data-bl=\"card\"]`), styled from the skin stylesheet. */\nexport type ProjectsSlot = SectionSlot | SectionHeadingSlot | 'list' | 'more'\nexport interface ProjectsLayout {\n /** Columns on wide screens. Defaults to 3. */\n readonly columns?: 2 | 3 | 4\n /** The photos' crop. Defaults to 4:3. */\n readonly ratio?: BrandlabMediaRatio\n}\nexport type ProjectsSkin = SkinEntry<ProjectsSlot, never, ProjectsLayout>\n\n/** The most project cards a section shows (K11; the in-content intent-prefetch limit). */\nexport const PROJECTS_CARD_LIMIT = LINK_PREFETCH_LIMITS.contentCard\n\n/** One completed job as a card. */\nexport interface ProjectCardData {\n /** A stable slug (the job id), rendered as `data-item`. */\n readonly id: string\n /** The card's H3 and its link's whole name, for example \"Gum removal over a pool, Epping\". */\n readonly title: string\n /** The project page, for example `/projects/epping-gum-removal`. From the site's data. */\n readonly href: string\n /** What was done, in a line. */\n readonly summary?: ReactNode\n /** A short line under the summary, for example the suburb and month. */\n readonly meta?: ReactNode\n /** A genuine photo of this job from the media manifest. */\n readonly image?: UiMedia\n}\n\n/**\n * A card from the job log: the job's first photo (`jobPhotos`, genuine ones only) and its outcome as the summary. The\n * site gives the title, the project page and the meta line in its own words.\n */\nexport function projectCardFromJob(\n job: BrandlabJob,\n options: { readonly media: BrandlabMediaManifest; readonly title: string; readonly href: string; readonly meta?: ReactNode },\n): ProjectCardData {\n const photo = jobPhotos(job, options.media).find((entry) => entry.provenance === 'genuine')\n return {\n id: job.id,\n title: options.title,\n href: options.href,\n summary: job.outcome,\n ...(options.meta !== undefined ? { meta: options.meta } : {}),\n ...(photo ? { image: photo } : {}),\n }\n}\n\nexport interface ProjectsProps extends ProjectsSkin {\n /** The H2, for example \"Recent tree removal jobs\". */\n readonly title: ReactNode\n readonly eyebrow?: ReactNode\n readonly lead?: ReactNode\n /** Up to 8 projects, newest or nearest first. */\n readonly projects: readonly ProjectCardData[]\n /** A link after the cards, for example the projects hub. Never prefetched. */\n readonly more?: UiLink\n /** The photos' `sizes`: a fluid `vw` string, or a number for a fixed CSS width. Defaults to one grid column. */\n readonly imageSizes?: string | number\n /** Defaults to `projects-heading`. */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n}\n\n/** The section's guard for `SectionSequence`: skipped when no project is recorded. */\nexport function projectsReady({ projects }: Pick<ProjectsProps, 'projects'>): BrandlabSectionReadiness {\n return projects.length > 0 ? SECTION_READY : { render: false, reason: 'no-projects' }\n}\n\nfunction assertProjects(projects: readonly ProjectCardData[]): void {\n if (projects.length > PROJECTS_CARD_LIMIT) {\n throw new Error(`Projects: ${projects.length} projects; a section shows at most ${PROJECTS_CARD_LIMIT} (K11). Link the projects hub for the rest.`)\n }\n const ids = new Set<string>()\n const hrefs = new Set<string>()\n const images = new Map<string, string>()\n for (const project of projects) {\n if (!project.id || ids.has(project.id)) throw new Error(`Projects: every project needs its own id (\"${project.id}\" is empty or repeated).`)\n ids.add(project.id)\n if (!project.title?.trim()) throw new Error(`Projects: the project \"${project.id}\" has no title.`)\n if (!isInternalPath(project.href) || project.href.split('/').filter(Boolean).length < 2) {\n throw new Error(`Projects: \"${project.id}\" links ${project.href}; a project card links its own page under the projects hub, such as /projects/${project.id}.`)\n }\n if (hrefs.has(project.href)) throw new Error(`Projects: two projects link to ${project.href}.`)\n hrefs.add(project.href)\n if (project.image) {\n assertGenuineWork('Projects', project.image, `the photo of \"${project.id}\"`)\n const other = images.get(project.image.src)\n if (other) throw new Error(`Projects: \"${project.id}\" and \"${other}\" use the same photo (${project.image.src}).`)\n images.set(project.image.src, project.id)\n }\n }\n}\n\nexport function Projects({\n title,\n eyebrow,\n lead,\n projects,\n more,\n imageSizes,\n headingId = 'projects-heading',\n tone,\n layout,\n classes,\n}: ProjectsProps) {\n if (!projectsReady({ projects }).render) return null\n assertProjects(projects)\n const columns = layout?.columns ?? 3\n const ratio = layout?.ratio ?? '4:3'\n const sizing = frameSizing(imageSizes ?? gridImageSizes(columns))\n return (\n <Section id=\"projects\" labelledBy={headingId} tone={tone} classes={classes} hooks={{ 'data-columns': String(columns), 'data-ratio': ratio }}>\n <SectionHeading id={headingId} title={title} eyebrow={eyebrow} lead={lead} classes={classes} />\n <ul role=\"list\" {...slot(classes, 'list')}>\n {projects.map((project) => (\n <Card\n key={project.id}\n as=\"li\"\n title={project.title}\n href={project.href}\n prefetch={LINK_PREFETCH_POLICY.contentCard}\n media={project.image ? <MediaFrame media={project.image} ratio={ratio} {...sizing} /> : undefined}\n meta={project.meta}\n >\n {project.summary}\n </Card>\n ))}\n </ul>\n {more ? (\n <p {...slot(classes, 'more')}>\n <Anchor href={more.href} prefetch={false} hooks={{}}>\n {more.label}\n </Anchor>\n </p>\n ) : null}\n </Section>\n )\n}\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `QuoteChecklist` (`data-bl="quote-checklist"`, 3.15.0): what to send for a fast, fair quote in this trade (photos of
|
|
3
|
+
* the tree and its access, the suburb, what's underneath it…), as a real list of ticks, then the button to the quote
|
|
4
|
+
* form. Server component built on `TickList`. It is a block, not a sequenced section of the matrix: a site places it
|
|
5
|
+
* inside its service detail, beside the contact form or as its own band (`headingLevel`).
|
|
6
|
+
*
|
|
7
|
+
* Locked: the list comes before the action; the action is the site's quote button (`lead_cta_click` placement
|
|
8
|
+
* `section`, id `quote-checklist`) to its quote form. No default visible copy: the heading, every item and the button
|
|
9
|
+
* label are the site's (spec: checklist content lives in each site's copy file, so same-trade sites never repeat each
|
|
10
|
+
* other). At most 8 items, so the list stays a checklist.
|
|
11
|
+
*/
|
|
12
|
+
import type { ReactNode } from 'react';
|
|
13
|
+
import { type TickItem } from '../primitives/lists.js';
|
|
14
|
+
import type { ButtonVariant } from '../primitives/links.js';
|
|
15
|
+
import { type SkinEntry } from '../skin.js';
|
|
16
|
+
export type QuoteChecklistSlot = 'root' | 'heading' | 'lead' | 'list' | 'action' | 'quote';
|
|
17
|
+
export interface QuoteChecklistLayout {
|
|
18
|
+
/** The list's columns from the phone breakpoint up. Defaults to 1. */
|
|
19
|
+
readonly columns?: 1 | 2;
|
|
20
|
+
/** The quote button's variant. Defaults to `primary`. */
|
|
21
|
+
readonly quoteVariant?: ButtonVariant;
|
|
22
|
+
}
|
|
23
|
+
export type QuoteChecklistSkin = SkinEntry<QuoteChecklistSlot, never, QuoteChecklistLayout>;
|
|
24
|
+
/** The most items a checklist holds. */
|
|
25
|
+
export declare const QUOTE_CHECKLIST_MAX = 8;
|
|
26
|
+
export interface QuoteChecklistProps extends QuoteChecklistSkin {
|
|
27
|
+
/** The heading, for example "What to send for a fast quote". */
|
|
28
|
+
readonly title: ReactNode;
|
|
29
|
+
readonly lead?: ReactNode;
|
|
30
|
+
/** What to send, in the site's words: 2 to 8 items. */
|
|
31
|
+
readonly items: readonly TickItem[];
|
|
32
|
+
/** The site's recorded quote label and its quote form, for example `{ label: 'Get a free quote', href: '#quote-hero' }`. */
|
|
33
|
+
readonly quote: {
|
|
34
|
+
readonly label: string;
|
|
35
|
+
readonly href: string;
|
|
36
|
+
};
|
|
37
|
+
/** 2 (the default) makes the block its own `<section>` labelled by an H2; 3 sits it inside a section's outline. */
|
|
38
|
+
readonly headingLevel?: 2 | 3;
|
|
39
|
+
/** Defaults to `quote-checklist-heading`. */
|
|
40
|
+
readonly headingId?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function QuoteChecklist({ title, lead, items, quote, headingLevel, headingId, layout, classes }: QuoteChecklistProps): import("react/jsx-runtime").JSX.Element | null;
|
|
43
|
+
//# sourceMappingURL=quote-checklist.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quote-checklist.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/quote-checklist.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AAEhE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AAEjD,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAA;AAC1F,MAAM,WAAW,oBAAoB;IACnC,sEAAsE;IACtE,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;IACxB,yDAAyD;IACzD,QAAQ,CAAC,YAAY,CAAC,EAAE,aAAa,CAAA;CACtC;AACD,MAAM,MAAM,kBAAkB,GAAG,SAAS,CAAC,kBAAkB,EAAE,KAAK,EAAE,oBAAoB,CAAC,CAAA;AAE3F,wCAAwC;AACxC,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAEpC,MAAM,WAAW,mBAAoB,SAAQ,kBAAkB;IAC7D,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAA;IACzB,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAA;IACnC,4HAA4H;IAC5H,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACjE,mHAAmH;IACnH,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;IAC7B,6CAA6C;IAC7C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,YAAgB,EAAE,SAAqC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,mBAAmB,kDAoB1J"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { TickList } from '../primitives/lists.js';
|
|
3
|
+
import { QuoteButton } from '../primitives/quote-button.js';
|
|
4
|
+
import { slot } from '../skin.js';
|
|
5
|
+
/** The most items a checklist holds. */
|
|
6
|
+
export const QUOTE_CHECKLIST_MAX = 8;
|
|
7
|
+
export function QuoteChecklist({ title, lead, items, quote, headingLevel = 2, headingId = 'quote-checklist-heading', layout, classes }) {
|
|
8
|
+
if (items.length === 0)
|
|
9
|
+
return null;
|
|
10
|
+
if (items.length < 2 || items.length > QUOTE_CHECKLIST_MAX) {
|
|
11
|
+
throw new Error(`QuoteChecklist: ${items.length} items; a checklist holds 2 to ${QUOTE_CHECKLIST_MAX}.`);
|
|
12
|
+
}
|
|
13
|
+
if (!quote?.label?.trim() || !quote.href?.trim())
|
|
14
|
+
throw new Error('QuoteChecklist: the quote button needs the site\'s label and its quote form.');
|
|
15
|
+
const Root = headingLevel === 2 ? 'section' : 'div';
|
|
16
|
+
const Heading = `h${headingLevel}`;
|
|
17
|
+
return (_jsxs(Root, { "data-bl": "quote-checklist", "aria-labelledby": headingLevel === 2 ? headingId : undefined, className: classes?.root, children: [_jsx(Heading, { id: headingId, ...slot(classes, 'heading'), children: title }), lead ? _jsx("p", { ...slot(classes, 'lead'), children: lead }) : null, _jsx(TickList, { items: items, layout: { columns: layout?.columns ?? 1 }, classes: { root: classes?.list } }), _jsx("p", { ...slot(classes, 'action'), children: _jsx(QuoteButton, { label: quote.label, href: quote.href, placement: "section", ctaId: "quote-checklist", variant: layout?.quoteVariant ?? 'primary', classes: { root: classes?.quote } }) })] }));
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=quote-checklist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quote-checklist.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/quote-checklist.tsx"],"names":[],"mappings":";AAYA,OAAO,EAAE,QAAQ,EAAiB,MAAM,wBAAwB,CAAA;AAChE,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAA;AAE3D,OAAO,EAAE,IAAI,EAAkB,MAAM,YAAY,CAAA;AAWjD,wCAAwC;AACxC,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAA;AAgBpC,MAAM,UAAU,cAAc,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,GAAG,CAAC,EAAE,SAAS,GAAG,yBAAyB,EAAE,MAAM,EAAE,OAAO,EAAuB;IACzJ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACnC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,mBAAmB,EAAE,CAAC;QAC3D,MAAM,IAAI,KAAK,CAAC,mBAAmB,KAAK,CAAC,MAAM,kCAAkC,mBAAmB,GAAG,CAAC,CAAA;IAC1G,CAAC;IACD,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,8EAA8E,CAAC,CAAA;IACjJ,MAAM,IAAI,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAA;IACnD,MAAM,OAAO,GAAG,IAAI,YAAY,EAAW,CAAA;IAC3C,OAAO,CACL,MAAC,IAAI,eAAS,iBAAiB,qBAAkB,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,aACnH,KAAC,OAAO,IAAC,EAAE,EAAE,SAAS,KAAM,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,YACjD,KAAK,GACE,EACT,IAAI,CAAC,CAAC,CAAC,eAAO,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,YAAG,IAAI,GAAK,CAAC,CAAC,CAAC,IAAI,EACvD,KAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,GAAI,EACvG,eAAO,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,YAC5B,KAAC,WAAW,IAAC,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,SAAS,EAAC,SAAS,EAAC,KAAK,EAAC,iBAAiB,EAAC,OAAO,EAAE,MAAM,EAAE,YAAY,IAAI,SAAS,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,GAAI,GAC9K,IACC,CACR,CAAA;AACH,CAAC","sourcesContent":["/**\n * `QuoteChecklist` (`data-bl=\"quote-checklist\"`, 3.15.0): what to send for a fast, fair quote in this trade (photos of\n * the tree and its access, the suburb, what's underneath it…), as a real list of ticks, then the button to the quote\n * form. Server component built on `TickList`. It is a block, not a sequenced section of the matrix: a site places it\n * inside its service detail, beside the contact form or as its own band (`headingLevel`).\n *\n * Locked: the list comes before the action; the action is the site's quote button (`lead_cta_click` placement\n * `section`, id `quote-checklist`) to its quote form. No default visible copy: the heading, every item and the button\n * label are the site's (spec: checklist content lives in each site's copy file, so same-trade sites never repeat each\n * other). At most 8 items, so the list stays a checklist.\n */\nimport type { ReactNode } from 'react'\nimport { TickList, type TickItem } from '../primitives/lists.js'\nimport { QuoteButton } from '../primitives/quote-button.js'\nimport type { ButtonVariant } from '../primitives/links.js'\nimport { slot, type SkinEntry } from '../skin.js'\n\nexport type QuoteChecklistSlot = 'root' | 'heading' | 'lead' | 'list' | 'action' | 'quote'\nexport interface QuoteChecklistLayout {\n /** The list's columns from the phone breakpoint up. Defaults to 1. */\n readonly columns?: 1 | 2\n /** The quote button's variant. Defaults to `primary`. */\n readonly quoteVariant?: ButtonVariant\n}\nexport type QuoteChecklistSkin = SkinEntry<QuoteChecklistSlot, never, QuoteChecklistLayout>\n\n/** The most items a checklist holds. */\nexport const QUOTE_CHECKLIST_MAX = 8\n\nexport interface QuoteChecklistProps extends QuoteChecklistSkin {\n /** The heading, for example \"What to send for a fast quote\". */\n readonly title: ReactNode\n readonly lead?: ReactNode\n /** What to send, in the site's words: 2 to 8 items. */\n readonly items: readonly TickItem[]\n /** The site's recorded quote label and its quote form, for example `{ label: 'Get a free quote', href: '#quote-hero' }`. */\n readonly quote: { readonly label: string; readonly href: string }\n /** 2 (the default) makes the block its own `<section>` labelled by an H2; 3 sits it inside a section's outline. */\n readonly headingLevel?: 2 | 3\n /** Defaults to `quote-checklist-heading`. */\n readonly headingId?: string\n}\n\nexport function QuoteChecklist({ title, lead, items, quote, headingLevel = 2, headingId = 'quote-checklist-heading', layout, classes }: QuoteChecklistProps) {\n if (items.length === 0) return null\n if (items.length < 2 || items.length > QUOTE_CHECKLIST_MAX) {\n throw new Error(`QuoteChecklist: ${items.length} items; a checklist holds 2 to ${QUOTE_CHECKLIST_MAX}.`)\n }\n if (!quote?.label?.trim() || !quote.href?.trim()) throw new Error('QuoteChecklist: the quote button needs the site\\'s label and its quote form.')\n const Root = headingLevel === 2 ? 'section' : 'div'\n const Heading = `h${headingLevel}` as const\n return (\n <Root data-bl=\"quote-checklist\" aria-labelledby={headingLevel === 2 ? headingId : undefined} className={classes?.root}>\n <Heading id={headingId} {...slot(classes, 'heading')}>\n {title}\n </Heading>\n {lead ? <p {...slot(classes, 'lead')}>{lead}</p> : null}\n <TickList items={items} layout={{ columns: layout?.columns ?? 1 }} classes={{ root: classes?.list }} />\n <p {...slot(classes, 'action')}>\n <QuoteButton label={quote.label} href={quote.href} placement=\"section\" ctaId=\"quote-checklist\" variant={layout?.quoteVariant ?? 'primary'} classes={{ root: classes?.quote }} />\n </p>\n </Root>\n )\n}\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order';
|
|
3
|
+
import { type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js';
|
|
4
|
+
import type { RepeatedSlotOf } from '../repeated-slots.js';
|
|
5
|
+
import { type SkinEntry } from '../skin.js';
|
|
6
|
+
import type { UiLink } from '../types.js';
|
|
7
|
+
import { type ServiceCardData, type ServicesGridLayout } from './services-grid.js';
|
|
8
|
+
/** The most services the section lists (K11; the in-content intent-prefetch limit). */
|
|
9
|
+
export declare const RELATED_SERVICES_LIMIT: 8;
|
|
10
|
+
export declare const RELATED_SERVICES_VARIANTS: readonly ["list", "cards"];
|
|
11
|
+
export type RelatedServicesVariant = (typeof RELATED_SERVICES_VARIANTS)[number];
|
|
12
|
+
export type RelatedServicesSlot = SectionSlot | SectionHeadingSlot | 'list' | 'more' | RepeatedSlotOf<'relatedServices'>;
|
|
13
|
+
/** `cards`: rendered once per service, styled from the skin stylesheet, never by class. */
|
|
14
|
+
export type RelatedServicesRepeatedSlot = RepeatedSlotOf<'relatedServices'>;
|
|
15
|
+
export interface RelatedServicesLayout {
|
|
16
|
+
/** Columns on wide screens: `list` 1 to 4 (default 3), `cards` 2 to 4 (default 3). */
|
|
17
|
+
readonly columns?: 1 | 2 | 3 | 4;
|
|
18
|
+
/** `cards`: the photo's crop. Defaults to 4:3. */
|
|
19
|
+
readonly ratio?: ServicesGridLayout['ratio'];
|
|
20
|
+
}
|
|
21
|
+
export type RelatedServicesSkin = SkinEntry<RelatedServicesSlot, RelatedServicesVariant, RelatedServicesLayout, RelatedServicesRepeatedSlot>;
|
|
22
|
+
export interface RelatedServicesProps extends RelatedServicesSkin {
|
|
23
|
+
/** The H2, for example "Other services in Castle Hill". */
|
|
24
|
+
readonly title: ReactNode;
|
|
25
|
+
readonly eyebrow?: ReactNode;
|
|
26
|
+
readonly lead?: ReactNode;
|
|
27
|
+
/** The other services, in money order; `cards` uses their summaries and photos. */
|
|
28
|
+
readonly services: readonly ServiceCardData[];
|
|
29
|
+
/** A link after the list, for example the services hub. Never prefetched. */
|
|
30
|
+
readonly more?: UiLink;
|
|
31
|
+
/** `cards`: the photo `sizes`. */
|
|
32
|
+
readonly imageSizes?: string;
|
|
33
|
+
/** Defaults to `related-services-heading`. */
|
|
34
|
+
readonly headingId?: string;
|
|
35
|
+
/** Set by `SectionSequence`. */
|
|
36
|
+
readonly tone?: SectionTone;
|
|
37
|
+
}
|
|
38
|
+
/** The guard for `SectionSequence`. */
|
|
39
|
+
export declare function relatedServicesReady({ services }: Pick<RelatedServicesProps, 'services'>): BrandlabSectionReadiness;
|
|
40
|
+
export declare function RelatedServices({ title, eyebrow, lead, services, more, imageSizes, headingId, tone, variant, layout, classes, }: RelatedServicesProps): import("react/jsx-runtime").JSX.Element | null;
|
|
41
|
+
//# sourceMappingURL=related-services.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"related-services.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/related-services.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,EAAiB,KAAK,wBAAwB,EAAE,MAAM,2CAA2C,CAAA;AAGxG,OAAO,EAA2B,KAAK,kBAAkB,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC9H,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAkC,KAAK,eAAe,EAAE,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAElH,uFAAuF;AACvF,eAAO,MAAM,sBAAsB,GAAmC,CAAA;AAEtE,eAAO,MAAM,yBAAyB,4BAA6B,CAAA;AACnE,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,yBAAyB,CAAC,CAAC,MAAM,CAAC,CAAA;AAC/E,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,kBAAkB,GAAG,MAAM,GAAG,MAAM,GAAG,cAAc,CAAC,iBAAiB,CAAC,CAAA;AACxH,2FAA2F;AAC3F,MAAM,MAAM,2BAA2B,GAAG,cAAc,CAAC,iBAAiB,CAAC,CAAA;AAC3E,MAAM,WAAW,qBAAqB;IACpC,sFAAsF;IACtF,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAChC,kDAAkD;IAClD,QAAQ,CAAC,KAAK,CAAC,EAAE,kBAAkB,CAAC,OAAO,CAAC,CAAA;CAC7C;AACD,MAAM,MAAM,mBAAmB,GAAG,SAAS,CAAC,mBAAmB,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,2BAA2B,CAAC,CAAA;AAE5I,MAAM,WAAW,oBAAqB,SAAQ,mBAAmB;IAC/D,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAA;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAA;IACzB,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAA;IAC7C,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,kCAAkC;IAClC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,gCAAgC;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAC5B;AAED,uCAAuC;AACvC,wBAAgB,oBAAoB,CAAC,EAAE,QAAQ,EAAE,EAAE,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC,GAAG,wBAAwB,CAEnH;AAiBD,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,OAAO,EACP,IAAI,EACJ,QAAQ,EACR,IAAI,EACJ,UAAU,EACV,SAAsC,EACtC,IAAI,EACJ,OAAgB,EAChB,MAAM,EACN,OAAO,GACR,EAAE,oBAAoB,kDA+CtB"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* `RelatedServices` (S28, `related-services`, 3.15.0): the other services in this place ("Other services in Castle
|
|
4
|
+
* Hill") or a service page's siblings. Server component.
|
|
5
|
+
*
|
|
6
|
+
* Each entry links to its service-in-suburb page where one exists, or to its section on the suburb page
|
|
7
|
+
* (`#tree-pruning`); the href always comes from the site's data. At most 8 (K11). Variants:
|
|
8
|
+
* - `list` (the default, lean): a `LinkList` of plain `<li><a>` entries, nothing prefetched
|
|
9
|
+
* (`data-bl-links="other-services"`, the `otherServicesInSuburb` role in `LINK_PREFETCH_POLICY`);
|
|
10
|
+
* - `cards`: `ServiceCard`s with a summary and photo, each card's one link prefetching on intent (`contentCard`).
|
|
11
|
+
*/
|
|
12
|
+
import Link from 'next/link';
|
|
13
|
+
import { SECTION_READY } from '@brandlab-digital/site-core/section-order';
|
|
14
|
+
import { LINK_PREFETCH_LIMITS, LINK_PREFETCH_POLICY } from '../prefetch.js';
|
|
15
|
+
import { LinkList } from '../primitives/lists.js';
|
|
16
|
+
import { Section, SectionHeading } from '../primitives/layout.js';
|
|
17
|
+
import { slot } from '../skin.js';
|
|
18
|
+
import { ServiceCard, defaultImageSizes } from './services-grid.js';
|
|
19
|
+
/** The most services the section lists (K11; the in-content intent-prefetch limit). */
|
|
20
|
+
export const RELATED_SERVICES_LIMIT = LINK_PREFETCH_LIMITS.contentCard;
|
|
21
|
+
export const RELATED_SERVICES_VARIANTS = ['list', 'cards'];
|
|
22
|
+
/** The guard for `SectionSequence`. */
|
|
23
|
+
export function relatedServicesReady({ services }) {
|
|
24
|
+
return services.length > 0 ? SECTION_READY : { render: false, reason: 'no-other-services' };
|
|
25
|
+
}
|
|
26
|
+
function assertServices(services) {
|
|
27
|
+
if (services.length > RELATED_SERVICES_LIMIT) {
|
|
28
|
+
throw new Error(`RelatedServices: ${services.length} services; the section lists at most ${RELATED_SERVICES_LIMIT} (K11). Link the services hub for the rest.`);
|
|
29
|
+
}
|
|
30
|
+
const ids = new Set();
|
|
31
|
+
const hrefs = new Set();
|
|
32
|
+
for (const service of services) {
|
|
33
|
+
if (!service.id || ids.has(service.id))
|
|
34
|
+
throw new Error(`RelatedServices: every service needs its own id ("${service.id}" is empty or repeated).`);
|
|
35
|
+
ids.add(service.id);
|
|
36
|
+
if (!service.title?.trim() || !service.href?.trim())
|
|
37
|
+
throw new Error(`RelatedServices: "${service.id}" needs a title and a link.`);
|
|
38
|
+
if (hrefs.has(service.href))
|
|
39
|
+
throw new Error(`RelatedServices: two services link to ${service.href}.`);
|
|
40
|
+
hrefs.add(service.href);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function RelatedServices({ title, eyebrow, lead, services, more, imageSizes, headingId = 'related-services-heading', tone, variant = 'list', layout, classes, }) {
|
|
44
|
+
if (services.length === 0)
|
|
45
|
+
return null;
|
|
46
|
+
assertServices(services);
|
|
47
|
+
const columns = layout?.columns ?? 3;
|
|
48
|
+
const ratio = layout?.ratio ?? '4:3';
|
|
49
|
+
const cardColumns = (columns < 2 ? 2 : columns);
|
|
50
|
+
return (_jsxs(Section, { id: "related-services", labelledBy: headingId, tone: tone, variant: variant, classes: classes, hooks: variant === 'cards' ? { 'data-columns': String(cardColumns), 'data-ratio': ratio } : undefined, children: [_jsx(SectionHeading, { id: headingId, title: title, eyebrow: eyebrow, lead: lead, classes: classes }), variant === 'cards' ? (_jsx("ul", { role: "list", ...slot(classes, 'list'), children: services.map((service) => (_jsx(ServiceCard, { service: service, variant: "cards", imageSizes: imageSizes ?? defaultImageSizes('cards', cardColumns), ratio: ratio, prefetch: LINK_PREFETCH_POLICY.contentCard }, service.id))) })) : (_jsx(LinkList, { links: services.map((service) => ({ label: service.title, href: service.href })), kind: "other-services", prefetch: LINK_PREFETCH_POLICY.otherServicesInSuburb, layout: { columns }, classes: { root: classes?.list } })), more ? (_jsx("p", { ...slot(classes, 'more'), children: _jsx(Link, { href: more.href, prefetch: false, children: more.label }) })) : null] }));
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=related-services.js.map
|