@buildcanada/components 0.3.4 → 0.3.5

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 (69) hide show
  1. package/package.json +3 -2
  2. package/src/assets/fonts/financier-text-regular.woff2 +0 -0
  3. package/src/assets/fonts/founders-grotesk-mono-regular.woff2 +0 -0
  4. package/src/assets/fonts/soehne-kraftig.woff2 +0 -0
  5. package/src/content/Card/Card.scss +281 -0
  6. package/src/content/Card/Card.stories.tsx +389 -0
  7. package/src/content/Card/Card.tsx +170 -0
  8. package/src/content/Card/index.ts +22 -0
  9. package/src/content/Hero/Hero.scss +150 -0
  10. package/src/content/Hero/Hero.stories.tsx +299 -0
  11. package/src/content/Hero/Hero.tsx +63 -0
  12. package/src/content/Hero/index.ts +13 -0
  13. package/src/content/StatBlock/StatBlock.scss +83 -0
  14. package/src/content/StatBlock/StatBlock.stories.tsx +331 -0
  15. package/src/content/StatBlock/StatBlock.tsx +52 -0
  16. package/src/content/StatBlock/index.ts +2 -0
  17. package/src/feedback/Dialog/Dialog.scss +158 -0
  18. package/src/feedback/Dialog/Dialog.stories.tsx +286 -0
  19. package/src/feedback/Dialog/Dialog.tsx +120 -0
  20. package/src/feedback/Dialog/index.ts +1 -0
  21. package/src/feedback/PopupForm/PopupForm.scss +34 -0
  22. package/src/feedback/PopupForm/PopupForm.stories.tsx +341 -0
  23. package/src/feedback/PopupForm/PopupForm.tsx +90 -0
  24. package/src/feedback/PopupForm/index.ts +1 -0
  25. package/src/index.ts +61 -0
  26. package/src/layout/Container/Container.scss +40 -0
  27. package/src/layout/Container/Container.stories.tsx +153 -0
  28. package/src/layout/Container/Container.tsx +29 -0
  29. package/src/layout/Container/index.ts +2 -0
  30. package/src/layout/Divider/Divider.scss +117 -0
  31. package/src/layout/Divider/Divider.stories.tsx +204 -0
  32. package/src/layout/Divider/Divider.tsx +32 -0
  33. package/src/layout/Divider/index.ts +2 -0
  34. package/src/layout/Grid/Grid.scss +81 -0
  35. package/src/layout/Grid/Grid.stories.tsx +263 -0
  36. package/src/layout/Grid/Grid.tsx +75 -0
  37. package/src/layout/Grid/index.ts +2 -0
  38. package/src/layout/Section/Section.scss +74 -0
  39. package/src/layout/Section/Section.stories.tsx +173 -0
  40. package/src/layout/Section/Section.tsx +37 -0
  41. package/src/layout/Section/index.ts +2 -0
  42. package/src/layout/Stack/Stack.scss +61 -0
  43. package/src/layout/Stack/Stack.stories.tsx +342 -0
  44. package/src/layout/Stack/Stack.tsx +48 -0
  45. package/src/layout/Stack/index.ts +9 -0
  46. package/src/navigation/Footer/Footer.scss +233 -0
  47. package/src/navigation/Footer/Footer.stories.tsx +351 -0
  48. package/src/navigation/Footer/Footer.tsx +174 -0
  49. package/src/navigation/Footer/index.ts +2 -0
  50. package/src/navigation/Header/Header.scss +325 -0
  51. package/src/navigation/Header/Header.stories.tsx +346 -0
  52. package/src/navigation/Header/Header.tsx +185 -0
  53. package/src/navigation/Header/index.ts +2 -0
  54. package/src/primitives/Button/Button.scss +218 -0
  55. package/src/primitives/Button/Button.stories.tsx +300 -0
  56. package/src/primitives/Button/Button.tsx +120 -0
  57. package/src/primitives/Button/index.ts +2 -0
  58. package/src/primitives/Checkbox/Checkbox.scss +114 -0
  59. package/src/primitives/Checkbox/Checkbox.stories.tsx +204 -0
  60. package/src/primitives/Checkbox/Checkbox.tsx +75 -0
  61. package/src/primitives/Checkbox/index.ts +2 -0
  62. package/src/primitives/TextField/TextField.scss +93 -0
  63. package/src/primitives/TextField/TextField.stories.tsx +265 -0
  64. package/src/primitives/TextField/TextField.tsx +105 -0
  65. package/src/primitives/TextField/index.ts +2 -0
  66. package/src/styles/fonts.scss +27 -0
  67. package/src/styles/main.scss +36 -0
  68. package/src/styles/tokens.scss +301 -0
  69. package/src/styles/typography.scss +232 -0
@@ -0,0 +1,351 @@
1
+ import type { Meta, StoryObj } from "@storybook/react"
2
+ import { within, userEvent, expect, fn } from "@storybook/test"
3
+
4
+ import { Footer } from "./Footer"
5
+
6
+ const meta: Meta<typeof Footer> = {
7
+ title: "Components/Navigation/Footer",
8
+ component: Footer,
9
+ parameters: {
10
+ layout: "fullscreen",
11
+ docs: {
12
+ description: {
13
+ component: `
14
+ A site footer component with logo, navigation links, social links, newsletter signup, and optional quote.
15
+
16
+ ## Usage
17
+
18
+ \`\`\`tsx
19
+ import { Footer } from "@buildcanada/components"
20
+
21
+ <Footer
22
+ logo={<Logo />}
23
+ links={[
24
+ { label: "Home", href: "/" },
25
+ { label: "About", href: "/about" },
26
+ { label: "Privacy", href: "/privacy" },
27
+ ]}
28
+ socialLinks={[
29
+ { platform: "twitter", href: "https://twitter.com/..." },
30
+ { platform: "github", href: "https://github.com/..." },
31
+ ]}
32
+ copyright="© 2024 Company Name"
33
+ />
34
+ \`\`\`
35
+
36
+ ## With Newsletter
37
+
38
+ \`\`\`tsx
39
+ <Footer
40
+ logo={<Logo />}
41
+ links={links}
42
+ newsletter={{
43
+ heading: "Stay Updated",
44
+ description: "Get the latest news delivered to your inbox.",
45
+ placeholder: "Enter your email",
46
+ buttonText: "Subscribe",
47
+ onSubmit: (email) => console.log(email),
48
+ }}
49
+ copyright="© 2024 Company Name"
50
+ />
51
+ \`\`\`
52
+
53
+ ## Social Platforms
54
+
55
+ Supported platforms: twitter, linkedin, github, email, bluesky
56
+ `,
57
+ },
58
+ },
59
+ },
60
+ }
61
+
62
+ export default meta
63
+ type Story = StoryObj<typeof Footer>
64
+
65
+ const Logo = () => (
66
+ <span style={{
67
+ fontFamily: "'Soehne Kraftig', 'Helvetica Neue', Helvetica, Arial, sans-serif",
68
+ fontSize: "18px",
69
+ fontWeight: 500,
70
+ color: "#ffffff",
71
+ letterSpacing: "-0.01em",
72
+ }}>
73
+ Build Canada
74
+ </span>
75
+ )
76
+
77
+ const basicLinks = [
78
+ { label: "Home", href: "/" },
79
+ { label: "About", href: "/about" },
80
+ { label: "Projects", href: "/projects" },
81
+ { label: "Contact", href: "/contact" },
82
+ ]
83
+
84
+ const extendedLinks = [
85
+ { label: "Home", href: "/" },
86
+ { label: "About", href: "/about" },
87
+ { label: "Projects", href: "/projects" },
88
+ { label: "Research", href: "/research" },
89
+ { label: "Data", href: "/data" },
90
+ { label: "Blog", href: "/blog" },
91
+ { label: "Privacy", href: "/privacy" },
92
+ { label: "Terms", href: "/terms" },
93
+ ]
94
+
95
+ const socialLinks = [
96
+ { platform: "twitter" as const, href: "https://twitter.com/buildcanada" },
97
+ { platform: "linkedin" as const, href: "https://linkedin.com/company/buildcanada" },
98
+ { platform: "github" as const, href: "https://github.com/buildcanada" },
99
+ { platform: "email" as const, href: "mailto:hello@buildcanada.com", label: "Email us" },
100
+ ]
101
+
102
+ const socialLinksWithBluesky = [
103
+ { platform: "twitter" as const, href: "https://twitter.com/buildcanada" },
104
+ { platform: "bluesky" as const, href: "https://bsky.app/profile/buildcanada" },
105
+ { platform: "linkedin" as const, href: "https://linkedin.com/company/buildcanada" },
106
+ { platform: "github" as const, href: "https://github.com/buildcanada" },
107
+ ]
108
+
109
+ export const Default: Story = {
110
+ args: {
111
+ logo: <Logo />,
112
+ links: basicLinks,
113
+ socialLinks: socialLinks,
114
+ copyright: "© 2024 Build Canada. All rights reserved.",
115
+ },
116
+ }
117
+
118
+ export const WithNewsletter: Story = {
119
+ args: {
120
+ logo: <Logo />,
121
+ links: basicLinks,
122
+ socialLinks: socialLinks,
123
+ newsletter: {
124
+ heading: "Stay Updated",
125
+ description: "Get the latest insights delivered to your inbox.",
126
+ placeholder: "Enter your email",
127
+ buttonText: "Subscribe",
128
+ onSubmit: (email) => console.log("Newsletter signup:", email),
129
+ },
130
+ copyright: "© 2024 Build Canada. All rights reserved.",
131
+ },
132
+ }
133
+
134
+ export const WithQuote: Story = {
135
+ args: {
136
+ logo: <Logo />,
137
+ links: basicLinks,
138
+ socialLinks: socialLinks,
139
+ quote: {
140
+ text: "The measure of a great democracy is how well it serves its citizens through transparent and accountable governance.",
141
+ attribution: "Build Canada Mission Statement",
142
+ },
143
+ copyright: "© 2024 Build Canada. All rights reserved.",
144
+ },
145
+ }
146
+
147
+ export const FullFeatured: Story = {
148
+ args: {
149
+ logo: <Logo />,
150
+ links: extendedLinks,
151
+ socialLinks: socialLinks,
152
+ newsletter: {
153
+ heading: "Join Our Newsletter",
154
+ description: "Weekly insights on Canadian government spending and policy.",
155
+ placeholder: "you@example.com",
156
+ buttonText: "Subscribe",
157
+ },
158
+ quote: {
159
+ text: "Democracy requires an informed citizenry. We make government data accessible to all.",
160
+ attribution: "Build Canada",
161
+ },
162
+ copyright: "© 2024 Build Canada. All rights reserved.",
163
+ },
164
+ }
165
+
166
+ export const MinimalFooter: Story = {
167
+ args: {
168
+ copyright: "© 2024 Build Canada",
169
+ },
170
+ }
171
+
172
+ export const WithLogoOnly: Story = {
173
+ args: {
174
+ logo: <Logo />,
175
+ copyright: "© 2024 Build Canada. All rights reserved.",
176
+ },
177
+ }
178
+
179
+ export const WithSocialOnly: Story = {
180
+ args: {
181
+ socialLinks: socialLinks,
182
+ copyright: "© 2024 Build Canada",
183
+ },
184
+ }
185
+
186
+ export const WithBluesky: Story = {
187
+ args: {
188
+ logo: <Logo />,
189
+ links: basicLinks,
190
+ socialLinks: socialLinksWithBluesky,
191
+ copyright: "© 2024 Build Canada. All rights reserved.",
192
+ },
193
+ }
194
+
195
+ export const CanadaSpendsFooter: Story = {
196
+ args: {
197
+ logo: (
198
+ <span style={{ color: "#fff", fontWeight: 600, fontSize: "18px", fontFamily: "sans-serif" }}>
199
+ Canada Spends
200
+ </span>
201
+ ),
202
+ links: [
203
+ { label: "Charts", href: "/charts" },
204
+ { label: "Data", href: "/data" },
205
+ { label: "Topics", href: "/topics" },
206
+ { label: "About", href: "/about" },
207
+ { label: "Methodology", href: "/methodology" },
208
+ { label: "API", href: "/api" },
209
+ ],
210
+ socialLinks: [
211
+ { platform: "twitter" as const, href: "https://twitter.com/canadaspends" },
212
+ { platform: "github" as const, href: "https://github.com/buildcanada/canada-spends" },
213
+ ],
214
+ newsletter: {
215
+ heading: "Data Updates",
216
+ description: "Get notified when new datasets are published.",
217
+ placeholder: "your@email.com",
218
+ buttonText: "Notify Me",
219
+ },
220
+ copyright: "© 2024 Canada Spends. A Build Canada Project.",
221
+ },
222
+ }
223
+
224
+ export const BuildCanadaFooter: Story = {
225
+ args: {
226
+ logo: (
227
+ <span style={{ color: "#fff", fontWeight: 600, fontSize: "18px", fontFamily: "sans-serif" }}>
228
+ Build Canada
229
+ </span>
230
+ ),
231
+ links: [
232
+ { label: "Home", href: "/" },
233
+ { label: "About", href: "/about" },
234
+ { label: "Projects", href: "/projects" },
235
+ { label: "Memos", href: "/memos" },
236
+ { label: "Team", href: "/team" },
237
+ { label: "Contact", href: "/contact" },
238
+ ],
239
+ socialLinks: socialLinks,
240
+ newsletter: {
241
+ heading: "Stay Informed",
242
+ description: "Join our mailing list for weekly updates on Canadian policy and data.",
243
+ placeholder: "Enter your email",
244
+ buttonText: "Subscribe",
245
+ },
246
+ quote: {
247
+ text: "We believe an informed citizenry is essential to a functioning democracy.",
248
+ attribution: "Build Canada",
249
+ },
250
+ copyright: "© 2024 Build Canada. All rights reserved.",
251
+ },
252
+ }
253
+
254
+ export const LongLinkList: Story = {
255
+ args: {
256
+ logo: <Logo />,
257
+ links: [
258
+ { label: "Home", href: "/" },
259
+ { label: "About Us", href: "/about" },
260
+ { label: "Our Mission", href: "/mission" },
261
+ { label: "Projects", href: "/projects" },
262
+ { label: "Canada Spends", href: "/canada-spends" },
263
+ { label: "Research", href: "/research" },
264
+ { label: "Reports", href: "/reports" },
265
+ { label: "Data", href: "/data" },
266
+ { label: "Blog", href: "/blog" },
267
+ { label: "Careers", href: "/careers" },
268
+ { label: "Press", href: "/press" },
269
+ { label: "Contact", href: "/contact" },
270
+ { label: "Privacy Policy", href: "/privacy" },
271
+ { label: "Terms of Use", href: "/terms" },
272
+ ],
273
+ socialLinks: socialLinks,
274
+ copyright: "© 2024 Build Canada. All rights reserved.",
275
+ },
276
+ }
277
+
278
+ // Interactive test: Footer links
279
+ export const LinksTest: Story = {
280
+ args: {
281
+ links: basicLinks,
282
+ copyright: "© 2024 Build Canada",
283
+ },
284
+ play: async ({ canvasElement }) => {
285
+ const canvas = within(canvasElement)
286
+
287
+ const homeLink = canvas.getByRole("link", { name: /home/i })
288
+ const aboutLink = canvas.getByRole("link", { name: /about/i })
289
+
290
+ await expect(homeLink).toBeInTheDocument()
291
+ await expect(aboutLink).toBeInTheDocument()
292
+ await expect(homeLink).toHaveAttribute("href", "/")
293
+ },
294
+ }
295
+
296
+ // Interactive test: Social links
297
+ export const SocialLinksTest: Story = {
298
+ args: {
299
+ socialLinks: socialLinks,
300
+ copyright: "© 2024 Build Canada",
301
+ },
302
+ play: async ({ canvasElement }) => {
303
+ const canvas = within(canvasElement)
304
+
305
+ // Check that social links are present (they should be links with icons)
306
+ const twitterLink = canvas.getByRole("link", { name: /twitter/i })
307
+ const githubLink = canvas.getByRole("link", { name: /github/i })
308
+
309
+ await expect(twitterLink).toBeInTheDocument()
310
+ await expect(githubLink).toBeInTheDocument()
311
+ },
312
+ }
313
+
314
+ // Interactive test: Newsletter form
315
+ export const NewsletterTest: Story = {
316
+ args: {
317
+ newsletter: {
318
+ heading: "Subscribe",
319
+ description: "Get updates",
320
+ placeholder: "your@email.com",
321
+ buttonText: "Subscribe",
322
+ onSubmit: fn(),
323
+ },
324
+ copyright: "© 2024 Build Canada",
325
+ },
326
+ play: async ({ canvasElement, args }) => {
327
+ const canvas = within(canvasElement)
328
+
329
+ const emailInput = canvas.getByPlaceholderText("your@email.com")
330
+ const submitButton = canvas.getByRole("button", { name: /subscribe/i })
331
+
332
+ await expect(emailInput).toBeInTheDocument()
333
+ await expect(submitButton).toBeInTheDocument()
334
+
335
+ await userEvent.type(emailInput, "test@example.com")
336
+ await expect(emailInput).toHaveValue("test@example.com")
337
+ },
338
+ }
339
+
340
+ // Interactive test: Copyright text
341
+ export const CopyrightTest: Story = {
342
+ args: {
343
+ copyright: "© 2024 Test Company. All rights reserved.",
344
+ },
345
+ play: async ({ canvasElement }) => {
346
+ const canvas = within(canvasElement)
347
+
348
+ const copyright = canvas.getByText(/2024 Test Company/i)
349
+ await expect(copyright).toBeInTheDocument()
350
+ },
351
+ }
@@ -0,0 +1,174 @@
1
+ import cx from "classnames"
2
+
3
+ export interface FooterLink {
4
+ label: string
5
+ href: string
6
+ }
7
+
8
+ export interface SocialLink {
9
+ platform: "twitter" | "linkedin" | "instagram" | "github" | "email" | "bluesky"
10
+ href: string
11
+ label?: string
12
+ }
13
+
14
+ export interface FooterProps {
15
+ logo?: React.ReactNode
16
+ links?: FooterLink[]
17
+ socialLinks?: SocialLink[]
18
+ newsletter?: {
19
+ heading: string
20
+ description?: string
21
+ placeholder?: string
22
+ buttonText?: string
23
+ onSubmit?: (email: string) => void
24
+ }
25
+ quote?: {
26
+ text: string
27
+ attribution: string
28
+ }
29
+ copyright?: string
30
+ className?: string
31
+ style?: React.CSSProperties
32
+ }
33
+
34
+ const socialIcons: Record<SocialLink["platform"], React.ReactNode> = {
35
+ twitter: (
36
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
37
+ <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
38
+ </svg>
39
+ ),
40
+ linkedin: (
41
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
42
+ <path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
43
+ </svg>
44
+ ),
45
+ instagram: (
46
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
47
+ <path d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z" />
48
+ </svg>
49
+ ),
50
+ github: (
51
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
52
+ <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
53
+ </svg>
54
+ ),
55
+ email: (
56
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
57
+ <path d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
58
+ </svg>
59
+ ),
60
+ bluesky: (
61
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
62
+ <path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 01-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8z" />
63
+ </svg>
64
+ ),
65
+ }
66
+
67
+ export function Footer({
68
+ logo,
69
+ links = [],
70
+ socialLinks = [],
71
+ newsletter,
72
+ quote,
73
+ copyright,
74
+ className,
75
+ style,
76
+ }: FooterProps) {
77
+ const classes = cx("bc-footer", className)
78
+
79
+ const handleNewsletterSubmit = (e: React.FormEvent<HTMLFormElement>) => {
80
+ e.preventDefault()
81
+ const formData = new FormData(e.currentTarget)
82
+ const email = formData.get("email") as string
83
+ newsletter?.onSubmit?.(email)
84
+ }
85
+
86
+ return (
87
+ <footer className={classes} style={style}>
88
+ <div className="bc-footer__inner">
89
+ <div className="bc-footer__main">
90
+ {logo && <div className="bc-footer__logo">{logo}</div>}
91
+
92
+ {links.length > 0 && (
93
+ <nav className="bc-footer__nav" aria-label="Footer navigation">
94
+ <ul className="bc-footer__nav-list">
95
+ {links.map((link) => (
96
+ <li key={link.label}>
97
+ <a href={link.href} className="bc-footer__nav-link">
98
+ {link.label}
99
+ </a>
100
+ </li>
101
+ ))}
102
+ </ul>
103
+ </nav>
104
+ )}
105
+
106
+ {newsletter && (
107
+ <div className="bc-footer__newsletter">
108
+ <h4 className="bc-footer__newsletter-heading">
109
+ {newsletter.heading}
110
+ </h4>
111
+ {newsletter.description && (
112
+ <p className="bc-footer__newsletter-description">
113
+ {newsletter.description}
114
+ </p>
115
+ )}
116
+ <form
117
+ className="bc-footer__newsletter-form"
118
+ onSubmit={handleNewsletterSubmit}
119
+ >
120
+ <input
121
+ type="email"
122
+ name="email"
123
+ placeholder={newsletter.placeholder || "Enter your email"}
124
+ className="bc-footer__newsletter-input"
125
+ required
126
+ />
127
+ <button
128
+ type="submit"
129
+ className="bc-footer__newsletter-button"
130
+ >
131
+ {newsletter.buttonText || "Subscribe"}
132
+ </button>
133
+ </form>
134
+ </div>
135
+ )}
136
+ </div>
137
+
138
+ <div className="bc-footer__bottom">
139
+ {socialLinks.length > 0 && (
140
+ <div className="bc-footer__social">
141
+ {socialLinks.map((link) => (
142
+ <a
143
+ key={link.platform}
144
+ href={link.href}
145
+ className="bc-footer__social-link"
146
+ aria-label={link.label || link.platform}
147
+ >
148
+ {socialIcons[link.platform]}
149
+ </a>
150
+ ))}
151
+ </div>
152
+ )}
153
+
154
+ {copyright && (
155
+ <p className="bc-footer__copyright">{copyright}</p>
156
+ )}
157
+ </div>
158
+
159
+ {quote && (
160
+ <div className="bc-footer__quote">
161
+ <blockquote className="bc-footer__quote-text">
162
+ {quote.text}
163
+ </blockquote>
164
+ <cite className="bc-footer__quote-attribution">
165
+ — {quote.attribution}
166
+ </cite>
167
+ </div>
168
+ )}
169
+ </div>
170
+ </footer>
171
+ )
172
+ }
173
+
174
+ export default Footer
@@ -0,0 +1,2 @@
1
+ export { Footer, type FooterProps, type FooterLink, type SocialLink } from "./Footer.js"
2
+ export { default } from "./Footer.js"