@brandlab-digital/site-next 3.14.0 → 3.15.1
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/image-runtime.js +6 -1
- package/dist/image-runtime.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/next-sharp.d.ts +14 -9
- package/dist/next-sharp.d.ts.map +1 -1
- package/dist/next-sharp.js +26 -25
- package/dist/next-sharp.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,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `LocalFacts` (S16, `local-facts`, 3.15.0): what makes a suburb or service-in-suburb page true for this place, the
|
|
3
|
+
* block that keeps location pages from being doorways. Server component.
|
|
4
|
+
*
|
|
5
|
+
* Only true items render, all from the site's data: an optional `intro` first (the page's own local overview, before
|
|
6
|
+
* the heading, 3.15.0), the site's own sentences about real work here (`body`; 2 to 4
|
|
7
|
+
* sentences on standard pages, the head page's owner-confirmed copy on `detailed`), sourced facts, one line about the
|
|
8
|
+
* job here or nearby, and, in permit trades, the permit note from the council register. Locked (Romie, v1 §4.4): no
|
|
9
|
+
* suburb trivia, no council facts beyond the permit note, no kilometre figures or "based in" (L10), no copy rotated from
|
|
10
|
+
* a pool (L4). A phone link inside reports the `location` placement.
|
|
11
|
+
*
|
|
12
|
+
* The permit note is the `permit` slot (K12-slots): its line carries `data-bl-slot="permit"`. When the page passes a
|
|
13
|
+
* `permit` but the register has no current record for the council (none, more than six months since it was checked,
|
|
14
|
+
* or past its fees' financial year), the line renders nothing and the section lists
|
|
15
|
+
* `data-bl-slot-skip="permit:no-current-council-record"`, which K12-slots reports as a warning until the record is
|
|
16
|
+
* checked again. A page in a trade without a permit note passes no `permit`.
|
|
17
|
+
*/
|
|
18
|
+
import type { ReactNode } from 'react';
|
|
19
|
+
import { type BrandlabCouncilPermitRecord } from '@brandlab-digital/site-core/council-register';
|
|
20
|
+
import { type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order';
|
|
21
|
+
import { type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js';
|
|
22
|
+
import type { RepeatedSlotOf } from '../repeated-slots.js';
|
|
23
|
+
import { type SkinEntry } from '../skin.js';
|
|
24
|
+
import type { UiLink } from '../types.js';
|
|
25
|
+
import { type CouncilRecordSlot, type CouncilRuleWording } from './council-rules.js';
|
|
26
|
+
export declare const LOCAL_FACTS_VARIANTS: readonly ["intro", "detailed"];
|
|
27
|
+
export type LocalFactsVariant = (typeof LOCAL_FACTS_VARIANTS)[number];
|
|
28
|
+
export type LocalFactsSlot = SectionSlot | SectionHeadingSlot | 'intro' | 'body' | 'facts' | 'fact' | 'factSource' | 'job' | 'permit' | CouncilRecordSlot | 'fee';
|
|
29
|
+
/** Rendered once per fact or fee: styled from the skin stylesheet, never by class. */
|
|
30
|
+
export type LocalFactsRepeatedSlot = RepeatedSlotOf<'localFacts'>;
|
|
31
|
+
export type LocalFactsSkin = SkinEntry<LocalFactsSlot, LocalFactsVariant, Record<never, never>, LocalFactsRepeatedSlot>;
|
|
32
|
+
/** One sourced fact that changes the job here (a job or owner source; never a templated terrain line). */
|
|
33
|
+
export interface LocalFact {
|
|
34
|
+
/** A stable id, rendered as `data-item`. */
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly text: ReactNode;
|
|
37
|
+
/** Where the fact can be checked. */
|
|
38
|
+
readonly source?: UiLink;
|
|
39
|
+
}
|
|
40
|
+
/** The permit note's data: the council's register record and how this site words it. */
|
|
41
|
+
export interface LocalFactsPermit {
|
|
42
|
+
/** The register record for the suburb's council; `undefined` when the register has none. */
|
|
43
|
+
readonly record: BrandlabCouncilPermitRecord | undefined;
|
|
44
|
+
/** The site's own wording of the rule in one or two lines. Defaults to the record's rule. */
|
|
45
|
+
readonly rule?: CouncilRuleWording;
|
|
46
|
+
/** The source link's text. Defaults to the council's name. */
|
|
47
|
+
readonly sourceLabel?: string;
|
|
48
|
+
/** The site's own council page, for example its "Tree removal and Hornsby Council" guide. */
|
|
49
|
+
readonly councilPage?: UiLink;
|
|
50
|
+
/** The day the page is built or refreshed (Sydney). Defaults to now. */
|
|
51
|
+
readonly asOf?: Date;
|
|
52
|
+
}
|
|
53
|
+
export interface LocalFactsProps extends LocalFactsSkin {
|
|
54
|
+
/** The H2, for example "Tree work in Castle Hill". */
|
|
55
|
+
readonly title: ReactNode;
|
|
56
|
+
readonly eyebrow?: ReactNode;
|
|
57
|
+
readonly lead?: ReactNode;
|
|
58
|
+
/**
|
|
59
|
+
* The page's own introduction to the place (3.15.0), for example a suburb page's local overview paragraph. Site copy,
|
|
60
|
+
* rendered first inside the section, before its heading, so the suburb page's intro lives in the `local-facts` place.
|
|
61
|
+
*/
|
|
62
|
+
readonly intro?: ReactNode;
|
|
63
|
+
/** The site's own copy about real work here: sentences on `intro`, the head page's copy (with H3s) on `detailed`. */
|
|
64
|
+
readonly body?: ReactNode;
|
|
65
|
+
readonly facts?: readonly LocalFact[];
|
|
66
|
+
/** One line about the job here, or the nearest one labelled as nearby. */
|
|
67
|
+
readonly job?: ReactNode;
|
|
68
|
+
/** Permit trades only. */
|
|
69
|
+
readonly permit?: LocalFactsPermit;
|
|
70
|
+
/** Defaults to `local-facts-heading`. */
|
|
71
|
+
readonly headingId?: string;
|
|
72
|
+
/** Set by `SectionSequence`. */
|
|
73
|
+
readonly tone?: SectionTone;
|
|
74
|
+
}
|
|
75
|
+
/** The guard for `SectionSequence`: renders when it has at least one true item to show. */
|
|
76
|
+
export declare function localFactsReady({ intro, body, facts, job, permit }: Pick<LocalFactsProps, 'intro' | 'body' | 'facts' | 'job' | 'permit'>): BrandlabSectionReadiness;
|
|
77
|
+
export declare function LocalFacts({ title, eyebrow, lead, intro, body, facts, job, permit, headingId, tone, variant, classes, }: LocalFactsProps): import("react/jsx-runtime").JSX.Element | null;
|
|
78
|
+
//# sourceMappingURL=local-facts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-facts.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/local-facts.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { COUNCIL_PERMIT_SKIP_REASON, visibleCouncilRecord } from '@brandlab-digital/site-core/council-register';
|
|
3
|
+
import { SECTION_READY, SECTION_SLOT_ATTRIBUTE, SECTION_SLOT_SKIP_ATTRIBUTE, formatSectionSlotSkips, } from '@brandlab-digital/site-core/section-order';
|
|
4
|
+
import { Anchor } from '../primitives/links.js';
|
|
5
|
+
import { Section, SectionHeading } from '../primitives/layout.js';
|
|
6
|
+
import { repeatedSlot, slot } from '../skin.js';
|
|
7
|
+
import { CouncilRecordLines } from './council-rules.js';
|
|
8
|
+
export const LOCAL_FACTS_VARIANTS = ['intro', 'detailed'];
|
|
9
|
+
/** The guard for `SectionSequence`: renders when it has at least one true item to show. */
|
|
10
|
+
export function localFactsReady({ intro, body, facts, job, permit }) {
|
|
11
|
+
const permitShown = permit ? visibleCouncilRecord(permit.record, permit.asOf) !== undefined : false;
|
|
12
|
+
if (intro || body || (facts && facts.length > 0) || job || permitShown)
|
|
13
|
+
return SECTION_READY;
|
|
14
|
+
return { render: false, reason: 'no-local-facts' };
|
|
15
|
+
}
|
|
16
|
+
function assertFacts(facts) {
|
|
17
|
+
const ids = new Set();
|
|
18
|
+
for (const fact of facts) {
|
|
19
|
+
if (!fact.id || ids.has(fact.id))
|
|
20
|
+
throw new Error(`LocalFacts: every fact needs its own id ("${fact.id}" is empty or repeated).`);
|
|
21
|
+
ids.add(fact.id);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function LocalFacts({ title, eyebrow, lead, intro, body, facts = [], job, permit, headingId = 'local-facts-heading', tone, variant = 'intro', classes, }) {
|
|
25
|
+
if (!localFactsReady({ intro, body, facts, job, permit }).render)
|
|
26
|
+
return null;
|
|
27
|
+
assertFacts(facts);
|
|
28
|
+
const record = permit ? visibleCouncilRecord(permit.record, permit.asOf) : undefined;
|
|
29
|
+
const skip = permit && !record ? { [SECTION_SLOT_SKIP_ATTRIBUTE]: formatSectionSlotSkips([{ slot: 'permit', reason: COUNCIL_PERMIT_SKIP_REASON }]) } : undefined;
|
|
30
|
+
return (_jsxs(Section, { id: "local-facts", labelledBy: headingId, tone: tone, variant: variant, classes: classes, phonePlacement: "location", hooks: skip, children: [intro ? _jsx("div", { ...slot(classes, 'intro'), children: intro }) : null, _jsx(SectionHeading, { id: headingId, title: title, eyebrow: eyebrow, lead: lead, classes: classes }), body ? _jsx("div", { ...slot(classes, 'body'), children: body }) : null, facts.length > 0 ? (_jsx("ul", { role: "list", ...slot(classes, 'facts'), children: facts.map((fact) => (_jsxs("li", { ...repeatedSlot('fact'), "data-item": fact.id, children: [fact.text, fact.source ? (_jsxs(_Fragment, { children: [' ', _jsx(Anchor, { href: fact.source.href, prefetch: false, hooks: repeatedSlot('factSource'), children: fact.source.label })] })) : null] }, fact.id))) })) : null, job ? _jsx("p", { ...slot(classes, 'job'), children: job }) : null, permit && record ? (_jsx("div", { ...slot(classes, 'permit'), [SECTION_SLOT_ATTRIBUTE]: 'permit', "data-council": record.council, children: _jsx(CouncilRecordLines, { record: record, rule: permit.rule, sourceLabel: permit.sourceLabel, councilPage: permit.councilPage, classes: classes }) })) : null] }));
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=local-facts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-facts.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/local-facts.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * `LocalFacts` (S16, `local-facts`, 3.15.0): what makes a suburb or service-in-suburb page true for this place, the\n * block that keeps location pages from being doorways. Server component.\n *\n * Only true items render, all from the site's data: an optional `intro` first (the page's own local overview, before\n * the heading, 3.15.0), the site's own sentences about real work here (`body`; 2 to 4\n * sentences on standard pages, the head page's owner-confirmed copy on `detailed`), sourced facts, one line about the\n * job here or nearby, and, in permit trades, the permit note from the council register. Locked (Romie, v1 §4.4): no\n * suburb trivia, no council facts beyond the permit note, no kilometre figures or \"based in\" (L10), no copy rotated from\n * a pool (L4). A phone link inside reports the `location` placement.\n *\n * The permit note is the `permit` slot (K12-slots): its line carries `data-bl-slot=\"permit\"`. When the page passes a\n * `permit` but the register has no current record for the council (none, more than six months since it was checked,\n * or past its fees' financial year), the line renders nothing and the section lists\n * `data-bl-slot-skip=\"permit:no-current-council-record\"`, which K12-slots reports as a warning until the record is\n * checked again. A page in a trade without a permit note passes no `permit`.\n */\nimport type { ReactNode } from 'react'\nimport { COUNCIL_PERMIT_SKIP_REASON, visibleCouncilRecord, type BrandlabCouncilPermitRecord } from '@brandlab-digital/site-core/council-register'\nimport {\n SECTION_READY,\n SECTION_SLOT_ATTRIBUTE,\n SECTION_SLOT_SKIP_ATTRIBUTE,\n formatSectionSlotSkips,\n type BrandlabSectionReadiness,\n} from '@brandlab-digital/site-core/section-order'\nimport { Anchor } from '../primitives/links.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'\nimport { CouncilRecordLines, type CouncilRecordSlot, type CouncilRuleWording } from './council-rules.js'\n\nexport const LOCAL_FACTS_VARIANTS = ['intro', 'detailed'] as const\nexport type LocalFactsVariant = (typeof LOCAL_FACTS_VARIANTS)[number]\nexport type LocalFactsSlot = SectionSlot | SectionHeadingSlot | 'intro' | 'body' | 'facts' | 'fact' | 'factSource' | 'job' | 'permit' | CouncilRecordSlot | 'fee'\n/** Rendered once per fact or fee: styled from the skin stylesheet, never by class. */\nexport type LocalFactsRepeatedSlot = RepeatedSlotOf<'localFacts'>\nexport type LocalFactsSkin = SkinEntry<LocalFactsSlot, LocalFactsVariant, Record<never, never>, LocalFactsRepeatedSlot>\n\n/** One sourced fact that changes the job here (a job or owner source; never a templated terrain line). */\nexport interface LocalFact {\n /** A stable id, rendered as `data-item`. */\n readonly id: string\n readonly text: ReactNode\n /** Where the fact can be checked. */\n readonly source?: UiLink\n}\n\n/** The permit note's data: the council's register record and how this site words it. */\nexport interface LocalFactsPermit {\n /** The register record for the suburb's council; `undefined` when the register has none. */\n readonly record: BrandlabCouncilPermitRecord | undefined\n /** The site's own wording of the rule in one or two lines. Defaults to the record's rule. */\n readonly rule?: CouncilRuleWording\n /** The source link's text. Defaults to the council's name. */\n readonly sourceLabel?: string\n /** The site's own council page, for example its \"Tree removal and Hornsby Council\" guide. */\n readonly councilPage?: UiLink\n /** The day the page is built or refreshed (Sydney). Defaults to now. */\n readonly asOf?: Date\n}\n\nexport interface LocalFactsProps extends LocalFactsSkin {\n /** The H2, for example \"Tree work in Castle Hill\". */\n readonly title: ReactNode\n readonly eyebrow?: ReactNode\n readonly lead?: ReactNode\n /**\n * The page's own introduction to the place (3.15.0), for example a suburb page's local overview paragraph. Site copy,\n * rendered first inside the section, before its heading, so the suburb page's intro lives in the `local-facts` place.\n */\n readonly intro?: ReactNode\n /** The site's own copy about real work here: sentences on `intro`, the head page's copy (with H3s) on `detailed`. */\n readonly body?: ReactNode\n readonly facts?: readonly LocalFact[]\n /** One line about the job here, or the nearest one labelled as nearby. */\n readonly job?: ReactNode\n /** Permit trades only. */\n readonly permit?: LocalFactsPermit\n /** Defaults to `local-facts-heading`. */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n}\n\n/** The guard for `SectionSequence`: renders when it has at least one true item to show. */\nexport function localFactsReady({ intro, body, facts, job, permit }: Pick<LocalFactsProps, 'intro' | 'body' | 'facts' | 'job' | 'permit'>): BrandlabSectionReadiness {\n const permitShown = permit ? visibleCouncilRecord(permit.record, permit.asOf) !== undefined : false\n if (intro || body || (facts && facts.length > 0) || job || permitShown) return SECTION_READY\n return { render: false, reason: 'no-local-facts' }\n}\n\nfunction assertFacts(facts: readonly LocalFact[]) {\n const ids = new Set<string>()\n for (const fact of facts) {\n if (!fact.id || ids.has(fact.id)) throw new Error(`LocalFacts: every fact needs its own id (\"${fact.id}\" is empty or repeated).`)\n ids.add(fact.id)\n }\n}\n\nexport function LocalFacts({\n title,\n eyebrow,\n lead,\n intro,\n body,\n facts = [],\n job,\n permit,\n headingId = 'local-facts-heading',\n tone,\n variant = 'intro',\n classes,\n}: LocalFactsProps) {\n if (!localFactsReady({ intro, body, facts, job, permit }).render) return null\n assertFacts(facts)\n const record = permit ? visibleCouncilRecord(permit.record, permit.asOf) : undefined\n const skip = permit && !record ? { [SECTION_SLOT_SKIP_ATTRIBUTE]: formatSectionSlotSkips([{ slot: 'permit', reason: COUNCIL_PERMIT_SKIP_REASON }]) } : undefined\n return (\n <Section id=\"local-facts\" labelledBy={headingId} tone={tone} variant={variant} classes={classes} phonePlacement=\"location\" hooks={skip}>\n {intro ? <div {...slot(classes, 'intro')}>{intro}</div> : null}\n <SectionHeading id={headingId} title={title} eyebrow={eyebrow} lead={lead} classes={classes} />\n {body ? <div {...slot(classes, 'body')}>{body}</div> : null}\n {facts.length > 0 ? (\n <ul role=\"list\" {...slot(classes, 'facts')}>\n {facts.map((fact) => (\n <li key={fact.id} {...repeatedSlot('fact')} data-item={fact.id}>\n {fact.text}\n {fact.source ? (\n <>\n {' '}\n <Anchor href={fact.source.href} prefetch={false} hooks={repeatedSlot('factSource')}>\n {fact.source.label}\n </Anchor>\n </>\n ) : null}\n </li>\n ))}\n </ul>\n ) : null}\n {job ? <p {...slot(classes, 'job')}>{job}</p> : null}\n {permit && record ? (\n <div {...slot(classes, 'permit')} {...{ [SECTION_SLOT_ATTRIBUTE]: 'permit' }} data-council={record.council}>\n <CouncilRecordLines record={record} rule={permit.rule} sourceLabel={permit.sourceLabel} councilPage={permit.councilPage} classes={classes} />\n </div>\n ) : null}\n </Section>\n )\n}\n"]}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { type SuburbCentreLookup } from '@brandlab-digital/site-core/locations';
|
|
3
|
+
import { type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order';
|
|
4
|
+
import { type LinkListLayout } from '../primitives/lists.js';
|
|
5
|
+
import { type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js';
|
|
6
|
+
import { type SkinEntry } from '../skin.js';
|
|
7
|
+
import type { UiLink } from '../types.js';
|
|
8
|
+
/** The most neighbours the section lists. */
|
|
9
|
+
export declare const NEARBY_AREAS_LIMIT = 12;
|
|
10
|
+
/** The neighbours `nearbyAreaLinks` returns by default (v1 §4.4: 4 to 6). */
|
|
11
|
+
export declare const NEARBY_AREAS_DEFAULT = 6;
|
|
12
|
+
export declare const NEARBY_AREAS_VARIANTS: readonly ["list", "chips"];
|
|
13
|
+
export type NearbyAreasVariant = (typeof NEARBY_AREAS_VARIANTS)[number];
|
|
14
|
+
export type NearbyAreasSlot = SectionSlot | SectionHeadingSlot | 'list' | 'region' | 'more';
|
|
15
|
+
export type NearbyAreasLayout = LinkListLayout;
|
|
16
|
+
export type NearbyAreasSkin = SkinEntry<NearbyAreasSlot, NearbyAreasVariant, NearbyAreasLayout>;
|
|
17
|
+
export interface NearbyAreasProps extends NearbyAreasSkin {
|
|
18
|
+
/** The H2, for example "Tree removal near Castle Hill". */
|
|
19
|
+
readonly title: ReactNode;
|
|
20
|
+
readonly eyebrow?: ReactNode;
|
|
21
|
+
readonly lead?: ReactNode;
|
|
22
|
+
/** The neighbours, nearest first (`nearbyAreaLinks`). */
|
|
23
|
+
readonly areas: readonly UiLink[];
|
|
24
|
+
/**
|
|
25
|
+
* The page of the region these suburbs belong to, for example `{ label: 'All of the Hills District', href:
|
|
26
|
+
* '/tree-services/hills-district' }` (the label is site copy). It sits after the ranked list, outside it, so it is
|
|
27
|
+
* never ranked by distance, and it is never prefetched.
|
|
28
|
+
*/
|
|
29
|
+
readonly regionLink?: UiLink;
|
|
30
|
+
/** A link after the list (and the region link), for example the areas hub. Never prefetched. */
|
|
31
|
+
readonly more?: UiLink;
|
|
32
|
+
/** Defaults to `nearby-areas-heading`. */
|
|
33
|
+
readonly headingId?: string;
|
|
34
|
+
/** Set by `SectionSequence`. */
|
|
35
|
+
readonly tone?: SectionTone;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The nearest served suburbs to `origin` as links, nearest first by real distance (`nearestSuburbsWithin`). `link`
|
|
39
|
+
* gives a suburb's page (or `undefined` to leave it out, for a suburb without that page).
|
|
40
|
+
*/
|
|
41
|
+
export declare function nearbyAreaLinks(origin: string, served: readonly string[], options: {
|
|
42
|
+
readonly centreOf: SuburbCentreLookup;
|
|
43
|
+
readonly link: (suburb: string) => UiLink | undefined;
|
|
44
|
+
readonly limit?: number;
|
|
45
|
+
readonly maxKm?: number;
|
|
46
|
+
}): UiLink[];
|
|
47
|
+
/** The guard for `SectionSequence`. */
|
|
48
|
+
export declare function nearbyAreasReady({ areas }: Pick<NearbyAreasProps, 'areas'>): BrandlabSectionReadiness;
|
|
49
|
+
export declare function NearbyAreas({ title, eyebrow, lead, areas, regionLink, more, headingId, tone, variant, layout, classes }: NearbyAreasProps): import("react/jsx-runtime").JSX.Element | null;
|
|
50
|
+
//# sourceMappingURL=nearby-areas.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nearby-areas.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/nearby-areas.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,EAAwB,KAAK,kBAAkB,EAAE,MAAM,uCAAuC,CAAA;AACrG,OAAO,EAAiB,KAAK,wBAAwB,EAAE,MAAM,2CAA2C,CAAA;AACxG,OAAO,EAAY,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AACtE,OAAO,EAA2B,KAAK,kBAAkB,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC9H,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,6CAA6C;AAC7C,eAAO,MAAM,kBAAkB,KAAK,CAAA;AACpC,6EAA6E;AAC7E,eAAO,MAAM,oBAAoB,IAAI,CAAA;AAErC,eAAO,MAAM,qBAAqB,4BAA6B,CAAA;AAC/D,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAA;AACvE,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,kBAAkB,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;AAC3F,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAA;AAC9C,MAAM,MAAM,eAAe,GAAG,SAAS,CAAC,eAAe,EAAE,kBAAkB,EAAE,iBAAiB,CAAC,CAAA;AAE/F,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,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,yDAAyD;IACzD,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,gGAAgG;IAChG,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,0CAA0C;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,gCAAgC;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAC5B;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,OAAO,EAAE;IAAE,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1J,MAAM,EAAE,CAOV;AAED,uCAAuC;AACvC,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,CAAC,gBAAgB,EAAE,OAAO,CAAC,GAAG,wBAAwB,CAErG;AAED,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,SAAkC,EAAE,IAAI,EAAE,OAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,gBAAgB,kDAyB3K"}
|
|
@@ -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":"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"}
|
|
@@ -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
|