@brandlab-digital/site-next 3.14.0 → 3.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/dist/analytics.d.ts +1 -1
  2. package/dist/analytics.d.ts.map +1 -1
  3. package/dist/analytics.js +2 -0
  4. package/dist/analytics.js.map +1 -1
  5. package/dist/navigation/constants.d.ts +11 -2
  6. package/dist/navigation/constants.d.ts.map +1 -1
  7. package/dist/navigation/constants.js +11 -2
  8. package/dist/navigation/constants.js.map +1 -1
  9. package/dist/navigation/index.d.ts +1 -1
  10. package/dist/navigation/index.d.ts.map +1 -1
  11. package/dist/navigation/index.js +1 -1
  12. package/dist/navigation/index.js.map +1 -1
  13. package/dist/navigation/use-back-to-top.d.ts.map +1 -1
  14. package/dist/navigation/use-back-to-top.js +12 -4
  15. package/dist/navigation/use-back-to-top.js.map +1 -1
  16. package/dist/navigation/use-header-metrics.d.ts +1 -1
  17. package/dist/navigation/use-header-metrics.js +1 -1
  18. package/dist/navigation/use-header-metrics.js.map +1 -1
  19. package/dist/navigation/use-sticky-call-bar.d.ts +2 -1
  20. package/dist/navigation/use-sticky-call-bar.d.ts.map +1 -1
  21. package/dist/navigation/use-sticky-call-bar.js +11 -2
  22. package/dist/navigation/use-sticky-call-bar.js.map +1 -1
  23. package/dist/ui-kit/brandlab-skin.d.ts +26 -0
  24. package/dist/ui-kit/brandlab-skin.d.ts.map +1 -1
  25. package/dist/ui-kit/brandlab-skin.js.map +1 -1
  26. package/dist/ui-kit/prefetch.d.ts +19 -10
  27. package/dist/ui-kit/prefetch.d.ts.map +1 -1
  28. package/dist/ui-kit/prefetch.js +23 -10
  29. package/dist/ui-kit/prefetch.js.map +1 -1
  30. package/dist/ui-kit/primitives/index.d.ts +1 -1
  31. package/dist/ui-kit/primitives/index.d.ts.map +1 -1
  32. package/dist/ui-kit/primitives/index.js.map +1 -1
  33. package/dist/ui-kit/primitives/links.js +1 -1
  34. package/dist/ui-kit/primitives/links.js.map +1 -1
  35. package/dist/ui-kit/primitives/lists.d.ts +9 -1
  36. package/dist/ui-kit/primitives/lists.d.ts.map +1 -1
  37. package/dist/ui-kit/primitives/lists.js +1 -1
  38. package/dist/ui-kit/primitives/lists.js.map +1 -1
  39. package/dist/ui-kit/primitives/quote-button.d.ts +25 -2
  40. package/dist/ui-kit/primitives/quote-button.d.ts.map +1 -1
  41. package/dist/ui-kit/primitives/quote-button.js +25 -3
  42. package/dist/ui-kit/primitives/quote-button.js.map +1 -1
  43. package/dist/ui-kit/repeated-slots.d.ts +32 -0
  44. package/dist/ui-kit/repeated-slots.d.ts.map +1 -1
  45. package/dist/ui-kit/repeated-slots.js +16 -0
  46. package/dist/ui-kit/repeated-slots.js.map +1 -1
  47. package/dist/ui-kit/sections/area-map.d.ts +64 -0
  48. package/dist/ui-kit/sections/area-map.d.ts.map +1 -0
  49. package/dist/ui-kit/sections/area-map.js +142 -0
  50. package/dist/ui-kit/sections/area-map.js.map +1 -0
  51. package/dist/ui-kit/sections/before-after.d.ts +59 -0
  52. package/dist/ui-kit/sections/before-after.d.ts.map +1 -0
  53. package/dist/ui-kit/sections/before-after.js +42 -0
  54. package/dist/ui-kit/sections/before-after.js.map +1 -0
  55. package/dist/ui-kit/sections/council-rules.d.ts +64 -0
  56. package/dist/ui-kit/sections/council-rules.d.ts.map +1 -0
  57. package/dist/ui-kit/sections/council-rules.js +46 -0
  58. package/dist/ui-kit/sections/council-rules.js.map +1 -0
  59. package/dist/ui-kit/sections/coverage.d.ts +53 -0
  60. package/dist/ui-kit/sections/coverage.d.ts.map +1 -0
  61. package/dist/ui-kit/sections/coverage.js +57 -0
  62. package/dist/ui-kit/sections/coverage.js.map +1 -0
  63. package/dist/ui-kit/sections/emergency-band.d.ts +60 -0
  64. package/dist/ui-kit/sections/emergency-band.d.ts.map +1 -0
  65. package/dist/ui-kit/sections/emergency-band.js +43 -0
  66. package/dist/ui-kit/sections/emergency-band.js.map +1 -0
  67. package/dist/ui-kit/sections/gallery.d.ts +60 -0
  68. package/dist/ui-kit/sections/gallery.d.ts.map +1 -0
  69. package/dist/ui-kit/sections/gallery.js +39 -0
  70. package/dist/ui-kit/sections/gallery.js.map +1 -0
  71. package/dist/ui-kit/sections/hero.d.ts +5 -0
  72. package/dist/ui-kit/sections/hero.d.ts.map +1 -1
  73. package/dist/ui-kit/sections/hero.js +1 -1
  74. package/dist/ui-kit/sections/hero.js.map +1 -1
  75. package/dist/ui-kit/sections/index.d.ts +14 -1
  76. package/dist/ui-kit/sections/index.d.ts.map +1 -1
  77. package/dist/ui-kit/sections/index.js +14 -1
  78. package/dist/ui-kit/sections/index.js.map +1 -1
  79. package/dist/ui-kit/sections/licences.d.ts +72 -0
  80. package/dist/ui-kit/sections/licences.d.ts.map +1 -0
  81. package/dist/ui-kit/sections/licences.js +46 -0
  82. package/dist/ui-kit/sections/licences.js.map +1 -0
  83. package/dist/ui-kit/sections/local-facts.d.ts +78 -0
  84. package/dist/ui-kit/sections/local-facts.d.ts.map +1 -0
  85. package/dist/ui-kit/sections/local-facts.js +32 -0
  86. package/dist/ui-kit/sections/local-facts.js.map +1 -0
  87. package/dist/ui-kit/sections/nearby-areas.d.ts +50 -0
  88. package/dist/ui-kit/sections/nearby-areas.d.ts.map +1 -0
  89. package/dist/ui-kit/sections/nearby-areas.js +49 -0
  90. package/dist/ui-kit/sections/nearby-areas.js.map +1 -0
  91. package/dist/ui-kit/sections/nearby-jobs.d.ts +66 -0
  92. package/dist/ui-kit/sections/nearby-jobs.d.ts.map +1 -0
  93. package/dist/ui-kit/sections/nearby-jobs.js +49 -0
  94. package/dist/ui-kit/sections/nearby-jobs.js.map +1 -0
  95. package/dist/ui-kit/sections/projects.d.ts +79 -0
  96. package/dist/ui-kit/sections/projects.d.ts.map +1 -0
  97. package/dist/ui-kit/sections/projects.js +69 -0
  98. package/dist/ui-kit/sections/projects.js.map +1 -0
  99. package/dist/ui-kit/sections/quote-checklist.d.ts +43 -0
  100. package/dist/ui-kit/sections/quote-checklist.d.ts.map +1 -0
  101. package/dist/ui-kit/sections/quote-checklist.js +19 -0
  102. package/dist/ui-kit/sections/quote-checklist.js.map +1 -0
  103. package/dist/ui-kit/sections/related-services.d.ts +41 -0
  104. package/dist/ui-kit/sections/related-services.d.ts.map +1 -0
  105. package/dist/ui-kit/sections/related-services.js +52 -0
  106. package/dist/ui-kit/sections/related-services.js.map +1 -0
  107. package/dist/ui-kit/sections/reviews-band.d.ts +11 -0
  108. package/dist/ui-kit/sections/reviews-band.d.ts.map +1 -1
  109. package/dist/ui-kit/sections/reviews-band.js +11 -0
  110. package/dist/ui-kit/sections/reviews-band.js.map +1 -1
  111. package/dist/ui-kit/sections/services-grid.js +1 -1
  112. package/dist/ui-kit/sections/services-grid.js.map +1 -1
  113. package/dist/ui-kit/sections/work-photos.d.ts +15 -0
  114. package/dist/ui-kit/sections/work-photos.d.ts.map +1 -0
  115. package/dist/ui-kit/sections/work-photos.js +24 -0
  116. package/dist/ui-kit/sections/work-photos.js.map +1 -0
  117. package/dist/ui-kit/shell/back-to-top.js +1 -1
  118. package/dist/ui-kit/shell/back-to-top.js.map +1 -1
  119. package/dist/ui-kit/shell/current-link.d.ts +21 -5
  120. package/dist/ui-kit/shell/current-link.d.ts.map +1 -1
  121. package/dist/ui-kit/shell/current-link.js +16 -5
  122. package/dist/ui-kit/shell/current-link.js.map +1 -1
  123. package/dist/ui-kit/shell/drawer-client.js +1 -1
  124. package/dist/ui-kit/shell/drawer-client.js.map +1 -1
  125. package/dist/ui-kit/shell/intent-prefetch.d.ts.map +1 -1
  126. package/dist/ui-kit/shell/intent-prefetch.js +3 -2
  127. package/dist/ui-kit/shell/intent-prefetch.js.map +1 -1
  128. package/dist/ui-kit/shell/mobile-call-bar.d.ts +6 -1
  129. package/dist/ui-kit/shell/mobile-call-bar.d.ts.map +1 -1
  130. package/dist/ui-kit/shell/mobile-call-bar.js +6 -3
  131. package/dist/ui-kit/shell/mobile-call-bar.js.map +1 -1
  132. package/dist/ui-kit/shell/mobile-drawer.js +2 -2
  133. package/dist/ui-kit/shell/mobile-drawer.js.map +1 -1
  134. package/dist/ui-kit/shell/shell-quote.d.ts +10 -0
  135. package/dist/ui-kit/shell/shell-quote.d.ts.map +1 -0
  136. package/dist/ui-kit/shell/shell-quote.js +13 -0
  137. package/dist/ui-kit/shell/shell-quote.js.map +1 -0
  138. package/dist/ui-kit/shell/site-header.d.ts +1 -1
  139. package/dist/ui-kit/shell/site-header.js +4 -4
  140. package/dist/ui-kit/shell/site-header.js.map +1 -1
  141. package/dist/ui-kit/structure.css +410 -7
  142. package/package.json +2 -2
@@ -60,6 +60,22 @@ export const REPEATED_SLOTS = {
60
60
  slots: ['item', 'body', 'media', 'title', 'summary', 'points', 'point', 'action', 'link'],
61
61
  },
62
62
  breadcrumbs: { scope: '[data-bl="breadcrumbs"]', slots: ['item', 'link', 'current'] },
63
+ // Location sections (3.15.0). Their link lists are `LinkList`s, reached through `linkList` (`[data-bl="link-list"]`).
64
+ localFacts: { scope: '[data-bl-section="local-facts"]', slots: ['fact', 'factSource', 'fee'] },
65
+ councilRules: { scope: '[data-bl-section="council-rules"]', slots: ['fee'] },
66
+ nearbyJobs: { scope: '[data-bl-section="nearby-jobs"] [data-slot="list"]', slots: ['item', 'body', 'media', 'title', 'link', 'outcome'] },
67
+ coverage: { scope: '[data-bl-section="coverage"] [data-slot="groups"]', slots: ['group', 'groupHeading'] },
68
+ relatedServices: {
69
+ scope: '[data-bl-section="related-services"] [data-slot="list"]',
70
+ slots: ['item', 'body', 'media', 'title', 'summary', 'link'],
71
+ },
72
+ // Proof sections (3.15.0). Their photos are nested `[data-bl="media"]` frames; project cards are `[data-bl="card"]`.
73
+ gallery: { scope: '[data-bl-section="gallery"] [data-slot="list"]', slots: ['item', 'link'] },
74
+ beforeAfter: { scope: '[data-bl-section="before-after"] [data-slot="list"]', slots: ['pair', 'frames', 'frame', 'note'] },
75
+ licences: {
76
+ scope: '[data-bl-section="licences"] [data-slot="list"]',
77
+ slots: ['row', 'label', 'value', 'number', 'holder', 'classes', 'class', 'register', 'cover', 'amount'],
78
+ },
63
79
  thankYou: { scope: '[data-bl="thank-you"]', slots: ['step'] },
64
80
  photoUpload: { scope: '[data-bl="photo-upload"]', slots: ['photo', 'thumb', 'remove'] },
65
81
  quoteForm: { scope: '[data-bl="quote-form"] [data-bl="photo-upload"]', slots: ['photo', 'thumb', 'remove'] },
@@ -1 +1 @@
1
- {"version":3,"file":"repeated-slots.js","sourceRoot":"","sources":["../../src/ui-kit/repeated-slots.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The repeated slots of every shared component (3.14.0): the parts a component renders once per entry of a list in\n * its data. They carry their `data-slot` hook and never a class, so a long class string is not repeated in the HTML and\n * the RSC payload for every link, review, star, card or chip. A site styles them once in `src/site/skin.css`:\n *\n * ```css\n * @layer components {\n * [data-bl=\"footer\"] [data-slot=\"link\"] { @apply text-sm text-white/80 hover:text-white; }\n * [data-bl=\"header\"] [data-slot=\"list\"] [data-slot=\"panelLink\"] { padding: 0.5rem 0.75rem; }\n * }\n * ```\n *\n * `scope` is the selector that holds the repeated slots without reaching a nested component that reuses a slot name\n * (the drawer sits inside the header; the rating chip inside the reviews band uses `stars` too).\n * `repeatedSlotSelector()` joins the two. Pure data: safe to import anywhere.\n */\nexport const REPEATED_SLOTS = {\n siteHeader: {\n scope: '[data-bl=\"header\"] [data-slot=\"list\"]',\n slots: [\n 'item', 'itemRow', 'link', 'trigger', 'panel', 'panelBody', 'panelList', 'panelGroup', 'panelHeading', 'panelLink',\n 'panelLabel', 'panelText', 'featured', 'featuredMedia', 'featuredLabel', 'featuredText',\n ],\n },\n mobileDrawer: {\n scope: '[data-bl=\"drawer\"]',\n slots: ['group', 'groupTrigger', 'groupLinks', 'groupLink', 'link', 'socialLink'],\n },\n utilityBar: { scope: '[data-bl=\"utility\"]', slots: ['item', 'licence', 'socialLink'] },\n siteFooter: {\n scope: '[data-bl=\"footer\"]',\n slots: ['licenceLink', 'socialLink', 'column', 'heading', 'toggle', 'list', 'link', 'policyLink'],\n },\n accordion: { scope: '[data-bl=\"accordion\"]', slots: ['item', 'summary', 'label', 'marker', 'content'] },\n faqs: {\n scope: '[data-bl-section=\"faqs\"] [data-slot=\"columns\"]',\n slots: ['column', 'group', 'subheading', 'accordion', 'item', 'summary', 'label', 'marker', 'content'],\n /** The nested accordion's root is its own component: `data-bl=\"accordion\"`, not a `data-slot`. */\n hooks: { accordion: '[data-bl=\"accordion\"]' },\n },\n priceTable: {\n scope: '[data-bl=\"price-table\"]',\n slots: ['row', 'job', 'cell', 'label', 'range', 'unit', 'gst', 'parts', 'part', 'includes', 'excludes'],\n },\n priceGuide: {\n scope: '[data-bl-section=\"price-guide\"]',\n slots: [\n 'row', 'job', 'cell', 'label', 'range', 'unit', 'gst', 'parts', 'part', 'includes', 'excludes', 'card', 'title',\n 'figures', 'band', 'factor', 'name', 'effect', 'detail',\n ],\n },\n proofStrip: { scope: '[data-bl-section=\"proof-strip\"] [data-slot=\"list\"]', slots: ['item', 'icon', 'stars', 'value', 'label'] },\n reviewsBand: {\n scope: '[data-bl-section=\"reviews\"] [data-slot=\"list\"]',\n slots: ['item', 'stars', 'quote', 'more', 'author', 'avatar', 'name', 'date'],\n },\n contactBlock: { scope: '[data-bl-section=\"contact\"] [data-slot=\"details\"]', slots: ['line'] },\n servicesGrid: {\n scope: '[data-bl-section=\"services\"] [data-slot=\"list\"]',\n slots: ['item', 'body', 'media', 'title', 'summary', 'points', 'point', 'action', 'link'],\n },\n breadcrumbs: { scope: '[data-bl=\"breadcrumbs\"]', slots: ['item', 'link', 'current'] },\n thankYou: { scope: '[data-bl=\"thank-you\"]', slots: ['step'] },\n photoUpload: { scope: '[data-bl=\"photo-upload\"]', slots: ['photo', 'thumb', 'remove'] },\n quoteForm: { scope: '[data-bl=\"quote-form\"] [data-bl=\"photo-upload\"]', slots: ['photo', 'thumb', 'remove'] },\n // Primitives rendered once per list entry (3.14.0): every part is repeated, the root included (style the root\n // through its own hook, `[data-bl=\"card\"]`).\n card: { scope: '[data-bl=\"card\"]', slots: ['media', 'body', 'title', 'link', 'text', 'meta'] },\n badge: { scope: '[data-bl=\"badge\"]', slots: ['icon', 'label'] },\n chip: { scope: '[data-bl=\"chip\"]', slots: ['icon', 'label'] },\n stat: { scope: '[data-bl=\"stat\"]', slots: ['value', 'label', 'link'] },\n tickList: { scope: '[data-bl=\"tick-list\"]', slots: ['item', 'tick', 'text', 'link'] },\n /** A lean list (cost item 1f): plain `<li><a>` entries with no `data-slot`, reached by element. */\n linkList: { scope: '[data-bl=\"link-list\"]', slots: ['item', 'link'], hooks: { item: '> li', link: '> li > a' } },\n} as const\n\nexport type RepeatedSlotComponent = keyof typeof REPEATED_SLOTS\n/** The repeated slots of one component, for example `RepeatedSlotOf<'siteFooter'>` is `'licenceLink' | … | 'policyLink'`. */\nexport type RepeatedSlotOf<Component extends RepeatedSlotComponent> = (typeof REPEATED_SLOTS)[Component]['slots'][number]\n\n/** The selector a skin stylesheet uses for one repeated slot, for example `[data-bl=\"footer\"] [data-slot=\"link\"]`. */\nexport function repeatedSlotSelector<Component extends RepeatedSlotComponent>(component: Component, slot: RepeatedSlotOf<Component>): string {\n const entry: { readonly scope: string; readonly hooks?: Readonly<Record<string, string>> } = REPEATED_SLOTS[component]\n return `${entry.scope} ${entry.hooks?.[slot] ?? `[data-slot=\"${slot}\"]`}`\n}\n"]}
1
+ {"version":3,"file":"repeated-slots.js","sourceRoot":"","sources":["../../src/ui-kit/repeated-slots.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The repeated slots of every shared component (3.14.0): the parts a component renders once per entry of a list in\n * its data. They carry their `data-slot` hook and never a class, so a long class string is not repeated in the HTML and\n * the RSC payload for every link, review, star, card or chip. A site styles them once in `src/site/skin.css`:\n *\n * ```css\n * @layer components {\n * [data-bl=\"footer\"] [data-slot=\"link\"] { @apply text-sm text-white/80 hover:text-white; }\n * [data-bl=\"header\"] [data-slot=\"list\"] [data-slot=\"panelLink\"] { padding: 0.5rem 0.75rem; }\n * }\n * ```\n *\n * `scope` is the selector that holds the repeated slots without reaching a nested component that reuses a slot name\n * (the drawer sits inside the header; the rating chip inside the reviews band uses `stars` too).\n * `repeatedSlotSelector()` joins the two. Pure data: safe to import anywhere.\n */\nexport const REPEATED_SLOTS = {\n siteHeader: {\n scope: '[data-bl=\"header\"] [data-slot=\"list\"]',\n slots: [\n 'item', 'itemRow', 'link', 'trigger', 'panel', 'panelBody', 'panelList', 'panelGroup', 'panelHeading', 'panelLink',\n 'panelLabel', 'panelText', 'featured', 'featuredMedia', 'featuredLabel', 'featuredText',\n ],\n },\n mobileDrawer: {\n scope: '[data-bl=\"drawer\"]',\n slots: ['group', 'groupTrigger', 'groupLinks', 'groupLink', 'link', 'socialLink'],\n },\n utilityBar: { scope: '[data-bl=\"utility\"]', slots: ['item', 'licence', 'socialLink'] },\n siteFooter: {\n scope: '[data-bl=\"footer\"]',\n slots: ['licenceLink', 'socialLink', 'column', 'heading', 'toggle', 'list', 'link', 'policyLink'],\n },\n accordion: { scope: '[data-bl=\"accordion\"]', slots: ['item', 'summary', 'label', 'marker', 'content'] },\n faqs: {\n scope: '[data-bl-section=\"faqs\"] [data-slot=\"columns\"]',\n slots: ['column', 'group', 'subheading', 'accordion', 'item', 'summary', 'label', 'marker', 'content'],\n /** The nested accordion's root is its own component: `data-bl=\"accordion\"`, not a `data-slot`. */\n hooks: { accordion: '[data-bl=\"accordion\"]' },\n },\n priceTable: {\n scope: '[data-bl=\"price-table\"]',\n slots: ['row', 'job', 'cell', 'label', 'range', 'unit', 'gst', 'parts', 'part', 'includes', 'excludes'],\n },\n priceGuide: {\n scope: '[data-bl-section=\"price-guide\"]',\n slots: [\n 'row', 'job', 'cell', 'label', 'range', 'unit', 'gst', 'parts', 'part', 'includes', 'excludes', 'card', 'title',\n 'figures', 'band', 'factor', 'name', 'effect', 'detail',\n ],\n },\n proofStrip: { scope: '[data-bl-section=\"proof-strip\"] [data-slot=\"list\"]', slots: ['item', 'icon', 'stars', 'value', 'label'] },\n reviewsBand: {\n scope: '[data-bl-section=\"reviews\"] [data-slot=\"list\"]',\n slots: ['item', 'stars', 'quote', 'more', 'author', 'avatar', 'name', 'date'],\n },\n contactBlock: { scope: '[data-bl-section=\"contact\"] [data-slot=\"details\"]', slots: ['line'] },\n servicesGrid: {\n scope: '[data-bl-section=\"services\"] [data-slot=\"list\"]',\n slots: ['item', 'body', 'media', 'title', 'summary', 'points', 'point', 'action', 'link'],\n },\n breadcrumbs: { scope: '[data-bl=\"breadcrumbs\"]', slots: ['item', 'link', 'current'] },\n // Location sections (3.15.0). Their link lists are `LinkList`s, reached through `linkList` (`[data-bl=\"link-list\"]`).\n localFacts: { scope: '[data-bl-section=\"local-facts\"]', slots: ['fact', 'factSource', 'fee'] },\n councilRules: { scope: '[data-bl-section=\"council-rules\"]', slots: ['fee'] },\n nearbyJobs: { scope: '[data-bl-section=\"nearby-jobs\"] [data-slot=\"list\"]', slots: ['item', 'body', 'media', 'title', 'link', 'outcome'] },\n coverage: { scope: '[data-bl-section=\"coverage\"] [data-slot=\"groups\"]', slots: ['group', 'groupHeading'] },\n relatedServices: {\n scope: '[data-bl-section=\"related-services\"] [data-slot=\"list\"]',\n slots: ['item', 'body', 'media', 'title', 'summary', 'link'],\n },\n // Proof sections (3.15.0). Their photos are nested `[data-bl=\"media\"]` frames; project cards are `[data-bl=\"card\"]`.\n gallery: { scope: '[data-bl-section=\"gallery\"] [data-slot=\"list\"]', slots: ['item', 'link'] },\n beforeAfter: { scope: '[data-bl-section=\"before-after\"] [data-slot=\"list\"]', slots: ['pair', 'frames', 'frame', 'note'] },\n licences: {\n scope: '[data-bl-section=\"licences\"] [data-slot=\"list\"]',\n slots: ['row', 'label', 'value', 'number', 'holder', 'classes', 'class', 'register', 'cover', 'amount'],\n },\n thankYou: { scope: '[data-bl=\"thank-you\"]', slots: ['step'] },\n photoUpload: { scope: '[data-bl=\"photo-upload\"]', slots: ['photo', 'thumb', 'remove'] },\n quoteForm: { scope: '[data-bl=\"quote-form\"] [data-bl=\"photo-upload\"]', slots: ['photo', 'thumb', 'remove'] },\n // Primitives rendered once per list entry (3.14.0): every part is repeated, the root included (style the root\n // through its own hook, `[data-bl=\"card\"]`).\n card: { scope: '[data-bl=\"card\"]', slots: ['media', 'body', 'title', 'link', 'text', 'meta'] },\n badge: { scope: '[data-bl=\"badge\"]', slots: ['icon', 'label'] },\n chip: { scope: '[data-bl=\"chip\"]', slots: ['icon', 'label'] },\n stat: { scope: '[data-bl=\"stat\"]', slots: ['value', 'label', 'link'] },\n tickList: { scope: '[data-bl=\"tick-list\"]', slots: ['item', 'tick', 'text', 'link'] },\n /** A lean list (cost item 1f): plain `<li><a>` entries with no `data-slot`, reached by element. */\n linkList: { scope: '[data-bl=\"link-list\"]', slots: ['item', 'link'], hooks: { item: '> li', link: '> li > a' } },\n} as const\n\nexport type RepeatedSlotComponent = keyof typeof REPEATED_SLOTS\n/** The repeated slots of one component, for example `RepeatedSlotOf<'siteFooter'>` is `'licenceLink' | … | 'policyLink'`. */\nexport type RepeatedSlotOf<Component extends RepeatedSlotComponent> = (typeof REPEATED_SLOTS)[Component]['slots'][number]\n\n/** The selector a skin stylesheet uses for one repeated slot, for example `[data-bl=\"footer\"] [data-slot=\"link\"]`. */\nexport function repeatedSlotSelector<Component extends RepeatedSlotComponent>(component: Component, slot: RepeatedSlotOf<Component>): string {\n const entry: { readonly scope: string; readonly hooks?: Readonly<Record<string, string>> } = REPEATED_SLOTS[component]\n return `${entry.scope} ${entry.hooks?.[slot] ?? `[data-slot=\"${slot}\"]`}`\n}\n"]}
@@ -0,0 +1,64 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { AreaMapModel } from '@brandlab-digital/site-core/area-map';
3
+ import { type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order';
4
+ import { type LinkListEntry, type LinkListLayout } from '../primitives/lists.js';
5
+ import { type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js';
6
+ import { type SkinEntry } from '../skin.js';
7
+ import type { UiLink } from '../types.js';
8
+ /** The map a page shows, from its model's kind: `locator` (suburb or town), `region` or `overview`. */
9
+ export declare const AREA_MAP_VARIANTS: readonly ["locator", "region", "overview"];
10
+ export type AreaMapVariant = (typeof AREA_MAP_VARIANTS)[number];
11
+ export type AreaMapSlot = SectionSlot | SectionHeadingSlot | 'body' | 'figure' | 'frame' | 'map' | 'caption' | 'legend' | 'legendBase' | 'legendJobs' | 'credit' | 'places' | 'placesTitle' | 'list' | 'region' | 'more';
12
+ export interface AreaMapLayout extends LinkListLayout {
13
+ /** The place list beside the map on wide screens (default) or below it. */
14
+ readonly list?: 'beside' | 'below';
15
+ /** Draw place names inside their shapes where they fit (default true). */
16
+ readonly names?: boolean;
17
+ }
18
+ /** The variant follows the model, so a skin sets the look (layout and classes) only. */
19
+ export type AreaMapSkin = SkinEntry<AreaMapSlot, never, AreaMapLayout>;
20
+ /** A model from site-core's map engine and the URL of its base layer file (written with `linked: true`). */
21
+ export interface AreaMapData {
22
+ readonly model: AreaMapModel;
23
+ /** For example `/area-maps/suburb-castle-hill.svg`. */
24
+ readonly baseHref: string;
25
+ }
26
+ export interface AreaMapProps extends AreaMapSkin {
27
+ /** The H2, for example "Where we work around Castle Hill". */
28
+ readonly title: ReactNode;
29
+ readonly eyebrow?: ReactNode;
30
+ readonly lead?: ReactNode;
31
+ /** The model and its base file; `undefined` (a locator with no neighbours or jobs) renders nothing. */
32
+ readonly map: AreaMapData | undefined;
33
+ /** What the map shows, in the site's words (the figure's caption, before the boundary credit). */
34
+ readonly caption: ReactNode;
35
+ /** A heading over the place list (an H3), for example "Suburbs we serve near Castle Hill". */
36
+ readonly placesTitle?: ReactNode;
37
+ /** The text alternative. Defaults to `areaMapPlaces(map.model)`. */
38
+ readonly places?: readonly LinkListEntry[];
39
+ /** Legend lines in the site's words, shown only when the map has that marker. */
40
+ readonly legend?: {
41
+ readonly base?: ReactNode;
42
+ readonly jobs?: ReactNode;
43
+ };
44
+ /** The region's page on a locator, for example `{ label: 'All of the Hills District', href }`. Never prefetched. */
45
+ readonly regionLink?: UiLink;
46
+ /** A link after the list, for example the areas hub. Never prefetched. */
47
+ readonly more?: UiLink;
48
+ /** Defaults to `area-map-heading`. */
49
+ readonly headingId?: string;
50
+ /** Set by `SectionSequence`. */
51
+ readonly tone?: SectionTone;
52
+ }
53
+ /**
54
+ * The map's text alternative: its linked places as links and its served places without an indexable page as plain
55
+ * names, in the model's order (a locator's nearest first, a region's by name). The page's own place is left out.
56
+ * `links: false` lists every name as plain text, for a page whose `NearbyAreas` or `Coverage` links them already.
57
+ */
58
+ export declare function areaMapPlaces(model: AreaMapModel, { links }?: {
59
+ readonly links?: boolean;
60
+ }): LinkListEntry[];
61
+ /** The guard for `SectionSequence`: a locator with no neighbours and no jobs has no map. */
62
+ export declare function areaMapReady({ map }: Pick<AreaMapProps, 'map'>): BrandlabSectionReadiness;
63
+ export declare function AreaMap({ title, eyebrow, lead, map, caption, placesTitle, places, legend, regionLink, more, headingId, tone, layout, classes, }: AreaMapProps): import("react/jsx-runtime").JSX.Element | null;
64
+ //# sourceMappingURL=area-map.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"area-map.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/area-map.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,142 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * `AreaMap` (`area-map`, 3.15.0): the service-area map of a suburb page (`locator`: the suburb, its nearest served
4
+ * neighbours and nearby jobs) or a region page (`region`: the region's suburbs), and the overview of the home page or
5
+ * locations hub (`overview`). Server component, no client JS.
6
+ *
7
+ * Data comes from site-core's map engine (`./area-map`, since 3.6.0): `suburbLocator`, `regionMap` or `overviewMap`
8
+ * build a plain model on the server, and the site serves the model's static base layer once as a cached SVG file
9
+ * (`renderAreaMapBaseSvg(site, model, { linked: true })`, for example at `/area-maps/<model.id>.svg`). The page's SVG
10
+ * is lean: land, the focus area, served areas and outlines are `<use>` references into that file, and each linked
11
+ * place is an `<a>` around a `<use>` of its shape (`areaMapShapeId`), so no boundary path sits in the page's HTML.
12
+ *
13
+ * The drawing is `aria-hidden` and its links are out of the tab order; the text alternative is the place list beside
14
+ * it (`areaMapPlaces(model)`: the linked places as links, served places without an indexable page as plain names), a
15
+ * lean `LinkList` marked `data-bl-links="map"` that never prefetches (W2). Where the page's `NearbyAreas` or `Coverage`
16
+ * already links the same places, pass `areaMapPlaces(model, { links: false })` so the map lists their names only. The
17
+ * figure's caption is site copy; the boundary credit (CC BY 4.0) always follows it.
18
+ */
19
+ import Link from 'next/link';
20
+ import { SECTION_READY } from '@brandlab-digital/site-core/section-order';
21
+ import { LinkList } from '../primitives/lists.js';
22
+ import { Section, SectionHeading } from '../primitives/layout.js';
23
+ import { slot } from '../skin.js';
24
+ /** The map a page shows, from its model's kind: `locator` (suburb or town), `region` or `overview`. */
25
+ export const AREA_MAP_VARIANTS = ['locator', 'region', 'overview'];
26
+ const VARIANT_OF = { overview: 'overview', region: 'region', suburb: 'locator' };
27
+ /** Which shapes carry their name: the overview names every region with room; close-ups name the served places. */
28
+ const NAMED = {
29
+ overview: ['linked', 'served', 'muted'],
30
+ region: ['linked', 'served'],
31
+ suburb: ['focus', 'linked', 'served'],
32
+ };
33
+ /** A link's element id in the base file, as site-core's `areaMapShapeId` (kept here so the page never loads the boundary data). */
34
+ const shapeId = (key) => key.replace(':', '-');
35
+ /**
36
+ * The map's text alternative: its linked places as links and its served places without an indexable page as plain
37
+ * names, in the model's order (a locator's nearest first, a region's by name). The page's own place is left out.
38
+ * `links: false` lists every name as plain text, for a page whose `NearbyAreas` or `Coverage` links them already.
39
+ */
40
+ export function areaMapPlaces(model, { links = true } = {}) {
41
+ const hrefOf = new Map(model.shapes.map((shape) => [shape.key, shape.href]));
42
+ const seen = new Set();
43
+ const entries = [];
44
+ const add = (key, label) => {
45
+ if (seen.has(key))
46
+ return;
47
+ seen.add(key);
48
+ const href = hrefOf.get(key);
49
+ entries.push(links && href !== undefined ? { label, href } : { label });
50
+ };
51
+ for (const label of model.labels)
52
+ if (label.state === 'linked' || label.state === 'served')
53
+ add(label.key, label.name);
54
+ for (const shape of model.shapes)
55
+ add(shape.key, shape.name);
56
+ // A region's outlying served places are left out of its frame (3.15.0), so only the list names them.
57
+ for (const place of model.outside ?? []) {
58
+ if (seen.has(place.key))
59
+ continue;
60
+ seen.add(place.key);
61
+ entries.push(links && place.href !== undefined ? { label: place.name, href: place.href } : { label: place.name });
62
+ }
63
+ if (model.kind === 'region')
64
+ entries.sort((a, b) => a.label.localeCompare(b.label));
65
+ return entries;
66
+ }
67
+ /** The guard for `SectionSequence`: a locator with no neighbours and no jobs has no map. */
68
+ export function areaMapReady({ map }) {
69
+ return map ? SECTION_READY : { render: false, reason: 'no-area-map' };
70
+ }
71
+ /** Whether a name fits inside its shape around the label point (`room` is the distance to the nearest edge). */
72
+ function fits(label, size) {
73
+ const halfWidth = (label.name.length * size * 0.56) / 2;
74
+ return halfWidth <= label.room * 1.4 && size * 0.6 <= label.room;
75
+ }
76
+ const percent = (value) => `${Math.round(value * 100) / 100}%`;
77
+ /** A marker's centre moved inside the frame by its radius and stroke. */
78
+ function keepInside(point, r, model) {
79
+ const pad = r * 1.2;
80
+ return { x: Math.round(Math.min(Math.max(point.x, pad), model.width - pad)), y: Math.round(Math.min(Math.max(point.y, pad), model.height - pad)), r };
81
+ }
82
+ /**
83
+ * Where a name sits (3.15.0): centred on its label point and inside the frame, or, when that would cover a job pin or
84
+ * the base, just below or above the marker if that stays near its shape. A name with no clear place is not drawn (the
85
+ * place list still names it). Returns the text's x and baseline, or undefined.
86
+ */
87
+ function placeName(label, size, marks, model) {
88
+ const half = (label.name.length * size * 0.56) / 2;
89
+ const pad = size * 0.3;
90
+ const x = Math.min(Math.max(label.x, half + pad), model.width - half - pad);
91
+ const top = size * 0.8 + pad;
92
+ const bottom = model.height - size * 0.25 - pad;
93
+ const clear = (baseline) => marks.every((mark) => mark.x + mark.r + pad < x - half ||
94
+ mark.x - mark.r - pad > x + half ||
95
+ mark.y + mark.r + pad < baseline - size * 0.8 ||
96
+ mark.y - mark.r - pad > baseline + size * 0.25);
97
+ const natural = label.y + size * 0.35;
98
+ // The nearest clear place first: just below or above whichever marker it would cover.
99
+ const candidates = [natural, ...marks.flatMap((mark) => [mark.y + mark.r + pad + size * 0.85, mark.y - mark.r - pad - size * 0.3])].sort((one, other) => Math.abs(one - natural) - Math.abs(other - natural));
100
+ for (const candidate of candidates) {
101
+ const y = Math.min(Math.max(candidate, top), bottom);
102
+ if (Math.abs(y - natural) > Math.max(label.room * 1.5, size))
103
+ continue;
104
+ if (clear(y))
105
+ return { x: Math.round(x), y: Math.round(y) };
106
+ }
107
+ return undefined;
108
+ }
109
+ export function AreaMap({ title, eyebrow, lead, map, caption, placesTitle, places, legend, regionLink, more, headingId = 'area-map-heading', tone, layout, classes, }) {
110
+ if (!map)
111
+ return null;
112
+ const { model, baseHref } = map;
113
+ if (!/\.svg(?:$|[?#])/.test(baseHref))
114
+ throw new Error(`AreaMap: baseHref must be the model's base SVG file, not "${baseHref}".`);
115
+ const list = places ?? areaMapPlaces(model);
116
+ if (regionLink && list.some((entry) => entry.href === regionLink.href))
117
+ throw new Error('AreaMap: the region link repeats a listed place.');
118
+ const size = Math.round(model.width * 0.022);
119
+ const marker = Math.round(model.width * 0.009);
120
+ const names = layout?.names === false ? [] : model.labels.filter((label) => NAMED[model.kind].includes(label.state) && fits(label, size));
121
+ const baseMarker = model.markers.find((point) => point.kind === 'base');
122
+ const base = baseMarker ? keepInside(baseMarker, marker, model) : undefined;
123
+ const jobs = model.markers.filter((point) => point.kind === 'job').map((point) => ({ slug: point.slug, ...keepInside(point, marker, model) }));
124
+ const marks = base ? [...jobs, base] : jobs;
125
+ const placed = names.flatMap((label) => {
126
+ const at = placeName(label, size, marks, model);
127
+ return at ? [{ label, ...at }] : [];
128
+ });
129
+ const { crop } = model;
130
+ const frame = {
131
+ '--bl-map-ratio': `${model.width} / ${model.height}`,
132
+ '--bl-map-crop-ratio': `${crop.width} / ${crop.height}`,
133
+ '--bl-map-crop-size': percent((model.width / crop.width) * 100),
134
+ '--bl-map-crop-x': percent((-crop.x / crop.width) * 100),
135
+ '--bl-map-crop-y': percent((-crop.y / crop.height) * 100),
136
+ };
137
+ const use = (id) => `${baseHref}#${id}`;
138
+ const legendBase = base && legend?.base ? legend.base : null;
139
+ const legendJobs = jobs.length > 0 && legend?.jobs ? legend.jobs : null;
140
+ return (_jsxs(Section, { id: "area-map", labelledBy: headingId, tone: tone, variant: VARIANT_OF[model.kind], classes: classes, hooks: { 'data-list': layout?.list ?? 'beside' }, children: [_jsx(SectionHeading, { id: headingId, title: title, eyebrow: eyebrow, lead: lead, classes: classes }), _jsxs("div", { ...slot(classes, 'body'), children: [_jsxs("figure", { ...slot(classes, 'figure'), children: [_jsx("div", { ...slot(classes, 'frame'), style: frame, children: _jsxs("svg", { ...slot(classes, 'map'), viewBox: model.viewBox, "aria-hidden": "true", focusable: "false", children: [_jsx("rect", { "data-layer": "water", width: model.width, height: model.height }), _jsx("use", { "data-layer": "land", href: use('land') }), model.layers.focus.length > 0 ? _jsx("use", { "data-layer": "focus", href: use('focus') }) : null, model.layers.served.length > 0 ? _jsx("use", { "data-layer": "served", href: use('served') }) : null, model.shapes.length > 0 ? (_jsx("g", { "data-layer": "linked", children: model.shapes.map((shape) => (_jsx("a", { href: shape.href, tabIndex: -1, children: _jsx("use", { href: use(shapeId(shape.key)) }) }, shape.key))) })) : null, _jsx("use", { "data-layer": "outline", href: use('outline') }), placed.length > 0 ? (_jsx("g", { "data-layer": "names", fontSize: size, strokeWidth: Math.round(size * 0.2), children: placed.map(({ label, x, y }) => (_jsx("text", { x: x, y: y, "data-state": label.state, children: label.name }, label.key))) })) : null, jobs.length > 0 ? (_jsx("g", { "data-layer": "jobs", strokeWidth: Math.round(marker * 0.35), children: jobs.map((point) => (_jsx("circle", { cx: point.x, cy: point.y, r: marker }, point.slug))) })) : null, base ? (_jsx("rect", { "data-layer": "base", x: base.x - marker, y: base.y - marker, width: marker * 2, height: marker * 2, strokeWidth: Math.round(marker * 0.35) })) : null] }) }), _jsxs("figcaption", { ...slot(classes, 'caption'), children: [caption, legendBase || legendJobs ? (_jsxs("span", { ...slot(classes, 'legend'), children: [legendBase ? (_jsxs("span", { ...slot(classes, 'legendBase'), children: [_jsx("svg", { viewBox: "0 0 12 12", "aria-hidden": "true", focusable: "false", "data-layer": "base", children: _jsx("rect", { x: "2", y: "2", width: "8", height: "8", strokeWidth: "1.5" }) }), legendBase] })) : null, legendJobs ? (_jsxs("span", { ...slot(classes, 'legendJobs'), children: [_jsx("svg", { viewBox: "0 0 12 12", "aria-hidden": "true", focusable: "false", "data-layer": "jobs", children: _jsx("circle", { cx: "6", cy: "6", r: "4.5", strokeWidth: "1.5" }) }), legendJobs] })) : null] })) : null, _jsx("small", { ...slot(classes, 'credit'), children: model.attribution })] })] }), list.length > 0 || regionLink || more ? (_jsxs("div", { ...slot(classes, 'places'), children: [placesTitle && list.length > 0 ? _jsx("h3", { ...slot(classes, 'placesTitle'), children: placesTitle }) : null, _jsx(LinkList, { links: list, kind: "map", layout: layout?.columns !== undefined ? { columns: layout.columns } : undefined, classes: { root: classes?.list } }), regionLink ? (_jsx("p", { ...slot(classes, 'region'), children: _jsx(Link, { href: regionLink.href, prefetch: false, children: regionLink.label }) })) : null, more ? (_jsx("p", { ...slot(classes, 'more'), children: _jsx(Link, { href: more.href, prefetch: false, children: more.label }) })) : null] })) : null] })] }));
141
+ }
142
+ //# sourceMappingURL=area-map.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"area-map.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/area-map.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * `AreaMap` (`area-map`, 3.15.0): the service-area map of a suburb page (`locator`: the suburb, its nearest served\n * neighbours and nearby jobs) or a region page (`region`: the region's suburbs), and the overview of the home page or\n * locations hub (`overview`). Server component, no client JS.\n *\n * Data comes from site-core's map engine (`./area-map`, since 3.6.0): `suburbLocator`, `regionMap` or `overviewMap`\n * build a plain model on the server, and the site serves the model's static base layer once as a cached SVG file\n * (`renderAreaMapBaseSvg(site, model, { linked: true })`, for example at `/area-maps/<model.id>.svg`). The page's SVG\n * is lean: land, the focus area, served areas and outlines are `<use>` references into that file, and each linked\n * place is an `<a>` around a `<use>` of its shape (`areaMapShapeId`), so no boundary path sits in the page's HTML.\n *\n * The drawing is `aria-hidden` and its links are out of the tab order; the text alternative is the place list beside\n * it (`areaMapPlaces(model)`: the linked places as links, served places without an indexable page as plain names), a\n * lean `LinkList` marked `data-bl-links=\"map\"` that never prefetches (W2). Where the page's `NearbyAreas` or `Coverage`\n * already links the same places, pass `areaMapPlaces(model, { links: false })` so the map lists their names only. The\n * figure's caption is site copy; the boundary credit (CC BY 4.0) always follows it.\n */\nimport Link from 'next/link'\nimport type { CSSProperties, ReactNode } from 'react'\nimport type { AreaMapLabel, AreaMapModel } from '@brandlab-digital/site-core/area-map'\nimport { SECTION_READY, type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order'\nimport { LinkList, type LinkListEntry, type LinkListLayout } from '../primitives/lists.js'\nimport { Section, SectionHeading, type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js'\nimport { slot, type SkinEntry } from '../skin.js'\nimport type { UiLink } from '../types.js'\n\n/** The map a page shows, from its model's kind: `locator` (suburb or town), `region` or `overview`. */\nexport const AREA_MAP_VARIANTS = ['locator', 'region', 'overview'] as const\nexport type AreaMapVariant = (typeof AREA_MAP_VARIANTS)[number]\n\nexport type AreaMapSlot =\n | SectionSlot\n | SectionHeadingSlot\n | 'body'\n | 'figure'\n | 'frame'\n | 'map'\n | 'caption'\n | 'legend'\n | 'legendBase'\n | 'legendJobs'\n | 'credit'\n | 'places'\n | 'placesTitle'\n | 'list'\n | 'region'\n | 'more'\n\nexport interface AreaMapLayout extends LinkListLayout {\n /** The place list beside the map on wide screens (default) or below it. */\n readonly list?: 'beside' | 'below'\n /** Draw place names inside their shapes where they fit (default true). */\n readonly names?: boolean\n}\n\n/** The variant follows the model, so a skin sets the look (layout and classes) only. */\nexport type AreaMapSkin = SkinEntry<AreaMapSlot, never, AreaMapLayout>\n\n/** A model from site-core's map engine and the URL of its base layer file (written with `linked: true`). */\nexport interface AreaMapData {\n readonly model: AreaMapModel\n /** For example `/area-maps/suburb-castle-hill.svg`. */\n readonly baseHref: string\n}\n\nexport interface AreaMapProps extends AreaMapSkin {\n /** The H2, for example \"Where we work around Castle Hill\". */\n readonly title: ReactNode\n readonly eyebrow?: ReactNode\n readonly lead?: ReactNode\n /** The model and its base file; `undefined` (a locator with no neighbours or jobs) renders nothing. */\n readonly map: AreaMapData | undefined\n /** What the map shows, in the site's words (the figure's caption, before the boundary credit). */\n readonly caption: ReactNode\n /** A heading over the place list (an H3), for example \"Suburbs we serve near Castle Hill\". */\n readonly placesTitle?: ReactNode\n /** The text alternative. Defaults to `areaMapPlaces(map.model)`. */\n readonly places?: readonly LinkListEntry[]\n /** Legend lines in the site's words, shown only when the map has that marker. */\n readonly legend?: { readonly base?: ReactNode; readonly jobs?: ReactNode }\n /** The region's page on a locator, for example `{ label: 'All of the Hills District', href }`. Never prefetched. */\n readonly regionLink?: UiLink\n /** A link after the list, for example the areas hub. Never prefetched. */\n readonly more?: UiLink\n /** Defaults to `area-map-heading`. */\n readonly headingId?: string\n /** Set by `SectionSequence`. */\n readonly tone?: SectionTone\n}\n\nconst VARIANT_OF: Readonly<Record<AreaMapModel['kind'], AreaMapVariant>> = { overview: 'overview', region: 'region', suburb: 'locator' }\n\n/** Which shapes carry their name: the overview names every region with room; close-ups name the served places. */\nconst NAMED: Readonly<Record<AreaMapModel['kind'], readonly AreaMapLabel['state'][]>> = {\n overview: ['linked', 'served', 'muted'],\n region: ['linked', 'served'],\n suburb: ['focus', 'linked', 'served'],\n}\n\n/** A link's element id in the base file, as site-core's `areaMapShapeId` (kept here so the page never loads the boundary data). */\nconst shapeId = (key: string) => key.replace(':', '-')\n\n/**\n * The map's text alternative: its linked places as links and its served places without an indexable page as plain\n * names, in the model's order (a locator's nearest first, a region's by name). The page's own place is left out.\n * `links: false` lists every name as plain text, for a page whose `NearbyAreas` or `Coverage` links them already.\n */\nexport function areaMapPlaces(model: AreaMapModel, { links = true }: { readonly links?: boolean } = {}): LinkListEntry[] {\n const hrefOf = new Map(model.shapes.map((shape) => [shape.key, shape.href]))\n const seen = new Set<string>()\n const entries: LinkListEntry[] = []\n const add = (key: string, label: string) => {\n if (seen.has(key)) return\n seen.add(key)\n const href = hrefOf.get(key)\n entries.push(links && href !== undefined ? { label, href } : { label })\n }\n for (const label of model.labels) if (label.state === 'linked' || label.state === 'served') add(label.key, label.name)\n for (const shape of model.shapes) add(shape.key, shape.name)\n // A region's outlying served places are left out of its frame (3.15.0), so only the list names them.\n for (const place of model.outside ?? []) {\n if (seen.has(place.key)) continue\n seen.add(place.key)\n entries.push(links && place.href !== undefined ? { label: place.name, href: place.href } : { label: place.name })\n }\n if (model.kind === 'region') entries.sort((a, b) => a.label.localeCompare(b.label))\n return entries\n}\n\n/** The guard for `SectionSequence`: a locator with no neighbours and no jobs has no map. */\nexport function areaMapReady({ map }: Pick<AreaMapProps, 'map'>): BrandlabSectionReadiness {\n return map ? SECTION_READY : { render: false, reason: 'no-area-map' }\n}\n\n/** Whether a name fits inside its shape around the label point (`room` is the distance to the nearest edge). */\nfunction fits(label: AreaMapLabel, size: number): boolean {\n const halfWidth = (label.name.length * size * 0.56) / 2\n return halfWidth <= label.room * 1.4 && size * 0.6 <= label.room\n}\n\nconst percent = (value: number) => `${Math.round(value * 100) / 100}%`\n\ninterface Mark {\n readonly x: number\n readonly y: number\n readonly r: number\n}\n\n/** A marker's centre moved inside the frame by its radius and stroke. */\nfunction keepInside(point: { x: number; y: number }, r: number, model: AreaMapModel): Mark {\n const pad = r * 1.2\n return { x: Math.round(Math.min(Math.max(point.x, pad), model.width - pad)), y: Math.round(Math.min(Math.max(point.y, pad), model.height - pad)), r }\n}\n\n/**\n * Where a name sits (3.15.0): centred on its label point and inside the frame, or, when that would cover a job pin or\n * the base, just below or above the marker if that stays near its shape. A name with no clear place is not drawn (the\n * place list still names it). Returns the text's x and baseline, or undefined.\n */\nfunction placeName(label: AreaMapLabel, size: number, marks: readonly Mark[], model: AreaMapModel): { x: number; y: number } | undefined {\n const half = (label.name.length * size * 0.56) / 2\n const pad = size * 0.3\n const x = Math.min(Math.max(label.x, half + pad), model.width - half - pad)\n const top = size * 0.8 + pad\n const bottom = model.height - size * 0.25 - pad\n const clear = (baseline: number) =>\n marks.every(\n (mark) =>\n mark.x + mark.r + pad < x - half ||\n mark.x - mark.r - pad > x + half ||\n mark.y + mark.r + pad < baseline - size * 0.8 ||\n mark.y - mark.r - pad > baseline + size * 0.25,\n )\n const natural = label.y + size * 0.35\n // The nearest clear place first: just below or above whichever marker it would cover.\n const candidates = [natural, ...marks.flatMap((mark) => [mark.y + mark.r + pad + size * 0.85, mark.y - mark.r - pad - size * 0.3])].sort(\n (one, other) => Math.abs(one - natural) - Math.abs(other - natural),\n )\n for (const candidate of candidates) {\n const y = Math.min(Math.max(candidate, top), bottom)\n if (Math.abs(y - natural) > Math.max(label.room * 1.5, size)) continue\n if (clear(y)) return { x: Math.round(x), y: Math.round(y) }\n }\n return undefined\n}\n\nexport function AreaMap({\n title,\n eyebrow,\n lead,\n map,\n caption,\n placesTitle,\n places,\n legend,\n regionLink,\n more,\n headingId = 'area-map-heading',\n tone,\n layout,\n classes,\n}: AreaMapProps) {\n if (!map) return null\n const { model, baseHref } = map\n if (!/\\.svg(?:$|[?#])/.test(baseHref)) throw new Error(`AreaMap: baseHref must be the model's base SVG file, not \"${baseHref}\".`)\n const list = places ?? areaMapPlaces(model)\n if (regionLink && list.some((entry) => entry.href === regionLink.href)) throw new Error('AreaMap: the region link repeats a listed place.')\n const size = Math.round(model.width * 0.022)\n const marker = Math.round(model.width * 0.009)\n const names = layout?.names === false ? [] : model.labels.filter((label) => NAMED[model.kind].includes(label.state) && fits(label, size))\n const baseMarker = model.markers.find((point) => point.kind === 'base')\n const base = baseMarker ? keepInside(baseMarker, marker, model) : undefined\n const jobs = model.markers.filter((point) => point.kind === 'job').map((point) => ({ slug: point.slug, ...keepInside(point, marker, model) }))\n const marks: readonly Mark[] = base ? [...jobs, base] : jobs\n const placed = names.flatMap((label) => {\n const at = placeName(label, size, marks, model)\n return at ? [{ label, ...at }] : []\n })\n const { crop } = model\n const frame = {\n '--bl-map-ratio': `${model.width} / ${model.height}`,\n '--bl-map-crop-ratio': `${crop.width} / ${crop.height}`,\n '--bl-map-crop-size': percent((model.width / crop.width) * 100),\n '--bl-map-crop-x': percent((-crop.x / crop.width) * 100),\n '--bl-map-crop-y': percent((-crop.y / crop.height) * 100),\n } as CSSProperties\n const use = (id: string) => `${baseHref}#${id}`\n const legendBase = base && legend?.base ? legend.base : null\n const legendJobs = jobs.length > 0 && legend?.jobs ? legend.jobs : null\n return (\n <Section\n id=\"area-map\"\n labelledBy={headingId}\n tone={tone}\n variant={VARIANT_OF[model.kind]}\n classes={classes}\n hooks={{ 'data-list': layout?.list ?? 'beside' }}\n >\n <SectionHeading id={headingId} title={title} eyebrow={eyebrow} lead={lead} classes={classes} />\n <div {...slot(classes, 'body')}>\n <figure {...slot(classes, 'figure')}>\n <div {...slot(classes, 'frame')} style={frame}>\n <svg {...slot(classes, 'map')} viewBox={model.viewBox} aria-hidden=\"true\" focusable=\"false\">\n <rect data-layer=\"water\" width={model.width} height={model.height} />\n <use data-layer=\"land\" href={use('land')} />\n {model.layers.focus.length > 0 ? <use data-layer=\"focus\" href={use('focus')} /> : null}\n {model.layers.served.length > 0 ? <use data-layer=\"served\" href={use('served')} /> : null}\n {model.shapes.length > 0 ? (\n <g data-layer=\"linked\">\n {model.shapes.map((shape) => (\n <a key={shape.key} href={shape.href} tabIndex={-1}>\n <use href={use(shapeId(shape.key))} />\n </a>\n ))}\n </g>\n ) : null}\n <use data-layer=\"outline\" href={use('outline')} />\n {placed.length > 0 ? (\n <g data-layer=\"names\" fontSize={size} strokeWidth={Math.round(size * 0.2)}>\n {placed.map(({ label, x, y }) => (\n <text key={label.key} x={x} y={y} data-state={label.state}>\n {label.name}\n </text>\n ))}\n </g>\n ) : null}\n {jobs.length > 0 ? (\n <g data-layer=\"jobs\" strokeWidth={Math.round(marker * 0.35)}>\n {jobs.map((point) => (\n <circle key={point.slug} cx={point.x} cy={point.y} r={marker} />\n ))}\n </g>\n ) : null}\n {base ? (\n <rect\n data-layer=\"base\"\n x={base.x - marker}\n y={base.y - marker}\n width={marker * 2}\n height={marker * 2}\n strokeWidth={Math.round(marker * 0.35)}\n />\n ) : null}\n </svg>\n </div>\n <figcaption {...slot(classes, 'caption')}>\n {caption}\n {legendBase || legendJobs ? (\n <span {...slot(classes, 'legend')}>\n {legendBase ? (\n <span {...slot(classes, 'legendBase')}>\n <svg viewBox=\"0 0 12 12\" aria-hidden=\"true\" focusable=\"false\" data-layer=\"base\">\n <rect x=\"2\" y=\"2\" width=\"8\" height=\"8\" strokeWidth=\"1.5\" />\n </svg>\n {legendBase}\n </span>\n ) : null}\n {legendJobs ? (\n <span {...slot(classes, 'legendJobs')}>\n <svg viewBox=\"0 0 12 12\" aria-hidden=\"true\" focusable=\"false\" data-layer=\"jobs\">\n <circle cx=\"6\" cy=\"6\" r=\"4.5\" strokeWidth=\"1.5\" />\n </svg>\n {legendJobs}\n </span>\n ) : null}\n </span>\n ) : null}\n <small {...slot(classes, 'credit')}>{model.attribution}</small>\n </figcaption>\n </figure>\n {list.length > 0 || regionLink || more ? (\n <div {...slot(classes, 'places')}>\n {placesTitle && list.length > 0 ? <h3 {...slot(classes, 'placesTitle')}>{placesTitle}</h3> : null}\n <LinkList links={list} kind=\"map\" layout={layout?.columns !== undefined ? { columns: layout.columns } : undefined} classes={{ root: classes?.list }} />\n {regionLink ? (\n <p {...slot(classes, 'region')}>\n <Link href={regionLink.href} prefetch={false}>\n {regionLink.label}\n </Link>\n </p>\n ) : null}\n {more ? (\n <p {...slot(classes, 'more')}>\n <Link href={more.href} prefetch={false}>\n {more.label}\n </Link>\n </p>\n ) : null}\n </div>\n ) : null}\n </div>\n </Section>\n )\n}\n"]}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * `BeforeAfter` (`before-after`, 3.15.0): genuine photos of the same job before and after the work, side by side. Server
3
+ * component, no client JS and no slider: two frames, each a `<figure>` whose caption is the site's "Before" or "After"
4
+ * label, so the pair reads the same to a screen reader, in print and with scripts off.
5
+ *
6
+ * Locked:
7
+ * - both photos are genuine and come from the same job: each media entry names the job (`job`, the job log id) and
8
+ * the two must match; a stock or AI image, two different jobs or one photo used twice fails the build;
9
+ * - before first, after second, in source order and on screen;
10
+ * - both frames crop to one ratio (4:3 by default) so they line up; every photo loads lazily;
11
+ * - the "Before" and "After" labels are props (no default copy), as is the caption under the pair.
12
+ */
13
+ import type { ReactNode } from 'react';
14
+ import type { BrandlabMediaEntry, BrandlabMediaRatio } from '@brandlab-digital/site-core/media';
15
+ import { type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order';
16
+ import { type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js';
17
+ import type { RepeatedSlotOf } from '../repeated-slots.js';
18
+ import { type SkinEntry } from '../skin.js';
19
+ /** Rendered once per pair: styled from the skin stylesheet, never by class. The frames are `[data-bl="media"]`. */
20
+ export type BeforeAfterRepeatedSlot = RepeatedSlotOf<'beforeAfter'>;
21
+ export type BeforeAfterSlot = SectionSlot | SectionHeadingSlot | 'list' | BeforeAfterRepeatedSlot;
22
+ export interface BeforeAfterLayout {
23
+ /** Both frames' crop. Defaults to 4:3. */
24
+ readonly ratio?: BrandlabMediaRatio;
25
+ /** `side` (the default) puts the frames side by side from 640 px of the section's width; `stack` keeps them stacked. */
26
+ readonly arrange?: 'side' | 'stack';
27
+ }
28
+ export type BeforeAfterSkin = SkinEntry<BeforeAfterSlot, never, BeforeAfterLayout, BeforeAfterRepeatedSlot>;
29
+ /** One job, before and after. Both entries name the job (`job`). */
30
+ export interface BeforeAfterPair {
31
+ /** A stable slug (the job id), rendered as `data-item`. */
32
+ readonly id: string;
33
+ readonly before: BrandlabMediaEntry;
34
+ readonly after: BrandlabMediaEntry;
35
+ /** Under the pair: "{what}, {suburb}, {month year}". */
36
+ readonly caption?: ReactNode;
37
+ }
38
+ export interface BeforeAfterProps extends BeforeAfterSkin {
39
+ /** The H2, for example "Before and after". */
40
+ readonly title: ReactNode;
41
+ readonly eyebrow?: ReactNode;
42
+ readonly lead?: ReactNode;
43
+ readonly pairs: readonly BeforeAfterPair[];
44
+ /** The frames' visible captions, in the site's words: `{ before: 'Before', after: 'After' }`. */
45
+ readonly labels: {
46
+ readonly before: string;
47
+ readonly after: string;
48
+ };
49
+ /** The frames' `sizes` (a `vw` string). Defaults to half the width from 640 px side by side, else the full width. */
50
+ readonly imageSizes?: string;
51
+ /** Defaults to `before-after-heading`. */
52
+ readonly headingId?: string;
53
+ /** Set by `SectionSequence`. */
54
+ readonly tone?: SectionTone;
55
+ }
56
+ /** The section's guard for `SectionSequence`: skipped when no job has a genuine pair. */
57
+ export declare function beforeAfterReady({ pairs }: Pick<BeforeAfterProps, 'pairs'>): BrandlabSectionReadiness;
58
+ export declare function BeforeAfter({ title, eyebrow, lead, pairs, labels, imageSizes, headingId, tone, layout, classes, }: BeforeAfterProps): import("react/jsx-runtime").JSX.Element | null;
59
+ //# sourceMappingURL=before-after.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"before-after.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/before-after.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAA;AAC/F,OAAO,EAAiB,KAAK,wBAAwB,EAAE,MAAM,2CAA2C,CAAA;AACxG,OAAO,EAA2B,KAAK,kBAAkB,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAE9H,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AAG/D,mHAAmH;AACnH,MAAM,MAAM,uBAAuB,GAAG,cAAc,CAAC,aAAa,CAAC,CAAA;AACnE,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,kBAAkB,GAAG,MAAM,GAAG,uBAAuB,CAAA;AACjG,MAAM,WAAW,iBAAiB;IAChC,0CAA0C;IAC1C,QAAQ,CAAC,KAAK,CAAC,EAAE,kBAAkB,CAAA;IACnC,wHAAwH;IACxH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACpC;AACD,MAAM,MAAM,eAAe,GAAG,SAAS,CAAC,eAAe,EAAE,KAAK,EAAE,iBAAiB,EAAE,uBAAuB,CAAC,CAAA;AAE3G,oEAAoE;AACpE,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAA;IACnC,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAA;IAClC,wDAAwD;IACxD,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAA;CAC7B;AAED,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAA;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,eAAe,EAAE,CAAA;IAC1C,iGAAiG;IACjG,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IACpE,qHAAqH;IACrH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,0CAA0C;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,gCAAgC;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAC5B;AAED,yFAAyF;AACzF,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,CAAC,gBAAgB,EAAE,OAAO,CAAC,GAAG,wBAAwB,CAErG;AAwBD,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,OAAO,EACP,IAAI,EACJ,KAAK,EACL,MAAM,EACN,UAAU,EACV,SAAkC,EAClC,IAAI,EACJ,MAAM,EACN,OAAO,GACR,EAAE,gBAAgB,kDA0BlB"}
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { SECTION_READY } from '@brandlab-digital/site-core/section-order';
3
+ import { Section, SectionHeading } from '../primitives/layout.js';
4
+ import { MediaFrame } from '../primitives/media-frame.js';
5
+ import { repeatedSlot, slot } from '../skin.js';
6
+ import { assertGenuineWork } from './work-photos.js';
7
+ /** The section's guard for `SectionSequence`: skipped when no job has a genuine pair. */
8
+ export function beforeAfterReady({ pairs }) {
9
+ return pairs.length > 0 ? SECTION_READY : { render: false, reason: 'no-before-after-pairs' };
10
+ }
11
+ function assertPairs(pairs, labels) {
12
+ if (!labels?.before?.trim() || !labels?.after?.trim())
13
+ throw new Error('BeforeAfter: give both frame labels, in the site\'s words (labels.before and labels.after).');
14
+ const ids = new Set();
15
+ const sources = new Map();
16
+ for (const pair of pairs) {
17
+ if (!pair.id || ids.has(pair.id))
18
+ throw new Error(`BeforeAfter: every pair needs its own id ("${pair.id}" is empty or repeated).`);
19
+ ids.add(pair.id);
20
+ assertGenuineWork('BeforeAfter', pair.before, `the before photo of "${pair.id}"`);
21
+ assertGenuineWork('BeforeAfter', pair.after, `the after photo of "${pair.id}"`);
22
+ if (!pair.before.job || pair.before.job !== pair.after.job) {
23
+ throw new Error(`BeforeAfter: "${pair.id}" pairs photos of ${pair.before.job ?? 'no recorded job'} and ${pair.after.job ?? 'no recorded job'}; both photos must name the same job in the media manifest.`);
24
+ }
25
+ for (const photo of [pair.before, pair.after]) {
26
+ const other = sources.get(photo.src);
27
+ if (other)
28
+ throw new Error(`BeforeAfter: ${photo.src} is used twice ("${other}" and "${pair.id}").`);
29
+ sources.set(photo.src, pair.id);
30
+ }
31
+ }
32
+ }
33
+ export function BeforeAfter({ title, eyebrow, lead, pairs, labels, imageSizes, headingId = 'before-after-heading', tone, layout, classes, }) {
34
+ if (!beforeAfterReady({ pairs }).render)
35
+ return null;
36
+ assertPairs(pairs, labels);
37
+ const ratio = layout?.ratio ?? '4:3';
38
+ const arrange = layout?.arrange ?? 'side';
39
+ const sizes = imageSizes ?? (arrange === 'side' ? '(min-width: 640px) 50vw, 100vw' : '100vw');
40
+ return (_jsxs(Section, { id: "before-after", labelledBy: headingId, tone: tone, classes: classes, hooks: { 'data-ratio': ratio, 'data-arrange': arrange }, children: [_jsx(SectionHeading, { id: headingId, title: title, eyebrow: eyebrow, lead: lead, classes: classes }), _jsx("ul", { role: "list", ...slot(classes, 'list'), children: pairs.map((pair) => (_jsxs("li", { ...repeatedSlot('pair'), "data-item": pair.id, children: [_jsxs("div", { ...repeatedSlot('frames'), children: [_jsx("div", { ...repeatedSlot('frame'), "data-side": "before", children: _jsx(MediaFrame, { media: pair.before, ratio: ratio, sizes: sizes, caption: labels.before }) }), _jsx("div", { ...repeatedSlot('frame'), "data-side": "after", children: _jsx(MediaFrame, { media: pair.after, ratio: ratio, sizes: sizes, caption: labels.after }) })] }), pair.caption ? _jsx("p", { ...repeatedSlot('note'), children: pair.caption }) : null] }, pair.id))) })] }));
41
+ }
42
+ //# sourceMappingURL=before-after.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"before-after.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/before-after.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * `BeforeAfter` (`before-after`, 3.15.0): genuine photos of the same job before and after the work, side by side. Server\n * component, no client JS and no slider: two frames, each a `<figure>` whose caption is the site's \"Before\" or \"After\"\n * label, so the pair reads the same to a screen reader, in print and with scripts off.\n *\n * Locked:\n * - both photos are genuine and come from the same job: each media entry names the job (`job`, the job log id) and\n * the two must match; a stock or AI image, two different jobs or one photo used twice fails the build;\n * - before first, after second, in source order and on screen;\n * - both frames crop to one ratio (4:3 by default) so they line up; every photo loads lazily;\n * - the \"Before\" and \"After\" labels are props (no default copy), as is the caption under the pair.\n */\nimport type { ReactNode } from 'react'\nimport type { BrandlabMediaEntry, BrandlabMediaRatio } from '@brandlab-digital/site-core/media'\nimport { SECTION_READY, type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order'\nimport { Section, SectionHeading, type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js'\nimport { MediaFrame } from '../primitives/media-frame.js'\nimport type { RepeatedSlotOf } from '../repeated-slots.js'\nimport { repeatedSlot, slot, type SkinEntry } from '../skin.js'\nimport { assertGenuineWork } from './work-photos.js'\n\n/** Rendered once per pair: styled from the skin stylesheet, never by class. The frames are `[data-bl=\"media\"]`. */\nexport type BeforeAfterRepeatedSlot = RepeatedSlotOf<'beforeAfter'>\nexport type BeforeAfterSlot = SectionSlot | SectionHeadingSlot | 'list' | BeforeAfterRepeatedSlot\nexport interface BeforeAfterLayout {\n /** Both frames' crop. Defaults to 4:3. */\n readonly ratio?: BrandlabMediaRatio\n /** `side` (the default) puts the frames side by side from 640 px of the section's width; `stack` keeps them stacked. */\n readonly arrange?: 'side' | 'stack'\n}\nexport type BeforeAfterSkin = SkinEntry<BeforeAfterSlot, never, BeforeAfterLayout, BeforeAfterRepeatedSlot>\n\n/** One job, before and after. Both entries name the job (`job`). */\nexport interface BeforeAfterPair {\n /** A stable slug (the job id), rendered as `data-item`. */\n readonly id: string\n readonly before: BrandlabMediaEntry\n readonly after: BrandlabMediaEntry\n /** Under the pair: \"{what}, {suburb}, {month year}\". */\n readonly caption?: ReactNode\n}\n\nexport interface BeforeAfterProps extends BeforeAfterSkin {\n /** The H2, for example \"Before and after\". */\n readonly title: ReactNode\n readonly eyebrow?: ReactNode\n readonly lead?: ReactNode\n readonly pairs: readonly BeforeAfterPair[]\n /** The frames' visible captions, in the site's words: `{ before: 'Before', after: 'After' }`. */\n readonly labels: { readonly before: string; readonly after: string }\n /** The frames' `sizes` (a `vw` string). Defaults to half the width from 640 px side by side, else the full width. */\n readonly imageSizes?: string\n /** Defaults to `before-after-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 job has a genuine pair. */\nexport function beforeAfterReady({ pairs }: Pick<BeforeAfterProps, 'pairs'>): BrandlabSectionReadiness {\n return pairs.length > 0 ? SECTION_READY : { render: false, reason: 'no-before-after-pairs' }\n}\n\nfunction assertPairs(pairs: readonly BeforeAfterPair[], labels: BeforeAfterProps['labels']): void {\n if (!labels?.before?.trim() || !labels?.after?.trim()) throw new Error('BeforeAfter: give both frame labels, in the site\\'s words (labels.before and labels.after).')\n const ids = new Set<string>()\n const sources = new Map<string, string>()\n for (const pair of pairs) {\n if (!pair.id || ids.has(pair.id)) throw new Error(`BeforeAfter: every pair needs its own id (\"${pair.id}\" is empty or repeated).`)\n ids.add(pair.id)\n assertGenuineWork('BeforeAfter', pair.before, `the before photo of \"${pair.id}\"`)\n assertGenuineWork('BeforeAfter', pair.after, `the after photo of \"${pair.id}\"`)\n if (!pair.before.job || pair.before.job !== pair.after.job) {\n throw new Error(\n `BeforeAfter: \"${pair.id}\" pairs photos of ${pair.before.job ?? 'no recorded job'} and ${pair.after.job ?? 'no recorded job'}; both photos must name the same job in the media manifest.`,\n )\n }\n for (const photo of [pair.before, pair.after]) {\n const other = sources.get(photo.src)\n if (other) throw new Error(`BeforeAfter: ${photo.src} is used twice (\"${other}\" and \"${pair.id}\").`)\n sources.set(photo.src, pair.id)\n }\n }\n}\n\nexport function BeforeAfter({\n title,\n eyebrow,\n lead,\n pairs,\n labels,\n imageSizes,\n headingId = 'before-after-heading',\n tone,\n layout,\n classes,\n}: BeforeAfterProps) {\n if (!beforeAfterReady({ pairs }).render) return null\n assertPairs(pairs, labels)\n const ratio = layout?.ratio ?? '4:3'\n const arrange = layout?.arrange ?? 'side'\n const sizes = imageSizes ?? (arrange === 'side' ? '(min-width: 640px) 50vw, 100vw' : '100vw')\n return (\n <Section id=\"before-after\" labelledBy={headingId} tone={tone} classes={classes} hooks={{ 'data-ratio': ratio, 'data-arrange': arrange }}>\n <SectionHeading id={headingId} title={title} eyebrow={eyebrow} lead={lead} classes={classes} />\n <ul role=\"list\" {...slot(classes, 'list')}>\n {pairs.map((pair) => (\n <li key={pair.id} {...repeatedSlot('pair')} data-item={pair.id}>\n <div {...repeatedSlot('frames')}>\n <div {...repeatedSlot('frame')} data-side=\"before\">\n <MediaFrame media={pair.before} ratio={ratio} sizes={sizes} caption={labels.before} />\n </div>\n <div {...repeatedSlot('frame')} data-side=\"after\">\n <MediaFrame media={pair.after} ratio={ratio} sizes={sizes} caption={labels.after} />\n </div>\n </div>\n {pair.caption ? <p {...repeatedSlot('note')}>{pair.caption}</p> : null}\n </li>\n ))}\n </ul>\n </Section>\n )\n}\n"]}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * `CouncilRules` (`council-rules`, 3.15.0) and the council record lines it shares with `LocalFacts`' permit slot.
3
+ * Server components.
4
+ *
5
+ * Both render one record of the council register (site-core `./council-register`, playbook-trees §2.4): the rule, each
6
+ * fee with its financial year, the council's own page as the source and the month the record was checked. Nothing is
7
+ * invented: a record the register hides (six months after it was checked, or on 1 July after its fees' financial year)
8
+ * renders nothing, and the section's guard says why (`no-current-council-record`). The register's `rule` is a fact;
9
+ * each site words its own sentence from it (`rule` as a function of the record), so same-trade sites never share a
10
+ * council sentence. Locked truth wording: a fee reads "{label}: $201 (2026/27)" and the source line ends ", checked
11
+ * {Month Year}".
12
+ */
13
+ import type { ReactNode } from 'react';
14
+ import { type BrandlabCouncilFee, type BrandlabCouncilPermitRecord } from '@brandlab-digital/site-core/council-register';
15
+ import { type BrandlabSectionReadiness } from '@brandlab-digital/site-core/section-order';
16
+ import { type SectionHeadingSlot, type SectionSlot, type SectionTone } from '../primitives/layout.js';
17
+ import type { RepeatedSlotOf } from '../repeated-slots.js';
18
+ import { type SkinEntry, type SlotClasses } from '../skin.js';
19
+ import type { UiLink } from '../types.js';
20
+ /** The site's wording of the register's rule: text, or a function of the record. Left out, the record's own rule. */
21
+ export type CouncilRuleWording = ReactNode | ((record: BrandlabCouncilPermitRecord) => ReactNode);
22
+ /** The one-off parts of a council record's lines (shared by `CouncilRules` and `LocalFacts`' permit slot). */
23
+ export type CouncilRecordSlot = 'rule' | 'fees' | 'source' | 'councilLink';
24
+ /** A fee in Australian dollars: whole dollars without cents ("$201", "$1,100"), otherwise two places ("$58.50"). */
25
+ export declare function formatCouncilFeeAmount(amountAud: number): string;
26
+ /** The locked fee line: "{label}: $201 (2026/27)". Every figure carries its financial year. */
27
+ export declare function councilFeeLine(fee: BrandlabCouncilFee, financialYear: string): string;
28
+ /** The record when it may show on `asOf`; the guard for a section that renders it. */
29
+ export declare function councilRecordReady(record: BrandlabCouncilPermitRecord | undefined, asOf?: Date): BrandlabSectionReadiness;
30
+ /** The record's lines: the rule, its fees, the source with the check month and the site's own council page. */
31
+ export declare function CouncilRecordLines({ record, rule, sourceLabel, councilPage, classes, }: {
32
+ readonly record: BrandlabCouncilPermitRecord;
33
+ readonly rule?: CouncilRuleWording;
34
+ readonly sourceLabel?: string;
35
+ readonly councilPage?: UiLink;
36
+ readonly classes?: SlotClasses<CouncilRecordSlot> | undefined;
37
+ }): import("react/jsx-runtime").JSX.Element;
38
+ export type CouncilRulesSlot = SectionSlot | SectionHeadingSlot | CouncilRecordSlot | 'fee';
39
+ /** The fees, one per register entry: styled from the skin stylesheet, never by class. */
40
+ export type CouncilRulesRepeatedSlot = RepeatedSlotOf<'councilRules'>;
41
+ export type CouncilRulesSkin = SkinEntry<CouncilRulesSlot, never, Record<never, never>, CouncilRulesRepeatedSlot>;
42
+ export interface CouncilRulesProps extends CouncilRulesSkin {
43
+ /** The council's register record, for example `councilRegister.find((record) => record.council === page.council)`. */
44
+ readonly record: BrandlabCouncilPermitRecord | undefined;
45
+ /** The H2, for example "Hornsby Shire Council tree rules". */
46
+ readonly title: ReactNode;
47
+ readonly eyebrow?: ReactNode;
48
+ readonly lead?: ReactNode;
49
+ /** The site's own wording of the rule (a function of the record keeps it true when the record changes). */
50
+ readonly rule?: CouncilRuleWording;
51
+ /** The source link's text. Defaults to the council's name. */
52
+ readonly sourceLabel?: string;
53
+ /** The day the page is built or refreshed (Sydney). Defaults to now. */
54
+ readonly asOf?: Date;
55
+ /** Defaults to `council-rules-heading`. */
56
+ readonly headingId?: string;
57
+ /** Set by `SectionSequence`. */
58
+ readonly tone?: SectionTone;
59
+ }
60
+ /** The guard for `SectionSequence`: renders only while the register shows the record. */
61
+ export declare function councilRulesReady({ record, asOf }: Pick<CouncilRulesProps, 'record' | 'asOf'>): BrandlabSectionReadiness;
62
+ /** A council page's rules from the register (playbook-trees §2.4): the rule, the fees with their year, the source. */
63
+ export declare function CouncilRules({ record, title, eyebrow, lead, rule, sourceLabel, asOf, headingId, tone, classes }: CouncilRulesProps): import("react/jsx-runtime").JSX.Element | null;
64
+ //# sourceMappingURL=council-rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"council-rules.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/council-rules.tsx"],"names":[],"mappings":"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"}