@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.
Files changed (148) 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/image-runtime.js +6 -1
  6. package/dist/image-runtime.js.map +1 -1
  7. package/dist/navigation/constants.d.ts +11 -2
  8. package/dist/navigation/constants.d.ts.map +1 -1
  9. package/dist/navigation/constants.js +11 -2
  10. package/dist/navigation/constants.js.map +1 -1
  11. package/dist/navigation/index.d.ts +1 -1
  12. package/dist/navigation/index.d.ts.map +1 -1
  13. package/dist/navigation/index.js +1 -1
  14. package/dist/navigation/index.js.map +1 -1
  15. package/dist/navigation/use-back-to-top.d.ts.map +1 -1
  16. package/dist/navigation/use-back-to-top.js +12 -4
  17. package/dist/navigation/use-back-to-top.js.map +1 -1
  18. package/dist/navigation/use-header-metrics.d.ts +1 -1
  19. package/dist/navigation/use-header-metrics.js +1 -1
  20. package/dist/navigation/use-header-metrics.js.map +1 -1
  21. package/dist/navigation/use-sticky-call-bar.d.ts +2 -1
  22. package/dist/navigation/use-sticky-call-bar.d.ts.map +1 -1
  23. package/dist/navigation/use-sticky-call-bar.js +11 -2
  24. package/dist/navigation/use-sticky-call-bar.js.map +1 -1
  25. package/dist/next-sharp.d.ts +14 -9
  26. package/dist/next-sharp.d.ts.map +1 -1
  27. package/dist/next-sharp.js +26 -25
  28. package/dist/next-sharp.js.map +1 -1
  29. package/dist/ui-kit/brandlab-skin.d.ts +26 -0
  30. package/dist/ui-kit/brandlab-skin.d.ts.map +1 -1
  31. package/dist/ui-kit/brandlab-skin.js.map +1 -1
  32. package/dist/ui-kit/prefetch.d.ts +19 -10
  33. package/dist/ui-kit/prefetch.d.ts.map +1 -1
  34. package/dist/ui-kit/prefetch.js +23 -10
  35. package/dist/ui-kit/prefetch.js.map +1 -1
  36. package/dist/ui-kit/primitives/index.d.ts +1 -1
  37. package/dist/ui-kit/primitives/index.d.ts.map +1 -1
  38. package/dist/ui-kit/primitives/index.js.map +1 -1
  39. package/dist/ui-kit/primitives/links.js +1 -1
  40. package/dist/ui-kit/primitives/links.js.map +1 -1
  41. package/dist/ui-kit/primitives/lists.d.ts +9 -1
  42. package/dist/ui-kit/primitives/lists.d.ts.map +1 -1
  43. package/dist/ui-kit/primitives/lists.js +1 -1
  44. package/dist/ui-kit/primitives/lists.js.map +1 -1
  45. package/dist/ui-kit/primitives/quote-button.d.ts +25 -2
  46. package/dist/ui-kit/primitives/quote-button.d.ts.map +1 -1
  47. package/dist/ui-kit/primitives/quote-button.js +25 -3
  48. package/dist/ui-kit/primitives/quote-button.js.map +1 -1
  49. package/dist/ui-kit/repeated-slots.d.ts +32 -0
  50. package/dist/ui-kit/repeated-slots.d.ts.map +1 -1
  51. package/dist/ui-kit/repeated-slots.js +16 -0
  52. package/dist/ui-kit/repeated-slots.js.map +1 -1
  53. package/dist/ui-kit/sections/area-map.d.ts +64 -0
  54. package/dist/ui-kit/sections/area-map.d.ts.map +1 -0
  55. package/dist/ui-kit/sections/area-map.js +142 -0
  56. package/dist/ui-kit/sections/area-map.js.map +1 -0
  57. package/dist/ui-kit/sections/before-after.d.ts +59 -0
  58. package/dist/ui-kit/sections/before-after.d.ts.map +1 -0
  59. package/dist/ui-kit/sections/before-after.js +42 -0
  60. package/dist/ui-kit/sections/before-after.js.map +1 -0
  61. package/dist/ui-kit/sections/council-rules.d.ts +64 -0
  62. package/dist/ui-kit/sections/council-rules.d.ts.map +1 -0
  63. package/dist/ui-kit/sections/council-rules.js +46 -0
  64. package/dist/ui-kit/sections/council-rules.js.map +1 -0
  65. package/dist/ui-kit/sections/coverage.d.ts +53 -0
  66. package/dist/ui-kit/sections/coverage.d.ts.map +1 -0
  67. package/dist/ui-kit/sections/coverage.js +57 -0
  68. package/dist/ui-kit/sections/coverage.js.map +1 -0
  69. package/dist/ui-kit/sections/emergency-band.d.ts +60 -0
  70. package/dist/ui-kit/sections/emergency-band.d.ts.map +1 -0
  71. package/dist/ui-kit/sections/emergency-band.js +43 -0
  72. package/dist/ui-kit/sections/emergency-band.js.map +1 -0
  73. package/dist/ui-kit/sections/gallery.d.ts +60 -0
  74. package/dist/ui-kit/sections/gallery.d.ts.map +1 -0
  75. package/dist/ui-kit/sections/gallery.js +39 -0
  76. package/dist/ui-kit/sections/gallery.js.map +1 -0
  77. package/dist/ui-kit/sections/hero.d.ts +5 -0
  78. package/dist/ui-kit/sections/hero.d.ts.map +1 -1
  79. package/dist/ui-kit/sections/hero.js +1 -1
  80. package/dist/ui-kit/sections/hero.js.map +1 -1
  81. package/dist/ui-kit/sections/index.d.ts +14 -1
  82. package/dist/ui-kit/sections/index.d.ts.map +1 -1
  83. package/dist/ui-kit/sections/index.js +14 -1
  84. package/dist/ui-kit/sections/index.js.map +1 -1
  85. package/dist/ui-kit/sections/licences.d.ts +72 -0
  86. package/dist/ui-kit/sections/licences.d.ts.map +1 -0
  87. package/dist/ui-kit/sections/licences.js +46 -0
  88. package/dist/ui-kit/sections/licences.js.map +1 -0
  89. package/dist/ui-kit/sections/local-facts.d.ts +78 -0
  90. package/dist/ui-kit/sections/local-facts.d.ts.map +1 -0
  91. package/dist/ui-kit/sections/local-facts.js +32 -0
  92. package/dist/ui-kit/sections/local-facts.js.map +1 -0
  93. package/dist/ui-kit/sections/nearby-areas.d.ts +50 -0
  94. package/dist/ui-kit/sections/nearby-areas.d.ts.map +1 -0
  95. package/dist/ui-kit/sections/nearby-areas.js +49 -0
  96. package/dist/ui-kit/sections/nearby-areas.js.map +1 -0
  97. package/dist/ui-kit/sections/nearby-jobs.d.ts +66 -0
  98. package/dist/ui-kit/sections/nearby-jobs.d.ts.map +1 -0
  99. package/dist/ui-kit/sections/nearby-jobs.js +49 -0
  100. package/dist/ui-kit/sections/nearby-jobs.js.map +1 -0
  101. package/dist/ui-kit/sections/projects.d.ts +79 -0
  102. package/dist/ui-kit/sections/projects.d.ts.map +1 -0
  103. package/dist/ui-kit/sections/projects.js +69 -0
  104. package/dist/ui-kit/sections/projects.js.map +1 -0
  105. package/dist/ui-kit/sections/quote-checklist.d.ts +43 -0
  106. package/dist/ui-kit/sections/quote-checklist.d.ts.map +1 -0
  107. package/dist/ui-kit/sections/quote-checklist.js +19 -0
  108. package/dist/ui-kit/sections/quote-checklist.js.map +1 -0
  109. package/dist/ui-kit/sections/related-services.d.ts +41 -0
  110. package/dist/ui-kit/sections/related-services.d.ts.map +1 -0
  111. package/dist/ui-kit/sections/related-services.js +52 -0
  112. package/dist/ui-kit/sections/related-services.js.map +1 -0
  113. package/dist/ui-kit/sections/reviews-band.d.ts +11 -0
  114. package/dist/ui-kit/sections/reviews-band.d.ts.map +1 -1
  115. package/dist/ui-kit/sections/reviews-band.js +11 -0
  116. package/dist/ui-kit/sections/reviews-band.js.map +1 -1
  117. package/dist/ui-kit/sections/services-grid.js +1 -1
  118. package/dist/ui-kit/sections/services-grid.js.map +1 -1
  119. package/dist/ui-kit/sections/work-photos.d.ts +15 -0
  120. package/dist/ui-kit/sections/work-photos.d.ts.map +1 -0
  121. package/dist/ui-kit/sections/work-photos.js +24 -0
  122. package/dist/ui-kit/sections/work-photos.js.map +1 -0
  123. package/dist/ui-kit/shell/back-to-top.js +1 -1
  124. package/dist/ui-kit/shell/back-to-top.js.map +1 -1
  125. package/dist/ui-kit/shell/current-link.d.ts +21 -5
  126. package/dist/ui-kit/shell/current-link.d.ts.map +1 -1
  127. package/dist/ui-kit/shell/current-link.js +16 -5
  128. package/dist/ui-kit/shell/current-link.js.map +1 -1
  129. package/dist/ui-kit/shell/drawer-client.js +1 -1
  130. package/dist/ui-kit/shell/drawer-client.js.map +1 -1
  131. package/dist/ui-kit/shell/intent-prefetch.d.ts.map +1 -1
  132. package/dist/ui-kit/shell/intent-prefetch.js +3 -2
  133. package/dist/ui-kit/shell/intent-prefetch.js.map +1 -1
  134. package/dist/ui-kit/shell/mobile-call-bar.d.ts +6 -1
  135. package/dist/ui-kit/shell/mobile-call-bar.d.ts.map +1 -1
  136. package/dist/ui-kit/shell/mobile-call-bar.js +6 -3
  137. package/dist/ui-kit/shell/mobile-call-bar.js.map +1 -1
  138. package/dist/ui-kit/shell/mobile-drawer.js +2 -2
  139. package/dist/ui-kit/shell/mobile-drawer.js.map +1 -1
  140. package/dist/ui-kit/shell/shell-quote.d.ts +10 -0
  141. package/dist/ui-kit/shell/shell-quote.d.ts.map +1 -0
  142. package/dist/ui-kit/shell/shell-quote.js +13 -0
  143. package/dist/ui-kit/shell/shell-quote.js.map +1 -0
  144. package/dist/ui-kit/shell/site-header.d.ts +1 -1
  145. package/dist/ui-kit/shell/site-header.js +4 -4
  146. package/dist/ui-kit/shell/site-header.js.map +1 -1
  147. package/dist/ui-kit/structure.css +410 -7
  148. package/package.json +2 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projects.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/projects.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * `Projects` (`projects`, 3.15.0): completed jobs as cards, each linking its project page (`/projects/{slug}`). Server\n * component. AA's `ProjectGrid`, re-implemented on the `Card` primitive.\n *\n * Locked:\n * - genuine photos only (v2 §11 imagery rule 4): a stock or AI image on a project card fails the build, and a project\n * without a photo shows as a card without one;\n * - at most 8 cards (K11; the in-content intent-prefetch limit); each card is one link, its title, which prefetches on\n * intent. The hub link after the list never prefetches;\n * - each card links its own project page, a child of the site's projects hub (`/projects/{slug}`, `/our-work/{slug}`);\n * - no default visible copy: the heading, titles, summaries and the suburb-and-month line all come from the site.\n *\n * `projectCardFromJob` builds a card from a site-core job log row (`./jobs`): the job's first genuine photo and its\n * outcome line; the title, the link and the meta line stay the site's.\n */\nimport type { ReactNode } from 'react'\nimport type { BrandlabJob } from '@brandlab-digital/site-core/jobs'\nimport { jobPhotos } from '@brandlab-digital/site-core/jobs'\nimport type { BrandlabMediaManifest, BrandlabMediaRatio } from '@brandlab-digital/site-core/media'\nimport { SECTION_READY, type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order'\nimport { Card } from '../primitives/card.js'\nimport { Section, SectionHeading, type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js'\nimport { Anchor } from '../primitives/links.js'\nimport { MediaFrame, type UiMedia } from '../primitives/media-frame.js'\nimport { LINK_PREFETCH_LIMITS, LINK_PREFETCH_POLICY } from '../prefetch.js'\nimport { slot, type SkinEntry } from '../skin.js'\nimport type { UiLink } from '../types.js'\nimport { assertGenuineWork, frameSizing, gridImageSizes, isInternalPath } from './work-photos.js'\n\n/** The cards' parts are the `Card` primitive's (`[data-bl=\"card\"]`), styled from the skin stylesheet. */\nexport type ProjectsSlot = SectionSlot | SectionHeadingSlot | 'list' | 'more'\nexport interface ProjectsLayout {\n /** Columns on wide screens. Defaults to 3. */\n readonly columns?: 2 | 3 | 4\n /** The photos' crop. Defaults to 4:3. */\n readonly ratio?: BrandlabMediaRatio\n}\nexport type ProjectsSkin = SkinEntry<ProjectsSlot, never, ProjectsLayout>\n\n/** The most project cards a section shows (K11; the in-content intent-prefetch limit). */\nexport const PROJECTS_CARD_LIMIT = LINK_PREFETCH_LIMITS.contentCard\n\n/** One completed job as a card. */\nexport interface ProjectCardData {\n /** A stable slug (the job id), rendered as `data-item`. */\n readonly id: string\n /** The card's H3 and its link's whole name, for example \"Gum removal over a pool, Epping\". */\n readonly title: string\n /** The project page, for example `/projects/epping-gum-removal`. From the site's data. */\n readonly href: string\n /** What was done, in a line. */\n readonly summary?: ReactNode\n /** A short line under the summary, for example the suburb and month. */\n readonly meta?: ReactNode\n /** A genuine photo of this job from the media manifest. */\n readonly image?: UiMedia\n}\n\n/**\n * A card from the job log: the job's first photo (`jobPhotos`, genuine ones only) and its outcome as the summary. The\n * site gives the title, the project page and the meta line in its own words.\n */\nexport function projectCardFromJob(\n job: BrandlabJob,\n options: { readonly media: BrandlabMediaManifest; readonly title: string; readonly href: string; readonly meta?: ReactNode },\n): ProjectCardData {\n const photo = jobPhotos(job, options.media).find((entry) => entry.provenance === 'genuine')\n return {\n id: job.id,\n title: options.title,\n href: options.href,\n summary: job.outcome,\n ...(options.meta !== undefined ? { meta: options.meta } : {}),\n ...(photo ? { image: photo } : {}),\n }\n}\n\nexport interface ProjectsProps extends ProjectsSkin {\n /** The H2, for example \"Recent tree removal jobs\". */\n readonly title: ReactNode\n readonly eyebrow?: ReactNode\n readonly lead?: ReactNode\n /** Up to 8 projects, newest or nearest first. */\n readonly projects: readonly ProjectCardData[]\n /** A link after the cards, for example the projects hub. Never prefetched. */\n readonly more?: UiLink\n /** The photos' `sizes`: a fluid `vw` string, or a number for a fixed CSS width. Defaults to one grid column. */\n readonly imageSizes?: string | number\n /** Defaults to `projects-heading`. */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n}\n\n/** The section's guard for `SectionSequence`: skipped when no project is recorded. */\nexport function projectsReady({ projects }: Pick<ProjectsProps, 'projects'>): BrandlabSectionReadiness {\n return projects.length > 0 ? SECTION_READY : { render: false, reason: 'no-projects' }\n}\n\nfunction assertProjects(projects: readonly ProjectCardData[]): void {\n if (projects.length > PROJECTS_CARD_LIMIT) {\n throw new Error(`Projects: ${projects.length} projects; a section shows at most ${PROJECTS_CARD_LIMIT} (K11). Link the projects hub for the rest.`)\n }\n const ids = new Set<string>()\n const hrefs = new Set<string>()\n const images = new Map<string, string>()\n for (const project of projects) {\n if (!project.id || ids.has(project.id)) throw new Error(`Projects: every project needs its own id (\"${project.id}\" is empty or repeated).`)\n ids.add(project.id)\n if (!project.title?.trim()) throw new Error(`Projects: the project \"${project.id}\" has no title.`)\n if (!isInternalPath(project.href) || project.href.split('/').filter(Boolean).length < 2) {\n throw new Error(`Projects: \"${project.id}\" links ${project.href}; a project card links its own page under the projects hub, such as /projects/${project.id}.`)\n }\n if (hrefs.has(project.href)) throw new Error(`Projects: two projects link to ${project.href}.`)\n hrefs.add(project.href)\n if (project.image) {\n assertGenuineWork('Projects', project.image, `the photo of \"${project.id}\"`)\n const other = images.get(project.image.src)\n if (other) throw new Error(`Projects: \"${project.id}\" and \"${other}\" use the same photo (${project.image.src}).`)\n images.set(project.image.src, project.id)\n }\n }\n}\n\nexport function Projects({\n title,\n eyebrow,\n lead,\n projects,\n more,\n imageSizes,\n headingId = 'projects-heading',\n tone,\n layout,\n classes,\n}: ProjectsProps) {\n if (!projectsReady({ projects }).render) return null\n assertProjects(projects)\n const columns = layout?.columns ?? 3\n const ratio = layout?.ratio ?? '4:3'\n const sizing = frameSizing(imageSizes ?? gridImageSizes(columns))\n return (\n <Section id=\"projects\" labelledBy={headingId} tone={tone} classes={classes} hooks={{ 'data-columns': String(columns), 'data-ratio': ratio }}>\n <SectionHeading id={headingId} title={title} eyebrow={eyebrow} lead={lead} classes={classes} />\n <ul role=\"list\" {...slot(classes, 'list')}>\n {projects.map((project) => (\n <Card\n key={project.id}\n as=\"li\"\n title={project.title}\n href={project.href}\n prefetch={LINK_PREFETCH_POLICY.contentCard}\n media={project.image ? <MediaFrame media={project.image} ratio={ratio} {...sizing} /> : undefined}\n meta={project.meta}\n >\n {project.summary}\n </Card>\n ))}\n </ul>\n {more ? (\n <p {...slot(classes, 'more')}>\n <Anchor href={more.href} prefetch={false} hooks={{}}>\n {more.label}\n </Anchor>\n </p>\n ) : null}\n </Section>\n )\n}\n"]}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `QuoteChecklist` (`data-bl="quote-checklist"`, 3.15.0): what to send for a fast, fair quote in this trade (photos of
3
+ * the tree and its access, the suburb, what's underneath it…), as a real list of ticks, then the button to the quote
4
+ * form. Server component built on `TickList`. It is a block, not a sequenced section of the matrix: a site places it
5
+ * inside its service detail, beside the contact form or as its own band (`headingLevel`).
6
+ *
7
+ * Locked: the list comes before the action; the action is the site's quote button (`lead_cta_click` placement
8
+ * `section`, id `quote-checklist`) to its quote form. No default visible copy: the heading, every item and the button
9
+ * label are the site's (spec: checklist content lives in each site's copy file, so same-trade sites never repeat each
10
+ * other). At most 8 items, so the list stays a checklist.
11
+ */
12
+ import type { ReactNode } from 'react';
13
+ import { type TickItem } from '../primitives/lists.js';
14
+ import type { ButtonVariant } from '../primitives/links.js';
15
+ import { type SkinEntry } from '../skin.js';
16
+ export type QuoteChecklistSlot = 'root' | 'heading' | 'lead' | 'list' | 'action' | 'quote';
17
+ export interface QuoteChecklistLayout {
18
+ /** The list's columns from the phone breakpoint up. Defaults to 1. */
19
+ readonly columns?: 1 | 2;
20
+ /** The quote button's variant. Defaults to `primary`. */
21
+ readonly quoteVariant?: ButtonVariant;
22
+ }
23
+ export type QuoteChecklistSkin = SkinEntry<QuoteChecklistSlot, never, QuoteChecklistLayout>;
24
+ /** The most items a checklist holds. */
25
+ export declare const QUOTE_CHECKLIST_MAX = 8;
26
+ export interface QuoteChecklistProps extends QuoteChecklistSkin {
27
+ /** The heading, for example "What to send for a fast quote". */
28
+ readonly title: ReactNode;
29
+ readonly lead?: ReactNode;
30
+ /** What to send, in the site's words: 2 to 8 items. */
31
+ readonly items: readonly TickItem[];
32
+ /** The site's recorded quote label and its quote form, for example `{ label: 'Get a free quote', href: '#quote-hero' }`. */
33
+ readonly quote: {
34
+ readonly label: string;
35
+ readonly href: string;
36
+ };
37
+ /** 2 (the default) makes the block its own `<section>` labelled by an H2; 3 sits it inside a section's outline. */
38
+ readonly headingLevel?: 2 | 3;
39
+ /** Defaults to `quote-checklist-heading`. */
40
+ readonly headingId?: string;
41
+ }
42
+ export declare function QuoteChecklist({ title, lead, items, quote, headingLevel, headingId, layout, classes }: QuoteChecklistProps): import("react/jsx-runtime").JSX.Element | null;
43
+ //# sourceMappingURL=quote-checklist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quote-checklist.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/quote-checklist.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AAEhE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AAEjD,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAA;AAC1F,MAAM,WAAW,oBAAoB;IACnC,sEAAsE;IACtE,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;IACxB,yDAAyD;IACzD,QAAQ,CAAC,YAAY,CAAC,EAAE,aAAa,CAAA;CACtC;AACD,MAAM,MAAM,kBAAkB,GAAG,SAAS,CAAC,kBAAkB,EAAE,KAAK,EAAE,oBAAoB,CAAC,CAAA;AAE3F,wCAAwC;AACxC,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAEpC,MAAM,WAAW,mBAAoB,SAAQ,kBAAkB;IAC7D,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAA;IACzB,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAA;IACnC,4HAA4H;IAC5H,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACjE,mHAAmH;IACnH,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;IAC7B,6CAA6C;IAC7C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,YAAgB,EAAE,SAAqC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,mBAAmB,kDAoB1J"}
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { TickList } from '../primitives/lists.js';
3
+ import { QuoteButton } from '../primitives/quote-button.js';
4
+ import { slot } from '../skin.js';
5
+ /** The most items a checklist holds. */
6
+ export const QUOTE_CHECKLIST_MAX = 8;
7
+ export function QuoteChecklist({ title, lead, items, quote, headingLevel = 2, headingId = 'quote-checklist-heading', layout, classes }) {
8
+ if (items.length === 0)
9
+ return null;
10
+ if (items.length < 2 || items.length > QUOTE_CHECKLIST_MAX) {
11
+ throw new Error(`QuoteChecklist: ${items.length} items; a checklist holds 2 to ${QUOTE_CHECKLIST_MAX}.`);
12
+ }
13
+ if (!quote?.label?.trim() || !quote.href?.trim())
14
+ throw new Error('QuoteChecklist: the quote button needs the site\'s label and its quote form.');
15
+ const Root = headingLevel === 2 ? 'section' : 'div';
16
+ const Heading = `h${headingLevel}`;
17
+ return (_jsxs(Root, { "data-bl": "quote-checklist", "aria-labelledby": headingLevel === 2 ? headingId : undefined, className: classes?.root, children: [_jsx(Heading, { id: headingId, ...slot(classes, 'heading'), children: title }), lead ? _jsx("p", { ...slot(classes, 'lead'), children: lead }) : null, _jsx(TickList, { items: items, layout: { columns: layout?.columns ?? 1 }, classes: { root: classes?.list } }), _jsx("p", { ...slot(classes, 'action'), children: _jsx(QuoteButton, { label: quote.label, href: quote.href, placement: "section", ctaId: "quote-checklist", variant: layout?.quoteVariant ?? 'primary', classes: { root: classes?.quote } }) })] }));
18
+ }
19
+ //# sourceMappingURL=quote-checklist.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quote-checklist.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/quote-checklist.tsx"],"names":[],"mappings":";AAYA,OAAO,EAAE,QAAQ,EAAiB,MAAM,wBAAwB,CAAA;AAChE,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAA;AAE3D,OAAO,EAAE,IAAI,EAAkB,MAAM,YAAY,CAAA;AAWjD,wCAAwC;AACxC,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAA;AAgBpC,MAAM,UAAU,cAAc,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,GAAG,CAAC,EAAE,SAAS,GAAG,yBAAyB,EAAE,MAAM,EAAE,OAAO,EAAuB;IACzJ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACnC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,mBAAmB,EAAE,CAAC;QAC3D,MAAM,IAAI,KAAK,CAAC,mBAAmB,KAAK,CAAC,MAAM,kCAAkC,mBAAmB,GAAG,CAAC,CAAA;IAC1G,CAAC;IACD,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,8EAA8E,CAAC,CAAA;IACjJ,MAAM,IAAI,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAA;IACnD,MAAM,OAAO,GAAG,IAAI,YAAY,EAAW,CAAA;IAC3C,OAAO,CACL,MAAC,IAAI,eAAS,iBAAiB,qBAAkB,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,aACnH,KAAC,OAAO,IAAC,EAAE,EAAE,SAAS,KAAM,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,YACjD,KAAK,GACE,EACT,IAAI,CAAC,CAAC,CAAC,eAAO,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,YAAG,IAAI,GAAK,CAAC,CAAC,CAAC,IAAI,EACvD,KAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,GAAI,EACvG,eAAO,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,YAC5B,KAAC,WAAW,IAAC,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,SAAS,EAAC,SAAS,EAAC,KAAK,EAAC,iBAAiB,EAAC,OAAO,EAAE,MAAM,EAAE,YAAY,IAAI,SAAS,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,GAAI,GAC9K,IACC,CACR,CAAA;AACH,CAAC","sourcesContent":["/**\n * `QuoteChecklist` (`data-bl=\"quote-checklist\"`, 3.15.0): what to send for a fast, fair quote in this trade (photos of\n * the tree and its access, the suburb, what's underneath it…), as a real list of ticks, then the button to the quote\n * form. Server component built on `TickList`. It is a block, not a sequenced section of the matrix: a site places it\n * inside its service detail, beside the contact form or as its own band (`headingLevel`).\n *\n * Locked: the list comes before the action; the action is the site's quote button (`lead_cta_click` placement\n * `section`, id `quote-checklist`) to its quote form. No default visible copy: the heading, every item and the button\n * label are the site's (spec: checklist content lives in each site's copy file, so same-trade sites never repeat each\n * other). At most 8 items, so the list stays a checklist.\n */\nimport type { ReactNode } from 'react'\nimport { TickList, type TickItem } from '../primitives/lists.js'\nimport { QuoteButton } from '../primitives/quote-button.js'\nimport type { ButtonVariant } from '../primitives/links.js'\nimport { slot, type SkinEntry } from '../skin.js'\n\nexport type QuoteChecklistSlot = 'root' | 'heading' | 'lead' | 'list' | 'action' | 'quote'\nexport interface QuoteChecklistLayout {\n /** The list's columns from the phone breakpoint up. Defaults to 1. */\n readonly columns?: 1 | 2\n /** The quote button's variant. Defaults to `primary`. */\n readonly quoteVariant?: ButtonVariant\n}\nexport type QuoteChecklistSkin = SkinEntry<QuoteChecklistSlot, never, QuoteChecklistLayout>\n\n/** The most items a checklist holds. */\nexport const QUOTE_CHECKLIST_MAX = 8\n\nexport interface QuoteChecklistProps extends QuoteChecklistSkin {\n /** The heading, for example \"What to send for a fast quote\". */\n readonly title: ReactNode\n readonly lead?: ReactNode\n /** What to send, in the site's words: 2 to 8 items. */\n readonly items: readonly TickItem[]\n /** The site's recorded quote label and its quote form, for example `{ label: 'Get a free quote', href: '#quote-hero' }`. */\n readonly quote: { readonly label: string; readonly href: string }\n /** 2 (the default) makes the block its own `<section>` labelled by an H2; 3 sits it inside a section's outline. */\n readonly headingLevel?: 2 | 3\n /** Defaults to `quote-checklist-heading`. */\n readonly headingId?: string\n}\n\nexport function QuoteChecklist({ title, lead, items, quote, headingLevel = 2, headingId = 'quote-checklist-heading', layout, classes }: QuoteChecklistProps) {\n if (items.length === 0) return null\n if (items.length < 2 || items.length > QUOTE_CHECKLIST_MAX) {\n throw new Error(`QuoteChecklist: ${items.length} items; a checklist holds 2 to ${QUOTE_CHECKLIST_MAX}.`)\n }\n if (!quote?.label?.trim() || !quote.href?.trim()) throw new Error('QuoteChecklist: the quote button needs the site\\'s label and its quote form.')\n const Root = headingLevel === 2 ? 'section' : 'div'\n const Heading = `h${headingLevel}` as const\n return (\n <Root data-bl=\"quote-checklist\" aria-labelledby={headingLevel === 2 ? headingId : undefined} className={classes?.root}>\n <Heading id={headingId} {...slot(classes, 'heading')}>\n {title}\n </Heading>\n {lead ? <p {...slot(classes, 'lead')}>{lead}</p> : null}\n <TickList items={items} layout={{ columns: layout?.columns ?? 1 }} classes={{ root: classes?.list }} />\n <p {...slot(classes, 'action')}>\n <QuoteButton label={quote.label} href={quote.href} placement=\"section\" ctaId=\"quote-checklist\" variant={layout?.quoteVariant ?? 'primary'} classes={{ root: classes?.quote }} />\n </p>\n </Root>\n )\n}\n"]}
@@ -0,0 +1,41 @@
1
+ import type { ReactNode } from 'react';
2
+ import { type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order';
3
+ import { type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js';
4
+ import type { RepeatedSlotOf } from '../repeated-slots.js';
5
+ import { type SkinEntry } from '../skin.js';
6
+ import type { UiLink } from '../types.js';
7
+ import { type ServiceCardData, type ServicesGridLayout } from './services-grid.js';
8
+ /** The most services the section lists (K11; the in-content intent-prefetch limit). */
9
+ export declare const RELATED_SERVICES_LIMIT: 8;
10
+ export declare const RELATED_SERVICES_VARIANTS: readonly ["list", "cards"];
11
+ export type RelatedServicesVariant = (typeof RELATED_SERVICES_VARIANTS)[number];
12
+ export type RelatedServicesSlot = SectionSlot | SectionHeadingSlot | 'list' | 'more' | RepeatedSlotOf<'relatedServices'>;
13
+ /** `cards`: rendered once per service, styled from the skin stylesheet, never by class. */
14
+ export type RelatedServicesRepeatedSlot = RepeatedSlotOf<'relatedServices'>;
15
+ export interface RelatedServicesLayout {
16
+ /** Columns on wide screens: `list` 1 to 4 (default 3), `cards` 2 to 4 (default 3). */
17
+ readonly columns?: 1 | 2 | 3 | 4;
18
+ /** `cards`: the photo's crop. Defaults to 4:3. */
19
+ readonly ratio?: ServicesGridLayout['ratio'];
20
+ }
21
+ export type RelatedServicesSkin = SkinEntry<RelatedServicesSlot, RelatedServicesVariant, RelatedServicesLayout, RelatedServicesRepeatedSlot>;
22
+ export interface RelatedServicesProps extends RelatedServicesSkin {
23
+ /** The H2, for example "Other services in Castle Hill". */
24
+ readonly title: ReactNode;
25
+ readonly eyebrow?: ReactNode;
26
+ readonly lead?: ReactNode;
27
+ /** The other services, in money order; `cards` uses their summaries and photos. */
28
+ readonly services: readonly ServiceCardData[];
29
+ /** A link after the list, for example the services hub. Never prefetched. */
30
+ readonly more?: UiLink;
31
+ /** `cards`: the photo `sizes`. */
32
+ readonly imageSizes?: string;
33
+ /** Defaults to `related-services-heading`. */
34
+ readonly headingId?: string;
35
+ /** Set by `SectionSequence`. */
36
+ readonly tone?: SectionTone;
37
+ }
38
+ /** The guard for `SectionSequence`. */
39
+ export declare function relatedServicesReady({ services }: Pick<RelatedServicesProps, 'services'>): BrandlabSectionReadiness;
40
+ export declare function RelatedServices({ title, eyebrow, lead, services, more, imageSizes, headingId, tone, variant, layout, classes, }: RelatedServicesProps): import("react/jsx-runtime").JSX.Element | null;
41
+ //# sourceMappingURL=related-services.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"related-services.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/related-services.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,EAAiB,KAAK,wBAAwB,EAAE,MAAM,2CAA2C,CAAA;AAGxG,OAAO,EAA2B,KAAK,kBAAkB,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC9H,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAkC,KAAK,eAAe,EAAE,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAElH,uFAAuF;AACvF,eAAO,MAAM,sBAAsB,GAAmC,CAAA;AAEtE,eAAO,MAAM,yBAAyB,4BAA6B,CAAA;AACnE,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,yBAAyB,CAAC,CAAC,MAAM,CAAC,CAAA;AAC/E,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,kBAAkB,GAAG,MAAM,GAAG,MAAM,GAAG,cAAc,CAAC,iBAAiB,CAAC,CAAA;AACxH,2FAA2F;AAC3F,MAAM,MAAM,2BAA2B,GAAG,cAAc,CAAC,iBAAiB,CAAC,CAAA;AAC3E,MAAM,WAAW,qBAAqB;IACpC,sFAAsF;IACtF,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAChC,kDAAkD;IAClD,QAAQ,CAAC,KAAK,CAAC,EAAE,kBAAkB,CAAC,OAAO,CAAC,CAAA;CAC7C;AACD,MAAM,MAAM,mBAAmB,GAAG,SAAS,CAAC,mBAAmB,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,2BAA2B,CAAC,CAAA;AAE5I,MAAM,WAAW,oBAAqB,SAAQ,mBAAmB;IAC/D,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAA;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAA;IACzB,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAA;IAC7C,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,kCAAkC;IAClC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,gCAAgC;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAC5B;AAED,uCAAuC;AACvC,wBAAgB,oBAAoB,CAAC,EAAE,QAAQ,EAAE,EAAE,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC,GAAG,wBAAwB,CAEnH;AAiBD,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,OAAO,EACP,IAAI,EACJ,QAAQ,EACR,IAAI,EACJ,UAAU,EACV,SAAsC,EACtC,IAAI,EACJ,OAAgB,EAChB,MAAM,EACN,OAAO,GACR,EAAE,oBAAoB,kDA+CtB"}
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * `RelatedServices` (S28, `related-services`, 3.15.0): the other services in this place ("Other services in Castle
4
+ * Hill") or a service page's siblings. Server component.
5
+ *
6
+ * Each entry links to its service-in-suburb page where one exists, or to its section on the suburb page
7
+ * (`#tree-pruning`); the href always comes from the site's data. At most 8 (K11). Variants:
8
+ * - `list` (the default, lean): a `LinkList` of plain `<li><a>` entries, nothing prefetched
9
+ * (`data-bl-links="other-services"`, the `otherServicesInSuburb` role in `LINK_PREFETCH_POLICY`);
10
+ * - `cards`: `ServiceCard`s with a summary and photo, each card's one link prefetching on intent (`contentCard`).
11
+ */
12
+ import Link from 'next/link';
13
+ import { SECTION_READY } from '@brandlab-digital/site-core/section-order';
14
+ import { LINK_PREFETCH_LIMITS, LINK_PREFETCH_POLICY } from '../prefetch.js';
15
+ import { LinkList } from '../primitives/lists.js';
16
+ import { Section, SectionHeading } from '../primitives/layout.js';
17
+ import { slot } from '../skin.js';
18
+ import { ServiceCard, defaultImageSizes } from './services-grid.js';
19
+ /** The most services the section lists (K11; the in-content intent-prefetch limit). */
20
+ export const RELATED_SERVICES_LIMIT = LINK_PREFETCH_LIMITS.contentCard;
21
+ export const RELATED_SERVICES_VARIANTS = ['list', 'cards'];
22
+ /** The guard for `SectionSequence`. */
23
+ export function relatedServicesReady({ services }) {
24
+ return services.length > 0 ? SECTION_READY : { render: false, reason: 'no-other-services' };
25
+ }
26
+ function assertServices(services) {
27
+ if (services.length > RELATED_SERVICES_LIMIT) {
28
+ throw new Error(`RelatedServices: ${services.length} services; the section lists at most ${RELATED_SERVICES_LIMIT} (K11). Link the services hub for the rest.`);
29
+ }
30
+ const ids = new Set();
31
+ const hrefs = new Set();
32
+ for (const service of services) {
33
+ if (!service.id || ids.has(service.id))
34
+ throw new Error(`RelatedServices: every service needs its own id ("${service.id}" is empty or repeated).`);
35
+ ids.add(service.id);
36
+ if (!service.title?.trim() || !service.href?.trim())
37
+ throw new Error(`RelatedServices: "${service.id}" needs a title and a link.`);
38
+ if (hrefs.has(service.href))
39
+ throw new Error(`RelatedServices: two services link to ${service.href}.`);
40
+ hrefs.add(service.href);
41
+ }
42
+ }
43
+ export function RelatedServices({ title, eyebrow, lead, services, more, imageSizes, headingId = 'related-services-heading', tone, variant = 'list', layout, classes, }) {
44
+ if (services.length === 0)
45
+ return null;
46
+ assertServices(services);
47
+ const columns = layout?.columns ?? 3;
48
+ const ratio = layout?.ratio ?? '4:3';
49
+ const cardColumns = (columns < 2 ? 2 : columns);
50
+ return (_jsxs(Section, { id: "related-services", labelledBy: headingId, tone: tone, variant: variant, classes: classes, hooks: variant === 'cards' ? { 'data-columns': String(cardColumns), 'data-ratio': ratio } : undefined, children: [_jsx(SectionHeading, { id: headingId, title: title, eyebrow: eyebrow, lead: lead, classes: classes }), variant === 'cards' ? (_jsx("ul", { role: "list", ...slot(classes, 'list'), children: services.map((service) => (_jsx(ServiceCard, { service: service, variant: "cards", imageSizes: imageSizes ?? defaultImageSizes('cards', cardColumns), ratio: ratio, prefetch: LINK_PREFETCH_POLICY.contentCard }, service.id))) })) : (_jsx(LinkList, { links: services.map((service) => ({ label: service.title, href: service.href })), kind: "other-services", prefetch: LINK_PREFETCH_POLICY.otherServicesInSuburb, layout: { columns }, classes: { root: classes?.list } })), more ? (_jsx("p", { ...slot(classes, 'more'), children: _jsx(Link, { href: more.href, prefetch: false, children: more.label }) })) : null] }));
51
+ }
52
+ //# sourceMappingURL=related-services.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"related-services.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/related-services.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * `RelatedServices` (S28, `related-services`, 3.15.0): the other services in this place (\"Other services in Castle\n * Hill\") or a service page's siblings. Server component.\n *\n * Each entry links to its service-in-suburb page where one exists, or to its section on the suburb page\n * (`#tree-pruning`); the href always comes from the site's data. At most 8 (K11). Variants:\n * - `list` (the default, lean): a `LinkList` of plain `<li><a>` entries, nothing prefetched\n * (`data-bl-links=\"other-services\"`, the `otherServicesInSuburb` role in `LINK_PREFETCH_POLICY`);\n * - `cards`: `ServiceCard`s with a summary and photo, each card's one link prefetching on intent (`contentCard`).\n */\nimport Link from 'next/link'\nimport type { ReactNode } from 'react'\nimport { SECTION_READY, type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order'\nimport { LINK_PREFETCH_LIMITS, LINK_PREFETCH_POLICY } from '../prefetch.js'\nimport { LinkList } from '../primitives/lists.js'\nimport { Section, SectionHeading, type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js'\nimport type { RepeatedSlotOf } from '../repeated-slots.js'\nimport { slot, type SkinEntry } from '../skin.js'\nimport type { UiLink } from '../types.js'\nimport { ServiceCard, defaultImageSizes, type ServiceCardData, type ServicesGridLayout } from './services-grid.js'\n\n/** The most services the section lists (K11; the in-content intent-prefetch limit). */\nexport const RELATED_SERVICES_LIMIT = LINK_PREFETCH_LIMITS.contentCard\n\nexport const RELATED_SERVICES_VARIANTS = ['list', 'cards'] as const\nexport type RelatedServicesVariant = (typeof RELATED_SERVICES_VARIANTS)[number]\nexport type RelatedServicesSlot = SectionSlot | SectionHeadingSlot | 'list' | 'more' | RepeatedSlotOf<'relatedServices'>\n/** `cards`: rendered once per service, styled from the skin stylesheet, never by class. */\nexport type RelatedServicesRepeatedSlot = RepeatedSlotOf<'relatedServices'>\nexport interface RelatedServicesLayout {\n /** Columns on wide screens: `list` 1 to 4 (default 3), `cards` 2 to 4 (default 3). */\n readonly columns?: 1 | 2 | 3 | 4\n /** `cards`: the photo's crop. Defaults to 4:3. */\n readonly ratio?: ServicesGridLayout['ratio']\n}\nexport type RelatedServicesSkin = SkinEntry<RelatedServicesSlot, RelatedServicesVariant, RelatedServicesLayout, RelatedServicesRepeatedSlot>\n\nexport interface RelatedServicesProps extends RelatedServicesSkin {\n /** The H2, for example \"Other services in Castle Hill\". */\n readonly title: ReactNode\n readonly eyebrow?: ReactNode\n readonly lead?: ReactNode\n /** The other services, in money order; `cards` uses their summaries and photos. */\n readonly services: readonly ServiceCardData[]\n /** A link after the list, for example the services hub. Never prefetched. */\n readonly more?: UiLink\n /** `cards`: the photo `sizes`. */\n readonly imageSizes?: string\n /** Defaults to `related-services-heading`. */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n}\n\n/** The guard for `SectionSequence`. */\nexport function relatedServicesReady({ services }: Pick<RelatedServicesProps, 'services'>): BrandlabSectionReadiness {\n return services.length > 0 ? SECTION_READY : { render: false, reason: 'no-other-services' }\n}\n\nfunction assertServices(services: readonly ServiceCardData[]) {\n if (services.length > RELATED_SERVICES_LIMIT) {\n throw new Error(`RelatedServices: ${services.length} services; the section lists at most ${RELATED_SERVICES_LIMIT} (K11). Link the services hub for the rest.`)\n }\n const ids = new Set<string>()\n const hrefs = new Set<string>()\n for (const service of services) {\n if (!service.id || ids.has(service.id)) throw new Error(`RelatedServices: every service needs its own id (\"${service.id}\" is empty or repeated).`)\n ids.add(service.id)\n if (!service.title?.trim() || !service.href?.trim()) throw new Error(`RelatedServices: \"${service.id}\" needs a title and a link.`)\n if (hrefs.has(service.href)) throw new Error(`RelatedServices: two services link to ${service.href}.`)\n hrefs.add(service.href)\n }\n}\n\nexport function RelatedServices({\n title,\n eyebrow,\n lead,\n services,\n more,\n imageSizes,\n headingId = 'related-services-heading',\n tone,\n variant = 'list',\n layout,\n classes,\n}: RelatedServicesProps) {\n if (services.length === 0) return null\n assertServices(services)\n const columns = layout?.columns ?? 3\n const ratio = layout?.ratio ?? '4:3'\n const cardColumns = (columns < 2 ? 2 : columns) as 2 | 3 | 4\n return (\n <Section\n id=\"related-services\"\n labelledBy={headingId}\n tone={tone}\n variant={variant}\n classes={classes}\n hooks={variant === 'cards' ? { 'data-columns': String(cardColumns), 'data-ratio': ratio } : undefined}\n >\n <SectionHeading id={headingId} title={title} eyebrow={eyebrow} lead={lead} classes={classes} />\n {variant === 'cards' ? (\n <ul role=\"list\" {...slot(classes, 'list')}>\n {services.map((service) => (\n <ServiceCard\n key={service.id}\n service={service}\n variant=\"cards\"\n imageSizes={imageSizes ?? defaultImageSizes('cards', cardColumns)}\n ratio={ratio}\n prefetch={LINK_PREFETCH_POLICY.contentCard}\n />\n ))}\n </ul>\n ) : (\n <LinkList\n links={services.map((service) => ({ label: service.title, href: service.href }))}\n kind=\"other-services\"\n prefetch={LINK_PREFETCH_POLICY.otherServicesInSuburb}\n layout={{ columns }}\n classes={{ root: classes?.list }}\n />\n )}\n {more ? (\n <p {...slot(classes, 'more')}>\n <Link href={more.href} prefetch={false}>\n {more.label}\n </Link>\n </p>\n ) : null}\n </Section>\n )\n}\n"]}
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { type ReactNode } from 'react';
17
17
  import { type BusinessProfileReview, type PreparedBusinessProfileReviews } from '@brandlab-digital/site-core/google-business-reviews';
18
+ import { type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order';
18
19
  import { type SectionTone } from '../primitives/layout.js';
19
20
  import type { RepeatedSlotOf } from '../repeated-slots.js';
20
21
  import { type SkinEntry, type SlotClasses } from '../skin.js';
@@ -34,6 +35,16 @@ export type ReviewsBandRepeatedSlot = RepeatedSlotOf<'reviewsBand'>;
34
35
  export type ReviewsBandSkin = SkinEntry<ReviewsBandSlot, ReviewsBandVariant, ReviewsBandLayout, ReviewsBandRepeatedSlot>;
35
36
  type Classes = SlotClasses<Exclude<ReviewsBandSlot, ReviewsBandRepeatedSlot>> | undefined;
36
37
  export declare const REVIEWS_BAND_HEADING_ID = "bl-reviews-heading";
38
+ /** The skip reason a band with nothing to show records in the section manifest: the feed has no reviews or rating. */
39
+ export declare const REVIEWS_BAND_SKIP_REASON = "no-feed-reviews";
40
+ /**
41
+ * The guard for `SectionSequence` (3.15.0), from the same prepared reviews the band renders: ready when the band
42
+ * renders (written reviews, or Google's rating alone as the summary fallback); otherwise `no-feed-reviews`, for a
43
+ * missing feed, an empty one, or the `summary` variant without a rating.
44
+ */
45
+ export declare function reviewsBandReady(reviews: PreparedBusinessProfileReviews | null | undefined, { variant }?: {
46
+ readonly variant?: ReviewsBandVariant;
47
+ }): BrandlabSectionReadiness;
37
48
  /**
38
49
  * A long review split at a sentence end (or else a space) near `limit`, so the first part reads on its own. The two
39
50
  * parts together are the review word for word. Short reviews, and reviews only a little over the limit, don't fold.
@@ -1 +1 @@
1
- {"version":3,"file":"reviews-band.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/reviews-band.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"reviews-band.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/reviews-band.tsx"],"names":[],"mappings":"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"}
@@ -16,6 +16,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
16
16
  */
17
17
  import { Children } from 'react';
18
18
  import { businessProfileReviewMentions, } from '@brandlab-digital/site-core/google-business-reviews';
19
+ import { SECTION_READY } from '@brandlab-digital/site-core/section-order';
19
20
  import { RatingStars, Section, SectionHeading } from '../primitives/layout.js';
20
21
  import { repeatedSlot, slot } from '../skin.js';
21
22
  import { hasGoogleRating, RatingChip } from './rating-chip.js';
@@ -34,6 +35,16 @@ function shownVariant(reviews, variant) {
34
35
  }
35
36
  const MONTH = new Intl.DateTimeFormat('en-AU', { month: 'long', year: 'numeric', timeZone: 'Australia/Sydney' });
36
37
  const DAY = new Intl.DateTimeFormat('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit', timeZone: 'Australia/Sydney' });
38
+ /** The skip reason a band with nothing to show records in the section manifest: the feed has no reviews or rating. */
39
+ export const REVIEWS_BAND_SKIP_REASON = 'no-feed-reviews';
40
+ /**
41
+ * The guard for `SectionSequence` (3.15.0), from the same prepared reviews the band renders: ready when the band
42
+ * renders (written reviews, or Google's rating alone as the summary fallback); otherwise `no-feed-reviews`, for a
43
+ * missing feed, an empty one, or the `summary` variant without a rating.
44
+ */
45
+ export function reviewsBandReady(reviews, { variant = 'band' } = {}) {
46
+ return shownVariant(reviews, variant) ? SECTION_READY : { render: false, reason: REVIEWS_BAND_SKIP_REASON };
47
+ }
37
48
  /**
38
49
  * A long review split at a sentence end (or else a space) near `limit`, so the first part reads on its own. The two
39
50
  * parts together are the review word for word. Short reviews, and reviews only a little over the limit, don't fold.
@@ -1 +1 @@
1
- {"version":3,"file":"reviews-band.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/reviews-band.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * S7 `ReviewsBand` (`reviews`): prominent, honest proof from the Business Profile feed. Server component, no client\n * JS. Google's own average and total sit beside the selected reviews with a link to the profile; the text is verbatim\n * and always in the HTML (a long review folds behind a native \"Read more\" disclosure); the band says how the reviews\n * were chosen (\"Selected five-star Google reviews\" or \"Recent Google reviews\"). No review or rating schema: a business\n * that controls its own reviews isn't eligible for review stars.\n *\n * Data comes only from `prepareBusinessProfileReviews` (site-core), which applies the policy, mention ordering, the\n * 3.7.0 exclude guard and the limit. No prepared reviews, or none and no rating, renders nothing at all (no heading).\n * With no written reviews but a rating, every variant renders as `summary`: the heading and the rating chip only.\n * Never place it on the contact page or inside another section.\n *\n * Sites that arrange their own layout use the parts (`ReviewsBand.Root`, `.Heading`, `.Summary`, `.List`, `.Item`):\n * the root keeps the section semantics, the empty rule and the disclosure line; the parts keep the data and markup.\n */\nimport { Children, type ReactNode } from 'react'\nimport {\n businessProfileReviewMentions,\n type BusinessProfileReview,\n type PreparedBusinessProfileReviews,\n} from '@brandlab-digital/site-core/google-business-reviews'\nimport { RatingStars, Section, SectionHeading, type SectionTone } from '../primitives/layout.js'\nimport type { RepeatedSlotOf } from '../repeated-slots.js'\nimport { repeatedSlot, slot, type SkinEntry, type SlotClasses } from '../skin.js'\nimport { hasGoogleRating, RatingChip } from './rating-chip.js'\n\nexport const REVIEWS_BAND_VARIANTS = ['band', 'feature', 'cards', 'wall', 'summary'] as const\nexport type ReviewsBandVariant = (typeof REVIEWS_BAND_VARIANTS)[number]\n\nexport type ReviewsBandSlot =\n | 'root'\n | 'inner'\n | 'top'\n | 'head'\n | 'eyebrow'\n | 'heading'\n | 'lead'\n | 'summary'\n | 'rating'\n | 'source'\n | 'list'\n | 'item'\n | 'stars'\n | 'quote'\n | 'more'\n | 'author'\n | 'avatar'\n | 'name'\n | 'date'\n | 'disclosure'\n | 'profileLink'\n\nexport interface ReviewsBandLayout {\n readonly columns?: 1 | 2 | 3\n readonly align?: 'start' | 'center'\n /** Reviewer photos beside the names (Google's own small photos, decorative). */\n readonly avatars?: boolean\n /** Characters shown before a long review folds behind \"Read more\" (default 320); `false` never folds. */\n readonly excerpt?: number | false\n}\n\n/** Rendered once per review: styled from the skin stylesheet, never by class (3.14.0). */\nexport type ReviewsBandRepeatedSlot = RepeatedSlotOf<'reviewsBand'>\nexport type ReviewsBandSkin = SkinEntry<ReviewsBandSlot, ReviewsBandVariant, ReviewsBandLayout, ReviewsBandRepeatedSlot>\n\ntype Classes = SlotClasses<Exclude<ReviewsBandSlot, ReviewsBandRepeatedSlot>> | undefined\n\nexport const REVIEWS_BAND_HEADING_ID = 'bl-reviews-heading'\nconst DEFAULT_EXCERPT = 320\n\n/**\n * The variant a band renders, or null when it has nothing true to show. With no written reviews every variant is\n * `summary`, and `summary` needs Google's rating: a heading over nothing never renders.\n */\nfunction shownVariant(reviews: PreparedBusinessProfileReviews | null | undefined, variant: ReviewsBandVariant): ReviewsBandVariant | null {\n if (!reviews) return null\n const shown = reviews.reviews.length === 0 ? 'summary' : variant\n return shown === 'summary' && !hasGoogleRating(reviews) ? null : shown\n}\n\nconst MONTH = new Intl.DateTimeFormat('en-AU', { month: 'long', year: 'numeric', timeZone: 'Australia/Sydney' })\nconst DAY = new Intl.DateTimeFormat('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit', timeZone: 'Australia/Sydney' })\n\n/**\n * A long review split at a sentence end (or else a space) near `limit`, so the first part reads on its own. The two\n * parts together are the review word for word. Short reviews, and reviews only a little over the limit, don't fold.\n */\nexport function splitReviewText(text: string, limit: number | false): readonly [string, string] | null {\n if (limit === false || text.length <= limit * 1.25) return null\n const window = text.slice(0, limit)\n const sentence = Math.max(...['. ', '! ', '? ', '\\n'].map((mark) => window.lastIndexOf(mark)))\n const space = window.lastIndexOf(' ')\n const cut = sentence >= limit * 0.4 ? sentence + 1 : space > 0 ? space : limit\n return [text.slice(0, cut).trimEnd(), text.slice(cut).trimStart()]\n}\n\n/** The `feature` variant leads with the longest review naming a mention word, or else the longest review. */\nfunction featureOrder(reviews: PreparedBusinessProfileReviews): BusinessProfileReview[] {\n const named = reviews.mention.length > 0 ? reviews.reviews.filter((review) => businessProfileReviewMentions(review, reviews.mention)) : []\n const pool = named.length > 0 ? named : reviews.reviews\n const lead = pool.reduce<BusinessProfileReview | undefined>((longest, review) => (!longest || review.text.length > longest.text.length ? review : longest), undefined)\n return lead ? [lead, ...reviews.reviews.filter((review) => review !== lead)] : []\n}\n\nexport interface ReviewsBandRootProps extends ReviewsBandSkin {\n readonly reviews: PreparedBusinessProfileReviews | null | undefined\n /** The id of the band's H2 (default `bl-reviews-heading`). */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n /** Signature art, rendered `aria-hidden`. */\n readonly decoration?: ReactNode\n readonly children: ReactNode\n}\n\n/**\n * The section: `data-bl-section=\"reviews\"` with `data-bl-source=\"feed\"` (the ready gate's mark for feed numbers),\n * the empty rule, and the locked disclosure line after the children.\n */\nexport function ReviewsBandRoot({\n reviews,\n headingId = REVIEWS_BAND_HEADING_ID,\n tone,\n decoration,\n variant = 'band',\n layout,\n classes,\n children,\n}: ReviewsBandRootProps) {\n const shown = shownVariant(reviews, variant)\n if (!shown || !reviews) return null\n return (\n <Section\n id=\"reviews\"\n labelledBy={headingId}\n tone={tone}\n variant={shown}\n classes={{ root: classes?.root, inner: classes?.inner }}\n decoration={decoration}\n hooks={{ 'data-bl-source': 'feed', 'data-columns': String(layout?.columns ?? 3), 'data-align': layout?.align ?? 'start' }}\n >\n {children}\n {shown === 'summary' ? null : (\n <p {...slot(classes, 'disclosure')}>\n {reviews.policy === 'five-star' ? 'Selected five-star Google reviews.' : 'Recent Google reviews.'}{' '}\n <a {...slot(classes, 'profileLink')} href={reviews.profileUrl} target=\"_blank\" rel=\"noopener noreferrer\">\n Read all reviews on Google\n </a>\n </p>\n )}\n </Section>\n )\n}\n\nexport interface ReviewsBandHeadingProps {\n /** The H2's id; the same `headingId` the root has (default `bl-reviews-heading`). */\n readonly id?: string\n /** The H2 text the site owns. */\n readonly children: string\n readonly eyebrow?: string\n readonly intro?: string\n readonly layout?: Pick<ReviewsBandLayout, 'align'>\n readonly classes?: Classes\n}\n\n/** The heading block: an optional eyebrow, the H2 and an optional intro. */\nexport function ReviewsBandHeading({ id = REVIEWS_BAND_HEADING_ID, children, eyebrow, intro, layout, classes }: ReviewsBandHeadingProps) {\n return (\n <SectionHeading\n id={id}\n title={children}\n eyebrow={eyebrow}\n lead={intro}\n layout={{ align: layout?.align ?? 'start' }}\n classes={{ head: classes?.head, eyebrow: classes?.eyebrow, heading: classes?.heading, lead: classes?.lead }}\n />\n )\n}\n\nexport interface ReviewsBandSummaryProps {\n readonly reviews: PreparedBusinessProfileReviews | null | undefined\n readonly classes?: Classes\n}\n\n/** Google's own average and total with the profile link; nothing when the feed has no rating. */\nexport function ReviewsBandSummary({ reviews, classes }: ReviewsBandSummaryProps) {\n if (!hasGoogleRating(reviews)) return null\n return (\n <div {...slot(classes, 'summary')}>\n <RatingChip reviews={reviews} classes={{ rating: classes?.rating, source: classes?.source }} />\n </div>\n )\n}\n\n/** The review list; nothing without items. */\nexport function ReviewsBandList({ classes, children }: { readonly classes?: Classes; readonly children: ReactNode }) {\n if (Children.toArray(children).length === 0) return null\n return <ul {...slot(classes, 'list')}>{children}</ul>\n}\n\nexport interface ReviewsBandItemProps {\n readonly review: BusinessProfileReview\n /** The `feature` variant's wide lead card. */\n readonly lead?: boolean\n readonly layout?: Pick<ReviewsBandLayout, 'avatars' | 'excerpt'>\n readonly classes?: Classes\n}\n\n/** One review, word for word: its stars, the quote and the reviewer with the month. */\nexport function ReviewsBandItem({ review, lead = false, layout, classes }: ReviewsBandItemProps) {\n const parts = splitReviewText(review.text, layout?.excerpt ?? DEFAULT_EXCERPT)\n const date = new Date(review.createTime)\n return (\n <li {...repeatedSlot('item')} data-lead={lead ? 'true' : undefined}>\n <figure>\n <span {...repeatedSlot('stars')}>\n <RatingStars rating={review.rating} label={`Rated ${review.rating} out of 5`} />\n </span>\n <blockquote {...repeatedSlot('quote')}>\n {parts ? (\n <>\n <p>{parts[0]}</p>\n <details {...repeatedSlot('more')}>\n <summary>Read more</summary>\n <p>{parts[1]}</p>\n </details>\n </>\n ) : (\n <p>{review.text}</p>\n )}\n </blockquote>\n <figcaption {...repeatedSlot('author')}>\n {layout?.avatars && review.authorPhotoUrl ? (\n <img\n {...repeatedSlot('avatar')}\n src={review.authorPhotoUrl}\n alt=\"\"\n width={48}\n height={48}\n loading=\"lazy\"\n decoding=\"async\"\n referrerPolicy=\"no-referrer\"\n />\n ) : null}\n <span {...repeatedSlot('name')}>{review.authorName}</span>{' '}\n <time {...repeatedSlot('date')} dateTime={DAY.format(date)}>\n {MONTH.format(date)}\n </time>\n </figcaption>\n </figure>\n </li>\n )\n}\n\nexport interface ReviewsBandProps extends ReviewsBandSkin {\n /** From `prepareBusinessProfileReviews`; null renders nothing at all. */\n readonly reviews: PreparedBusinessProfileReviews | null | undefined\n /** The H2 text the site owns. */\n readonly heading: string\n readonly eyebrow?: string\n readonly intro?: string\n /** The id of the band's H2 (default `bl-reviews-heading`). */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n /** Signature art, rendered `aria-hidden`. */\n readonly decoration?: ReactNode\n}\n\nfunction ReviewsBandSection({ reviews, heading, eyebrow, intro, headingId = REVIEWS_BAND_HEADING_ID, tone, decoration, variant = 'band', layout, classes }: ReviewsBandProps) {\n const shown = shownVariant(reviews, variant)\n if (!shown || !reviews) return null\n const items = shown === 'summary' ? [] : shown === 'feature' ? featureOrder(reviews) : reviews.reviews\n return (\n <ReviewsBandRoot reviews={reviews} headingId={headingId} tone={tone} decoration={decoration} variant={shown} layout={layout} classes={classes}>\n <div {...slot(classes, 'top')}>\n <ReviewsBandHeading id={headingId} eyebrow={eyebrow} intro={intro} layout={layout} classes={classes}>\n {heading}\n </ReviewsBandHeading>\n <ReviewsBandSummary reviews={reviews} classes={classes} />\n </div>\n <ReviewsBandList classes={classes}>\n {items.map((review, index) => (\n <ReviewsBandItem key={review.id} review={review} lead={shown === 'feature' && index === 0} layout={layout} classes={classes} />\n ))}\n </ReviewsBandList>\n </ReviewsBandRoot>\n )\n}\n\n/** The reviews band, with its parts for sites that arrange their own layout. */\nexport const ReviewsBand = Object.assign(ReviewsBandSection, {\n Root: ReviewsBandRoot,\n Heading: ReviewsBandHeading,\n Summary: ReviewsBandSummary,\n List: ReviewsBandList,\n Item: ReviewsBandItem,\n})\n"]}
1
+ {"version":3,"file":"reviews-band.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/reviews-band.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * S7 `ReviewsBand` (`reviews`): prominent, honest proof from the Business Profile feed. Server component, no client\n * JS. Google's own average and total sit beside the selected reviews with a link to the profile; the text is verbatim\n * and always in the HTML (a long review folds behind a native \"Read more\" disclosure); the band says how the reviews\n * were chosen (\"Selected five-star Google reviews\" or \"Recent Google reviews\"). No review or rating schema: a business\n * that controls its own reviews isn't eligible for review stars.\n *\n * Data comes only from `prepareBusinessProfileReviews` (site-core), which applies the policy, mention ordering, the\n * 3.7.0 exclude guard and the limit. No prepared reviews, or none and no rating, renders nothing at all (no heading).\n * With no written reviews but a rating, every variant renders as `summary`: the heading and the rating chip only.\n * Never place it on the contact page or inside another section.\n *\n * Sites that arrange their own layout use the parts (`ReviewsBand.Root`, `.Heading`, `.Summary`, `.List`, `.Item`):\n * the root keeps the section semantics, the empty rule and the disclosure line; the parts keep the data and markup.\n */\nimport { Children, type ReactNode } from 'react'\nimport {\n businessProfileReviewMentions,\n type BusinessProfileReview,\n type PreparedBusinessProfileReviews,\n} from '@brandlab-digital/site-core/google-business-reviews'\nimport { SECTION_READY, type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order'\nimport { RatingStars, Section, SectionHeading, type SectionTone } from '../primitives/layout.js'\nimport type { RepeatedSlotOf } from '../repeated-slots.js'\nimport { repeatedSlot, slot, type SkinEntry, type SlotClasses } from '../skin.js'\nimport { hasGoogleRating, RatingChip } from './rating-chip.js'\n\nexport const REVIEWS_BAND_VARIANTS = ['band', 'feature', 'cards', 'wall', 'summary'] as const\nexport type ReviewsBandVariant = (typeof REVIEWS_BAND_VARIANTS)[number]\n\nexport type ReviewsBandSlot =\n | 'root'\n | 'inner'\n | 'top'\n | 'head'\n | 'eyebrow'\n | 'heading'\n | 'lead'\n | 'summary'\n | 'rating'\n | 'source'\n | 'list'\n | 'item'\n | 'stars'\n | 'quote'\n | 'more'\n | 'author'\n | 'avatar'\n | 'name'\n | 'date'\n | 'disclosure'\n | 'profileLink'\n\nexport interface ReviewsBandLayout {\n readonly columns?: 1 | 2 | 3\n readonly align?: 'start' | 'center'\n /** Reviewer photos beside the names (Google's own small photos, decorative). */\n readonly avatars?: boolean\n /** Characters shown before a long review folds behind \"Read more\" (default 320); `false` never folds. */\n readonly excerpt?: number | false\n}\n\n/** Rendered once per review: styled from the skin stylesheet, never by class (3.14.0). */\nexport type ReviewsBandRepeatedSlot = RepeatedSlotOf<'reviewsBand'>\nexport type ReviewsBandSkin = SkinEntry<ReviewsBandSlot, ReviewsBandVariant, ReviewsBandLayout, ReviewsBandRepeatedSlot>\n\ntype Classes = SlotClasses<Exclude<ReviewsBandSlot, ReviewsBandRepeatedSlot>> | undefined\n\nexport const REVIEWS_BAND_HEADING_ID = 'bl-reviews-heading'\nconst DEFAULT_EXCERPT = 320\n\n/**\n * The variant a band renders, or null when it has nothing true to show. With no written reviews every variant is\n * `summary`, and `summary` needs Google's rating: a heading over nothing never renders.\n */\nfunction shownVariant(reviews: PreparedBusinessProfileReviews | null | undefined, variant: ReviewsBandVariant): ReviewsBandVariant | null {\n if (!reviews) return null\n const shown = reviews.reviews.length === 0 ? 'summary' : variant\n return shown === 'summary' && !hasGoogleRating(reviews) ? null : shown\n}\n\nconst MONTH = new Intl.DateTimeFormat('en-AU', { month: 'long', year: 'numeric', timeZone: 'Australia/Sydney' })\nconst DAY = new Intl.DateTimeFormat('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit', timeZone: 'Australia/Sydney' })\n\n/** The skip reason a band with nothing to show records in the section manifest: the feed has no reviews or rating. */\nexport const REVIEWS_BAND_SKIP_REASON = 'no-feed-reviews'\n\n/**\n * The guard for `SectionSequence` (3.15.0), from the same prepared reviews the band renders: ready when the band\n * renders (written reviews, or Google's rating alone as the summary fallback); otherwise `no-feed-reviews`, for a\n * missing feed, an empty one, or the `summary` variant without a rating.\n */\nexport function reviewsBandReady(\n reviews: PreparedBusinessProfileReviews | null | undefined,\n { variant = 'band' }: { readonly variant?: ReviewsBandVariant } = {},\n): BrandlabSectionReadiness {\n return shownVariant(reviews, variant) ? SECTION_READY : { render: false, reason: REVIEWS_BAND_SKIP_REASON }\n}\n\n/**\n * A long review split at a sentence end (or else a space) near `limit`, so the first part reads on its own. The two\n * parts together are the review word for word. Short reviews, and reviews only a little over the limit, don't fold.\n */\nexport function splitReviewText(text: string, limit: number | false): readonly [string, string] | null {\n if (limit === false || text.length <= limit * 1.25) return null\n const window = text.slice(0, limit)\n const sentence = Math.max(...['. ', '! ', '? ', '\\n'].map((mark) => window.lastIndexOf(mark)))\n const space = window.lastIndexOf(' ')\n const cut = sentence >= limit * 0.4 ? sentence + 1 : space > 0 ? space : limit\n return [text.slice(0, cut).trimEnd(), text.slice(cut).trimStart()]\n}\n\n/** The `feature` variant leads with the longest review naming a mention word, or else the longest review. */\nfunction featureOrder(reviews: PreparedBusinessProfileReviews): BusinessProfileReview[] {\n const named = reviews.mention.length > 0 ? reviews.reviews.filter((review) => businessProfileReviewMentions(review, reviews.mention)) : []\n const pool = named.length > 0 ? named : reviews.reviews\n const lead = pool.reduce<BusinessProfileReview | undefined>((longest, review) => (!longest || review.text.length > longest.text.length ? review : longest), undefined)\n return lead ? [lead, ...reviews.reviews.filter((review) => review !== lead)] : []\n}\n\nexport interface ReviewsBandRootProps extends ReviewsBandSkin {\n readonly reviews: PreparedBusinessProfileReviews | null | undefined\n /** The id of the band's H2 (default `bl-reviews-heading`). */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n /** Signature art, rendered `aria-hidden`. */\n readonly decoration?: ReactNode\n readonly children: ReactNode\n}\n\n/**\n * The section: `data-bl-section=\"reviews\"` with `data-bl-source=\"feed\"` (the ready gate's mark for feed numbers),\n * the empty rule, and the locked disclosure line after the children.\n */\nexport function ReviewsBandRoot({\n reviews,\n headingId = REVIEWS_BAND_HEADING_ID,\n tone,\n decoration,\n variant = 'band',\n layout,\n classes,\n children,\n}: ReviewsBandRootProps) {\n const shown = shownVariant(reviews, variant)\n if (!shown || !reviews) return null\n return (\n <Section\n id=\"reviews\"\n labelledBy={headingId}\n tone={tone}\n variant={shown}\n classes={{ root: classes?.root, inner: classes?.inner }}\n decoration={decoration}\n hooks={{ 'data-bl-source': 'feed', 'data-columns': String(layout?.columns ?? 3), 'data-align': layout?.align ?? 'start' }}\n >\n {children}\n {shown === 'summary' ? null : (\n <p {...slot(classes, 'disclosure')}>\n {reviews.policy === 'five-star' ? 'Selected five-star Google reviews.' : 'Recent Google reviews.'}{' '}\n <a {...slot(classes, 'profileLink')} href={reviews.profileUrl} target=\"_blank\" rel=\"noopener noreferrer\">\n Read all reviews on Google\n </a>\n </p>\n )}\n </Section>\n )\n}\n\nexport interface ReviewsBandHeadingProps {\n /** The H2's id; the same `headingId` the root has (default `bl-reviews-heading`). */\n readonly id?: string\n /** The H2 text the site owns. */\n readonly children: string\n readonly eyebrow?: string\n readonly intro?: string\n readonly layout?: Pick<ReviewsBandLayout, 'align'>\n readonly classes?: Classes\n}\n\n/** The heading block: an optional eyebrow, the H2 and an optional intro. */\nexport function ReviewsBandHeading({ id = REVIEWS_BAND_HEADING_ID, children, eyebrow, intro, layout, classes }: ReviewsBandHeadingProps) {\n return (\n <SectionHeading\n id={id}\n title={children}\n eyebrow={eyebrow}\n lead={intro}\n layout={{ align: layout?.align ?? 'start' }}\n classes={{ head: classes?.head, eyebrow: classes?.eyebrow, heading: classes?.heading, lead: classes?.lead }}\n />\n )\n}\n\nexport interface ReviewsBandSummaryProps {\n readonly reviews: PreparedBusinessProfileReviews | null | undefined\n readonly classes?: Classes\n}\n\n/** Google's own average and total with the profile link; nothing when the feed has no rating. */\nexport function ReviewsBandSummary({ reviews, classes }: ReviewsBandSummaryProps) {\n if (!hasGoogleRating(reviews)) return null\n return (\n <div {...slot(classes, 'summary')}>\n <RatingChip reviews={reviews} classes={{ rating: classes?.rating, source: classes?.source }} />\n </div>\n )\n}\n\n/** The review list; nothing without items. */\nexport function ReviewsBandList({ classes, children }: { readonly classes?: Classes; readonly children: ReactNode }) {\n if (Children.toArray(children).length === 0) return null\n return <ul {...slot(classes, 'list')}>{children}</ul>\n}\n\nexport interface ReviewsBandItemProps {\n readonly review: BusinessProfileReview\n /** The `feature` variant's wide lead card. */\n readonly lead?: boolean\n readonly layout?: Pick<ReviewsBandLayout, 'avatars' | 'excerpt'>\n readonly classes?: Classes\n}\n\n/** One review, word for word: its stars, the quote and the reviewer with the month. */\nexport function ReviewsBandItem({ review, lead = false, layout, classes }: ReviewsBandItemProps) {\n const parts = splitReviewText(review.text, layout?.excerpt ?? DEFAULT_EXCERPT)\n const date = new Date(review.createTime)\n return (\n <li {...repeatedSlot('item')} data-lead={lead ? 'true' : undefined}>\n <figure>\n <span {...repeatedSlot('stars')}>\n <RatingStars rating={review.rating} label={`Rated ${review.rating} out of 5`} />\n </span>\n <blockquote {...repeatedSlot('quote')}>\n {parts ? (\n <>\n <p>{parts[0]}</p>\n <details {...repeatedSlot('more')}>\n <summary>Read more</summary>\n <p>{parts[1]}</p>\n </details>\n </>\n ) : (\n <p>{review.text}</p>\n )}\n </blockquote>\n <figcaption {...repeatedSlot('author')}>\n {layout?.avatars && review.authorPhotoUrl ? (\n <img\n {...repeatedSlot('avatar')}\n src={review.authorPhotoUrl}\n alt=\"\"\n width={48}\n height={48}\n loading=\"lazy\"\n decoding=\"async\"\n referrerPolicy=\"no-referrer\"\n />\n ) : null}\n <span {...repeatedSlot('name')}>{review.authorName}</span>{' '}\n <time {...repeatedSlot('date')} dateTime={DAY.format(date)}>\n {MONTH.format(date)}\n </time>\n </figcaption>\n </figure>\n </li>\n )\n}\n\nexport interface ReviewsBandProps extends ReviewsBandSkin {\n /** From `prepareBusinessProfileReviews`; null renders nothing at all. */\n readonly reviews: PreparedBusinessProfileReviews | null | undefined\n /** The H2 text the site owns. */\n readonly heading: string\n readonly eyebrow?: string\n readonly intro?: string\n /** The id of the band's H2 (default `bl-reviews-heading`). */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n /** Signature art, rendered `aria-hidden`. */\n readonly decoration?: ReactNode\n}\n\nfunction ReviewsBandSection({ reviews, heading, eyebrow, intro, headingId = REVIEWS_BAND_HEADING_ID, tone, decoration, variant = 'band', layout, classes }: ReviewsBandProps) {\n const shown = shownVariant(reviews, variant)\n if (!shown || !reviews) return null\n const items = shown === 'summary' ? [] : shown === 'feature' ? featureOrder(reviews) : reviews.reviews\n return (\n <ReviewsBandRoot reviews={reviews} headingId={headingId} tone={tone} decoration={decoration} variant={shown} layout={layout} classes={classes}>\n <div {...slot(classes, 'top')}>\n <ReviewsBandHeading id={headingId} eyebrow={eyebrow} intro={intro} layout={layout} classes={classes}>\n {heading}\n </ReviewsBandHeading>\n <ReviewsBandSummary reviews={reviews} classes={classes} />\n </div>\n <ReviewsBandList classes={classes}>\n {items.map((review, index) => (\n <ReviewsBandItem key={review.id} review={review} lead={shown === 'feature' && index === 0} layout={layout} classes={classes} />\n ))}\n </ReviewsBandList>\n </ReviewsBandRoot>\n )\n}\n\n/** The reviews band, with its parts for sites that arrange their own layout. */\nexport const ReviewsBand = Object.assign(ReviewsBandSection, {\n Root: ReviewsBandRoot,\n Heading: ReviewsBandHeading,\n Summary: ReviewsBandSummary,\n List: ReviewsBandList,\n Item: ReviewsBandItem,\n})\n"]}
@@ -27,7 +27,7 @@ function ServiceLink({ href, prefetch, children }) {
27
27
  if (href.startsWith('#')) {
28
28
  return (_jsx("a", { href: href, ...repeatedSlot('link'), children: children }));
29
29
  }
30
- return (_jsx(Link, { href: href, ...linkPrefetchProps(prefetch), ...repeatedSlot('link'), children: children }));
30
+ return (_jsx(Link, { href: href, ...linkPrefetchProps(prefetch, href), ...repeatedSlot('link'), children: children }));
31
31
  }
32
32
  /**
33
33
  * One service as a list item of `ServicesGrid` (also the building block for `RelatedServices`). Its parts are the
@@ -1 +1 @@
1
- {"version":3,"file":"services-grid.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/services-grid.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * `ServicesGrid` (S4, `services`, 3.14.0) and `ServiceCard`: route the visitor to the right service. Server components.\n *\n * Variants:\n * - `rows` (the fleet reference, AA's `ServiceRows`): one full-width row per service, the photo beside the text and\n * alternating sides on wide screens, with an optional line of scope points and the service's own link;\n * - `cards`: a grid of cards, each headed by its linked title;\n * - `compact` (suburb and region pages): a lean list of every service offered there, each a single link to its\n * service-in-suburb page or to its anchored section on the same page (`#tree-pruning`). No photos and no summaries.\n *\n * Locked rules (S4): one link per card; no prices and no before-and-afters in cards; no image on two cards; at most 8\n * `rows` or `cards` (K11's card limit, and the most in-content links that prefetch on intent), so a page that offers\n * more services lists them `compact` or links its services hub. Card links prefetch on intent (`LINK_PREFETCH_POLICY`\n * `contentCard`); compact links, the hub link and same-page anchors never prefetch.\n */\nimport Link from 'next/link'\nimport type { ReactNode } from 'react'\nimport type { BrandlabMediaEntry } from '@brandlab-digital/site-core/media'\nimport { LINK_PREFETCH_LIMITS, LINK_PREFETCH_POLICY, linkPrefetchProps, type LinkPrefetch } from '../prefetch.js'\nimport { Media, type MediaLayout } from '../primitives/media.js'\nimport { Section, SectionHeading, type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js'\nimport type { RepeatedSlotOf } from '../repeated-slots.js'\nimport { repeatedSlot, slot, type SkinEntry } from '../skin.js'\nimport type { UiLink } from '../types.js'\n\nexport const SERVICES_GRID_VARIANTS = ['rows', 'cards', 'compact'] as const\nexport type ServicesGridVariant = (typeof SERVICES_GRID_VARIANTS)[number]\nexport type ServiceCardSlot = 'item' | 'media' | 'body' | 'title' | 'summary' | 'points' | 'point' | 'action' | 'link'\nexport type ServicesGridSlot = SectionSlot | SectionHeadingSlot | 'list' | 'more' | ServiceCardSlot\n/** Rendered once per service: styled from the skin stylesheet, never by class. */\nexport type ServicesGridRepeatedSlot = RepeatedSlotOf<'servicesGrid'>\nexport interface ServicesGridLayout {\n /** `cards`: columns on wide screens. Defaults to 3. */\n readonly columns?: 2 | 3 | 4\n /** `rows` and `cards`: the photo's crop. Defaults to 4:3. */\n readonly ratio?: NonNullable<MediaLayout['ratio']>\n /** `rows`: the photo's side on wide screens. Defaults to `alternate` (first photo at the start). */\n readonly mediaSide?: 'alternate' | 'start' | 'end'\n}\nexport type ServicesGridSkin = SkinEntry<ServicesGridSlot, ServicesGridVariant, ServicesGridLayout, ServicesGridRepeatedSlot>\n\n/** The most services `rows` and `cards` show (K11; the in-content intent-prefetch limit). */\nexport const SERVICES_GRID_CARD_LIMIT = LINK_PREFETCH_LIMITS.contentCard\n\n/** One service, in money order. */\nexport interface ServiceCardData {\n /** A stable slug, rendered as `data-item`. */\n readonly id: string\n /** The service's name, the card's H3 (`rows`, `cards`) or the link text (`compact`). */\n readonly title: string\n /**\n * Where the card goes: the service page, its service-in-suburb page or its anchored section on this page\n * (`#tree-pruning`). From the site's data, never built here.\n */\n readonly href: string\n /** One line on what the service is. `rows` and `cards`; never a price. */\n readonly summary?: ReactNode\n /** `rows`: a few scope points under the summary (verified service scope, never prices). */\n readonly points?: readonly string[]\n /** The service's own photo from the media manifest. `rows` and `cards`; no image on two cards. */\n readonly image?: BrandlabMediaEntry\n /**\n * `rows`: the link under the text, for example \"Tree removal in Castle Hill\". Left out, the title is the link, as on\n * `cards`. Name the service: never \"Learn more\".\n */\n readonly linkLabel?: string\n}\n\nexport interface ServiceCardProps {\n readonly service: ServiceCardData\n readonly variant: ServicesGridVariant\n /** `rows` and `cards`: the photo's `sizes`. */\n readonly imageSizes?: string\n readonly ratio?: ServicesGridLayout['ratio']\n /** `rows` and `cards` default to `intent`; `compact` is always off. */\n readonly prefetch?: LinkPrefetch\n}\n\nfunction ServiceLink({ href, prefetch, children }: { href: string; prefetch: LinkPrefetch; children: ReactNode }) {\n // A same-page section needs no route prefetch: a plain anchor.\n if (href.startsWith('#')) {\n return (\n <a href={href} {...repeatedSlot('link')}>\n {children}\n </a>\n )\n }\n return (\n <Link href={href} {...linkPrefetchProps(prefetch)} {...repeatedSlot('link')}>\n {children}\n </Link>\n )\n}\n\n/**\n * One service as a list item of `ServicesGrid` (also the building block for `RelatedServices`). Its parts are the\n * grid's repeated slots: the root sets no classes on them.\n */\nexport function ServiceCard({ service, variant, imageSizes, ratio = '4:3', prefetch }: ServiceCardProps) {\n if (variant === 'compact') {\n return (\n <li {...repeatedSlot('item')} data-item={service.id}>\n <ServiceLink href={service.href} prefetch={LINK_PREFETCH_POLICY.otherServicesInSuburb}>\n {service.title}\n </ServiceLink>\n </li>\n )\n }\n const linkPrefetch = prefetch ?? LINK_PREFETCH_POLICY.contentCard\n const separateLink = variant === 'rows' && service.linkLabel !== undefined && service.linkLabel.trim() !== ''\n return (\n <li {...repeatedSlot('item')} data-item={service.id}>\n <div {...repeatedSlot('body')}>\n <h3 {...repeatedSlot('title')}>\n {separateLink ? (\n service.title\n ) : (\n <ServiceLink href={service.href} prefetch={linkPrefetch}>\n {service.title}\n </ServiceLink>\n )}\n </h3>\n {service.summary ? <p {...repeatedSlot('summary')}>{service.summary}</p> : null}\n {variant === 'rows' && service.points && service.points.length > 0 ? (\n <ul role=\"list\" {...repeatedSlot('points')}>\n {service.points.map((point) => (\n <li key={point} {...repeatedSlot('point')}>\n {point}\n </li>\n ))}\n </ul>\n ) : null}\n {separateLink ? (\n <p {...repeatedSlot('action')}>\n <ServiceLink href={service.href} prefetch={linkPrefetch}>\n {service.linkLabel}\n </ServiceLink>\n </p>\n ) : null}\n </div>\n {service.image ? (\n <div {...repeatedSlot('media')}>\n <Media media={service.image} sizes={imageSizes ?? defaultImageSizes(variant, 3)} layout={{ ratio }} />\n </div>\n ) : null}\n </li>\n )\n}\n\n/** The photo `sizes` for each variant: half the container in rows, one column of the grid in cards. */\nexport function defaultImageSizes(variant: Exclude<ServicesGridVariant, 'compact'>, columns: 2 | 3 | 4): string {\n if (variant === 'rows') return '(min-width: 1024px) 50vw, 100vw'\n return `(min-width: 1024px) ${Math.round(100 / columns)}vw, (min-width: 640px) 50vw, 100vw`\n}\n\nexport interface ServicesGridProps extends ServicesGridSkin {\n /** The H2, for example \"Our tree services\" or \"Tree services in Castle Hill\". */\n readonly title: ReactNode\n readonly eyebrow?: ReactNode\n readonly lead?: ReactNode\n /** Every service the page offers, in money order. */\n readonly services: readonly ServiceCardData[]\n /** A link after the list, for example the services hub or \"Find your area\". Never prefetched. */\n readonly more?: UiLink\n /** `rows` and `cards`: override the photo `sizes` when the skin's grid differs from the defaults. */\n readonly imageSizes?: string\n /** Defaults to `services-heading`. */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n}\n\nfunction assertServices(services: readonly ServiceCardData[], variant: ServicesGridVariant): void {\n const ids = new Set<string>()\n const hrefs = new Set<string>()\n const images = new Map<string, string>()\n for (const service of services) {\n if (!service.id || ids.has(service.id)) throw new Error(`ServicesGrid: every service needs its own id (\"${service.id}\" is empty or repeated).`)\n ids.add(service.id)\n if (!service.title?.trim()) throw new Error(`ServicesGrid: the service \"${service.id}\" has no title.`)\n if (!service.href?.trim()) throw new Error(`ServicesGrid: the service \"${service.id}\" has no link.`)\n if (hrefs.has(service.href)) throw new Error(`ServicesGrid: two services link to ${service.href}; each card has its own page or section.`)\n hrefs.add(service.href)\n if (variant !== 'compact' && service.image) {\n const other = images.get(service.image.src)\n if (other) throw new Error(`ServicesGrid: \"${service.id}\" and \"${other}\" use the same image (${service.image.src}); no image is on two cards.`)\n images.set(service.image.src, service.id)\n }\n }\n if (variant !== 'compact' && services.length > SERVICES_GRID_CARD_LIMIT) {\n throw new Error(\n `ServicesGrid: ${services.length} services in ${variant}; ${variant} shows at most ${SERVICES_GRID_CARD_LIMIT} (K11). List every service with the compact variant, or show the money services and link the services hub.`,\n )\n }\n}\n\nexport function ServicesGrid({\n title,\n eyebrow,\n lead,\n services,\n more,\n imageSizes,\n headingId = 'services-heading',\n tone,\n variant = 'rows',\n layout,\n classes,\n}: ServicesGridProps) {\n if (services.length === 0) return null\n assertServices(services, variant)\n const columns = layout?.columns ?? 3\n const ratio = layout?.ratio ?? '4:3'\n const hooks =\n variant === 'compact'\n ? undefined\n : variant === 'cards'\n ? { 'data-columns': String(columns), 'data-ratio': ratio }\n : { 'data-media-side': layout?.mediaSide ?? 'alternate', 'data-ratio': ratio }\n const sizes = variant === 'compact' ? undefined : (imageSizes ?? defaultImageSizes(variant, columns))\n return (\n <Section id=\"services\" labelledBy={headingId} tone={tone} variant={variant} classes={classes} hooks={hooks}>\n <SectionHeading id={headingId} title={title} eyebrow={eyebrow} lead={lead} classes={classes} />\n <ul role=\"list\" {...slot(classes, 'list')}>\n {services.map((service) => (\n <ServiceCard key={service.id} service={service} variant={variant} imageSizes={sizes} ratio={ratio} />\n ))}\n </ul>\n {more ? (\n <p {...slot(classes, 'more')}>\n {more.href.startsWith('#') ? (\n <a href={more.href}>{more.label}</a>\n ) : (\n <Link href={more.href} prefetch={false}>\n {more.label}\n </Link>\n )}\n </p>\n ) : null}\n </Section>\n )\n}\n"]}
1
+ {"version":3,"file":"services-grid.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/services-grid.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * `ServicesGrid` (S4, `services`, 3.14.0) and `ServiceCard`: route the visitor to the right service. Server components.\n *\n * Variants:\n * - `rows` (the fleet reference, AA's `ServiceRows`): one full-width row per service, the photo beside the text and\n * alternating sides on wide screens, with an optional line of scope points and the service's own link;\n * - `cards`: a grid of cards, each headed by its linked title;\n * - `compact` (suburb and region pages): a lean list of every service offered there, each a single link to its\n * service-in-suburb page or to its anchored section on the same page (`#tree-pruning`). No photos and no summaries.\n *\n * Locked rules (S4): one link per card; no prices and no before-and-afters in cards; no image on two cards; at most 8\n * `rows` or `cards` (K11's card limit, and the most in-content links that prefetch on intent), so a page that offers\n * more services lists them `compact` or links its services hub. Card links prefetch on intent (`LINK_PREFETCH_POLICY`\n * `contentCard`); compact links, the hub link and same-page anchors never prefetch.\n */\nimport Link from 'next/link'\nimport type { ReactNode } from 'react'\nimport type { BrandlabMediaEntry } from '@brandlab-digital/site-core/media'\nimport { LINK_PREFETCH_LIMITS, LINK_PREFETCH_POLICY, linkPrefetchProps, type LinkPrefetch } from '../prefetch.js'\nimport { Media, type MediaLayout } from '../primitives/media.js'\nimport { Section, SectionHeading, type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js'\nimport type { RepeatedSlotOf } from '../repeated-slots.js'\nimport { repeatedSlot, slot, type SkinEntry } from '../skin.js'\nimport type { UiLink } from '../types.js'\n\nexport const SERVICES_GRID_VARIANTS = ['rows', 'cards', 'compact'] as const\nexport type ServicesGridVariant = (typeof SERVICES_GRID_VARIANTS)[number]\nexport type ServiceCardSlot = 'item' | 'media' | 'body' | 'title' | 'summary' | 'points' | 'point' | 'action' | 'link'\nexport type ServicesGridSlot = SectionSlot | SectionHeadingSlot | 'list' | 'more' | ServiceCardSlot\n/** Rendered once per service: styled from the skin stylesheet, never by class. */\nexport type ServicesGridRepeatedSlot = RepeatedSlotOf<'servicesGrid'>\nexport interface ServicesGridLayout {\n /** `cards`: columns on wide screens. Defaults to 3. */\n readonly columns?: 2 | 3 | 4\n /** `rows` and `cards`: the photo's crop. Defaults to 4:3. */\n readonly ratio?: NonNullable<MediaLayout['ratio']>\n /** `rows`: the photo's side on wide screens. Defaults to `alternate` (first photo at the start). */\n readonly mediaSide?: 'alternate' | 'start' | 'end'\n}\nexport type ServicesGridSkin = SkinEntry<ServicesGridSlot, ServicesGridVariant, ServicesGridLayout, ServicesGridRepeatedSlot>\n\n/** The most services `rows` and `cards` show (K11; the in-content intent-prefetch limit). */\nexport const SERVICES_GRID_CARD_LIMIT = LINK_PREFETCH_LIMITS.contentCard\n\n/** One service, in money order. */\nexport interface ServiceCardData {\n /** A stable slug, rendered as `data-item`. */\n readonly id: string\n /** The service's name, the card's H3 (`rows`, `cards`) or the link text (`compact`). */\n readonly title: string\n /**\n * Where the card goes: the service page, its service-in-suburb page or its anchored section on this page\n * (`#tree-pruning`). From the site's data, never built here.\n */\n readonly href: string\n /** One line on what the service is. `rows` and `cards`; never a price. */\n readonly summary?: ReactNode\n /** `rows`: a few scope points under the summary (verified service scope, never prices). */\n readonly points?: readonly string[]\n /** The service's own photo from the media manifest. `rows` and `cards`; no image on two cards. */\n readonly image?: BrandlabMediaEntry\n /**\n * `rows`: the link under the text, for example \"Tree removal in Castle Hill\". Left out, the title is the link, as on\n * `cards`. Name the service: never \"Learn more\".\n */\n readonly linkLabel?: string\n}\n\nexport interface ServiceCardProps {\n readonly service: ServiceCardData\n readonly variant: ServicesGridVariant\n /** `rows` and `cards`: the photo's `sizes`. */\n readonly imageSizes?: string\n readonly ratio?: ServicesGridLayout['ratio']\n /** `rows` and `cards` default to `intent`; `compact` is always off. */\n readonly prefetch?: LinkPrefetch\n}\n\nfunction ServiceLink({ href, prefetch, children }: { href: string; prefetch: LinkPrefetch; children: ReactNode }) {\n // A same-page section needs no route prefetch: a plain anchor.\n if (href.startsWith('#')) {\n return (\n <a href={href} {...repeatedSlot('link')}>\n {children}\n </a>\n )\n }\n return (\n <Link href={href} {...linkPrefetchProps(prefetch, href)} {...repeatedSlot('link')}>\n {children}\n </Link>\n )\n}\n\n/**\n * One service as a list item of `ServicesGrid` (also the building block for `RelatedServices`). Its parts are the\n * grid's repeated slots: the root sets no classes on them.\n */\nexport function ServiceCard({ service, variant, imageSizes, ratio = '4:3', prefetch }: ServiceCardProps) {\n if (variant === 'compact') {\n return (\n <li {...repeatedSlot('item')} data-item={service.id}>\n <ServiceLink href={service.href} prefetch={LINK_PREFETCH_POLICY.otherServicesInSuburb}>\n {service.title}\n </ServiceLink>\n </li>\n )\n }\n const linkPrefetch = prefetch ?? LINK_PREFETCH_POLICY.contentCard\n const separateLink = variant === 'rows' && service.linkLabel !== undefined && service.linkLabel.trim() !== ''\n return (\n <li {...repeatedSlot('item')} data-item={service.id}>\n <div {...repeatedSlot('body')}>\n <h3 {...repeatedSlot('title')}>\n {separateLink ? (\n service.title\n ) : (\n <ServiceLink href={service.href} prefetch={linkPrefetch}>\n {service.title}\n </ServiceLink>\n )}\n </h3>\n {service.summary ? <p {...repeatedSlot('summary')}>{service.summary}</p> : null}\n {variant === 'rows' && service.points && service.points.length > 0 ? (\n <ul role=\"list\" {...repeatedSlot('points')}>\n {service.points.map((point) => (\n <li key={point} {...repeatedSlot('point')}>\n {point}\n </li>\n ))}\n </ul>\n ) : null}\n {separateLink ? (\n <p {...repeatedSlot('action')}>\n <ServiceLink href={service.href} prefetch={linkPrefetch}>\n {service.linkLabel}\n </ServiceLink>\n </p>\n ) : null}\n </div>\n {service.image ? (\n <div {...repeatedSlot('media')}>\n <Media media={service.image} sizes={imageSizes ?? defaultImageSizes(variant, 3)} layout={{ ratio }} />\n </div>\n ) : null}\n </li>\n )\n}\n\n/** The photo `sizes` for each variant: half the container in rows, one column of the grid in cards. */\nexport function defaultImageSizes(variant: Exclude<ServicesGridVariant, 'compact'>, columns: 2 | 3 | 4): string {\n if (variant === 'rows') return '(min-width: 1024px) 50vw, 100vw'\n return `(min-width: 1024px) ${Math.round(100 / columns)}vw, (min-width: 640px) 50vw, 100vw`\n}\n\nexport interface ServicesGridProps extends ServicesGridSkin {\n /** The H2, for example \"Our tree services\" or \"Tree services in Castle Hill\". */\n readonly title: ReactNode\n readonly eyebrow?: ReactNode\n readonly lead?: ReactNode\n /** Every service the page offers, in money order. */\n readonly services: readonly ServiceCardData[]\n /** A link after the list, for example the services hub or \"Find your area\". Never prefetched. */\n readonly more?: UiLink\n /** `rows` and `cards`: override the photo `sizes` when the skin's grid differs from the defaults. */\n readonly imageSizes?: string\n /** Defaults to `services-heading`. */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n}\n\nfunction assertServices(services: readonly ServiceCardData[], variant: ServicesGridVariant): void {\n const ids = new Set<string>()\n const hrefs = new Set<string>()\n const images = new Map<string, string>()\n for (const service of services) {\n if (!service.id || ids.has(service.id)) throw new Error(`ServicesGrid: every service needs its own id (\"${service.id}\" is empty or repeated).`)\n ids.add(service.id)\n if (!service.title?.trim()) throw new Error(`ServicesGrid: the service \"${service.id}\" has no title.`)\n if (!service.href?.trim()) throw new Error(`ServicesGrid: the service \"${service.id}\" has no link.`)\n if (hrefs.has(service.href)) throw new Error(`ServicesGrid: two services link to ${service.href}; each card has its own page or section.`)\n hrefs.add(service.href)\n if (variant !== 'compact' && service.image) {\n const other = images.get(service.image.src)\n if (other) throw new Error(`ServicesGrid: \"${service.id}\" and \"${other}\" use the same image (${service.image.src}); no image is on two cards.`)\n images.set(service.image.src, service.id)\n }\n }\n if (variant !== 'compact' && services.length > SERVICES_GRID_CARD_LIMIT) {\n throw new Error(\n `ServicesGrid: ${services.length} services in ${variant}; ${variant} shows at most ${SERVICES_GRID_CARD_LIMIT} (K11). List every service with the compact variant, or show the money services and link the services hub.`,\n )\n }\n}\n\nexport function ServicesGrid({\n title,\n eyebrow,\n lead,\n services,\n more,\n imageSizes,\n headingId = 'services-heading',\n tone,\n variant = 'rows',\n layout,\n classes,\n}: ServicesGridProps) {\n if (services.length === 0) return null\n assertServices(services, variant)\n const columns = layout?.columns ?? 3\n const ratio = layout?.ratio ?? '4:3'\n const hooks =\n variant === 'compact'\n ? undefined\n : variant === 'cards'\n ? { 'data-columns': String(columns), 'data-ratio': ratio }\n : { 'data-media-side': layout?.mediaSide ?? 'alternate', 'data-ratio': ratio }\n const sizes = variant === 'compact' ? undefined : (imageSizes ?? defaultImageSizes(variant, columns))\n return (\n <Section id=\"services\" labelledBy={headingId} tone={tone} variant={variant} classes={classes} hooks={hooks}>\n <SectionHeading id={headingId} title={title} eyebrow={eyebrow} lead={lead} classes={classes} />\n <ul role=\"list\" {...slot(classes, 'list')}>\n {services.map((service) => (\n <ServiceCard key={service.id} service={service} variant={variant} imageSizes={sizes} ratio={ratio} />\n ))}\n </ul>\n {more ? (\n <p {...slot(classes, 'more')}>\n {more.href.startsWith('#') ? (\n <a href={more.href}>{more.label}</a>\n ) : (\n <Link href={more.href} prefetch={false}>\n {more.label}\n </Link>\n )}\n </p>\n ) : null}\n </Section>\n )\n}\n"]}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The rules the "our work" sections share (3.15.0): `Projects`, `Gallery` and `BeforeAfter` show the client's real work
3
+ * only (v2 §11 imagery rule 4, F13). A stock or AI image never stands for a job, so these sections refuse one and fail
4
+ * the build, the same way the hero refuses an AI photo; when there are no genuine photos the section is skipped.
5
+ */
6
+ import type { MediaFrameSizing, UiMedia } from '../primitives/media-frame.js';
7
+ /** Throws unless the image is a genuine, described photo of the work. */
8
+ export declare function assertGenuineWork(component: string, media: UiMedia, where: string): void;
9
+ /** Fluid `sizes` for one column of a grid that is one column on phones, two from 640 px and `columns` from 1024 px. */
10
+ export declare function gridImageSizes(columns: number): string;
11
+ /** `MediaFrame`'s sizing from a section's `imageSizes`: a number is a fixed CSS width, a string a fluid `vw` size. */
12
+ export declare function frameSizing(sizes: number | string): MediaFrameSizing;
13
+ /** An internal page path: starts with one `/`, no spaces. */
14
+ export declare function isInternalPath(href: string): boolean;
15
+ //# sourceMappingURL=work-photos.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"work-photos.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/work-photos.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAA;AAE7E,yEAAyE;AACzE,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CASxF;AAED,uHAAuH;AACvH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGtD;AAED,sHAAsH;AACtH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,gBAAgB,CAEpE;AAED,6DAA6D;AAC7D,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD"}
@@ -0,0 +1,24 @@
1
+ /** Throws unless the image is a genuine, described photo of the work. */
2
+ export function assertGenuineWork(component, media, where) {
3
+ if (media.provenance !== 'genuine') {
4
+ throw new Error(`${component}: ${where} (${media.src}) is ${media.provenance}. Projects, the gallery and before-and-after pairs show genuine photos of the work only; use the image elsewhere as supporting media.`);
5
+ }
6
+ if (media.decorative || !media.alt.trim()) {
7
+ throw new Error(`${component}: ${where} (${media.src}) has no alt text; a photo of the work says what it shows.`);
8
+ }
9
+ }
10
+ /** Fluid `sizes` for one column of a grid that is one column on phones, two from 640 px and `columns` from 1024 px. */
11
+ export function gridImageSizes(columns) {
12
+ if (columns <= 2)
13
+ return '(min-width: 640px) 50vw, 100vw';
14
+ return `(min-width: 1024px) ${Math.round(100 / columns)}vw, (min-width: 640px) 50vw, 100vw`;
15
+ }
16
+ /** `MediaFrame`'s sizing from a section's `imageSizes`: a number is a fixed CSS width, a string a fluid `vw` size. */
17
+ export function frameSizing(sizes) {
18
+ return typeof sizes === 'number' ? { displayWidth: sizes } : { sizes };
19
+ }
20
+ /** An internal page path: starts with one `/`, no spaces. */
21
+ export function isInternalPath(href) {
22
+ return /^\/(?!\/)\S*$/.test(href);
23
+ }
24
+ //# sourceMappingURL=work-photos.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"work-photos.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/work-photos.ts"],"names":[],"mappings":"AAOA,yEAAyE;AACzE,MAAM,UAAU,iBAAiB,CAAC,SAAiB,EAAE,KAAc,EAAE,KAAa;IAChF,IAAI,KAAK,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CACb,GAAG,SAAS,KAAK,KAAK,KAAK,KAAK,CAAC,GAAG,QAAQ,KAAK,CAAC,UAAU,uIAAuI,CACpM,CAAA;IACH,CAAC;IACD,IAAI,KAAK,CAAC,UAAU,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,GAAG,SAAS,KAAK,KAAK,KAAK,KAAK,CAAC,GAAG,4DAA4D,CAAC,CAAA;IACnH,CAAC;AACH,CAAC;AAED,uHAAuH;AACvH,MAAM,UAAU,cAAc,CAAC,OAAe;IAC5C,IAAI,OAAO,IAAI,CAAC;QAAE,OAAO,gCAAgC,CAAA;IACzD,OAAO,uBAAuB,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,OAAO,CAAC,oCAAoC,CAAA;AAC7F,CAAC;AAED,sHAAsH;AACtH,MAAM,UAAU,WAAW,CAAC,KAAsB;IAChD,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAA;AACxE,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACnC,CAAC","sourcesContent":["/**\n * The rules the \"our work\" sections share (3.15.0): `Projects`, `Gallery` and `BeforeAfter` show the client's real work\n * only (v2 §11 imagery rule 4, F13). A stock or AI image never stands for a job, so these sections refuse one and fail\n * the build, the same way the hero refuses an AI photo; when there are no genuine photos the section is skipped.\n */\nimport type { MediaFrameSizing, UiMedia } from '../primitives/media-frame.js'\n\n/** Throws unless the image is a genuine, described photo of the work. */\nexport function assertGenuineWork(component: string, media: UiMedia, where: string): void {\n if (media.provenance !== 'genuine') {\n throw new Error(\n `${component}: ${where} (${media.src}) is ${media.provenance}. Projects, the gallery and before-and-after pairs show genuine photos of the work only; use the image elsewhere as supporting media.`,\n )\n }\n if (media.decorative || !media.alt.trim()) {\n throw new Error(`${component}: ${where} (${media.src}) has no alt text; a photo of the work says what it shows.`)\n }\n}\n\n/** Fluid `sizes` for one column of a grid that is one column on phones, two from 640 px and `columns` from 1024 px. */\nexport function gridImageSizes(columns: number): string {\n if (columns <= 2) return '(min-width: 640px) 50vw, 100vw'\n return `(min-width: 1024px) ${Math.round(100 / columns)}vw, (min-width: 640px) 50vw, 100vw`\n}\n\n/** `MediaFrame`'s sizing from a section's `imageSizes`: a number is a fixed CSS width, a string a fluid `vw` size. */\nexport function frameSizing(sizes: number | string): MediaFrameSizing {\n return typeof sizes === 'number' ? { displayWidth: sizes } : { sizes }\n}\n\n/** An internal page path: starts with one `/`, no spaces. */\nexport function isInternalPath(href: string): boolean {\n return /^\\/(?!\\/)\\S*$/.test(href)\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  /**
3
3
  * F7: back-to-top (server component). A 44 px `<button>` named "Back to top" that appears after one and a half
4
- * screens, sits above the call bar on phones, scrolls smoothly unless the visitor prefers reduced motion and moves
4
+ * screens, stays hidden while the phone call bar shows (3.15.0; it never covers the bar or the content above it), scrolls smoothly unless the visitor prefers reduced motion and moves
5
5
  * focus to `#main`. Never a footer text link. Render it once, after the page's `<main>`.
6
6
  */
7
7
  import { Icon } from '../icons.js';