@brandlab-digital/site-next 3.9.0 → 3.11.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.
- package/README.md +33 -0
- package/dist/contact/attachments-client.d.ts +3 -0
- package/dist/contact/attachments-client.d.ts.map +1 -1
- package/dist/contact/attachments-client.js +31 -8
- package/dist/contact/attachments-client.js.map +1 -1
- package/dist/contact/client-entry.d.ts +1 -1
- package/dist/contact/client-entry.d.ts.map +1 -1
- package/dist/contact/client-entry.js.map +1 -1
- package/dist/contact/contract.d.ts.map +1 -1
- package/dist/contact/contract.js +2 -2
- package/dist/contact/contract.js.map +1 -1
- package/dist/contact/email.d.ts.map +1 -1
- package/dist/contact/email.js +9 -0
- package/dist/contact/email.js.map +1 -1
- package/dist/contact/form-client.d.ts +4 -2
- package/dist/contact/form-client.d.ts.map +1 -1
- package/dist/contact/form-client.js +49 -13
- package/dist/contact/form-client.js.map +1 -1
- package/dist/contact/rate-limit.d.ts +59 -0
- package/dist/contact/rate-limit.d.ts.map +1 -0
- package/dist/contact/rate-limit.js +109 -0
- package/dist/contact/rate-limit.js.map +1 -0
- package/dist/contact/route-handler.d.ts +6 -0
- package/dist/contact/route-handler.d.ts.map +1 -1
- package/dist/contact/route-handler.js +4 -0
- package/dist/contact/route-handler.js.map +1 -1
- package/dist/contact/server-entry.d.ts +1 -1
- package/dist/contact/server-entry.d.ts.map +1 -1
- package/dist/contact/server-entry.js +1 -1
- package/dist/contact/server-entry.js.map +1 -1
- package/dist/contact/server.d.ts +9 -0
- package/dist/contact/server.d.ts.map +1 -1
- package/dist/contact/server.js +71 -7
- package/dist/contact/server.js.map +1 -1
- package/dist/contact/shared.d.ts +11 -0
- package/dist/contact/shared.d.ts.map +1 -1
- package/dist/contact/shared.js +11 -0
- package/dist/contact/shared.js.map +1 -1
- package/dist/contact/testing.d.ts +5 -0
- package/dist/contact/testing.d.ts.map +1 -1
- package/dist/contact/testing.js +1 -0
- package/dist/contact/testing.js.map +1 -1
- package/dist/lead-reporting.d.ts +6 -0
- package/dist/lead-reporting.d.ts.map +1 -1
- package/dist/lead-reporting.js +13 -1
- package/dist/lead-reporting.js.map +1 -1
- package/dist/ui-kit/brandlab-skin.d.ts +34 -0
- package/dist/ui-kit/brandlab-skin.d.ts.map +1 -1
- package/dist/ui-kit/brandlab-skin.js.map +1 -1
- package/dist/ui-kit/form/copy.d.ts +3 -0
- package/dist/ui-kit/form/copy.d.ts.map +1 -0
- package/dist/ui-kit/form/copy.js +5 -0
- package/dist/ui-kit/form/copy.js.map +1 -0
- package/dist/ui-kit/form/dialog.d.ts +24 -0
- package/dist/ui-kit/form/dialog.d.ts.map +1 -0
- package/dist/ui-kit/form/dialog.js +17 -0
- package/dist/ui-kit/form/dialog.js.map +1 -0
- package/dist/ui-kit/form/field.d.ts +30 -0
- package/dist/ui-kit/form/field.d.ts.map +1 -0
- package/dist/ui-kit/form/field.js +11 -0
- package/dist/ui-kit/form/field.js.map +1 -0
- package/dist/ui-kit/form/index.d.ts +7 -0
- package/dist/ui-kit/form/index.d.ts.map +1 -0
- package/dist/ui-kit/form/index.js +7 -0
- package/dist/ui-kit/form/index.js.map +1 -0
- package/dist/ui-kit/form/photo-upload.d.ts +37 -0
- package/dist/ui-kit/form/photo-upload.d.ts.map +1 -0
- package/dist/ui-kit/form/photo-upload.js +19 -0
- package/dist/ui-kit/form/photo-upload.js.map +1 -0
- package/dist/ui-kit/form/quote-dialog.d.ts +15 -0
- package/dist/ui-kit/form/quote-dialog.d.ts.map +1 -0
- package/dist/ui-kit/form/quote-dialog.js +68 -0
- package/dist/ui-kit/form/quote-dialog.js.map +1 -0
- package/dist/ui-kit/form/quote-form.d.ts +165 -0
- package/dist/ui-kit/form/quote-form.d.ts.map +1 -0
- package/dist/ui-kit/form/quote-form.js +235 -0
- package/dist/ui-kit/form/quote-form.js.map +1 -0
- package/dist/ui-kit/form/thank-you.d.ts +37 -0
- package/dist/ui-kit/form/thank-you.d.ts.map +1 -0
- package/dist/ui-kit/form/thank-you.js +36 -0
- package/dist/ui-kit/form/thank-you.js.map +1 -0
- package/dist/ui-kit/sections/accordion.d.ts +25 -0
- package/dist/ui-kit/sections/accordion.d.ts.map +1 -0
- package/dist/ui-kit/sections/accordion.js +14 -0
- package/dist/ui-kit/sections/accordion.js.map +1 -0
- package/dist/ui-kit/sections/contact-block.d.ts +51 -0
- package/dist/ui-kit/sections/contact-block.d.ts.map +1 -0
- package/dist/ui-kit/sections/contact-block.js +15 -0
- package/dist/ui-kit/sections/contact-block.js.map +1 -0
- package/dist/ui-kit/sections/cta-band.d.ts +59 -0
- package/dist/ui-kit/sections/cta-band.d.ts.map +1 -0
- package/dist/ui-kit/sections/cta-band.js +11 -0
- package/dist/ui-kit/sections/cta-band.js.map +1 -0
- package/dist/ui-kit/sections/faqs.d.ts +43 -0
- package/dist/ui-kit/sections/faqs.d.ts.map +1 -0
- package/dist/ui-kit/sections/faqs.js +40 -0
- package/dist/ui-kit/sections/faqs.js.map +1 -0
- package/dist/ui-kit/sections/index.d.ts +13 -0
- package/dist/ui-kit/sections/index.d.ts.map +1 -0
- package/dist/ui-kit/sections/index.js +13 -0
- package/dist/ui-kit/sections/index.js.map +1 -0
- package/dist/ui-kit/sections/page-header.d.ts +29 -0
- package/dist/ui-kit/sections/page-header.d.ts.map +1 -0
- package/dist/ui-kit/sections/page-header.js +9 -0
- package/dist/ui-kit/sections/page-header.js.map +1 -0
- package/dist/ui-kit/sections/price-guide.d.ts +56 -0
- package/dist/ui-kit/sections/price-guide.d.ts.map +1 -0
- package/dist/ui-kit/sections/price-guide.js +28 -0
- package/dist/ui-kit/sections/price-guide.js.map +1 -0
- package/dist/ui-kit/sections/price-table.d.ts +44 -0
- package/dist/ui-kit/sections/price-table.d.ts.map +1 -0
- package/dist/ui-kit/sections/price-table.js +29 -0
- package/dist/ui-kit/sections/price-table.js.map +1 -0
- package/dist/ui-kit/sections/proof-strip.d.ts +57 -0
- package/dist/ui-kit/sections/proof-strip.d.ts.map +1 -0
- package/dist/ui-kit/sections/proof-strip.js +20 -0
- package/dist/ui-kit/sections/proof-strip.js.map +1 -0
- package/dist/ui-kit/sections/rating-chip.d.ts +25 -0
- package/dist/ui-kit/sections/rating-chip.d.ts.map +1 -0
- package/dist/ui-kit/sections/rating-chip.js +15 -0
- package/dist/ui-kit/sections/rating-chip.js.map +1 -0
- package/dist/ui-kit/sections/reviews-band.d.ts +110 -0
- package/dist/ui-kit/sections/reviews-band.d.ts.map +1 -0
- package/dist/ui-kit/sections/reviews-band.js +104 -0
- package/dist/ui-kit/sections/reviews-band.js.map +1 -0
- package/dist/ui-kit/sections/section-sequence.d.ts +39 -0
- package/dist/ui-kit/sections/section-sequence.d.ts.map +1 -0
- package/dist/ui-kit/sections/section-sequence.js +34 -0
- package/dist/ui-kit/sections/section-sequence.js.map +1 -0
- package/dist/ui-kit/sections/site-footer-columns.d.ts +12 -0
- package/dist/ui-kit/sections/site-footer-columns.d.ts.map +1 -0
- package/dist/ui-kit/sections/site-footer-columns.js +63 -0
- package/dist/ui-kit/sections/site-footer-columns.js.map +1 -0
- package/dist/ui-kit/sections/site-footer.d.ts +59 -0
- package/dist/ui-kit/sections/site-footer.d.ts.map +1 -0
- package/dist/ui-kit/sections/site-footer.js +41 -0
- package/dist/ui-kit/sections/site-footer.js.map +1 -0
- package/dist/ui-kit/structure.css +766 -0
- package/dist/ui.d.ts +10 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +3 -3
- package/dist/ui.js.map +1 -1
- package/package.json +10 -2
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SectionSequence` (F15): renders a page family's sections in order. Server component, ported from Omega's
|
|
3
|
+
* `Sequence` and `alternateTones`. Each section the page offers comes with its guard, computed from the same data it
|
|
4
|
+
* renders (`faqsReady(props)`, `priceGuideReady(props)` …), so a section that would render nothing is skipped with
|
|
5
|
+
* its reason without rendering twice. Then the cap drops optional sections first, the light tones alternate so
|
|
6
|
+
* neighbours never share a background (at most one `band` and one `photo`), and the page's manifest is written as
|
|
7
|
+
* `<script type="application/json" data-bl-manifest>` (`SECTION_MANIFEST_ATTRIBUTE`) for K12: what was planned, what
|
|
8
|
+
* rendered with which tone, and what was skipped and why.
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* <SectionSequence
|
|
12
|
+
* order={orders.service}
|
|
13
|
+
* page="/tree-removal"
|
|
14
|
+
* sections={[
|
|
15
|
+
* { id: 'hero', tone: 'photo', render: (tone) => <Hero tone={tone} {...hero} /> },
|
|
16
|
+
* { id: 'price-guide', ready: priceGuideReady(guide), render: (tone) => <PriceGuide tone={tone} {...guide} /> },
|
|
17
|
+
* { id: 'cta', tone: 'band', render: (tone) => <CTABand tone={tone} {...cta} /> },
|
|
18
|
+
* ]}
|
|
19
|
+
* />
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import { type ReactNode } from 'react';
|
|
23
|
+
import { type BrandlabFamilyOrder, type BrandlabSectionManifest, type BrandlabSequenceCandidate } from '@brandlab-digital/site-core/section-order';
|
|
24
|
+
import type { SectionTone } from '../primitives/layout.js';
|
|
25
|
+
/** One section the page offers: its matrix id, its guard, the tone it asks for and how to render it with a tone. */
|
|
26
|
+
export interface SequenceSection extends BrandlabSequenceCandidate {
|
|
27
|
+
readonly render: (tone: SectionTone) => ReactNode;
|
|
28
|
+
}
|
|
29
|
+
export interface SectionSequenceProps {
|
|
30
|
+
/** The family's order, from the site's `defineSectionOrder` (or `defaultSectionOrder`). */
|
|
31
|
+
readonly order: BrandlabFamilyOrder;
|
|
32
|
+
readonly sections: readonly SequenceSection[];
|
|
33
|
+
/** The page path, recorded in the manifest. */
|
|
34
|
+
readonly page?: string;
|
|
35
|
+
}
|
|
36
|
+
/** The manifest as safe inline JSON (no `<`, `>` or `&` that could close the script). */
|
|
37
|
+
export declare function serialiseSectionManifest(manifest: BrandlabSectionManifest): string;
|
|
38
|
+
export declare function SectionSequence({ order, sections, page }: SectionSequenceProps): import("react/jsx-runtime").JSX.Element;
|
|
39
|
+
//# sourceMappingURL=section-sequence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"section-sequence.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/section-sequence.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAEhD,OAAO,EAGL,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,KAAK,yBAAyB,EAC/B,MAAM,2CAA2C,CAAA;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAE1D,oHAAoH;AACpH,MAAM,WAAW,eAAgB,SAAQ,yBAAyB;IAChE,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,SAAS,CAAA;CAClD;AAED,MAAM,WAAW,oBAAoB;IACnC,2FAA2F;IAC3F,QAAQ,CAAC,KAAK,EAAE,mBAAmB,CAAA;IACnC,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAA;IAC7C,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,yFAAyF;AACzF,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,uBAAuB,GAAG,MAAM,CAElF;AAED,wBAAgB,eAAe,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,oBAAoB,2CAc9E"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* `SectionSequence` (F15): renders a page family's sections in order. Server component, ported from Omega's
|
|
4
|
+
* `Sequence` and `alternateTones`. Each section the page offers comes with its guard, computed from the same data it
|
|
5
|
+
* renders (`faqsReady(props)`, `priceGuideReady(props)` …), so a section that would render nothing is skipped with
|
|
6
|
+
* its reason without rendering twice. Then the cap drops optional sections first, the light tones alternate so
|
|
7
|
+
* neighbours never share a background (at most one `band` and one `photo`), and the page's manifest is written as
|
|
8
|
+
* `<script type="application/json" data-bl-manifest>` (`SECTION_MANIFEST_ATTRIBUTE`) for K12: what was planned, what
|
|
9
|
+
* rendered with which tone, and what was skipped and why.
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <SectionSequence
|
|
13
|
+
* order={orders.service}
|
|
14
|
+
* page="/tree-removal"
|
|
15
|
+
* sections={[
|
|
16
|
+
* { id: 'hero', tone: 'photo', render: (tone) => <Hero tone={tone} {...hero} /> },
|
|
17
|
+
* { id: 'price-guide', ready: priceGuideReady(guide), render: (tone) => <PriceGuide tone={tone} {...guide} /> },
|
|
18
|
+
* { id: 'cta', tone: 'band', render: (tone) => <CTABand tone={tone} {...cta} /> },
|
|
19
|
+
* ]}
|
|
20
|
+
* />
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import { Fragment } from 'react';
|
|
24
|
+
import { sanitizeJsonLd } from '@brandlab-digital/site-core/schema';
|
|
25
|
+
import { SECTION_MANIFEST_ATTRIBUTE, planSectionSequence, } from '@brandlab-digital/site-core/section-order';
|
|
26
|
+
/** The manifest as safe inline JSON (no `<`, `>` or `&` that could close the script). */
|
|
27
|
+
export function serialiseSectionManifest(manifest) {
|
|
28
|
+
return sanitizeJsonLd(manifest);
|
|
29
|
+
}
|
|
30
|
+
export function SectionSequence({ order, sections, page }) {
|
|
31
|
+
const plan = planSectionSequence(order, sections, page);
|
|
32
|
+
return (_jsxs(_Fragment, { children: [plan.rendered.map((item) => (_jsx(Fragment, { children: sections[item.candidate]?.render(item.tone) }, item.id))), _jsx("script", { type: "application/json", [SECTION_MANIFEST_ATTRIBUTE]: '', dangerouslySetInnerHTML: { __html: serialiseSectionManifest(plan.manifest) } })] }));
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=section-sequence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"section-sequence.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/section-sequence.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAA;AACnE,OAAO,EACL,0BAA0B,EAC1B,mBAAmB,GAIpB,MAAM,2CAA2C,CAAA;AAgBlD,yFAAyF;AACzF,MAAM,UAAU,wBAAwB,CAAC,QAAiC;IACxE,OAAO,cAAc,CAAC,QAAQ,CAAC,CAAA;AACjC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAwB;IAC7E,MAAM,IAAI,GAAG,mBAAmB,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAA;IACvD,OAAO,CACL,8BACG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAC3B,KAAC,QAAQ,cAAgB,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAArD,IAAI,CAAC,EAAE,CAA0D,CACjF,CAAC,EACF,iBACE,IAAI,EAAC,kBAAkB,EACjB,CAAC,0BAA0B,CAAC,EAAE,EAAE,EACtC,uBAAuB,EAAE,EAAE,MAAM,EAAE,wBAAwB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,GAC5E,IACD,CACJ,CAAA;AACH,CAAC","sourcesContent":["/**\n * `SectionSequence` (F15): renders a page family's sections in order. Server component, ported from Omega's\n * `Sequence` and `alternateTones`. Each section the page offers comes with its guard, computed from the same data it\n * renders (`faqsReady(props)`, `priceGuideReady(props)` …), so a section that would render nothing is skipped with\n * its reason without rendering twice. Then the cap drops optional sections first, the light tones alternate so\n * neighbours never share a background (at most one `band` and one `photo`), and the page's manifest is written as\n * `<script type=\"application/json\" data-bl-manifest>` (`SECTION_MANIFEST_ATTRIBUTE`) for K12: what was planned, what\n * rendered with which tone, and what was skipped and why.\n *\n * ```tsx\n * <SectionSequence\n * order={orders.service}\n * page=\"/tree-removal\"\n * sections={[\n * { id: 'hero', tone: 'photo', render: (tone) => <Hero tone={tone} {...hero} /> },\n * { id: 'price-guide', ready: priceGuideReady(guide), render: (tone) => <PriceGuide tone={tone} {...guide} /> },\n * { id: 'cta', tone: 'band', render: (tone) => <CTABand tone={tone} {...cta} /> },\n * ]}\n * />\n * ```\n */\nimport { Fragment, type ReactNode } from 'react'\nimport { sanitizeJsonLd } from '@brandlab-digital/site-core/schema'\nimport {\n SECTION_MANIFEST_ATTRIBUTE,\n planSectionSequence,\n type BrandlabFamilyOrder,\n type BrandlabSectionManifest,\n type BrandlabSequenceCandidate,\n} from '@brandlab-digital/site-core/section-order'\nimport type { SectionTone } from '../primitives/layout.js'\n\n/** One section the page offers: its matrix id, its guard, the tone it asks for and how to render it with a tone. */\nexport interface SequenceSection extends BrandlabSequenceCandidate {\n readonly render: (tone: SectionTone) => ReactNode\n}\n\nexport interface SectionSequenceProps {\n /** The family's order, from the site's `defineSectionOrder` (or `defaultSectionOrder`). */\n readonly order: BrandlabFamilyOrder\n readonly sections: readonly SequenceSection[]\n /** The page path, recorded in the manifest. */\n readonly page?: string\n}\n\n/** The manifest as safe inline JSON (no `<`, `>` or `&` that could close the script). */\nexport function serialiseSectionManifest(manifest: BrandlabSectionManifest): string {\n return sanitizeJsonLd(manifest)\n}\n\nexport function SectionSequence({ order, sections, page }: SectionSequenceProps) {\n const plan = planSectionSequence(order, sections, page)\n return (\n <>\n {plan.rendered.map((item) => (\n <Fragment key={item.id}>{sections[item.candidate]?.render(item.tone)}</Fragment>\n ))}\n <script\n type=\"application/json\"\n {...{ [SECTION_MANIFEST_ATTRIBUTE]: '' }}\n dangerouslySetInnerHTML={{ __html: serialiseSectionManifest(plan.manifest) }}\n />\n </>\n )\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The footer's phone accordion (client island). Below 640 px of footer width each column's H2 shows its disclosure
|
|
3
|
+
* button and one column is open at a time; wider, every list shows and the buttons stay hidden. The headings, buttons
|
|
4
|
+
* and links are server HTML passed in as children; this island only toggles `hidden`, `aria-expanded` and
|
|
5
|
+
* `data-state`, so without scripts every link shows.
|
|
6
|
+
*/
|
|
7
|
+
import { type ReactNode } from 'react';
|
|
8
|
+
export declare function SiteFooterColumns({ className, children }: {
|
|
9
|
+
readonly className?: string | undefined;
|
|
10
|
+
readonly children: ReactNode;
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
//# sourceMappingURL=site-footer-columns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"site-footer-columns.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/site-footer-columns.tsx"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAEzD,wBAAgB,iBAAiB,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,2CAsDnI"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* The footer's phone accordion (client island). Below 640 px of footer width each column's H2 shows its disclosure
|
|
5
|
+
* button and one column is open at a time; wider, every list shows and the buttons stay hidden. The headings, buttons
|
|
6
|
+
* and links are server HTML passed in as children; this island only toggles `hidden`, `aria-expanded` and
|
|
7
|
+
* `data-state`, so without scripts every link shows.
|
|
8
|
+
*/
|
|
9
|
+
import { useEffect, useRef } from 'react';
|
|
10
|
+
export function SiteFooterColumns({ className, children }) {
|
|
11
|
+
const ref = useRef(null);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const root = ref.current;
|
|
14
|
+
const footer = root?.closest('[data-bl="footer"]');
|
|
15
|
+
if (!root || !footer)
|
|
16
|
+
return;
|
|
17
|
+
const columns = Array.from(root.querySelectorAll('[data-slot="column"]'));
|
|
18
|
+
let narrow = false;
|
|
19
|
+
let open = null;
|
|
20
|
+
const render = () => {
|
|
21
|
+
root.dataset.accordion = String(narrow);
|
|
22
|
+
for (const column of columns) {
|
|
23
|
+
const expanded = narrow && column === open;
|
|
24
|
+
const toggle = column.querySelector('[data-slot="toggle"]');
|
|
25
|
+
const text = column.querySelector('[data-slot="headingText"]');
|
|
26
|
+
const list = column.querySelector('[data-slot="list"]');
|
|
27
|
+
if (toggle) {
|
|
28
|
+
toggle.hidden = !narrow;
|
|
29
|
+
toggle.setAttribute('aria-expanded', String(expanded));
|
|
30
|
+
}
|
|
31
|
+
if (text)
|
|
32
|
+
text.hidden = narrow;
|
|
33
|
+
if (list)
|
|
34
|
+
list.hidden = narrow && !expanded;
|
|
35
|
+
if (narrow)
|
|
36
|
+
column.dataset.state = expanded ? 'open' : 'closed';
|
|
37
|
+
else
|
|
38
|
+
delete column.dataset.state;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const onClick = (event) => {
|
|
42
|
+
const column = event.target?.closest('[data-slot="toggle"]')?.closest('[data-slot="column"]');
|
|
43
|
+
if (!narrow || !column)
|
|
44
|
+
return;
|
|
45
|
+
open = open === column ? null : column;
|
|
46
|
+
render();
|
|
47
|
+
};
|
|
48
|
+
const observer = new ResizeObserver(() => {
|
|
49
|
+
narrow = footer.getBoundingClientRect().width < 640;
|
|
50
|
+
if (!narrow)
|
|
51
|
+
open = null;
|
|
52
|
+
render();
|
|
53
|
+
});
|
|
54
|
+
observer.observe(footer);
|
|
55
|
+
root.addEventListener('click', onClick);
|
|
56
|
+
return () => {
|
|
57
|
+
observer.disconnect();
|
|
58
|
+
root.removeEventListener('click', onClick);
|
|
59
|
+
};
|
|
60
|
+
}, []);
|
|
61
|
+
return (_jsx("div", { ref: ref, "data-slot": "columns", className: className, children: children }));
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=site-footer-columns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"site-footer-columns.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/site-footer-columns.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\n/**\n * The footer's phone accordion (client island). Below 640 px of footer width each column's H2 shows its disclosure\n * button and one column is open at a time; wider, every list shows and the buttons stay hidden. The headings, buttons\n * and links are server HTML passed in as children; this island only toggles `hidden`, `aria-expanded` and\n * `data-state`, so without scripts every link shows.\n */\nimport { useEffect, useRef, type ReactNode } from 'react'\n\nexport function SiteFooterColumns({ className, children }: { readonly className?: string | undefined; readonly children: ReactNode }) {\n const ref = useRef<HTMLDivElement>(null)\n\n useEffect(() => {\n const root = ref.current\n const footer = root?.closest<HTMLElement>('[data-bl=\"footer\"]')\n if (!root || !footer) return\n const columns = Array.from(root.querySelectorAll<HTMLElement>('[data-slot=\"column\"]'))\n let narrow = false\n let open: HTMLElement | null = null\n\n const render = () => {\n root.dataset.accordion = String(narrow)\n for (const column of columns) {\n const expanded = narrow && column === open\n const toggle = column.querySelector<HTMLElement>('[data-slot=\"toggle\"]')\n const text = column.querySelector<HTMLElement>('[data-slot=\"headingText\"]')\n const list = column.querySelector<HTMLElement>('[data-slot=\"list\"]')\n if (toggle) {\n toggle.hidden = !narrow\n toggle.setAttribute('aria-expanded', String(expanded))\n }\n if (text) text.hidden = narrow\n if (list) list.hidden = narrow && !expanded\n if (narrow) column.dataset.state = expanded ? 'open' : 'closed'\n else delete column.dataset.state\n }\n }\n\n const onClick = (event: MouseEvent) => {\n const column = (event.target as Element | null)?.closest('[data-slot=\"toggle\"]')?.closest<HTMLElement>('[data-slot=\"column\"]')\n if (!narrow || !column) return\n open = open === column ? null : column\n render()\n }\n\n const observer = new ResizeObserver(() => {\n narrow = footer.getBoundingClientRect().width < 640\n if (!narrow) open = null\n render()\n })\n observer.observe(footer)\n root.addEventListener('click', onClick)\n return () => {\n observer.disconnect()\n root.removeEventListener('click', onClick)\n }\n }, [])\n\n return (\n <div ref={ref} data-slot=\"columns\" className={className}>\n {children}\n </div>\n )\n}\n"]}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { type SkinEntry } from '../skin.js';
|
|
3
|
+
import type { UiLink, UiPhone, UiSocial } from '../types.js';
|
|
4
|
+
export declare const SITE_FOOTER_VARIANTS: readonly ["columns", "photo"];
|
|
5
|
+
export type SiteFooterVariant = (typeof SITE_FOOTER_VARIANTS)[number];
|
|
6
|
+
export type SiteFooterSlot = 'root' | 'panel' | 'media' | 'inner' | 'brand' | 'logo' | 'description' | 'licences' | 'licenceLink' | 'social' | 'socialLink' | 'contact' | 'phone' | 'phoneLabel' | 'phoneNumber' | 'email' | 'address' | 'columns' | 'column' | 'heading' | 'toggle' | 'list' | 'link' | 'bottom' | 'bottomInner' | 'legal' | 'policies' | 'policyLink';
|
|
7
|
+
export type SiteFooterSkin = SkinEntry<SiteFooterSlot, SiteFooterVariant>;
|
|
8
|
+
/** One licence line: the text as the register shows it, and the register link when there is one. */
|
|
9
|
+
export interface SiteFooterLicence {
|
|
10
|
+
readonly text: string;
|
|
11
|
+
readonly href?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SiteFooterColumn {
|
|
14
|
+
/** The H2 text, for example "Services". */
|
|
15
|
+
readonly heading: string;
|
|
16
|
+
readonly links: readonly UiLink[];
|
|
17
|
+
}
|
|
18
|
+
export interface SiteFooterProps extends SiteFooterSkin {
|
|
19
|
+
/** The logo link's accessible name. */
|
|
20
|
+
readonly brandName: string;
|
|
21
|
+
readonly homeHref?: string;
|
|
22
|
+
/** The site's logo node (decorative inside the named link). */
|
|
23
|
+
readonly logo: ReactNode;
|
|
24
|
+
/** Two or three lines about the business. */
|
|
25
|
+
readonly description: string;
|
|
26
|
+
readonly licences?: readonly SiteFooterLicence[];
|
|
27
|
+
readonly socials?: readonly UiSocial[];
|
|
28
|
+
readonly phone: UiPhone;
|
|
29
|
+
/** Optional words before the number, for example "Call us". */
|
|
30
|
+
readonly phoneLabel?: string;
|
|
31
|
+
readonly email?: string;
|
|
32
|
+
/** Only when the business shows its address publicly. */
|
|
33
|
+
readonly address?: string;
|
|
34
|
+
/** Services, Areas and Company, rendered in that order; leave one out when the site has nothing true for it. */
|
|
35
|
+
readonly columns: {
|
|
36
|
+
readonly services?: SiteFooterColumn;
|
|
37
|
+
readonly areas?: SiteFooterColumn;
|
|
38
|
+
readonly company?: SiteFooterColumn;
|
|
39
|
+
};
|
|
40
|
+
readonly legal: {
|
|
41
|
+
/** The legal or trading name. */
|
|
42
|
+
readonly name: string;
|
|
43
|
+
readonly abn?: string;
|
|
44
|
+
/** Defaults to the current year in Sydney. */
|
|
45
|
+
readonly year?: number;
|
|
46
|
+
/** The privacy policy link, labelled by the site. */
|
|
47
|
+
readonly privacy?: UiLink;
|
|
48
|
+
/** The terms link, labelled by the site. */
|
|
49
|
+
readonly terms?: UiLink;
|
|
50
|
+
};
|
|
51
|
+
/** The `photo` variant's backdrop image for the panel (decorative). */
|
|
52
|
+
readonly media?: ReactNode;
|
|
53
|
+
/** Signature art, rendered `aria-hidden`. */
|
|
54
|
+
readonly decoration?: ReactNode;
|
|
55
|
+
/** Prefix for the column list ids (default `bl-footer`). */
|
|
56
|
+
readonly idPrefix?: string;
|
|
57
|
+
}
|
|
58
|
+
export declare function SiteFooter({ brandName, homeHref, logo, description, licences, socials, phone, phoneLabel, email, address, columns, legal, media, decoration, idPrefix, variant, classes, }: SiteFooterProps): import("react/jsx-runtime").JSX.Element;
|
|
59
|
+
//# sourceMappingURL=site-footer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"site-footer.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/sections/site-footer.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* S27 `SiteFooter`: the v1 §6 footer, structured the same on every site. Server component; one small client island
|
|
4
|
+
* (`SiteFooterColumns`) turns the columns into a phone accordion.
|
|
5
|
+
*
|
|
6
|
+
* Column 1: the logo, a short description, one line per licence (with its register link when given), social and
|
|
7
|
+
* Business Profile icons, then the phone (`phone_link_click` placement `footer`), the email and the address only when
|
|
8
|
+
* it is public. Then Services, Areas and Company, each under a real H2, in that order. A bottom bar: © year name ·
|
|
9
|
+
* ABN · the privacy and terms links. Never a quote button, "based in", opening hours, a guides column or a "back to
|
|
10
|
+
* top" text link: the props have no place for them.
|
|
11
|
+
*
|
|
12
|
+
* Variants: `columns`; `photo`, with the site's backdrop image for the panel in `media` (decorative, `aria-hidden`),
|
|
13
|
+
* for example Superior's sunlit water. Signature art goes in `decoration`.
|
|
14
|
+
*/
|
|
15
|
+
import Link from 'next/link';
|
|
16
|
+
import { Icon, SocialIcon } from '../icons.js';
|
|
17
|
+
import { PhoneLink } from '../primitives/links.js';
|
|
18
|
+
import { slot } from '../skin.js';
|
|
19
|
+
import { SiteFooterColumns } from './site-footer-columns.js';
|
|
20
|
+
export const SITE_FOOTER_VARIANTS = ['columns', 'photo'];
|
|
21
|
+
const COLUMN_KEYS = ['services', 'areas', 'company'];
|
|
22
|
+
function isExternal(href) {
|
|
23
|
+
return /^https?:\/\//i.test(href);
|
|
24
|
+
}
|
|
25
|
+
function FooterLink({ link, hooks }) {
|
|
26
|
+
return isExternal(link.href) ? (_jsx("a", { href: link.href, ...hooks, target: "_blank", rel: "noopener noreferrer", children: link.label })) : (_jsx(Link, { href: link.href, ...hooks, children: link.label }));
|
|
27
|
+
}
|
|
28
|
+
function currentYear() {
|
|
29
|
+
return Number(new Intl.DateTimeFormat('en-AU', { year: 'numeric', timeZone: 'Australia/Sydney' }).format(new Date()));
|
|
30
|
+
}
|
|
31
|
+
export function SiteFooter({ brandName, homeHref = '/', logo, description, licences = [], socials = [], phone, phoneLabel, email, address, columns, legal, media, decoration, idPrefix = 'bl-footer', variant = 'columns', classes, }) {
|
|
32
|
+
const policies = [legal.privacy, legal.terms].filter((link) => !!link);
|
|
33
|
+
return (_jsxs("footer", { "data-bl": "footer", "data-variant": variant, className: classes?.root, children: [decoration ? (_jsx("div", { "data-slot": "decoration", "aria-hidden": "true", children: decoration })) : null, _jsxs("div", { ...slot(classes, 'panel'), children: [variant === 'photo' && media ? (_jsx("div", { ...slot(classes, 'media'), "aria-hidden": "true", children: media })) : null, _jsxs("div", { ...slot(classes, 'inner'), children: [_jsxs("div", { ...slot(classes, 'brand'), children: [_jsx(Link, { href: homeHref, ...slot(classes, 'logo'), "aria-label": brandName, children: logo }), _jsx("p", { ...slot(classes, 'description'), children: description }), licences.length > 0 ? (_jsx("ul", { ...slot(classes, 'licences'), children: licences.map((licence) => (_jsx("li", { children: licence.href ? (_jsx("a", { ...slot(classes, 'licenceLink'), href: licence.href, target: "_blank", rel: "noopener noreferrer", children: licence.text })) : (licence.text) }, licence.text))) })) : null, socials.length > 0 ? (_jsx("ul", { ...slot(classes, 'social'), children: socials.map((social) => (_jsx("li", { children: _jsx("a", { ...slot(classes, 'socialLink'), href: social.href, "aria-label": social.label, target: "_blank", rel: "noopener noreferrer", children: _jsx(SocialIcon, { kind: social.kind }) }) }, social.href))) })) : null, _jsxs("div", { ...slot(classes, 'contact'), children: [_jsx(PhoneLink, { phone: phone, placement: "footer", label: phoneLabel, classes: { root: classes?.phone, label: classes?.phoneLabel, number: classes?.phoneNumber } }), email ? (_jsx("a", { ...slot(classes, 'email'), href: `mailto:${email}`, children: email })) : null, address ? _jsx("p", { ...slot(classes, 'address'), children: address }) : null] })] }), _jsx(SiteFooterColumns, { className: classes?.columns, children: COLUMN_KEYS.map((key) => {
|
|
34
|
+
const column = columns[key];
|
|
35
|
+
if (!column || column.links.length === 0)
|
|
36
|
+
return null;
|
|
37
|
+
const listId = `${idPrefix}-${key}`;
|
|
38
|
+
return (_jsxs("div", { ...slot(classes, 'column'), "data-column": key, children: [_jsxs("h2", { ...slot(classes, 'heading'), children: [_jsx("span", { "data-slot": "headingText", children: column.heading }), _jsxs("button", { type: "button", ...slot(classes, 'toggle'), "aria-expanded": "false", "aria-controls": listId, hidden: true, children: [_jsx("span", { children: column.heading }), _jsx(Icon, { name: "chevron" })] })] }), _jsx("ul", { id: listId, ...slot(classes, 'list'), children: column.links.map((link) => (_jsx("li", { children: _jsx(FooterLink, { link: link, hooks: slot(classes, 'link') }) }, link.href))) })] }, key));
|
|
39
|
+
}) })] })] }), _jsx("div", { ...slot(classes, 'bottom'), children: _jsxs("div", { ...slot(classes, 'bottomInner'), children: [_jsxs("p", { ...slot(classes, 'legal'), children: ["\u00A9 ", legal.year ?? currentYear(), " ", legal.name, legal.abn ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", children: " \u00B7 " }), _jsxs("span", { children: ["ABN ", legal.abn] })] })) : null] }), policies.length > 0 ? (_jsx("ul", { ...slot(classes, 'policies'), children: policies.map((link) => (_jsx("li", { children: _jsx(FooterLink, { link: link, hooks: slot(classes, 'policyLink') }) }, link.href))) })) : null] }) })] }));
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=site-footer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"site-footer.js","sourceRoot":"","sources":["../../../src/ui-kit/sections/site-footer.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * S27 `SiteFooter`: the v1 §6 footer, structured the same on every site. Server component; one small client island\n * (`SiteFooterColumns`) turns the columns into a phone accordion.\n *\n * Column 1: the logo, a short description, one line per licence (with its register link when given), social and\n * Business Profile icons, then the phone (`phone_link_click` placement `footer`), the email and the address only when\n * it is public. Then Services, Areas and Company, each under a real H2, in that order. A bottom bar: © year name ·\n * ABN · the privacy and terms links. Never a quote button, \"based in\", opening hours, a guides column or a \"back to\n * top\" text link: the props have no place for them.\n *\n * Variants: `columns`; `photo`, with the site's backdrop image for the panel in `media` (decorative, `aria-hidden`),\n * for example Superior's sunlit water. Signature art goes in `decoration`.\n */\nimport Link from 'next/link'\nimport type { ReactNode } from 'react'\nimport { Icon, SocialIcon } from '../icons.js'\nimport { PhoneLink } from '../primitives/links.js'\nimport { slot, type SkinEntry } from '../skin.js'\nimport type { UiLink, UiPhone, UiSocial } from '../types.js'\nimport { SiteFooterColumns } from './site-footer-columns.js'\n\nexport const SITE_FOOTER_VARIANTS = ['columns', 'photo'] as const\nexport type SiteFooterVariant = (typeof SITE_FOOTER_VARIANTS)[number]\n\nexport type SiteFooterSlot =\n | 'root'\n | 'panel'\n | 'media'\n | 'inner'\n | 'brand'\n | 'logo'\n | 'description'\n | 'licences'\n | 'licenceLink'\n | 'social'\n | 'socialLink'\n | 'contact'\n | 'phone'\n | 'phoneLabel'\n | 'phoneNumber'\n | 'email'\n | 'address'\n | 'columns'\n | 'column'\n | 'heading'\n | 'toggle'\n | 'list'\n | 'link'\n | 'bottom'\n | 'bottomInner'\n | 'legal'\n | 'policies'\n | 'policyLink'\n\nexport type SiteFooterSkin = SkinEntry<SiteFooterSlot, SiteFooterVariant>\n\n/** One licence line: the text as the register shows it, and the register link when there is one. */\nexport interface SiteFooterLicence {\n readonly text: string\n readonly href?: string\n}\n\nexport interface SiteFooterColumn {\n /** The H2 text, for example \"Services\". */\n readonly heading: string\n readonly links: readonly UiLink[]\n}\n\nexport interface SiteFooterProps extends SiteFooterSkin {\n /** The logo link's accessible name. */\n readonly brandName: string\n readonly homeHref?: string\n /** The site's logo node (decorative inside the named link). */\n readonly logo: ReactNode\n /** Two or three lines about the business. */\n readonly description: string\n readonly licences?: readonly SiteFooterLicence[]\n readonly socials?: readonly UiSocial[]\n readonly phone: UiPhone\n /** Optional words before the number, for example \"Call us\". */\n readonly phoneLabel?: string\n readonly email?: string\n /** Only when the business shows its address publicly. */\n readonly address?: string\n /** Services, Areas and Company, rendered in that order; leave one out when the site has nothing true for it. */\n readonly columns: {\n readonly services?: SiteFooterColumn\n readonly areas?: SiteFooterColumn\n readonly company?: SiteFooterColumn\n }\n readonly legal: {\n /** The legal or trading name. */\n readonly name: string\n readonly abn?: string\n /** Defaults to the current year in Sydney. */\n readonly year?: number\n /** The privacy policy link, labelled by the site. */\n readonly privacy?: UiLink\n /** The terms link, labelled by the site. */\n readonly terms?: UiLink\n }\n /** The `photo` variant's backdrop image for the panel (decorative). */\n readonly media?: ReactNode\n /** Signature art, rendered `aria-hidden`. */\n readonly decoration?: ReactNode\n /** Prefix for the column list ids (default `bl-footer`). */\n readonly idPrefix?: string\n}\n\nconst COLUMN_KEYS = ['services', 'areas', 'company'] as const\n\nfunction isExternal(href: string) {\n return /^https?:\\/\\//i.test(href)\n}\n\nfunction FooterLink({ link, hooks }: { link: UiLink; hooks?: { 'data-slot': string; className: string | undefined } }) {\n return isExternal(link.href) ? (\n <a href={link.href} {...hooks} target=\"_blank\" rel=\"noopener noreferrer\">\n {link.label}\n </a>\n ) : (\n <Link href={link.href} {...hooks}>\n {link.label}\n </Link>\n )\n}\n\nfunction currentYear() {\n return Number(new Intl.DateTimeFormat('en-AU', { year: 'numeric', timeZone: 'Australia/Sydney' }).format(new Date()))\n}\n\nexport function SiteFooter({\n brandName,\n homeHref = '/',\n logo,\n description,\n licences = [],\n socials = [],\n phone,\n phoneLabel,\n email,\n address,\n columns,\n legal,\n media,\n decoration,\n idPrefix = 'bl-footer',\n variant = 'columns',\n classes,\n}: SiteFooterProps) {\n const policies = [legal.privacy, legal.terms].filter((link): link is UiLink => !!link)\n return (\n <footer data-bl=\"footer\" data-variant={variant} className={classes?.root}>\n {decoration ? (\n <div data-slot=\"decoration\" aria-hidden=\"true\">\n {decoration}\n </div>\n ) : null}\n <div {...slot(classes, 'panel')}>\n {variant === 'photo' && media ? (\n <div {...slot(classes, 'media')} aria-hidden=\"true\">\n {media}\n </div>\n ) : null}\n <div {...slot(classes, 'inner')}>\n <div {...slot(classes, 'brand')}>\n <Link href={homeHref} {...slot(classes, 'logo')} aria-label={brandName}>\n {logo}\n </Link>\n <p {...slot(classes, 'description')}>{description}</p>\n {licences.length > 0 ? (\n <ul {...slot(classes, 'licences')}>\n {licences.map((licence) => (\n <li key={licence.text}>\n {licence.href ? (\n <a {...slot(classes, 'licenceLink')} href={licence.href} target=\"_blank\" rel=\"noopener noreferrer\">\n {licence.text}\n </a>\n ) : (\n licence.text\n )}\n </li>\n ))}\n </ul>\n ) : null}\n {socials.length > 0 ? (\n <ul {...slot(classes, 'social')}>\n {socials.map((social) => (\n <li key={social.href}>\n <a {...slot(classes, 'socialLink')} href={social.href} aria-label={social.label} target=\"_blank\" rel=\"noopener noreferrer\">\n <SocialIcon kind={social.kind} />\n </a>\n </li>\n ))}\n </ul>\n ) : null}\n <div {...slot(classes, 'contact')}>\n <PhoneLink phone={phone} placement=\"footer\" label={phoneLabel} classes={{ root: classes?.phone, label: classes?.phoneLabel, number: classes?.phoneNumber }} />\n {email ? (\n <a {...slot(classes, 'email')} href={`mailto:${email}`}>\n {email}\n </a>\n ) : null}\n {address ? <p {...slot(classes, 'address')}>{address}</p> : null}\n </div>\n </div>\n <SiteFooterColumns className={classes?.columns}>\n {COLUMN_KEYS.map((key) => {\n const column = columns[key]\n if (!column || column.links.length === 0) return null\n const listId = `${idPrefix}-${key}`\n return (\n <div key={key} {...slot(classes, 'column')} data-column={key}>\n <h2 {...slot(classes, 'heading')}>\n <span data-slot=\"headingText\">{column.heading}</span>\n <button type=\"button\" {...slot(classes, 'toggle')} aria-expanded=\"false\" aria-controls={listId} hidden>\n <span>{column.heading}</span>\n <Icon name=\"chevron\" />\n </button>\n </h2>\n <ul id={listId} {...slot(classes, 'list')}>\n {column.links.map((link) => (\n <li key={link.href}>\n <FooterLink link={link} hooks={slot(classes, 'link')} />\n </li>\n ))}\n </ul>\n </div>\n )\n })}\n </SiteFooterColumns>\n </div>\n </div>\n <div {...slot(classes, 'bottom')}>\n <div {...slot(classes, 'bottomInner')}>\n <p {...slot(classes, 'legal')}>\n © {legal.year ?? currentYear()} {legal.name}\n {legal.abn ? (\n <>\n <span aria-hidden=\"true\"> · </span>\n <span>ABN {legal.abn}</span>\n </>\n ) : null}\n </p>\n {policies.length > 0 ? (\n <ul {...slot(classes, 'policies')}>\n {policies.map((link) => (\n <li key={link.href}>\n <FooterLink link={link} hooks={slot(classes, 'policyLink')} />\n </li>\n ))}\n </ul>\n ) : null}\n </div>\n </div>\n </footer>\n )\n}\n"]}
|