@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,286 @@
1
+ import { useState } from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+
4
+ import { Dialog, type DialogPosition } from "./Dialog"
5
+ import { Button } from "../../primitives/Button"
6
+
7
+ const meta: Meta<typeof Dialog> = {
8
+ title: "Components/Feedback/Dialog",
9
+ component: Dialog,
10
+ parameters: {
11
+ layout: "fullscreen",
12
+ docs: {
13
+ description: {
14
+ component: `
15
+ A non-modal floating panel that doesn't block interaction with the rest of the UI.
16
+
17
+ ## Usage
18
+
19
+ \`\`\`tsx
20
+ import { Dialog } from "@buildcanada/components"
21
+
22
+ function MyComponent() {
23
+ const [open, setOpen] = useState(false)
24
+
25
+ return (
26
+ <>
27
+ <Button text="Open Dialog" onClick={() => setOpen(true)} />
28
+ <Dialog
29
+ open={open}
30
+ onClose={() => setOpen(false)}
31
+ title="Dialog Title"
32
+ position="bottom-right"
33
+ >
34
+ <p>Dialog content goes here.</p>
35
+ </Dialog>
36
+ </>
37
+ )
38
+ }
39
+ \`\`\`
40
+
41
+ ## Key Features
42
+
43
+ - **Non-modal**: Does not block interaction with the rest of the UI
44
+ - **Content-sized**: Automatically sizes to fit its contents
45
+ - **Corner positioning**: Place in any corner or center of the screen
46
+ - **No rounded corners**: Sharp edges for a clean, modern look
47
+
48
+ ## Positions
49
+
50
+ - \`top-left\`
51
+ - \`top-right\`
52
+ - \`bottom-left\`
53
+ - \`bottom-right\` (default)
54
+ - \`center\`
55
+
56
+ ## Offset
57
+
58
+ Use the \`offset\` prop to control distance from screen edges (default: 16px).
59
+ `,
60
+ },
61
+ },
62
+ },
63
+ args: {
64
+ open: true,
65
+ title: "Dialog Title",
66
+ description: "",
67
+ position: "bottom-right",
68
+ offset: 16,
69
+ closeOnEscape: true,
70
+ showCloseButton: true,
71
+ },
72
+ argTypes: {
73
+ open: {
74
+ control: "boolean",
75
+ description: "Whether the dialog is open",
76
+ },
77
+ title: {
78
+ control: "text",
79
+ description: "Title displayed in the dialog header",
80
+ },
81
+ description: {
82
+ control: "text",
83
+ description: "Optional description below the title",
84
+ },
85
+ position: {
86
+ control: "select",
87
+ options: ["top-left", "top-right", "bottom-left", "bottom-right", "center"],
88
+ description: "Position of the dialog on screen",
89
+ },
90
+ offset: {
91
+ control: { type: "number", min: 0, max: 100 },
92
+ description: "Distance from screen edges in pixels",
93
+ },
94
+ closeOnEscape: {
95
+ control: "boolean",
96
+ description: "Whether pressing Escape closes the dialog",
97
+ },
98
+ showCloseButton: {
99
+ control: "boolean",
100
+ description: "Whether to show the close button",
101
+ },
102
+ },
103
+ decorators: [
104
+ (Story) => (
105
+ <div style={{ minHeight: "400px" }}>
106
+ <PageContent />
107
+ <Story />
108
+ </div>
109
+ ),
110
+ ],
111
+ }
112
+
113
+ export default meta
114
+ type Story = StoryObj<typeof Dialog>
115
+
116
+ // Page content to demonstrate non-blocking behavior
117
+ function PageContent() {
118
+ return (
119
+ <div style={{ padding: "24px", fontFamily: "sans-serif" }}>
120
+ <h1>Page Content</h1>
121
+ <p>This content remains interactive when the dialog is open.</p>
122
+ <p>
123
+ <button onClick={() => console.log("Button clicked!")} style={{ padding: "8px 16px" }}>
124
+ Clickable Button
125
+ </button>
126
+ </p>
127
+ <p>
128
+ <input
129
+ type="text"
130
+ placeholder="Type here while dialog is open..."
131
+ style={{ padding: "8px", width: "300px" }}
132
+ />
133
+ </p>
134
+ </div>
135
+ )
136
+ }
137
+
138
+ // Simple template that renders the dialog with args
139
+ function DialogTemplate(args: React.ComponentProps<typeof Dialog>) {
140
+ return (
141
+ <Dialog
142
+ {...args}
143
+ onClose={() => console.log("Dialog closed")}
144
+ >
145
+ <p style={{ fontFamily: "sans-serif", margin: 0, maxWidth: "280px" }}>
146
+ This dialog doesn't block interaction with the page behind it.
147
+ </p>
148
+ </Dialog>
149
+ )
150
+ }
151
+
152
+ export const Default: Story = {
153
+ render: (args) => <DialogTemplate {...args} />,
154
+ }
155
+
156
+ export const TopLeft: Story = {
157
+ args: {
158
+ position: "top-left",
159
+ title: "Top Left",
160
+ },
161
+ render: (args) => <DialogTemplate {...args} />,
162
+ }
163
+
164
+ export const TopRight: Story = {
165
+ args: {
166
+ position: "top-right",
167
+ title: "Top Right",
168
+ },
169
+ render: (args) => <DialogTemplate {...args} />,
170
+ }
171
+
172
+ export const BottomLeft: Story = {
173
+ args: {
174
+ position: "bottom-left",
175
+ title: "Bottom Left",
176
+ },
177
+ render: (args) => <DialogTemplate {...args} />,
178
+ }
179
+
180
+ export const BottomRight: Story = {
181
+ args: {
182
+ position: "bottom-right",
183
+ title: "Bottom Right",
184
+ },
185
+ render: (args) => <DialogTemplate {...args} />,
186
+ }
187
+
188
+ export const Centered: Story = {
189
+ args: {
190
+ position: "center",
191
+ title: "Centered",
192
+ },
193
+ render: (args) => <DialogTemplate {...args} />,
194
+ }
195
+
196
+ export const WithDescription: Story = {
197
+ args: {
198
+ title: "Notifications",
199
+ description: "You have 3 unread messages.",
200
+ },
201
+ render: (args) => <DialogTemplate {...args} />,
202
+ }
203
+
204
+ export const CustomOffset: Story = {
205
+ args: {
206
+ title: "Custom Offset",
207
+ offset: 48,
208
+ },
209
+ render: (args) => <DialogTemplate {...args} />,
210
+ }
211
+
212
+ export const NoCloseButton: Story = {
213
+ args: {
214
+ title: "No Close Button",
215
+ showCloseButton: false,
216
+ description: "Press Escape to close.",
217
+ },
218
+ render: (args) => <DialogTemplate {...args} />,
219
+ }
220
+
221
+ export const WideContent: Story = {
222
+ args: {
223
+ title: "Wide Content",
224
+ },
225
+ render: (args) => (
226
+ <Dialog {...args} onClose={() => console.log("Dialog closed")}>
227
+ <div style={{ fontFamily: "sans-serif", width: "400px" }}>
228
+ <p>This dialog has wider content and will size accordingly.</p>
229
+ <p>The dialog always sizes to fit its contents.</p>
230
+ </div>
231
+ </Dialog>
232
+ ),
233
+ }
234
+
235
+ export const AllPositions: Story = {
236
+ parameters: {
237
+ controls: { disable: true },
238
+ },
239
+ render: function AllPositionsDemo() {
240
+ const [openDialog, setOpenDialog] = useState<string | null>(null)
241
+
242
+ const positions = [
243
+ { key: "top-left", label: "Top Left" },
244
+ { key: "top-right", label: "Top Right" },
245
+ { key: "bottom-left", label: "Bottom Left" },
246
+ { key: "bottom-right", label: "Bottom Right" },
247
+ { key: "center", label: "Center" },
248
+ ] as const
249
+
250
+ return (
251
+ <>
252
+ <div style={{
253
+ position: "fixed",
254
+ top: "24px",
255
+ right: "24px",
256
+ display: "flex",
257
+ flexDirection: "column",
258
+ gap: "8px",
259
+ zIndex: 1
260
+ }}>
261
+ {positions.map(({ key, label }) => (
262
+ <Button
263
+ key={key}
264
+ text={label}
265
+ onClick={() => setOpenDialog(openDialog === key ? null : key)}
266
+ variant={openDialog === key ? "solid-auburn" : "outline-charcoal"}
267
+ />
268
+ ))}
269
+ </div>
270
+ {positions.map(({ key, label }) => (
271
+ <Dialog
272
+ key={key}
273
+ open={openDialog === key}
274
+ onClose={() => setOpenDialog(null)}
275
+ title={label}
276
+ position={key}
277
+ >
278
+ <p style={{ fontFamily: "sans-serif", margin: 0 }}>
279
+ Positioned: {label}
280
+ </p>
281
+ </Dialog>
282
+ ))}
283
+ </>
284
+ )
285
+ },
286
+ }
@@ -0,0 +1,120 @@
1
+ import { useEffect, useRef, useCallback } from "react"
2
+ import cx from "classnames"
3
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
4
+ import { faXmark } from "@fortawesome/free-solid-svg-icons"
5
+
6
+ export type DialogPosition =
7
+ | "top-left"
8
+ | "top-right"
9
+ | "bottom-left"
10
+ | "bottom-right"
11
+ | "center"
12
+
13
+ export interface DialogProps {
14
+ open: boolean
15
+ onClose: () => void
16
+ children: React.ReactNode
17
+ title?: string
18
+ description?: string
19
+ position?: DialogPosition
20
+ className?: string
21
+ closeOnEscape?: boolean
22
+ showCloseButton?: boolean
23
+ ariaLabelledBy?: string
24
+ ariaDescribedBy?: string
25
+ offset?: number
26
+ }
27
+
28
+ export function Dialog({
29
+ open,
30
+ onClose,
31
+ children,
32
+ title,
33
+ description,
34
+ position = "bottom-right",
35
+ className,
36
+ closeOnEscape = true,
37
+ showCloseButton = true,
38
+ ariaLabelledBy,
39
+ ariaDescribedBy,
40
+ offset = 16,
41
+ }: DialogProps) {
42
+ const dialogRef = useRef<HTMLDivElement>(null)
43
+
44
+ const handleClose = useCallback(() => {
45
+ onClose()
46
+ }, [onClose])
47
+
48
+ // Handle escape key
49
+ useEffect(() => {
50
+ if (!closeOnEscape || !open) return
51
+
52
+ const handleKeyDown = (e: KeyboardEvent) => {
53
+ if (e.key === "Escape") {
54
+ handleClose()
55
+ }
56
+ }
57
+
58
+ document.addEventListener("keydown", handleKeyDown)
59
+ return () => document.removeEventListener("keydown", handleKeyDown)
60
+ }, [closeOnEscape, open, handleClose])
61
+
62
+ if (!open) return null
63
+
64
+ const classes = cx(
65
+ "bc-dialog",
66
+ `bc-dialog--${position}`,
67
+ className
68
+ )
69
+
70
+ const titleId = ariaLabelledBy || (title ? "bc-dialog-title" : undefined)
71
+ const descriptionId = ariaDescribedBy || (description ? "bc-dialog-description" : undefined)
72
+
73
+ const style = {
74
+ "--bc-dialog-offset": `${offset}px`,
75
+ } as React.CSSProperties
76
+
77
+ return (
78
+ <div
79
+ ref={dialogRef}
80
+ className={classes}
81
+ style={style}
82
+ role="dialog"
83
+ aria-modal="false"
84
+ aria-labelledby={titleId}
85
+ aria-describedby={descriptionId}
86
+ >
87
+ <div className="bc-dialog__container">
88
+ {(title || showCloseButton) && (
89
+ <header className="bc-dialog__header">
90
+ {title && (
91
+ <h2 id={titleId} className="bc-dialog__title">
92
+ {title}
93
+ </h2>
94
+ )}
95
+ {showCloseButton && (
96
+ <button
97
+ type="button"
98
+ className="bc-dialog__close"
99
+ onClick={handleClose}
100
+ aria-label="Close dialog"
101
+ >
102
+ <FontAwesomeIcon icon={faXmark} />
103
+ </button>
104
+ )}
105
+ </header>
106
+ )}
107
+ {description && (
108
+ <p id={descriptionId} className="bc-dialog__description">
109
+ {description}
110
+ </p>
111
+ )}
112
+ <div className="bc-dialog__content">
113
+ {children}
114
+ </div>
115
+ </div>
116
+ </div>
117
+ )
118
+ }
119
+
120
+ export default Dialog
@@ -0,0 +1 @@
1
+ export { Dialog, type DialogProps, type DialogPosition } from "./Dialog.js"
@@ -0,0 +1,34 @@
1
+ @use "../../styles/tokens" as *;
2
+
3
+ /*******************************************************************************
4
+ * PopupForm Component
5
+ ******************************************************************************/
6
+
7
+ .bc-popup-form {
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: $space-3;
11
+ }
12
+
13
+ /*******************************************************************************
14
+ * PopupForm Fields
15
+ ******************************************************************************/
16
+
17
+ .bc-popup-form__fields {
18
+ display: flex;
19
+ flex-direction: column;
20
+ gap: $space-3;
21
+ }
22
+
23
+ /*******************************************************************************
24
+ * PopupForm Actions
25
+ ******************************************************************************/
26
+
27
+ .bc-popup-form__actions {
28
+ display: flex;
29
+ flex-direction: row;
30
+ justify-content: flex-end;
31
+ gap: $space-2;
32
+ padding-top: $space-2;
33
+ border-top: 1px solid $border-muted;
34
+ }