@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
@@ -1 +1 @@
1
- {"version":3,"file":"lists.js","sourceRoot":"","sources":["../../../src/ui-kit/primitives/lists.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAGlC,OAAO,EAAE,YAAY,EAAkB,MAAM,YAAY,CAAA;AAEzD,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAsBnC,MAAM,QAAQ,GAAG,CAAC,IAAc,EAAE,EAAE,CAAC,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAElF,MAAM,UAAU,QAAQ,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAiB;IAChE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACnC,MAAM,IAAI,GAAG,MAAM,EAAE,IAAI,KAAK,MAAM,CAAA;IACpC,OAAO,CACL,aACE,IAAI,EAAC,MAAM,aACH,WAAW,kBACL,MAAM,CAAC,MAAM,EAAE,OAAO,IAAI,CAAC,CAAC,eAC/B,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EACpC,SAAS,EAAE,OAAO,EAAE,IAAI,YAEvB,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YAClB,MAAM,IAAI,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAA;YAC7D,OAAO,CACL,iBAA6B,YAAY,CAAC,MAAM,CAAC,aAC/C,kBAAU,YAAY,CAAC,MAAM,CAAC,iBAAc,MAAM,YAC/C,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,GAAG,GAC/B,EACP,mBAAU,YAAY,CAAC,MAAM,CAAC,aAC3B,QAAQ,CAAC,IAAI,CAAC,EACd,IAAI,CAAC,CAAC,CAAC,CACN,8BACG,GAAG,EACJ,KAAC,MAAM,IAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,YACrE,IAAI,CAAC,KAAK,GACJ,IACR,CACJ,CAAC,CAAC,CAAC,IAAI,IACH,KAdA,QAAQ,CAAC,IAAI,CAAC,CAelB,CACN,CAAA;QACH,CAAC,CAAC,GACC,CACN,CAAA;AACH,CAAC;AAED,+EAA+E;AAC/E,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,gBAAgB,CAAU,CAAA;AAkB7F,MAAM,UAAU,QAAQ,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,GAAG,KAAK,EAAE,MAAM,EAAE,OAAO,EAAiB;IACxF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACnC,OAAO,CACL,aAAI,IAAI,EAAC,MAAM,aAAS,WAAW,kBAAe,MAAM,CAAC,MAAM,EAAE,OAAO,IAAI,CAAC,CAAC,mBAAiB,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,YAC1H,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CACnB,uBACE,KAAC,MAAM,IAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,YACnD,IAAI,CAAC,KAAK,GACJ,IAHF,IAAI,CAAC,IAAI,CAIb,CACN,CAAC,GACC,CACN,CAAA;AACH,CAAC","sourcesContent":["/**\n * `TickList` and `LinkList` (3.14.0). Server components; both are a real `<ul>`.\n *\n * `TickList`: verified facts or the scope of a service, each after a decorative tick (the check icon, or a ruled mark\n * with `layout.tick: 'rule'`). One or two columns; the hero's trust checklist uses two (Romie, Prestige and Sydney\n * Waterproofing). An item may end with a link to where the fact can be checked, for example the licence register.\n *\n * `LinkList`: a ruled list of destinations (AA's `RuledLinkList`): service areas, nearby suburbs, other services. Lean\n * by design (cost item 1f): each entry is a plain `<li><a>` with no class, hook or icon, so a 120-suburb hub stays near\n * 120 bytes a link; a skin draws rules and arrows from `src/site/skin.css`. Its links never prefetch (area and nearby\n * lists are `false` in `LINK_PREFETCH_POLICY`), and `kind` marks the list for W2 (`data-bl-links`).\n */\nimport { Icon } from '../icons.js'\nimport type { LinkPrefetch } from '../prefetch.js'\nimport type { RepeatedSlotOf } from '../repeated-slots.js'\nimport { repeatedSlot, type SkinEntry } from '../skin.js'\nimport type { UiLink } from '../types.js'\nimport { Anchor } from './links.js'\n\n/** One checklist entry: the fact, and optionally where to check it. */\nexport type TickItem = string | { readonly text: string; readonly link?: UiLink }\n\nexport type TickListSlot = 'root' | RepeatedSlotOf<'tickList'>\nexport type TickListRepeatedSlot = RepeatedSlotOf<'tickList'>\nexport interface TickListLayout {\n /** One column (the default) or two, from the phone breakpoint up. */\n readonly columns?: 1 | 2\n /**\n * The tick: `icon` (the default) is the sprite's check; `rule` ships no svg, only an empty decorative element the\n * stylesheet draws as a short rule (`data-tick=\"rule\"` on the list), about 120 bytes less a fact.\n */\n readonly tick?: 'icon' | 'rule'\n}\nexport type TickListSkin = SkinEntry<TickListSlot, never, TickListLayout, TickListRepeatedSlot>\n\nexport interface TickListProps extends TickListSkin {\n readonly items: readonly TickItem[]\n}\n\nconst itemText = (item: TickItem) => (typeof item === 'string' ? item : item.text)\n\nexport function TickList({ items, layout, classes }: TickListProps) {\n if (items.length === 0) return null\n const rule = layout?.tick === 'rule'\n return (\n <ul\n role=\"list\"\n data-bl=\"tick-list\"\n data-columns={String(layout?.columns ?? 1)}\n data-tick={rule ? 'rule' : undefined}\n className={classes?.root}\n >\n {items.map((item) => {\n const link = typeof item === 'string' ? undefined : item.link\n return (\n <li key={itemText(item)} {...repeatedSlot('item')}>\n <span {...repeatedSlot('tick')} aria-hidden=\"true\">\n {rule ? null : <Icon name=\"check\" />}\n </span>\n <span {...repeatedSlot('text')}>\n {itemText(item)}\n {link ? (\n <>\n {' '}\n <Anchor href={link.href} prefetch={false} hooks={{ 'data-slot': 'link' }}>\n {link.label}\n </Anchor>\n </>\n ) : null}\n </span>\n </li>\n )\n })}\n </ul>\n )\n}\n\n/** The `data-bl-links` roles W2 reads: links in these lists never prefetch. */\nexport const LINK_LIST_KINDS = ['area', 'nearby', 'map', 'finder', 'other-services'] as const\nexport type LinkListKind = (typeof LINK_LIST_KINDS)[number]\n\nexport type LinkListSlot = 'root'\nexport interface LinkListLayout {\n /** Columns at the widest width (default 3); phones show one, then two. */\n readonly columns?: 1 | 2 | 3 | 4\n}\nexport type LinkListSkin = SkinEntry<LinkListSlot, never, LinkListLayout>\n\nexport interface LinkListProps extends LinkListSkin {\n readonly links: readonly UiLink[]\n /** What the list holds, rendered as `data-bl-links` for W2. Leave it out for a list of other destinations. */\n readonly kind?: LinkListKind\n /** Off by default; `intent` only for a short list of in-content destinations. */\n readonly prefetch?: Exclude<LinkPrefetch, 'auto'>\n}\n\nexport function LinkList({ links, kind, prefetch = false, layout, classes }: LinkListProps) {\n if (links.length === 0) return null\n return (\n <ul role=\"list\" data-bl=\"link-list\" data-columns={String(layout?.columns ?? 3)} data-bl-links={kind} className={classes?.root}>\n {links.map((link) => (\n <li key={link.href}>\n <Anchor href={link.href} prefetch={prefetch} hooks={{}}>\n {link.label}\n </Anchor>\n </li>\n ))}\n </ul>\n )\n}\n"]}
1
+ {"version":3,"file":"lists.js","sourceRoot":"","sources":["../../../src/ui-kit/primitives/lists.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * `TickList` and `LinkList` (3.14.0). Server components; both are a real `<ul>`.\n *\n * `TickList`: verified facts or the scope of a service, each after a decorative tick (the check icon, or a ruled mark\n * with `layout.tick: 'rule'`). One or two columns; the hero's trust checklist uses two (Romie, Prestige and Sydney\n * Waterproofing). An item may end with a link to where the fact can be checked, for example the licence register.\n *\n * `LinkList`: a ruled list of destinations (AA's `RuledLinkList`): service areas, nearby suburbs, other services. Lean\n * by design (cost item 1f): each entry is a plain `<li><a>` with no class, hook or icon, so a 120-suburb hub stays near\n * 120 bytes a link; a skin draws rules and arrows from `src/site/skin.css`. Its links never prefetch (area and nearby\n * lists are `false` in `LINK_PREFETCH_POLICY`), and `kind` marks the list for W2 (`data-bl-links`).\n */\nimport { Icon } from '../icons.js'\nimport type { LinkPrefetch } from '../prefetch.js'\nimport type { RepeatedSlotOf } from '../repeated-slots.js'\nimport { repeatedSlot, type SkinEntry } from '../skin.js'\nimport type { UiLink } from '../types.js'\nimport { Anchor } from './links.js'\n\n/** One checklist entry: the fact, and optionally where to check it. */\nexport type TickItem = string | { readonly text: string; readonly link?: UiLink }\n\nexport type TickListSlot = 'root' | RepeatedSlotOf<'tickList'>\nexport type TickListRepeatedSlot = RepeatedSlotOf<'tickList'>\nexport interface TickListLayout {\n /** One column (the default) or two, from the phone breakpoint up. */\n readonly columns?: 1 | 2\n /**\n * The tick: `icon` (the default) is the sprite's check; `rule` ships no svg, only an empty decorative element the\n * stylesheet draws as a short rule (`data-tick=\"rule\"` on the list), about 120 bytes less a fact.\n */\n readonly tick?: 'icon' | 'rule'\n}\nexport type TickListSkin = SkinEntry<TickListSlot, never, TickListLayout, TickListRepeatedSlot>\n\nexport interface TickListProps extends TickListSkin {\n readonly items: readonly TickItem[]\n}\n\nconst itemText = (item: TickItem) => (typeof item === 'string' ? item : item.text)\n\nexport function TickList({ items, layout, classes }: TickListProps) {\n if (items.length === 0) return null\n const rule = layout?.tick === 'rule'\n return (\n <ul\n role=\"list\"\n data-bl=\"tick-list\"\n data-columns={String(layout?.columns ?? 1)}\n data-tick={rule ? 'rule' : undefined}\n className={classes?.root}\n >\n {items.map((item) => {\n const link = typeof item === 'string' ? undefined : item.link\n return (\n <li key={itemText(item)} {...repeatedSlot('item')}>\n <span {...repeatedSlot('tick')} aria-hidden=\"true\">\n {rule ? null : <Icon name=\"check\" />}\n </span>\n <span {...repeatedSlot('text')}>\n {itemText(item)}\n {link ? (\n <>\n {' '}\n <Anchor href={link.href} prefetch={false} hooks={{ 'data-slot': 'link' }}>\n {link.label}\n </Anchor>\n </>\n ) : null}\n </span>\n </li>\n )\n })}\n </ul>\n )\n}\n\n/** The `data-bl-links` roles W2 reads: links in these lists never prefetch. */\nexport const LINK_LIST_KINDS = ['area', 'nearby', 'map', 'finder', 'other-services'] as const\nexport type LinkListKind = (typeof LINK_LIST_KINDS)[number]\n\nexport type LinkListSlot = 'root'\nexport interface LinkListLayout {\n /** Columns at the widest width (default 3); phones show one, then two. */\n readonly columns?: 1 | 2 | 3 | 4\n}\nexport type LinkListSkin = SkinEntry<LinkListSlot, never, LinkListLayout>\n\n/**\n * One entry: a link, or (3.15.0) a name with no `href`, rendered as plain text in the same list (`<li>Putney</li>`, no\n * anchor), for an area whose page is still noindex while it waits for its return wave.\n */\nexport type LinkListEntry = UiLink | { readonly label: string; readonly href?: undefined }\n\nexport interface LinkListProps extends LinkListSkin {\n readonly links: readonly LinkListEntry[]\n /** What the list holds, rendered as `data-bl-links` for W2. Leave it out for a list of other destinations. */\n readonly kind?: LinkListKind\n /** Off by default; `intent` only for a short list of in-content destinations. */\n readonly prefetch?: Exclude<LinkPrefetch, 'auto'>\n}\n\nexport function LinkList({ links, kind, prefetch = false, layout, classes }: LinkListProps) {\n if (links.length === 0) return null\n return (\n <ul role=\"list\" data-bl=\"link-list\" data-columns={String(layout?.columns ?? 3)} data-bl-links={kind} className={classes?.root}>\n {links.map((link) =>\n link.href === undefined ? (\n <li key={`text:${link.label}`}>{link.label}</li>\n ) : (\n <li key={link.href}>\n <Anchor href={link.href} prefetch={prefetch} hooks={{}}>\n {link.label}\n </Anchor>\n </li>\n ),\n )}\n </ul>\n )\n}\n"]}
@@ -16,8 +16,31 @@ export interface QuoteButtonProps extends QuoteButtonSkin {
16
16
  readonly placement: LeadCtaPlacement;
17
17
  /** A stable id for `lead_cta_click`, for example `quote-header` or `quote-cta`. */
18
18
  readonly ctaId: string;
19
- /** `auto` by default: the main quote action keeps Next's prefetch (`LINK_PREFETCH_POLICY.primaryAction`). */
19
+ /**
20
+ * `auto` by default: the main quote action keeps Next's prefetch (`LINK_PREFETCH_POLICY.primaryAction`). A link to a
21
+ * place on the same page (`#quote-hero`) never prefetches, and in the header, drawer and call bar the button never
22
+ * prefetches its own page (on `/contact`, `/contact#quote`).
23
+ */
20
24
  readonly prefetch?: LinkPrefetch;
21
25
  }
22
- export declare function QuoteButton({ label, href, placement, ctaId, prefetch, variant, layout, classes }: QuoteButtonProps): import("react/jsx-runtime").JSX.Element;
26
+ /**
27
+ * The quote button's anchor props and content (internal; not exported from the subpath). `QuoteButton` renders them
28
+ * through `next/link`; the shell's header, drawer and call bar render them through a link that also turns prefetch
29
+ * off on the quote page itself.
30
+ */
31
+ export declare function quoteButtonParts({ label, href, placement, ctaId, variant, layout, classes }: QuoteButtonProps, options?: {
32
+ readonly compactLabel?: string | undefined;
33
+ }): {
34
+ href: string;
35
+ hooks: {
36
+ 'data-bl': string;
37
+ 'data-variant': "primary" | "secondary" | "quiet";
38
+ 'data-size': "regular" | "large";
39
+ 'data-bl-cta': string;
40
+ 'data-bl-cta-placement': "header" | "footer" | "hero" | "page" | "section" | "navigation" | "floating";
41
+ className: string | undefined;
42
+ };
43
+ children: import("react/jsx-runtime").JSX.Element;
44
+ };
45
+ export declare function QuoteButton(props: QuoteButtonProps): import("react/jsx-runtime").JSX.Element;
23
46
  //# sourceMappingURL=quote-button.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"quote-button.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/primitives/quote-button.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAE1D,OAAO,EAAqB,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAA;AACrE,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAA;AACvD,MAAM,MAAM,eAAe,GAAG,SAAS,CAAC,eAAe,EAAE,aAAa,EAAE;IAAE,IAAI,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAAA;AAEvH,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,yGAAyG;IACzG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,uFAAuF;IACvF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,mDAAmD;IACnD,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAA;IACpC,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,6GAA6G;IAC7G,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAA;CACjC;AAED,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAmB,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,gBAAgB,2CAoB9H"}
1
+ {"version":3,"file":"quote-button.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/primitives/quote-button.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAE1D,OAAO,EAAqB,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAA;AACrE,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAA;AACvD,MAAM,MAAM,eAAe,GAAG,SAAS,CAAC,eAAe,EAAE,aAAa,EAAE;IAAE,IAAI,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAAA;AAEvH,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,yGAAyG;IACzG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,uFAAuF;IACvF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,mDAAmD;IACnD,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAA;IACpC,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAA;CACjC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,OAAmB,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,gBAAgB,EACzF,OAAO,GAAE;IAAE,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAO;;;;;;;;;;;EAiC7D;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,2CAOlD"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * `QuoteButton`: the quote action. A server-rendered link to the page's quote form (for example `/contact#quote`), so
4
4
  * it works before scripts run. When a `QuoteDialog` is on the page (3.10.0) it opens the dialog instead; on the
@@ -10,7 +10,29 @@ import Link from 'next/link';
10
10
  import { Icon } from '../icons.js';
11
11
  import { linkPrefetchProps } from '../prefetch.js';
12
12
  import { slot } from '../skin.js';
13
- export function QuoteButton({ label, href, placement, ctaId, prefetch, variant = 'primary', layout, classes }) {
14
- return (_jsxs(Link, { href: href, ...linkPrefetchProps(prefetch), "data-bl": "quote-button", "data-variant": variant, "data-size": layout?.size ?? 'regular', "data-bl-cta": ctaId, "data-bl-cta-placement": placement, className: classes?.root, children: [_jsx("span", { ...slot(classes, 'label'), children: label }), layout?.icon === false ? null : (_jsx("span", { ...slot(classes, 'icon'), children: _jsx(Icon, { name: "arrow" }) }))] }));
13
+ /**
14
+ * The quote button's anchor props and content (internal; not exported from the subpath). `QuoteButton` renders them
15
+ * through `next/link`; the shell's header, drawer and call bar render them through a link that also turns prefetch
16
+ * off on the quote page itself.
17
+ */
18
+ export function quoteButtonParts({ label, href, placement, ctaId, variant = 'primary', layout, classes }, options = {}) {
19
+ const compact = options.compactLabel?.trim();
20
+ if (compact !== undefined && (compact === '' || !label.toLowerCase().includes(compact.toLowerCase()))) {
21
+ throw new Error(`The compact quote label "${options.compactLabel}" must be part of the quote label "${label}" (WCAG 2.5.3: the visible label is in the name).`);
22
+ }
23
+ const hooks = {
24
+ 'data-bl': 'quote-button',
25
+ 'data-variant': variant,
26
+ 'data-size': layout?.size ?? 'regular',
27
+ 'data-bl-cta': ctaId,
28
+ 'data-bl-cta-placement': placement,
29
+ className: classes?.root,
30
+ };
31
+ const children = (_jsxs(_Fragment, { children: [compact ? (_jsx("span", { ...slot(classes, 'label'), "data-compact-label": compact, children: _jsx("span", { "data-bl-full-label": "", children: label }) })) : (_jsx("span", { ...slot(classes, 'label'), children: label })), layout?.icon === false ? null : (_jsx("span", { ...slot(classes, 'icon'), children: _jsx(Icon, { name: "arrow" }) }))] }));
32
+ return { href, hooks, children };
33
+ }
34
+ export function QuoteButton(props) {
35
+ const { href, hooks, children } = quoteButtonParts(props);
36
+ return (_jsx(Link, { href: href, ...linkPrefetchProps(props.prefetch, href), ...hooks, children: children }));
15
37
  }
16
38
  //# sourceMappingURL=quote-button.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"quote-button.js","sourceRoot":"","sources":["../../../src/ui-kit/primitives/quote-button.tsx"],"names":[],"mappings":";AAAA;;;;;;GAMG;AACH,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAClC,OAAO,EAAE,iBAAiB,EAAqB,MAAM,gBAAgB,CAAA;AACrE,OAAO,EAAE,IAAI,EAAkB,MAAM,YAAY,CAAA;AAmBjD,MAAM,UAAU,WAAW,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,GAAG,SAAS,EAAE,MAAM,EAAE,OAAO,EAAoB;IAC7H,OAAO,CACL,MAAC,IAAI,IACH,IAAI,EAAE,IAAI,KACN,iBAAiB,CAAC,QAAQ,CAAC,aACvB,cAAc,kBACR,OAAO,eACV,MAAM,EAAE,IAAI,IAAI,SAAS,iBACvB,KAAK,2BACK,SAAS,EAChC,SAAS,EAAE,OAAO,EAAE,IAAI,aAExB,kBAAU,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,YAAG,KAAK,GAAQ,EAC/C,MAAM,EAAE,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC/B,kBAAU,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,YAC7B,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,GAAG,GAChB,CACR,IACI,CACR,CAAA;AACH,CAAC","sourcesContent":["/**\n * `QuoteButton`: the quote action. A server-rendered link to the page's quote form (for example `/contact#quote`), so\n * it works before scripts run. When a `QuoteDialog` is on the page (3.10.0) it opens the dialog instead; on the\n * contact page the link scrolls to the form. It carries the `data-bl-cta` hooks only: from 3.9.0 the shared\n * instrumentation reads them and sends `lead_cta_click` when lead diagnostics are on. Nothing is sent in 3.8.0, and\n * nothing here emits events itself.\n */\nimport Link from 'next/link'\nimport type { LeadCtaPlacement } from '../../analytics.js'\nimport { Icon } from '../icons.js'\nimport { linkPrefetchProps, type LinkPrefetch } from '../prefetch.js'\nimport { slot, type SkinEntry } from '../skin.js'\nimport type { ButtonVariant } from './links.js'\n\nexport type QuoteButtonSlot = 'root' | 'label' | 'icon'\nexport type QuoteButtonSkin = SkinEntry<QuoteButtonSlot, ButtonVariant, { size?: 'regular' | 'large'; icon?: boolean }>\n\nexport interface QuoteButtonProps extends QuoteButtonSkin {\n /** The site's recorded quote label, or one from the closed list (C5), for example \"Get a free quote\". */\n readonly label: string\n /** The quote form this falls back to without scripts, for example `/contact#quote`. */\n readonly href: string\n /** Where the button sits, for `lead_cta_click`. */\n readonly placement: LeadCtaPlacement\n /** A stable id for `lead_cta_click`, for example `quote-header` or `quote-cta`. */\n readonly ctaId: string\n /** `auto` by default: the main quote action keeps Next's prefetch (`LINK_PREFETCH_POLICY.primaryAction`). */\n readonly prefetch?: LinkPrefetch\n}\n\nexport function QuoteButton({ label, href, placement, ctaId, prefetch, variant = 'primary', layout, classes }: QuoteButtonProps) {\n return (\n <Link\n href={href}\n {...linkPrefetchProps(prefetch)}\n data-bl=\"quote-button\"\n data-variant={variant}\n data-size={layout?.size ?? 'regular'}\n data-bl-cta={ctaId}\n data-bl-cta-placement={placement}\n className={classes?.root}\n >\n <span {...slot(classes, 'label')}>{label}</span>\n {layout?.icon === false ? null : (\n <span {...slot(classes, 'icon')}>\n <Icon name=\"arrow\" />\n </span>\n )}\n </Link>\n )\n}\n"]}
1
+ {"version":3,"file":"quote-button.js","sourceRoot":"","sources":["../../../src/ui-kit/primitives/quote-button.tsx"],"names":[],"mappings":";AAAA;;;;;;GAMG;AACH,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAClC,OAAO,EAAE,iBAAiB,EAAqB,MAAM,gBAAgB,CAAA;AACrE,OAAO,EAAE,IAAI,EAAkB,MAAM,YAAY,CAAA;AAuBjD;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC9B,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,EAAE,MAAM,EAAE,OAAO,EAAoB,EACzF,UAA0D,EAAE;IAE5D,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,EAAE,IAAI,EAAE,CAAA;IAC5C,IAAI,OAAO,KAAK,SAAS,IAAI,CAAC,OAAO,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,CAAC;QACtG,MAAM,IAAI,KAAK,CAAC,4BAA4B,OAAO,CAAC,YAAY,sCAAsC,KAAK,mDAAmD,CAAC,CAAA;IACjK,CAAC;IACD,MAAM,KAAK,GAAG;QACZ,SAAS,EAAE,cAAc;QACzB,cAAc,EAAE,OAAO;QACvB,WAAW,EAAE,MAAM,EAAE,IAAI,IAAI,SAAS;QACtC,aAAa,EAAE,KAAK;QACpB,uBAAuB,EAAE,SAAS;QAClC,SAAS,EAAE,OAAO,EAAE,IAAI;KACzB,CAAA;IACD,MAAM,QAAQ,GAAG,CACf,8BACG,OAAO,CAAC,CAAC,CAAC,CAGT,kBAAU,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,wBAAsB,OAAO,YAC3D,qCAAyB,EAAE,YAAE,KAAK,GAAQ,GACrC,CACR,CAAC,CAAC,CAAC,CACF,kBAAU,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,YAAG,KAAK,GAAQ,CACjD,EACA,MAAM,EAAE,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC/B,kBAAU,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,YAC7B,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,GAAG,GAChB,CACR,IACA,CACJ,CAAA;IACD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAA;AAClC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAuB;IACjD,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAA;IACzD,OAAO,CACL,KAAC,IAAI,IAAC,IAAI,EAAE,IAAI,KAAM,iBAAiB,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAM,KAAK,YACrE,QAAQ,GACJ,CACR,CAAA;AACH,CAAC","sourcesContent":["/**\n * `QuoteButton`: the quote action. A server-rendered link to the page's quote form (for example `/contact#quote`), so\n * it works before scripts run. When a `QuoteDialog` is on the page (3.10.0) it opens the dialog instead; on the\n * contact page the link scrolls to the form. It carries the `data-bl-cta` hooks only: from 3.9.0 the shared\n * instrumentation reads them and sends `lead_cta_click` when lead diagnostics are on. Nothing is sent in 3.8.0, and\n * nothing here emits events itself.\n */\nimport Link from 'next/link'\nimport type { LeadCtaPlacement } from '../../analytics.js'\nimport { Icon } from '../icons.js'\nimport { linkPrefetchProps, type LinkPrefetch } from '../prefetch.js'\nimport { slot, type SkinEntry } from '../skin.js'\nimport type { ButtonVariant } from './links.js'\n\nexport type QuoteButtonSlot = 'root' | 'label' | 'icon'\nexport type QuoteButtonSkin = SkinEntry<QuoteButtonSlot, ButtonVariant, { size?: 'regular' | 'large'; icon?: boolean }>\n\nexport interface QuoteButtonProps extends QuoteButtonSkin {\n /** The site's recorded quote label, or one from the closed list (C5), for example \"Get a free quote\". */\n readonly label: string\n /** The quote form this falls back to without scripts, for example `/contact#quote`. */\n readonly href: string\n /** Where the button sits, for `lead_cta_click`. */\n readonly placement: LeadCtaPlacement\n /** A stable id for `lead_cta_click`, for example `quote-header` or `quote-cta`. */\n readonly ctaId: string\n /**\n * `auto` by default: the main quote action keeps Next's prefetch (`LINK_PREFETCH_POLICY.primaryAction`). A link to a\n * place on the same page (`#quote-hero`) never prefetches, and in the header, drawer and call bar the button never\n * prefetches its own page (on `/contact`, `/contact#quote`).\n */\n readonly prefetch?: LinkPrefetch\n}\n\n/**\n * The quote button's anchor props and content (internal; not exported from the subpath). `QuoteButton` renders them\n * through `next/link`; the shell's header, drawer and call bar render them through a link that also turns prefetch\n * off on the quote page itself.\n */\nexport function quoteButtonParts(\n { label, href, placement, ctaId, variant = 'primary', layout, classes }: QuoteButtonProps,\n options: { readonly compactLabel?: string | undefined } = {},\n) {\n const compact = options.compactLabel?.trim()\n if (compact !== undefined && (compact === '' || !label.toLowerCase().includes(compact.toLowerCase()))) {\n throw new Error(`The compact quote label \"${options.compactLabel}\" must be part of the quote label \"${label}\" (WCAG 2.5.3: the visible label is in the name).`)\n }\n const hooks = {\n 'data-bl': 'quote-button',\n 'data-variant': variant,\n 'data-size': layout?.size ?? 'regular',\n 'data-bl-cta': ctaId,\n 'data-bl-cta-placement': placement,\n className: classes?.root,\n }\n const children = (\n <>\n {compact ? (\n // The compact label is drawn from the attribute by the structure CSS on narrow phones (with empty alternative\n // text), so the text, the accessible name and C5 still read the full label.\n <span {...slot(classes, 'label')} data-compact-label={compact}>\n <span data-bl-full-label=\"\">{label}</span>\n </span>\n ) : (\n <span {...slot(classes, 'label')}>{label}</span>\n )}\n {layout?.icon === false ? null : (\n <span {...slot(classes, 'icon')}>\n <Icon name=\"arrow\" />\n </span>\n )}\n </>\n )\n return { href, hooks, children }\n}\n\nexport function QuoteButton(props: QuoteButtonProps) {\n const { href, hooks, children } = quoteButtonParts(props)\n return (\n <Link href={href} {...linkPrefetchProps(props.prefetch, href)} {...hooks}>\n {children}\n </Link>\n )\n}\n"]}
@@ -71,6 +71,38 @@ export declare const REPEATED_SLOTS: {
71
71
  readonly scope: "[data-bl=\"breadcrumbs\"]";
72
72
  readonly slots: readonly ["item", "link", "current"];
73
73
  };
74
+ readonly localFacts: {
75
+ readonly scope: "[data-bl-section=\"local-facts\"]";
76
+ readonly slots: readonly ["fact", "factSource", "fee"];
77
+ };
78
+ readonly councilRules: {
79
+ readonly scope: "[data-bl-section=\"council-rules\"]";
80
+ readonly slots: readonly ["fee"];
81
+ };
82
+ readonly nearbyJobs: {
83
+ readonly scope: "[data-bl-section=\"nearby-jobs\"] [data-slot=\"list\"]";
84
+ readonly slots: readonly ["item", "body", "media", "title", "link", "outcome"];
85
+ };
86
+ readonly coverage: {
87
+ readonly scope: "[data-bl-section=\"coverage\"] [data-slot=\"groups\"]";
88
+ readonly slots: readonly ["group", "groupHeading"];
89
+ };
90
+ readonly relatedServices: {
91
+ readonly scope: "[data-bl-section=\"related-services\"] [data-slot=\"list\"]";
92
+ readonly slots: readonly ["item", "body", "media", "title", "summary", "link"];
93
+ };
94
+ readonly gallery: {
95
+ readonly scope: "[data-bl-section=\"gallery\"] [data-slot=\"list\"]";
96
+ readonly slots: readonly ["item", "link"];
97
+ };
98
+ readonly beforeAfter: {
99
+ readonly scope: "[data-bl-section=\"before-after\"] [data-slot=\"list\"]";
100
+ readonly slots: readonly ["pair", "frames", "frame", "note"];
101
+ };
102
+ readonly licences: {
103
+ readonly scope: "[data-bl-section=\"licences\"] [data-slot=\"list\"]";
104
+ readonly slots: readonly ["row", "label", "value", "number", "holder", "classes", "class", "register", "cover", "amount"];
105
+ };
74
106
  readonly thankYou: {
75
107
  readonly scope: "[data-bl=\"thank-you\"]";
76
108
  readonly slots: readonly ["step"];
@@ -1 +1 @@
1
- {"version":3,"file":"repeated-slots.d.ts","sourceRoot":"","sources":["../../src/ui-kit/repeated-slots.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;QAqBvB,kGAAkG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAmCpG,mGAAmG;;;;;;;;;CAE3F,CAAA;AAEV,MAAM,MAAM,qBAAqB,GAAG,MAAM,OAAO,cAAc,CAAA;AAC/D,6HAA6H;AAC7H,MAAM,MAAM,cAAc,CAAC,SAAS,SAAS,qBAAqB,IAAI,CAAC,OAAO,cAAc,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAA;AAEzH,sHAAsH;AACtH,wBAAgB,oBAAoB,CAAC,SAAS,SAAS,qBAAqB,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,cAAc,CAAC,SAAS,CAAC,GAAG,MAAM,CAG3I"}
1
+ {"version":3,"file":"repeated-slots.d.ts","sourceRoot":"","sources":["../../src/ui-kit/repeated-slots.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;QAqBvB,kGAAkG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAmDpG,mGAAmG;;;;;;;;;CAE3F,CAAA;AAEV,MAAM,MAAM,qBAAqB,GAAG,MAAM,OAAO,cAAc,CAAA;AAC/D,6HAA6H;AAC7H,MAAM,MAAM,cAAc,CAAC,SAAS,SAAS,qBAAqB,IAAI,CAAC,OAAO,cAAc,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAA;AAEzH,sHAAsH;AACtH,wBAAgB,oBAAoB,CAAC,SAAS,SAAS,qBAAqB,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,cAAc,CAAC,SAAS,CAAC,GAAG,MAAM,CAG3I"}
@@ -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"]}