@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.
Files changed (142) hide show
  1. package/dist/analytics.d.ts +1 -1
  2. package/dist/analytics.d.ts.map +1 -1
  3. package/dist/analytics.js +2 -0
  4. package/dist/analytics.js.map +1 -1
  5. package/dist/navigation/constants.d.ts +11 -2
  6. package/dist/navigation/constants.d.ts.map +1 -1
  7. package/dist/navigation/constants.js +11 -2
  8. package/dist/navigation/constants.js.map +1 -1
  9. package/dist/navigation/index.d.ts +1 -1
  10. package/dist/navigation/index.d.ts.map +1 -1
  11. package/dist/navigation/index.js +1 -1
  12. package/dist/navigation/index.js.map +1 -1
  13. package/dist/navigation/use-back-to-top.d.ts.map +1 -1
  14. package/dist/navigation/use-back-to-top.js +12 -4
  15. package/dist/navigation/use-back-to-top.js.map +1 -1
  16. package/dist/navigation/use-header-metrics.d.ts +1 -1
  17. package/dist/navigation/use-header-metrics.js +1 -1
  18. package/dist/navigation/use-header-metrics.js.map +1 -1
  19. package/dist/navigation/use-sticky-call-bar.d.ts +2 -1
  20. package/dist/navigation/use-sticky-call-bar.d.ts.map +1 -1
  21. package/dist/navigation/use-sticky-call-bar.js +11 -2
  22. package/dist/navigation/use-sticky-call-bar.js.map +1 -1
  23. package/dist/ui-kit/brandlab-skin.d.ts +26 -0
  24. package/dist/ui-kit/brandlab-skin.d.ts.map +1 -1
  25. package/dist/ui-kit/brandlab-skin.js.map +1 -1
  26. package/dist/ui-kit/prefetch.d.ts +19 -10
  27. package/dist/ui-kit/prefetch.d.ts.map +1 -1
  28. package/dist/ui-kit/prefetch.js +23 -10
  29. package/dist/ui-kit/prefetch.js.map +1 -1
  30. package/dist/ui-kit/primitives/index.d.ts +1 -1
  31. package/dist/ui-kit/primitives/index.d.ts.map +1 -1
  32. package/dist/ui-kit/primitives/index.js.map +1 -1
  33. package/dist/ui-kit/primitives/links.js +1 -1
  34. package/dist/ui-kit/primitives/links.js.map +1 -1
  35. package/dist/ui-kit/primitives/lists.d.ts +9 -1
  36. package/dist/ui-kit/primitives/lists.d.ts.map +1 -1
  37. package/dist/ui-kit/primitives/lists.js +1 -1
  38. package/dist/ui-kit/primitives/lists.js.map +1 -1
  39. package/dist/ui-kit/primitives/quote-button.d.ts +25 -2
  40. package/dist/ui-kit/primitives/quote-button.d.ts.map +1 -1
  41. package/dist/ui-kit/primitives/quote-button.js +25 -3
  42. package/dist/ui-kit/primitives/quote-button.js.map +1 -1
  43. package/dist/ui-kit/repeated-slots.d.ts +32 -0
  44. package/dist/ui-kit/repeated-slots.d.ts.map +1 -1
  45. package/dist/ui-kit/repeated-slots.js +16 -0
  46. package/dist/ui-kit/repeated-slots.js.map +1 -1
  47. package/dist/ui-kit/sections/area-map.d.ts +64 -0
  48. package/dist/ui-kit/sections/area-map.d.ts.map +1 -0
  49. package/dist/ui-kit/sections/area-map.js +142 -0
  50. package/dist/ui-kit/sections/area-map.js.map +1 -0
  51. package/dist/ui-kit/sections/before-after.d.ts +59 -0
  52. package/dist/ui-kit/sections/before-after.d.ts.map +1 -0
  53. package/dist/ui-kit/sections/before-after.js +42 -0
  54. package/dist/ui-kit/sections/before-after.js.map +1 -0
  55. package/dist/ui-kit/sections/council-rules.d.ts +64 -0
  56. package/dist/ui-kit/sections/council-rules.d.ts.map +1 -0
  57. package/dist/ui-kit/sections/council-rules.js +46 -0
  58. package/dist/ui-kit/sections/council-rules.js.map +1 -0
  59. package/dist/ui-kit/sections/coverage.d.ts +53 -0
  60. package/dist/ui-kit/sections/coverage.d.ts.map +1 -0
  61. package/dist/ui-kit/sections/coverage.js +57 -0
  62. package/dist/ui-kit/sections/coverage.js.map +1 -0
  63. package/dist/ui-kit/sections/emergency-band.d.ts +60 -0
  64. package/dist/ui-kit/sections/emergency-band.d.ts.map +1 -0
  65. package/dist/ui-kit/sections/emergency-band.js +43 -0
  66. package/dist/ui-kit/sections/emergency-band.js.map +1 -0
  67. package/dist/ui-kit/sections/gallery.d.ts +60 -0
  68. package/dist/ui-kit/sections/gallery.d.ts.map +1 -0
  69. package/dist/ui-kit/sections/gallery.js +39 -0
  70. package/dist/ui-kit/sections/gallery.js.map +1 -0
  71. package/dist/ui-kit/sections/hero.d.ts +5 -0
  72. package/dist/ui-kit/sections/hero.d.ts.map +1 -1
  73. package/dist/ui-kit/sections/hero.js +1 -1
  74. package/dist/ui-kit/sections/hero.js.map +1 -1
  75. package/dist/ui-kit/sections/index.d.ts +14 -1
  76. package/dist/ui-kit/sections/index.d.ts.map +1 -1
  77. package/dist/ui-kit/sections/index.js +14 -1
  78. package/dist/ui-kit/sections/index.js.map +1 -1
  79. package/dist/ui-kit/sections/licences.d.ts +72 -0
  80. package/dist/ui-kit/sections/licences.d.ts.map +1 -0
  81. package/dist/ui-kit/sections/licences.js +46 -0
  82. package/dist/ui-kit/sections/licences.js.map +1 -0
  83. package/dist/ui-kit/sections/local-facts.d.ts +78 -0
  84. package/dist/ui-kit/sections/local-facts.d.ts.map +1 -0
  85. package/dist/ui-kit/sections/local-facts.js +32 -0
  86. package/dist/ui-kit/sections/local-facts.js.map +1 -0
  87. package/dist/ui-kit/sections/nearby-areas.d.ts +50 -0
  88. package/dist/ui-kit/sections/nearby-areas.d.ts.map +1 -0
  89. package/dist/ui-kit/sections/nearby-areas.js +49 -0
  90. package/dist/ui-kit/sections/nearby-areas.js.map +1 -0
  91. package/dist/ui-kit/sections/nearby-jobs.d.ts +66 -0
  92. package/dist/ui-kit/sections/nearby-jobs.d.ts.map +1 -0
  93. package/dist/ui-kit/sections/nearby-jobs.js +49 -0
  94. package/dist/ui-kit/sections/nearby-jobs.js.map +1 -0
  95. package/dist/ui-kit/sections/projects.d.ts +79 -0
  96. package/dist/ui-kit/sections/projects.d.ts.map +1 -0
  97. package/dist/ui-kit/sections/projects.js +69 -0
  98. package/dist/ui-kit/sections/projects.js.map +1 -0
  99. package/dist/ui-kit/sections/quote-checklist.d.ts +43 -0
  100. package/dist/ui-kit/sections/quote-checklist.d.ts.map +1 -0
  101. package/dist/ui-kit/sections/quote-checklist.js +19 -0
  102. package/dist/ui-kit/sections/quote-checklist.js.map +1 -0
  103. package/dist/ui-kit/sections/related-services.d.ts +41 -0
  104. package/dist/ui-kit/sections/related-services.d.ts.map +1 -0
  105. package/dist/ui-kit/sections/related-services.js +52 -0
  106. package/dist/ui-kit/sections/related-services.js.map +1 -0
  107. package/dist/ui-kit/sections/reviews-band.d.ts +11 -0
  108. package/dist/ui-kit/sections/reviews-band.d.ts.map +1 -1
  109. package/dist/ui-kit/sections/reviews-band.js +11 -0
  110. package/dist/ui-kit/sections/reviews-band.js.map +1 -1
  111. package/dist/ui-kit/sections/services-grid.js +1 -1
  112. package/dist/ui-kit/sections/services-grid.js.map +1 -1
  113. package/dist/ui-kit/sections/work-photos.d.ts +15 -0
  114. package/dist/ui-kit/sections/work-photos.d.ts.map +1 -0
  115. package/dist/ui-kit/sections/work-photos.js +24 -0
  116. package/dist/ui-kit/sections/work-photos.js.map +1 -0
  117. package/dist/ui-kit/shell/back-to-top.js +1 -1
  118. package/dist/ui-kit/shell/back-to-top.js.map +1 -1
  119. package/dist/ui-kit/shell/current-link.d.ts +21 -5
  120. package/dist/ui-kit/shell/current-link.d.ts.map +1 -1
  121. package/dist/ui-kit/shell/current-link.js +16 -5
  122. package/dist/ui-kit/shell/current-link.js.map +1 -1
  123. package/dist/ui-kit/shell/drawer-client.js +1 -1
  124. package/dist/ui-kit/shell/drawer-client.js.map +1 -1
  125. package/dist/ui-kit/shell/intent-prefetch.d.ts.map +1 -1
  126. package/dist/ui-kit/shell/intent-prefetch.js +3 -2
  127. package/dist/ui-kit/shell/intent-prefetch.js.map +1 -1
  128. package/dist/ui-kit/shell/mobile-call-bar.d.ts +6 -1
  129. package/dist/ui-kit/shell/mobile-call-bar.d.ts.map +1 -1
  130. package/dist/ui-kit/shell/mobile-call-bar.js +6 -3
  131. package/dist/ui-kit/shell/mobile-call-bar.js.map +1 -1
  132. package/dist/ui-kit/shell/mobile-drawer.js +2 -2
  133. package/dist/ui-kit/shell/mobile-drawer.js.map +1 -1
  134. package/dist/ui-kit/shell/shell-quote.d.ts +10 -0
  135. package/dist/ui-kit/shell/shell-quote.d.ts.map +1 -0
  136. package/dist/ui-kit/shell/shell-quote.js +13 -0
  137. package/dist/ui-kit/shell/shell-quote.js.map +1 -0
  138. package/dist/ui-kit/shell/site-header.d.ts +1 -1
  139. package/dist/ui-kit/shell/site-header.js +4 -4
  140. package/dist/ui-kit/shell/site-header.js.map +1 -1
  141. package/dist/ui-kit/structure.css +410 -7
  142. package/package.json +2 -2
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { COUNCIL_PERMIT_SKIP_REASON, formatCheckedMonth, visibleCouncilRecord, } from '@brandlab-digital/site-core/council-register';
3
+ import { SECTION_READY } 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
+ /** A fee in Australian dollars: whole dollars without cents ("$201", "$1,100"), otherwise two places ("$58.50"). */
8
+ export function formatCouncilFeeAmount(amountAud) {
9
+ const whole = Number.isInteger(amountAud);
10
+ return new Intl.NumberFormat('en-AU', {
11
+ style: 'currency',
12
+ currency: 'AUD',
13
+ minimumFractionDigits: whole ? 0 : 2,
14
+ maximumFractionDigits: 2,
15
+ }).format(amountAud);
16
+ }
17
+ /** The locked fee line: "{label}: $201 (2026/27)". Every figure carries its financial year. */
18
+ export function councilFeeLine(fee, financialYear) {
19
+ return `${fee.label}: ${formatCouncilFeeAmount(fee.amountAud)} (${financialYear})`;
20
+ }
21
+ /** The record when it may show on `asOf`; the guard for a section that renders it. */
22
+ export function councilRecordReady(record, asOf) {
23
+ return visibleCouncilRecord(record, asOf) ? SECTION_READY : { render: false, reason: COUNCIL_PERMIT_SKIP_REASON };
24
+ }
25
+ function ruleText(record, wording) {
26
+ if (typeof wording === 'function')
27
+ return wording(record);
28
+ return wording ?? record.rule;
29
+ }
30
+ /** The record's lines: the rule, its fees, the source with the check month and the site's own council page. */
31
+ export function CouncilRecordLines({ record, rule, sourceLabel, councilPage, classes, }) {
32
+ const fees = record.fees && record.financialYear ? record.fees : [];
33
+ return (_jsxs(_Fragment, { children: [_jsx("p", { ...slot(classes, 'rule'), children: ruleText(record, rule) }), fees.length > 0 ? (_jsx("ul", { role: "list", ...slot(classes, 'fees'), children: fees.map((fee) => (_jsx("li", { ...repeatedSlot('fee'), children: councilFeeLine(fee, record.financialYear) }, fee.label))) })) : null, _jsxs("p", { ...slot(classes, 'source'), children: [_jsx(Anchor, { href: record.sourceUrl, external: true, hooks: {}, children: sourceLabel ?? record.councilName }), `, checked ${formatCheckedMonth(record.checkedOn)}`] }), councilPage ? (_jsx("p", { ...slot(classes, 'councilLink'), children: _jsx(Anchor, { href: councilPage.href, prefetch: false, hooks: {}, children: councilPage.label }) })) : null] }));
34
+ }
35
+ /** The guard for `SectionSequence`: renders only while the register shows the record. */
36
+ export function councilRulesReady({ record, asOf }) {
37
+ return councilRecordReady(record, asOf);
38
+ }
39
+ /** A council page's rules from the register (playbook-trees §2.4): the rule, the fees with their year, the source. */
40
+ export function CouncilRules({ record, title, eyebrow, lead, rule, sourceLabel, asOf, headingId = 'council-rules-heading', tone, classes }) {
41
+ const shown = visibleCouncilRecord(record, asOf);
42
+ if (!shown)
43
+ return null;
44
+ return (_jsxs(Section, { id: "council-rules", labelledBy: headingId, tone: tone, classes: classes, hooks: { 'data-council': shown.council }, children: [_jsx(SectionHeading, { id: headingId, title: title, eyebrow: eyebrow, lead: lead, classes: classes }), _jsx(CouncilRecordLines, { record: shown, rule: rule, sourceLabel: sourceLabel, classes: classes })] }));
45
+ }
46
+ //# sourceMappingURL=council-rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"council-rules.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/council-rules.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * `CouncilRules` (`council-rules`, 3.15.0) and the council record lines it shares with `LocalFacts`' permit slot.\n * Server components.\n *\n * Both render one record of the council register (site-core `./council-register`, playbook-trees §2.4): the rule, each\n * fee with its financial year, the council's own page as the source and the month the record was checked. Nothing is\n * invented: a record the register hides (six months after it was checked, or on 1 July after its fees' financial year)\n * renders nothing, and the section's guard says why (`no-current-council-record`). The register's `rule` is a fact;\n * each site words its own sentence from it (`rule` as a function of the record), so same-trade sites never share a\n * council sentence. Locked truth wording: a fee reads \"{label}: $201 (2026/27)\" and the source line ends \", checked\n * {Month Year}\".\n */\nimport type { ReactNode } from 'react'\nimport {\n COUNCIL_PERMIT_SKIP_REASON,\n formatCheckedMonth,\n visibleCouncilRecord,\n type BrandlabCouncilFee,\n type BrandlabCouncilPermitRecord,\n} from '@brandlab-digital/site-core/council-register'\nimport { SECTION_READY, type BrandlabSectionReadiness } 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, type SlotClasses } from '../skin.js'\nimport type { UiLink } from '../types.js'\n\n/** The site's wording of the register's rule: text, or a function of the record. Left out, the record's own rule. */\nexport type CouncilRuleWording = ReactNode | ((record: BrandlabCouncilPermitRecord) => ReactNode)\n\n/** The one-off parts of a council record's lines (shared by `CouncilRules` and `LocalFacts`' permit slot). */\nexport type CouncilRecordSlot = 'rule' | 'fees' | 'source' | 'councilLink'\n\n/** A fee in Australian dollars: whole dollars without cents (\"$201\", \"$1,100\"), otherwise two places (\"$58.50\"). */\nexport function formatCouncilFeeAmount(amountAud: number): string {\n const whole = Number.isInteger(amountAud)\n return new Intl.NumberFormat('en-AU', {\n style: 'currency',\n currency: 'AUD',\n minimumFractionDigits: whole ? 0 : 2,\n maximumFractionDigits: 2,\n }).format(amountAud)\n}\n\n/** The locked fee line: \"{label}: $201 (2026/27)\". Every figure carries its financial year. */\nexport function councilFeeLine(fee: BrandlabCouncilFee, financialYear: string): string {\n return `${fee.label}: ${formatCouncilFeeAmount(fee.amountAud)} (${financialYear})`\n}\n\n/** The record when it may show on `asOf`; the guard for a section that renders it. */\nexport function councilRecordReady(record: BrandlabCouncilPermitRecord | undefined, asOf?: Date): BrandlabSectionReadiness {\n return visibleCouncilRecord(record, asOf) ? SECTION_READY : { render: false, reason: COUNCIL_PERMIT_SKIP_REASON }\n}\n\nfunction ruleText(record: BrandlabCouncilPermitRecord, wording: CouncilRuleWording | undefined): ReactNode {\n if (typeof wording === 'function') return wording(record)\n return wording ?? record.rule\n}\n\n/** The record's lines: the rule, its fees, the source with the check month and the site's own council page. */\nexport function CouncilRecordLines({\n record,\n rule,\n sourceLabel,\n councilPage,\n classes,\n}: {\n readonly record: BrandlabCouncilPermitRecord\n readonly rule?: CouncilRuleWording\n readonly sourceLabel?: string\n readonly councilPage?: UiLink\n readonly classes?: SlotClasses<CouncilRecordSlot> | undefined\n}) {\n const fees = record.fees && record.financialYear ? record.fees : []\n return (\n <>\n <p {...slot(classes, 'rule')}>{ruleText(record, rule)}</p>\n {fees.length > 0 ? (\n <ul role=\"list\" {...slot(classes, 'fees')}>\n {fees.map((fee) => (\n <li key={fee.label} {...repeatedSlot('fee')}>\n {councilFeeLine(fee, record.financialYear!)}\n </li>\n ))}\n </ul>\n ) : null}\n <p {...slot(classes, 'source')}>\n <Anchor href={record.sourceUrl} external hooks={{}}>\n {sourceLabel ?? record.councilName}\n </Anchor>\n {`, checked ${formatCheckedMonth(record.checkedOn)}`}\n </p>\n {councilPage ? (\n <p {...slot(classes, 'councilLink')}>\n <Anchor href={councilPage.href} prefetch={false} hooks={{}}>\n {councilPage.label}\n </Anchor>\n </p>\n ) : null}\n </>\n )\n}\n\nexport type CouncilRulesSlot = SectionSlot | SectionHeadingSlot | CouncilRecordSlot | 'fee'\n/** The fees, one per register entry: styled from the skin stylesheet, never by class. */\nexport type CouncilRulesRepeatedSlot = RepeatedSlotOf<'councilRules'>\nexport type CouncilRulesSkin = SkinEntry<CouncilRulesSlot, never, Record<never, never>, CouncilRulesRepeatedSlot>\n\nexport interface CouncilRulesProps extends CouncilRulesSkin {\n /** The council's register record, for example `councilRegister.find((record) => record.council === page.council)`. */\n readonly record: BrandlabCouncilPermitRecord | undefined\n /** The H2, for example \"Hornsby Shire Council tree rules\". */\n readonly title: ReactNode\n readonly eyebrow?: ReactNode\n readonly lead?: ReactNode\n /** The site's own wording of the rule (a function of the record keeps it true when the record changes). */\n readonly rule?: CouncilRuleWording\n /** The source link's text. Defaults to the council's name. */\n readonly sourceLabel?: string\n /** The day the page is built or refreshed (Sydney). Defaults to now. */\n readonly asOf?: Date\n /** Defaults to `council-rules-heading`. */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n}\n\n/** The guard for `SectionSequence`: renders only while the register shows the record. */\nexport function councilRulesReady({ record, asOf }: Pick<CouncilRulesProps, 'record' | 'asOf'>): BrandlabSectionReadiness {\n return councilRecordReady(record, asOf)\n}\n\n/** A council page's rules from the register (playbook-trees §2.4): the rule, the fees with their year, the source. */\nexport function CouncilRules({ record, title, eyebrow, lead, rule, sourceLabel, asOf, headingId = 'council-rules-heading', tone, classes }: CouncilRulesProps) {\n const shown = visibleCouncilRecord(record, asOf)\n if (!shown) return null\n return (\n <Section id=\"council-rules\" labelledBy={headingId} tone={tone} classes={classes} hooks={{ 'data-council': shown.council }}>\n <SectionHeading id={headingId} title={title} eyebrow={eyebrow} lead={lead} classes={classes} />\n <CouncilRecordLines record={shown} rule={rule} sourceLabel={sourceLabel} classes={classes} />\n </Section>\n )\n}\n"]}
@@ -0,0 +1,53 @@
1
+ import type { ReactNode } from 'react';
2
+ import { type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order';
3
+ import { type LinkListEntry, type LinkListLayout } from '../primitives/lists.js';
4
+ import { type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js';
5
+ import type { RepeatedSlotOf } from '../repeated-slots.js';
6
+ import { type SkinEntry } from '../skin.js';
7
+ export declare const COVERAGE_VARIANTS: readonly ["list"];
8
+ export type CoverageVariant = (typeof COVERAGE_VARIANTS)[number];
9
+ export type CoverageSlot = SectionSlot | SectionHeadingSlot | 'groups' | 'hub' | RepeatedSlotOf<'coverage'>;
10
+ /** Rendered once per group: styled from the skin stylesheet, never by class. */
11
+ export type CoverageRepeatedSlot = RepeatedSlotOf<'coverage'>;
12
+ export type CoverageLayout = LinkListLayout;
13
+ export type CoverageSkin = SkinEntry<CoverageSlot, CoverageVariant, CoverageLayout, CoverageRepeatedSlot>;
14
+ /** One group of areas, for example a region or a council. */
15
+ export interface CoverageGroup {
16
+ /** A stable slug, rendered as `data-item`. */
17
+ readonly id: string;
18
+ /** The group's H3, for example "Hills District". Left out on a region page that lists one group. */
19
+ readonly title?: string;
20
+ /** The group's own page, for example its region page; its heading links there. */
21
+ readonly href?: string;
22
+ /**
23
+ * Every area in the group: `{ label, href }` links its page; `{ label }` with no `href` names an area whose page is
24
+ * still noindex (it waits for its return wave) as plain text. Both count towards the hub's "All {n} areas".
25
+ */
26
+ readonly links: readonly LinkListEntry[];
27
+ }
28
+ /** The hub link: every area the site serves. */
29
+ export interface CoverageHub {
30
+ readonly href: string;
31
+ /** How many areas the hub lists, for the locked label "All {n} areas". */
32
+ readonly count: number;
33
+ /** The site's own label instead. */
34
+ readonly label?: string;
35
+ }
36
+ export interface CoverageProps extends CoverageSkin {
37
+ /** The H2, for example "Tree services across the Hills District". */
38
+ readonly title: ReactNode;
39
+ readonly eyebrow?: ReactNode;
40
+ readonly lead?: ReactNode;
41
+ readonly groups: readonly CoverageGroup[];
42
+ readonly hub?: CoverageHub;
43
+ /** Defaults to `coverage-heading`. */
44
+ readonly headingId?: string;
45
+ /** Set by `SectionSequence`. */
46
+ readonly tone?: SectionTone;
47
+ }
48
+ /** The hub link's text: the site's label, or "All {n} areas" ("All 1 area" never reads well, so one is "All areas"). */
49
+ export declare function coverageHubLabel(hub: CoverageHub): string;
50
+ /** The guard for `SectionSequence`. */
51
+ export declare function coverageReady({ groups }: Pick<CoverageProps, 'groups'>): BrandlabSectionReadiness;
52
+ export declare function Coverage({ title, eyebrow, lead, groups, hub, headingId, tone, variant, layout, classes }: CoverageProps): import("react/jsx-runtime").JSX.Element | null;
53
+ //# sourceMappingURL=coverage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/coverage.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,EAAiB,KAAK,wBAAwB,EAAE,MAAM,2CAA2C,CAAA;AACxG,OAAO,EAAY,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAC1F,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,EAAsB,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AAE/D,eAAO,MAAM,iBAAiB,mBAAoB,CAAA;AAClD,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAA;AAChE,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,kBAAkB,GAAG,QAAQ,GAAG,KAAK,GAAG,cAAc,CAAC,UAAU,CAAC,CAAA;AAC3G,gFAAgF;AAChF,MAAM,MAAM,oBAAoB,GAAG,cAAc,CAAC,UAAU,CAAC,CAAA;AAC7D,MAAM,MAAM,cAAc,GAAG,cAAc,CAAA;AAC3C,MAAM,MAAM,YAAY,GAAG,SAAS,CAAC,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,oBAAoB,CAAC,CAAA;AAEzG,6DAA6D;AAC7D,MAAM,WAAW,aAAa;IAC5B,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,oGAAoG;IACpG,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,kFAAkF;IAClF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,CAAA;CACzC;AAED,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,oCAAoC;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAA;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,CAAA;IACzC,QAAQ,CAAC,GAAG,CAAC,EAAE,WAAW,CAAA;IAC1B,sCAAsC;IACtC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,gCAAgC;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAC5B;AAED,wHAAwH;AACxH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAIzD;AAED,uCAAuC;AACvC,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,GAAG,wBAAwB,CAEjG;AAiBD,wBAAgB,QAAQ,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,SAA8B,EAAE,IAAI,EAAE,OAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,aAAa,kDAkCrJ"}
@@ -0,0 +1,57 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * `Coverage` (S15, `coverage`, 3.15.0): the areas served. Server component.
4
+ *
5
+ * On a region page it lists every suburb in the region; on the areas hub every region's suburbs, grouped (D4: every
6
+ * suburb page is indexed and linked). While D4 return waves run, a suburb whose page is still noindex is an entry with
7
+ * no `href`: its name as plain text in the same list (`<li>Putney</li>`), because indexable pages never link a noindex
8
+ * location page. site-core's `coverageEntries()` decides link or text from the estate plan and the day. The lists are lean (cost item 1f): each group is a `LinkList` of plain
9
+ * `<li><a>` entries with no class, hook or icon, so a 120-suburb region stays near 120 bytes a link, and nothing in the
10
+ * section prefetches (`data-bl-links="area"`, W2). Groups come from the site's data in money order (regions or
11
+ * councils); a group's heading links to its own page when it has one. The groups and their lists are repeated parts:
12
+ * style them from `skin.css` (`[data-bl-section="coverage"] [data-slot="group"]`, `… [data-bl="link-list"]`). The hub link reads "All {n} areas" unless the
13
+ * site gives its own label. Locked: no invented region names, no "based in" or distance text.
14
+ */
15
+ import Link from 'next/link';
16
+ import { SECTION_READY } from '@brandlab-digital/site-core/section-order';
17
+ import { LinkList } from '../primitives/lists.js';
18
+ import { Section, SectionHeading } from '../primitives/layout.js';
19
+ import { repeatedSlot, slot } from '../skin.js';
20
+ export const COVERAGE_VARIANTS = ['list'];
21
+ /** The hub link's text: the site's label, or "All {n} areas" ("All 1 area" never reads well, so one is "All areas"). */
22
+ export function coverageHubLabel(hub) {
23
+ if (hub.label?.trim())
24
+ return hub.label;
25
+ if (!Number.isInteger(hub.count) || hub.count < 1)
26
+ throw new Error('Coverage: the hub count must be a whole number of areas, 1 or more.');
27
+ return hub.count === 1 ? 'All areas' : `All ${hub.count} areas`;
28
+ }
29
+ /** The guard for `SectionSequence`. */
30
+ export function coverageReady({ groups }) {
31
+ return groups.some((group) => group.links.length > 0) ? SECTION_READY : { render: false, reason: 'no-areas' };
32
+ }
33
+ function assertGroups(groups) {
34
+ const ids = new Set();
35
+ for (const group of groups) {
36
+ if (!group.id || ids.has(group.id))
37
+ throw new Error(`Coverage: every group needs its own id ("${group.id}" is empty or repeated).`);
38
+ ids.add(group.id);
39
+ const seen = new Set();
40
+ for (const link of group.links) {
41
+ const key = link.href ?? `text:${link.label}`;
42
+ if (seen.has(key))
43
+ throw new Error(`Coverage: ${link.href ?? link.label} is listed twice in "${group.id}".`);
44
+ seen.add(key);
45
+ }
46
+ }
47
+ if (groups.length > 1 && groups.some((group) => !group.title?.trim()))
48
+ throw new Error('Coverage: with more than one group, every group needs its title.');
49
+ }
50
+ export function Coverage({ title, eyebrow, lead, groups, hub, headingId = 'coverage-heading', tone, variant = 'list', layout, classes }) {
51
+ const shown = groups.filter((group) => group.links.length > 0);
52
+ if (shown.length === 0)
53
+ return null;
54
+ assertGroups(shown);
55
+ return (_jsxs(Section, { id: "coverage", labelledBy: headingId, tone: tone, variant: variant, classes: classes, children: [_jsx(SectionHeading, { id: headingId, title: title, eyebrow: eyebrow, lead: lead, classes: classes }), _jsx("div", { ...slot(classes, 'groups'), children: shown.map((group) => (_jsxs("div", { ...repeatedSlot('group'), "data-item": group.id, children: [group.title ? (_jsx("h3", { ...repeatedSlot('groupHeading'), children: group.href ? (_jsx(Link, { href: group.href, prefetch: false, children: group.title })) : (group.title) })) : null, _jsx(LinkList, { links: group.links, kind: "area", layout: layout })] }, group.id))) }), hub ? (_jsx("p", { ...slot(classes, 'hub'), children: _jsx(Link, { href: hub.href, prefetch: false, children: coverageHubLabel(hub) }) })) : null] }));
56
+ }
57
+ //# sourceMappingURL=coverage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/coverage.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * `Coverage` (S15, `coverage`, 3.15.0): the areas served. Server component.\n *\n * On a region page it lists every suburb in the region; on the areas hub every region's suburbs, grouped (D4: every\n * suburb page is indexed and linked). While D4 return waves run, a suburb whose page is still noindex is an entry with\n * no `href`: its name as plain text in the same list (`<li>Putney</li>`), because indexable pages never link a noindex\n * location page. site-core's `coverageEntries()` decides link or text from the estate plan and the day. The lists are lean (cost item 1f): each group is a `LinkList` of plain\n * `<li><a>` entries with no class, hook or icon, so a 120-suburb region stays near 120 bytes a link, and nothing in the\n * section prefetches (`data-bl-links=\"area\"`, W2). Groups come from the site's data in money order (regions or\n * councils); a group's heading links to its own page when it has one. The groups and their lists are repeated parts:\n * style them from `skin.css` (`[data-bl-section=\"coverage\"] [data-slot=\"group\"]`, `… [data-bl=\"link-list\"]`). The hub link reads \"All {n} areas\" unless the\n * site gives its own label. Locked: no invented region names, no \"based in\" or distance text.\n */\nimport Link from 'next/link'\nimport type { ReactNode } from 'react'\nimport { SECTION_READY, type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order'\nimport { LinkList, type LinkListEntry, type LinkListLayout } from '../primitives/lists.js'\nimport { Section, SectionHeading, type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js'\nimport type { RepeatedSlotOf } from '../repeated-slots.js'\nimport { repeatedSlot, slot, type SkinEntry } from '../skin.js'\n\nexport const COVERAGE_VARIANTS = ['list'] as const\nexport type CoverageVariant = (typeof COVERAGE_VARIANTS)[number]\nexport type CoverageSlot = SectionSlot | SectionHeadingSlot | 'groups' | 'hub' | RepeatedSlotOf<'coverage'>\n/** Rendered once per group: styled from the skin stylesheet, never by class. */\nexport type CoverageRepeatedSlot = RepeatedSlotOf<'coverage'>\nexport type CoverageLayout = LinkListLayout\nexport type CoverageSkin = SkinEntry<CoverageSlot, CoverageVariant, CoverageLayout, CoverageRepeatedSlot>\n\n/** One group of areas, for example a region or a council. */\nexport interface CoverageGroup {\n /** A stable slug, rendered as `data-item`. */\n readonly id: string\n /** The group's H3, for example \"Hills District\". Left out on a region page that lists one group. */\n readonly title?: string\n /** The group's own page, for example its region page; its heading links there. */\n readonly href?: string\n /**\n * Every area in the group: `{ label, href }` links its page; `{ label }` with no `href` names an area whose page is\n * still noindex (it waits for its return wave) as plain text. Both count towards the hub's \"All {n} areas\".\n */\n readonly links: readonly LinkListEntry[]\n}\n\n/** The hub link: every area the site serves. */\nexport interface CoverageHub {\n readonly href: string\n /** How many areas the hub lists, for the locked label \"All {n} areas\". */\n readonly count: number\n /** The site's own label instead. */\n readonly label?: string\n}\n\nexport interface CoverageProps extends CoverageSkin {\n /** The H2, for example \"Tree services across the Hills District\". */\n readonly title: ReactNode\n readonly eyebrow?: ReactNode\n readonly lead?: ReactNode\n readonly groups: readonly CoverageGroup[]\n readonly hub?: CoverageHub\n /** Defaults to `coverage-heading`. */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n}\n\n/** The hub link's text: the site's label, or \"All {n} areas\" (\"All 1 area\" never reads well, so one is \"All areas\"). */\nexport function coverageHubLabel(hub: CoverageHub): string {\n if (hub.label?.trim()) return hub.label\n if (!Number.isInteger(hub.count) || hub.count < 1) throw new Error('Coverage: the hub count must be a whole number of areas, 1 or more.')\n return hub.count === 1 ? 'All areas' : `All ${hub.count} areas`\n}\n\n/** The guard for `SectionSequence`. */\nexport function coverageReady({ groups }: Pick<CoverageProps, 'groups'>): BrandlabSectionReadiness {\n return groups.some((group) => group.links.length > 0) ? SECTION_READY : { render: false, reason: 'no-areas' }\n}\n\nfunction assertGroups(groups: readonly CoverageGroup[]) {\n const ids = new Set<string>()\n for (const group of groups) {\n if (!group.id || ids.has(group.id)) throw new Error(`Coverage: every group needs its own id (\"${group.id}\" is empty or repeated).`)\n ids.add(group.id)\n const seen = new Set<string>()\n for (const link of group.links) {\n const key = link.href ?? `text:${link.label}`\n if (seen.has(key)) throw new Error(`Coverage: ${link.href ?? link.label} is listed twice in \"${group.id}\".`)\n seen.add(key)\n }\n }\n if (groups.length > 1 && groups.some((group) => !group.title?.trim())) throw new Error('Coverage: with more than one group, every group needs its title.')\n}\n\nexport function Coverage({ title, eyebrow, lead, groups, hub, headingId = 'coverage-heading', tone, variant = 'list', layout, classes }: CoverageProps) {\n const shown = groups.filter((group) => group.links.length > 0)\n if (shown.length === 0) return null\n assertGroups(shown)\n return (\n <Section id=\"coverage\" labelledBy={headingId} tone={tone} variant={variant} classes={classes}>\n <SectionHeading id={headingId} title={title} eyebrow={eyebrow} lead={lead} classes={classes} />\n <div {...slot(classes, 'groups')}>\n {shown.map((group) => (\n <div key={group.id} {...repeatedSlot('group')} data-item={group.id}>\n {group.title ? (\n <h3 {...repeatedSlot('groupHeading')}>\n {group.href ? (\n <Link href={group.href} prefetch={false}>\n {group.title}\n </Link>\n ) : (\n group.title\n )}\n </h3>\n ) : null}\n <LinkList links={group.links} kind=\"area\" layout={layout} />\n </div>\n ))}\n </div>\n {hub ? (\n <p {...slot(classes, 'hub')}>\n <Link href={hub.href} prefetch={false}>\n {coverageHubLabel(hub)}\n </Link>\n </p>\n ) : null}\n </Section>\n )\n}\n"]}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `EmergencyBand` (`emergency`, 3.15.0): the after-hours call band, straight after the proof strip. Server component,
3
+ * no client JS.
4
+ *
5
+ * Locked:
6
+ * - it renders **only** while the business facts record real after-hours work: `credentials.afterHours`, read through
7
+ * `renderableCredentials(credentials, 'website-copy')`, so the claim needs its claims-register id, evidence that
8
+ * allows website copy and has not expired, and (for a storm season) a day before its `endsOn`. Otherwise it renders
9
+ * nothing (matrix: "C, real after-hours only");
10
+ * - phone first: the phone link (`phone_link_click` placement `emergency`) comes before the optional quote button, as
11
+ * in the header, hero, call bar and CTA band (K6);
12
+ * - no response time: no record carries one, so a heading or summary that promises one ("within 60 minutes", "in 2
13
+ * hours", "within the hour") fails the build;
14
+ * - no default visible copy: the heading, summary and labels are the site's.
15
+ */
16
+ import { type BrandlabAfterHours, type BrandlabBusiness } from '@brandlab-digital/site-core/business';
17
+ import { type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order';
18
+ import type { SectionTone } from '../primitives/layout.js';
19
+ import { type ButtonVariant } from '../primitives/links.js';
20
+ import { type SkinEntry } from '../skin.js';
21
+ import type { UiPhone } from '../types.js';
22
+ export type EmergencyBandSlot = 'root' | 'inner' | 'content' | 'heading' | 'summary' | 'actions' | 'phone' | 'quote';
23
+ export interface EmergencyBandLayout {
24
+ /** The phone link's variant. Defaults to `button`. */
25
+ readonly phoneVariant?: 'text' | 'button';
26
+ /** The quote button's variant. Defaults to `secondary`: the call is the band's main action. */
27
+ readonly quoteVariant?: ButtonVariant;
28
+ }
29
+ export type EmergencyBandSkin = SkinEntry<EmergencyBandSlot, never, EmergencyBandLayout>;
30
+ /** The business facts the band reads: `business.credentials`. */
31
+ export type EmergencyBandBusiness = Pick<BrandlabBusiness, 'credentials'>;
32
+ export interface EmergencyBandProps extends EmergencyBandSkin {
33
+ readonly business: EmergencyBandBusiness;
34
+ /** The H2, for example "Tree down after a storm?". No response time. */
35
+ readonly heading: string;
36
+ /** One line under the heading. No response time. */
37
+ readonly summary?: string;
38
+ readonly phone: UiPhone;
39
+ /** Words before the number, for example "Call". */
40
+ readonly phoneLabel?: string;
41
+ /** An optional quote action after the phone link, for work that can wait for a quote. */
42
+ readonly quote?: {
43
+ readonly label: string;
44
+ readonly href: string;
45
+ };
46
+ /** The day the page is built (tests pin it). */
47
+ readonly asOf?: Date;
48
+ /** Defaults to `emergency-heading`. */
49
+ readonly headingId?: string;
50
+ /** Set by `SectionSequence`. */
51
+ readonly tone?: SectionTone;
52
+ }
53
+ /** The after-hours work the band may stand on today, or nothing. */
54
+ export declare function renderableAfterHours(business: EmergencyBandBusiness, asOf?: Date): (BrandlabAfterHours & {
55
+ claimId: string;
56
+ }) | undefined;
57
+ /** The section's guard for `SectionSequence`: skipped unless real after-hours work is recorded and current. */
58
+ export declare function emergencyBandReady({ business, asOf }: Pick<EmergencyBandProps, 'business' | 'asOf'>): BrandlabSectionReadiness;
59
+ export declare function EmergencyBand({ business, heading, summary, phone, phoneLabel, quote, asOf, headingId, tone, layout, classes, }: EmergencyBandProps): import("react/jsx-runtime").JSX.Element | null;
60
+ //# sourceMappingURL=emergency-band.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emergency-band.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/emergency-band.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACtB,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EAAiB,KAAK,wBAAwB,EAAE,MAAM,2CAA2C,CAAA;AACxG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,EAAa,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAEtE,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAE1C,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;AACpH,MAAM,WAAW,mBAAmB;IAClC,sDAAsD;IACtD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAA;IACzC,+FAA+F;IAC/F,QAAQ,CAAC,YAAY,CAAC,EAAE,aAAa,CAAA;CACtC;AACD,MAAM,MAAM,iBAAiB,GAAG,SAAS,CAAC,iBAAiB,EAAE,KAAK,EAAE,mBAAmB,CAAC,CAAA;AAExF,iEAAiE;AACjE,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,gBAAgB,EAAE,aAAa,CAAC,CAAA;AAEzE,MAAM,WAAW,kBAAmB,SAAQ,iBAAiB;IAC3D,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,CAAA;IACxC,wEAAwE;IACxE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,oDAAoD;IACpD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,mDAAmD;IACnD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,yFAAyF;IACzF,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IAClE,gDAAgD;IAChD,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAA;IACpB,uCAAuC;IACvC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,gCAAgC;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAC5B;AAED,oEAAoE;AACpE,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,qBAAqB,EAAE,IAAI,GAAE,IAAiB,GAAG,CAAC,kBAAkB,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,SAAS,CAErJ;AAED,+GAA+G;AAC/G,wBAAgB,kBAAkB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,IAAI,CAAC,kBAAkB,EAAE,UAAU,GAAG,MAAM,CAAC,GAAG,wBAAwB,CAE9H;AAKD,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,OAAO,EACP,OAAO,EACP,KAAK,EACL,UAAU,EACV,KAAK,EACL,IAAI,EACJ,SAA+B,EAC/B,IAAI,EACJ,MAAM,EACN,OAAO,GACR,EAAE,kBAAkB,kDA6CpB"}
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * `EmergencyBand` (`emergency`, 3.15.0): the after-hours call band, straight after the proof strip. Server component,
4
+ * no client JS.
5
+ *
6
+ * Locked:
7
+ * - it renders **only** while the business facts record real after-hours work: `credentials.afterHours`, read through
8
+ * `renderableCredentials(credentials, 'website-copy')`, so the claim needs its claims-register id, evidence that
9
+ * allows website copy and has not expired, and (for a storm season) a day before its `endsOn`. Otherwise it renders
10
+ * nothing (matrix: "C, real after-hours only");
11
+ * - phone first: the phone link (`phone_link_click` placement `emergency`) comes before the optional quote button, as
12
+ * in the header, hero, call bar and CTA band (K6);
13
+ * - no response time: no record carries one, so a heading or summary that promises one ("within 60 minutes", "in 2
14
+ * hours", "within the hour") fails the build;
15
+ * - no default visible copy: the heading, summary and labels are the site's.
16
+ */
17
+ import { renderableCredentials, } from '@brandlab-digital/site-core/business';
18
+ import { SECTION_READY } from '@brandlab-digital/site-core/section-order';
19
+ import { PhoneLink } from '../primitives/links.js';
20
+ import { QuoteButton } from '../primitives/quote-button.js';
21
+ import { slot } from '../skin.js';
22
+ /** The after-hours work the band may stand on today, or nothing. */
23
+ export function renderableAfterHours(business, asOf = new Date()) {
24
+ return renderableCredentials(business.credentials, 'website-copy', asOf).afterHours;
25
+ }
26
+ /** The section's guard for `SectionSequence`: skipped unless real after-hours work is recorded and current. */
27
+ export function emergencyBandReady({ business, asOf }) {
28
+ return renderableAfterHours(business, asOf) ? SECTION_READY : { render: false, reason: 'no-after-hours-record' };
29
+ }
30
+ /** A promised response time: "60 minutes", "in 2 hours", "1-2 hrs", "within the hour", "within an hour". */
31
+ const RESPONSE_TIME = /\b\d+(?:\s*(?:-|–|to)\s*\d+)?\s*-?\s*(?:min(?:ute)?s?|h(?:ou)?rs?)\b|\bwithin\s+(?:an?|one|the)\s+hour\b|\bunder\s+an?\s+hour\b/i;
32
+ export function EmergencyBand({ business, heading, summary, phone, phoneLabel, quote, asOf, headingId = 'emergency-heading', tone, layout, classes, }) {
33
+ const afterHours = renderableAfterHours(business, asOf);
34
+ if (!afterHours)
35
+ return null;
36
+ for (const text of [heading, summary]) {
37
+ if (text && RESPONSE_TIME.test(text)) {
38
+ throw new Error(`EmergencyBand: "${text}" promises a response time, which no record supports. Say what is done after hours instead.`);
39
+ }
40
+ }
41
+ return (_jsx("section", { "data-bl-section": "emergency", "data-tone": tone, "aria-labelledby": headingId, className: classes?.root, children: _jsxs("div", { ...slot(classes, 'inner'), children: [_jsxs("div", { ...slot(classes, 'content'), children: [_jsx("h2", { id: headingId, ...slot(classes, 'heading'), children: heading }), summary ? _jsx("p", { ...slot(classes, 'summary'), children: summary }) : null] }), _jsxs("div", { ...slot(classes, 'actions'), children: [_jsx(PhoneLink, { phone: phone, placement: "emergency", label: phoneLabel, showIcon: true, variant: layout?.phoneVariant ?? 'button', classes: { root: classes?.phone } }), quote ? (_jsx(QuoteButton, { label: quote.label, href: quote.href, placement: "section", ctaId: "quote-emergency", variant: layout?.quoteVariant ?? 'secondary', classes: { root: classes?.quote } })) : null] })] }) }));
42
+ }
43
+ //# sourceMappingURL=emergency-band.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emergency-band.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/emergency-band.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EACL,qBAAqB,GAGtB,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EAAE,aAAa,EAAiC,MAAM,2CAA2C,CAAA;AAExG,OAAO,EAAE,SAAS,EAAsB,MAAM,wBAAwB,CAAA;AACtE,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAA;AAC3D,OAAO,EAAE,IAAI,EAAkB,MAAM,YAAY,CAAA;AAkCjD,oEAAoE;AACpE,MAAM,UAAU,oBAAoB,CAAC,QAA+B,EAAE,OAAa,IAAI,IAAI,EAAE;IAC3F,OAAO,qBAAqB,CAAC,QAAQ,CAAC,WAAW,EAAE,cAAc,EAAE,IAAI,CAAC,CAAC,UAAU,CAAA;AACrF,CAAC;AAED,+GAA+G;AAC/G,MAAM,UAAU,kBAAkB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAiD;IAClG,OAAO,oBAAoB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,uBAAuB,EAAE,CAAA;AAClH,CAAC;AAED,4GAA4G;AAC5G,MAAM,aAAa,GAAG,kIAAkI,CAAA;AAExJ,MAAM,UAAU,aAAa,CAAC,EAC5B,QAAQ,EACR,OAAO,EACP,OAAO,EACP,KAAK,EACL,UAAU,EACV,KAAK,EACL,IAAI,EACJ,SAAS,GAAG,mBAAmB,EAC/B,IAAI,EACJ,MAAM,EACN,OAAO,GACY;IACnB,MAAM,UAAU,GAAG,oBAAoB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IACvD,IAAI,CAAC,UAAU;QAAE,OAAO,IAAI,CAAA;IAC5B,KAAK,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC;QACtC,IAAI,IAAI,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,6FAA6F,CAAC,CAAA;QACvI,CAAC;IACH,CAAC;IACD,OAAO,CACL,qCACkB,WAAW,eAChB,IAAI,qBACE,SAAS,EAC1B,SAAS,EAAE,OAAO,EAAE,IAAI,YAExB,kBAAS,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,aAC7B,kBAAS,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,aAC/B,aAAI,EAAE,EAAE,SAAS,KAAM,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,YAC5C,OAAO,GACL,EACJ,OAAO,CAAC,CAAC,CAAC,eAAO,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,YAAG,OAAO,GAAK,CAAC,CAAC,CAAC,IAAI,IAC5D,EACN,kBAAS,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,aAC/B,KAAC,SAAS,IACR,KAAK,EAAE,KAAK,EACZ,SAAS,EAAC,WAAW,EACrB,KAAK,EAAE,UAAU,EACjB,QAAQ,QACR,OAAO,EAAE,MAAM,EAAE,YAAY,IAAI,QAAQ,EACzC,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,GACjC,EACD,KAAK,CAAC,CAAC,CAAC,CACP,KAAC,WAAW,IACV,KAAK,EAAE,KAAK,CAAC,KAAK,EAClB,IAAI,EAAE,KAAK,CAAC,IAAI,EAChB,SAAS,EAAC,SAAS,EACnB,KAAK,EAAC,iBAAiB,EACvB,OAAO,EAAE,MAAM,EAAE,YAAY,IAAI,WAAW,EAC5C,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,GACjC,CACH,CAAC,CAAC,CAAC,IAAI,IACJ,IACF,GACE,CACX,CAAA;AACH,CAAC","sourcesContent":["/**\n * `EmergencyBand` (`emergency`, 3.15.0): the after-hours call band, straight after the proof strip. Server component,\n * no client JS.\n *\n * Locked:\n * - it renders **only** while the business facts record real after-hours work: `credentials.afterHours`, read through\n * `renderableCredentials(credentials, 'website-copy')`, so the claim needs its claims-register id, evidence that\n * allows website copy and has not expired, and (for a storm season) a day before its `endsOn`. Otherwise it renders\n * nothing (matrix: \"C, real after-hours only\");\n * - phone first: the phone link (`phone_link_click` placement `emergency`) comes before the optional quote button, as\n * in the header, hero, call bar and CTA band (K6);\n * - no response time: no record carries one, so a heading or summary that promises one (\"within 60 minutes\", \"in 2\n * hours\", \"within the hour\") fails the build;\n * - no default visible copy: the heading, summary and labels are the site's.\n */\nimport {\n renderableCredentials,\n type BrandlabAfterHours,\n type BrandlabBusiness,\n} from '@brandlab-digital/site-core/business'\nimport { SECTION_READY, type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order'\nimport type { SectionTone } from '../primitives/layout.js'\nimport { PhoneLink, type ButtonVariant } from '../primitives/links.js'\nimport { QuoteButton } from '../primitives/quote-button.js'\nimport { slot, type SkinEntry } from '../skin.js'\nimport type { UiPhone } from '../types.js'\n\nexport type EmergencyBandSlot = 'root' | 'inner' | 'content' | 'heading' | 'summary' | 'actions' | 'phone' | 'quote'\nexport interface EmergencyBandLayout {\n /** The phone link's variant. Defaults to `button`. */\n readonly phoneVariant?: 'text' | 'button'\n /** The quote button's variant. Defaults to `secondary`: the call is the band's main action. */\n readonly quoteVariant?: ButtonVariant\n}\nexport type EmergencyBandSkin = SkinEntry<EmergencyBandSlot, never, EmergencyBandLayout>\n\n/** The business facts the band reads: `business.credentials`. */\nexport type EmergencyBandBusiness = Pick<BrandlabBusiness, 'credentials'>\n\nexport interface EmergencyBandProps extends EmergencyBandSkin {\n readonly business: EmergencyBandBusiness\n /** The H2, for example \"Tree down after a storm?\". No response time. */\n readonly heading: string\n /** One line under the heading. No response time. */\n readonly summary?: string\n readonly phone: UiPhone\n /** Words before the number, for example \"Call\". */\n readonly phoneLabel?: string\n /** An optional quote action after the phone link, for work that can wait for a quote. */\n readonly quote?: { readonly label: string; readonly href: string }\n /** The day the page is built (tests pin it). */\n readonly asOf?: Date\n /** Defaults to `emergency-heading`. */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n}\n\n/** The after-hours work the band may stand on today, or nothing. */\nexport function renderableAfterHours(business: EmergencyBandBusiness, asOf: Date = new Date()): (BrandlabAfterHours & { claimId: string }) | undefined {\n return renderableCredentials(business.credentials, 'website-copy', asOf).afterHours\n}\n\n/** The section's guard for `SectionSequence`: skipped unless real after-hours work is recorded and current. */\nexport function emergencyBandReady({ business, asOf }: Pick<EmergencyBandProps, 'business' | 'asOf'>): BrandlabSectionReadiness {\n return renderableAfterHours(business, asOf) ? SECTION_READY : { render: false, reason: 'no-after-hours-record' }\n}\n\n/** A promised response time: \"60 minutes\", \"in 2 hours\", \"1-2 hrs\", \"within the hour\", \"within an hour\". */\nconst RESPONSE_TIME = /\\b\\d+(?:\\s*(?:-|–|to)\\s*\\d+)?\\s*-?\\s*(?:min(?:ute)?s?|h(?:ou)?rs?)\\b|\\bwithin\\s+(?:an?|one|the)\\s+hour\\b|\\bunder\\s+an?\\s+hour\\b/i\n\nexport function EmergencyBand({\n business,\n heading,\n summary,\n phone,\n phoneLabel,\n quote,\n asOf,\n headingId = 'emergency-heading',\n tone,\n layout,\n classes,\n}: EmergencyBandProps) {\n const afterHours = renderableAfterHours(business, asOf)\n if (!afterHours) return null\n for (const text of [heading, summary]) {\n if (text && RESPONSE_TIME.test(text)) {\n throw new Error(`EmergencyBand: \"${text}\" promises a response time, which no record supports. Say what is done after hours instead.`)\n }\n }\n return (\n <section\n data-bl-section=\"emergency\"\n data-tone={tone}\n aria-labelledby={headingId}\n className={classes?.root}\n >\n <div {...slot(classes, 'inner')}>\n <div {...slot(classes, 'content')}>\n <h2 id={headingId} {...slot(classes, 'heading')}>\n {heading}\n </h2>\n {summary ? <p {...slot(classes, 'summary')}>{summary}</p> : null}\n </div>\n <div {...slot(classes, 'actions')}>\n <PhoneLink\n phone={phone}\n placement=\"emergency\"\n label={phoneLabel}\n showIcon\n variant={layout?.phoneVariant ?? 'button'}\n classes={{ root: classes?.phone }}\n />\n {quote ? (\n <QuoteButton\n label={quote.label}\n href={quote.href}\n placement=\"section\"\n ctaId=\"quote-emergency\"\n variant={layout?.quoteVariant ?? 'secondary'}\n classes={{ root: classes?.quote }}\n />\n ) : null}\n </div>\n </div>\n </section>\n )\n}\n"]}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `Gallery` (`gallery`, 3.15.0): a grid of genuine job photos, on the gallery page (eight or more) or as a short
3
+ * section on a money page. Server component, no client JS: every photo renders through `MediaFrame`, so the sizing
4
+ * rule (a fluid `vw` size or a fixed `displayWidth`, never `sizes="148px"`) and the provenance hook are the library's.
5
+ *
6
+ * Locked:
7
+ * - genuine photos only: a stock or AI image fails the build. An AI illustration is supporting media with its
8
+ * "Illustration" label elsewhere, never in an "our work" gallery (F13);
9
+ * - every photo loads lazily (no preload): a gallery is never the page's main image;
10
+ * - a photo may link its project page; those links never prefetch (a gallery can hold dozens);
11
+ * - no photo twice, and no default visible copy: captions ("{what}, {suburb}, {month year}") come from the site.
12
+ */
13
+ import type { ReactNode } from 'react';
14
+ import type { BrandlabMediaRatio } from '@brandlab-digital/site-core/media';
15
+ import { type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order';
16
+ import { type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js';
17
+ import { type UiMedia } from '../primitives/media-frame.js';
18
+ import type { RepeatedSlotOf } from '../repeated-slots.js';
19
+ import { type SkinEntry } from '../skin.js';
20
+ import type { UiLink } from '../types.js';
21
+ /** Rendered once per photo: styled from the skin stylesheet, never by class. The frames are `[data-bl="media"]`. */
22
+ export type GalleryRepeatedSlot = RepeatedSlotOf<'gallery'>;
23
+ export type GallerySlot = SectionSlot | SectionHeadingSlot | 'list' | 'more' | GalleryRepeatedSlot;
24
+ export interface GalleryLayout {
25
+ /** Columns on wide screens. Defaults to 3. */
26
+ readonly columns?: 2 | 3 | 4;
27
+ /** Crop every photo to one ratio, so the grid lines up. Left out, each photo keeps its own shape. */
28
+ readonly ratio?: BrandlabMediaRatio;
29
+ }
30
+ export type GallerySkin = SkinEntry<GallerySlot, never, GalleryLayout, GalleryRepeatedSlot>;
31
+ /** One photo of the work. */
32
+ export interface GalleryPhoto {
33
+ /** A stable slug (the media id), rendered as `data-item`. */
34
+ readonly id: string;
35
+ /** A genuine photo from the media manifest. */
36
+ readonly media: UiMedia;
37
+ /** "{what}, {suburb}, {month year}", only when the photo really shows that job. */
38
+ readonly caption?: ReactNode;
39
+ /** The job's project page, when it has one. Never prefetched. */
40
+ readonly href?: string;
41
+ }
42
+ export interface GalleryProps extends GallerySkin {
43
+ /** The H2, for example "Our recent tree work". */
44
+ readonly title: ReactNode;
45
+ readonly eyebrow?: ReactNode;
46
+ readonly lead?: ReactNode;
47
+ readonly photos: readonly GalleryPhoto[];
48
+ /** A link after the grid, for example the full gallery. Never prefetched. */
49
+ readonly more?: UiLink;
50
+ /** The photos' `sizes`: a fluid `vw` string, or a number for a fixed CSS width (thumbnails). Defaults to one column. */
51
+ readonly imageSizes?: string | number;
52
+ /** Defaults to `gallery-heading`. */
53
+ readonly headingId?: string;
54
+ /** Set by `SectionSequence`. */
55
+ readonly tone?: SectionTone;
56
+ }
57
+ /** The section's guard for `SectionSequence`: skipped when there is no genuine photo to show. */
58
+ export declare function galleryReady({ photos }: Pick<GalleryProps, 'photos'>): BrandlabSectionReadiness;
59
+ export declare function Gallery({ title, eyebrow, lead, photos, more, imageSizes, headingId, tone, layout, classes }: GalleryProps): import("react/jsx-runtime").JSX.Element | null;
60
+ //# sourceMappingURL=gallery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gallery.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/gallery.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAA;AAC3E,OAAO,EAAiB,KAAK,wBAAwB,EAAE,MAAM,2CAA2C,CAAA;AACxG,OAAO,EAA2B,KAAK,kBAAkB,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAE9H,OAAO,EAAc,KAAK,OAAO,EAAE,MAAM,8BAA8B,CAAA;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AAC/D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,oHAAoH;AACpH,MAAM,MAAM,mBAAmB,GAAG,cAAc,CAAC,SAAS,CAAC,CAAA;AAC3D,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,kBAAkB,GAAG,MAAM,GAAG,MAAM,GAAG,mBAAmB,CAAA;AAClG,MAAM,WAAW,aAAa;IAC5B,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC5B,qGAAqG;IACrG,QAAQ,CAAC,KAAK,CAAC,EAAE,kBAAkB,CAAA;CACpC;AACD,MAAM,MAAM,WAAW,GAAG,SAAS,CAAC,WAAW,EAAE,KAAK,EAAE,aAAa,EAAE,mBAAmB,CAAC,CAAA;AAE3F,6BAA6B;AAC7B,MAAM,WAAW,YAAY;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,mFAAmF;IACnF,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAA;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,YAAa,SAAQ,WAAW;IAC/C,kDAAkD;IAClD,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAA;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAA;IACxC,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,wHAAwH;IACxH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACrC,qCAAqC;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,gCAAgC;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAC5B;AAED,iGAAiG;AACjG,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,GAAG,wBAAwB,CAE/F;AAgBD,wBAAgB,OAAO,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,SAA6B,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,YAAY,kDAiC7I"}
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { SECTION_READY } from '@brandlab-digital/site-core/section-order';
3
+ import { Section, SectionHeading } from '../primitives/layout.js';
4
+ import { Anchor } from '../primitives/links.js';
5
+ import { MediaFrame } from '../primitives/media-frame.js';
6
+ import { repeatedSlot, slot } from '../skin.js';
7
+ import { assertGenuineWork, frameSizing, gridImageSizes, isInternalPath } from './work-photos.js';
8
+ /** The section's guard for `SectionSequence`: skipped when there is no genuine photo to show. */
9
+ export function galleryReady({ photos }) {
10
+ return photos.length > 0 ? SECTION_READY : { render: false, reason: 'no-genuine-photos' };
11
+ }
12
+ function assertPhotos(photos) {
13
+ const ids = new Set();
14
+ const sources = new Map();
15
+ for (const photo of photos) {
16
+ if (!photo.id || ids.has(photo.id))
17
+ throw new Error(`Gallery: every photo needs its own id ("${photo.id}" is empty or repeated).`);
18
+ ids.add(photo.id);
19
+ assertGenuineWork('Gallery', photo.media, `"${photo.id}"`);
20
+ const other = sources.get(photo.media.src);
21
+ if (other)
22
+ throw new Error(`Gallery: "${photo.id}" and "${other}" are the same photo (${photo.media.src}).`);
23
+ sources.set(photo.media.src, photo.id);
24
+ if (photo.href !== undefined && !isInternalPath(photo.href))
25
+ throw new Error(`Gallery: "${photo.id}" links ${photo.href}; a photo links its project page on this site.`);
26
+ }
27
+ }
28
+ export function Gallery({ title, eyebrow, lead, photos, more, imageSizes, headingId = 'gallery-heading', tone, layout, classes }) {
29
+ if (!galleryReady({ photos }).render)
30
+ return null;
31
+ assertPhotos(photos);
32
+ const columns = layout?.columns ?? 3;
33
+ const sizing = frameSizing(imageSizes ?? gridImageSizes(columns));
34
+ return (_jsxs(Section, { id: "gallery", labelledBy: headingId, tone: tone, classes: classes, hooks: { 'data-columns': String(columns), 'data-ratio': layout?.ratio }, children: [_jsx(SectionHeading, { id: headingId, title: title, eyebrow: eyebrow, lead: lead, classes: classes }), _jsx("ul", { role: "list", ...slot(classes, 'list'), children: photos.map((photo) => {
35
+ const frame = _jsx(MediaFrame, { media: photo.media, ratio: layout?.ratio, caption: photo.caption, ...sizing });
36
+ return (_jsx("li", { ...repeatedSlot('item'), "data-item": photo.id, children: photo.href ? (_jsx(Anchor, { href: photo.href, prefetch: false, hooks: { 'data-slot': 'link' }, children: frame })) : (frame) }, photo.id));
37
+ }) }), more ? (_jsx("p", { ...slot(classes, 'more'), children: _jsx(Anchor, { href: more.href, prefetch: false, hooks: {}, children: more.label }) })) : null] }));
38
+ }
39
+ //# sourceMappingURL=gallery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gallery.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/gallery.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * `Gallery` (`gallery`, 3.15.0): a grid of genuine job photos, on the gallery page (eight or more) or as a short\n * section on a money page. Server component, no client JS: every photo renders through `MediaFrame`, so the sizing\n * rule (a fluid `vw` size or a fixed `displayWidth`, never `sizes=\"148px\"`) and the provenance hook are the library's.\n *\n * Locked:\n * - genuine photos only: a stock or AI image fails the build. An AI illustration is supporting media with its\n * \"Illustration\" label elsewhere, never in an \"our work\" gallery (F13);\n * - every photo loads lazily (no preload): a gallery is never the page's main image;\n * - a photo may link its project page; those links never prefetch (a gallery can hold dozens);\n * - no photo twice, and no default visible copy: captions (\"{what}, {suburb}, {month year}\") come from the site.\n */\nimport type { ReactNode } from 'react'\nimport type { BrandlabMediaRatio } from '@brandlab-digital/site-core/media'\nimport { SECTION_READY, type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order'\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 type { RepeatedSlotOf } from '../repeated-slots.js'\nimport { repeatedSlot, slot, type SkinEntry } from '../skin.js'\nimport type { UiLink } from '../types.js'\nimport { assertGenuineWork, frameSizing, gridImageSizes, isInternalPath } from './work-photos.js'\n\n/** Rendered once per photo: styled from the skin stylesheet, never by class. The frames are `[data-bl=\"media\"]`. */\nexport type GalleryRepeatedSlot = RepeatedSlotOf<'gallery'>\nexport type GallerySlot = SectionSlot | SectionHeadingSlot | 'list' | 'more' | GalleryRepeatedSlot\nexport interface GalleryLayout {\n /** Columns on wide screens. Defaults to 3. */\n readonly columns?: 2 | 3 | 4\n /** Crop every photo to one ratio, so the grid lines up. Left out, each photo keeps its own shape. */\n readonly ratio?: BrandlabMediaRatio\n}\nexport type GallerySkin = SkinEntry<GallerySlot, never, GalleryLayout, GalleryRepeatedSlot>\n\n/** One photo of the work. */\nexport interface GalleryPhoto {\n /** A stable slug (the media id), rendered as `data-item`. */\n readonly id: string\n /** A genuine photo from the media manifest. */\n readonly media: UiMedia\n /** \"{what}, {suburb}, {month year}\", only when the photo really shows that job. */\n readonly caption?: ReactNode\n /** The job's project page, when it has one. Never prefetched. */\n readonly href?: string\n}\n\nexport interface GalleryProps extends GallerySkin {\n /** The H2, for example \"Our recent tree work\". */\n readonly title: ReactNode\n readonly eyebrow?: ReactNode\n readonly lead?: ReactNode\n readonly photos: readonly GalleryPhoto[]\n /** A link after the grid, for example the full gallery. Never prefetched. */\n readonly more?: UiLink\n /** The photos' `sizes`: a fluid `vw` string, or a number for a fixed CSS width (thumbnails). Defaults to one column. */\n readonly imageSizes?: string | number\n /** Defaults to `gallery-heading`. */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n}\n\n/** The section's guard for `SectionSequence`: skipped when there is no genuine photo to show. */\nexport function galleryReady({ photos }: Pick<GalleryProps, 'photos'>): BrandlabSectionReadiness {\n return photos.length > 0 ? SECTION_READY : { render: false, reason: 'no-genuine-photos' }\n}\n\nfunction assertPhotos(photos: readonly GalleryPhoto[]): void {\n const ids = new Set<string>()\n const sources = new Map<string, string>()\n for (const photo of photos) {\n if (!photo.id || ids.has(photo.id)) throw new Error(`Gallery: every photo needs its own id (\"${photo.id}\" is empty or repeated).`)\n ids.add(photo.id)\n assertGenuineWork('Gallery', photo.media, `\"${photo.id}\"`)\n const other = sources.get(photo.media.src)\n if (other) throw new Error(`Gallery: \"${photo.id}\" and \"${other}\" are the same photo (${photo.media.src}).`)\n sources.set(photo.media.src, photo.id)\n if (photo.href !== undefined && !isInternalPath(photo.href)) throw new Error(`Gallery: \"${photo.id}\" links ${photo.href}; a photo links its project page on this site.`)\n }\n}\n\nexport function Gallery({ title, eyebrow, lead, photos, more, imageSizes, headingId = 'gallery-heading', tone, layout, classes }: GalleryProps) {\n if (!galleryReady({ photos }).render) return null\n assertPhotos(photos)\n const columns = layout?.columns ?? 3\n const sizing = frameSizing(imageSizes ?? gridImageSizes(columns))\n return (\n <Section id=\"gallery\" labelledBy={headingId} tone={tone} classes={classes} hooks={{ 'data-columns': String(columns), 'data-ratio': layout?.ratio }}>\n <SectionHeading id={headingId} title={title} eyebrow={eyebrow} lead={lead} classes={classes} />\n <ul role=\"list\" {...slot(classes, 'list')}>\n {photos.map((photo) => {\n const frame = <MediaFrame media={photo.media} ratio={layout?.ratio} caption={photo.caption} {...sizing} />\n return (\n <li key={photo.id} {...repeatedSlot('item')} data-item={photo.id}>\n {photo.href ? (\n <Anchor href={photo.href} prefetch={false} hooks={{ 'data-slot': 'link' }}>\n {frame}\n </Anchor>\n ) : (\n frame\n )}\n </li>\n )\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"]}
@@ -54,6 +54,11 @@ export interface HeroLayout {
54
54
  readonly height?: 'standard' | 'tall';
55
55
  /** The trust checklist's columns (default 2). */
56
56
  readonly checklistColumns?: 1 | 2;
57
+ /**
58
+ * The checklist's ticks (3.15.0): `icon` (the default) is the sprite's check; `rule` ships no svg, only a mark the
59
+ * stylesheet draws as a short rule (`TickList`'s `layout.tick`), about 100 bytes less a fact.
60
+ */
61
+ readonly checklistTick?: 'icon' | 'rule';
57
62
  }
58
63
  export type HeroSkin = SkinEntry<HeroSlot, HeroVariant, HeroLayout>;
59
64
  /** The form's id and lead diagnostics `form_id` in the hero; the quote button links here. */
@@ -1 +1 @@
1
- {"version":3,"file":"hero.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/hero.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,sCAAsC,CAAA;AACvF,OAAO,EAAW,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAEnE,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AAChE,OAAO,EAAc,KAAK,OAAO,EAAE,MAAM,8BAA8B,CAAA;AAEvE,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAA+B,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAEnF,eAAO,MAAM,aAAa,kCAAmC,CAAA;AAC7D,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAA;AAExD;;;GAGG;AACH,eAAO,MAAM,aAAa,0CAA2C,CAAA;AACrE,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAA;AAEvD;;;GAGG;AACH,eAAO,MAAM,cAAc,yCAAiB,CAAA;AAC5C,MAAM,MAAM,WAAW,GAAG,WAAW,CAAA;AAErC,MAAM,MAAM,QAAQ,GAChB,MAAM,GACN,OAAO,GACP,SAAS,GACT,QAAQ,GACR,SAAS,GACT,IAAI,GACJ,SAAS,GACT,SAAS,GACT,OAAO,GACP,OAAO,GACP,WAAW,GACX,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,SAAS,GACT,OAAO,GACP,MAAM,CAAA;AAEV,MAAM,WAAW,UAAU;IACzB,4DAA4D;IAC5D,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,GAAG,KAAK,CAAA;IACpC,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,CAAA;IACrC,iDAAiD;IACjD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;CAClC;AAED,MAAM,MAAM,QAAQ,GAAG,SAAS,CAAC,QAAQ,EAAE,WAAW,EAAE,UAAU,CAAC,CAAA;AAEnE,6FAA6F;AAC7F,eAAO,MAAM,YAAY,eAAe,CAAA;AACxC,sEAAsE;AACtE,eAAO,MAAM,kBAAkB,IAAI,CAAA;AACnC,6CAA6C;AAC7C,eAAO,MAAM,sBAAsB,KAAK,CAAA;AAExC,MAAM,WAAW,SAAU,SAAQ,QAAQ;IACzC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;IAC3B,gHAAgH;IAChH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,0DAA0D;IAC1D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,kCAAkC;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,8GAA8G;IAC9G,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,CAAA;IAChC,wGAAwG;IACxG,QAAQ,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAA;IACvC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,CAAA;IACvC,iEAAiE;IACjE,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAA;IAC3B,4GAA4G;IAC5G,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,gGAAgG;IAChG,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,4DAA4D;IAC5D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAClE,sGAAsG;IACtG,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAA;IACzB,+FAA+F;IAC/F,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,CAAA;IAC9B,0CAA0C;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,gCAAgC;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAA;IAC3B,2FAA2F;IAC3F,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,CAAA;CAChC;AAeD,wBAAgB,IAAI,CAAC,KAAK,EAAE,SAAS,2CAoFpC"}
1
+ {"version":3,"file":"hero.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/hero.tsx"],"names":[],"mappings":"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"}
@@ -53,6 +53,6 @@ export function Hero(props) {
53
53
  'data-height': layout?.height ?? (family === 'home' ? 'tall' : 'standard'),
54
54
  'data-media-side': variant === 'split' ? (layout?.mediaSide ?? 'end') : undefined,
55
55
  'data-form': form ? '' : undefined,
56
- }, children: [_jsxs("div", { ...slot(classes, 'content'), children: [crumbs ? _jsx("div", { ...slot(classes, 'crumbs'), children: crumbs }) : null, eyebrow ? _jsx("p", { ...slot(classes, 'eyebrow'), children: eyebrow }) : null, _jsx("h1", { id: headingId, ...slot(classes, 'h1'), children: title }), _jsx("p", { ...slot(classes, 'summary'), children: summary }), _jsxs("div", { ...slot(classes, 'actions'), "data-bl-call-path": "", "data-primary": primary, children: [_jsx(PhoneLink, { phone: phone, placement: "hero", label: phoneLabel, showIcon: true, variant: "button", classes: { root: classes?.phone } }), _jsx(QuoteButton, { label: quote.label, href: quote.href ?? `#${HERO_FORM_ID}`, placement: "hero", ctaId: "quote-hero", variant: primary === 'quote' ? 'primary' : 'secondary', classes: { root: classes?.quote } })] }), checklist.length > 0 ? (_jsx("div", { ...slot(classes, 'checklist'), children: _jsx(TickList, { items: checklist, layout: { columns: layout?.checklistColumns ?? 2 } }) })) : null, hasGoogleRating(rating) ? _jsx(RatingChip, { reviews: rating, classes: { root: classes?.rating } }) : null, badges ? _jsx("div", { ...slot(classes, 'badges'), children: badges }) : null] }), _jsxs("div", { ...slot(classes, 'media'), children: [_jsx(MediaFrame, { media: media, ratio: "4:3", sizes: variant === 'full-bleed' ? '100vw' : '(min-width: 1024px) 50vw, 100vw', caption: caption, priority: true, classes: { caption: classes?.caption } }), variant === 'full-bleed' ? _jsx("div", { ...slot(classes, 'scrim'), "aria-hidden": "true" }) : null] }), form ? _jsx("div", { ...slot(classes, 'form'), children: form }) : null] }));
56
+ }, children: [_jsxs("div", { ...slot(classes, 'content'), children: [crumbs ? _jsx("div", { ...slot(classes, 'crumbs'), children: crumbs }) : null, eyebrow ? _jsx("p", { ...slot(classes, 'eyebrow'), children: eyebrow }) : null, _jsx("h1", { id: headingId, ...slot(classes, 'h1'), children: title }), _jsx("p", { ...slot(classes, 'summary'), children: summary }), _jsxs("div", { ...slot(classes, 'actions'), "data-bl-call-path": "", "data-primary": primary, children: [_jsx(PhoneLink, { phone: phone, placement: "hero", label: phoneLabel, showIcon: true, variant: "button", classes: { root: classes?.phone } }), _jsx(QuoteButton, { label: quote.label, href: quote.href ?? `#${HERO_FORM_ID}`, placement: "hero", ctaId: "quote-hero", variant: primary === 'quote' ? 'primary' : 'secondary', classes: { root: classes?.quote } })] }), checklist.length > 0 ? (_jsx("div", { ...slot(classes, 'checklist'), children: _jsx(TickList, { items: checklist, layout: { columns: layout?.checklistColumns ?? 2, ...(layout?.checklistTick ? { tick: layout.checklistTick } : {}) } }) })) : null, hasGoogleRating(rating) ? _jsx(RatingChip, { reviews: rating, classes: { root: classes?.rating } }) : null, badges ? _jsx("div", { ...slot(classes, 'badges'), children: badges }) : null] }), _jsxs("div", { ...slot(classes, 'media'), children: [_jsx(MediaFrame, { media: media, ratio: "4:3", sizes: variant === 'full-bleed' ? '100vw' : '(min-width: 1024px) 50vw, 100vw', caption: caption, priority: true, classes: { caption: classes?.caption } }), variant === 'full-bleed' ? _jsx("div", { ...slot(classes, 'scrim'), "aria-hidden": "true" }) : null] }), form ? _jsx("div", { ...slot(classes, 'form'), children: form }) : null] }));
57
57
  }
58
58
  //# sourceMappingURL=hero.js.map