@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.
- package/package.json +3 -2
- package/src/assets/fonts/financier-text-regular.woff2 +0 -0
- package/src/assets/fonts/founders-grotesk-mono-regular.woff2 +0 -0
- package/src/assets/fonts/soehne-kraftig.woff2 +0 -0
- package/src/content/Card/Card.scss +281 -0
- package/src/content/Card/Card.stories.tsx +389 -0
- package/src/content/Card/Card.tsx +170 -0
- package/src/content/Card/index.ts +22 -0
- package/src/content/Hero/Hero.scss +150 -0
- package/src/content/Hero/Hero.stories.tsx +299 -0
- package/src/content/Hero/Hero.tsx +63 -0
- package/src/content/Hero/index.ts +13 -0
- package/src/content/StatBlock/StatBlock.scss +83 -0
- package/src/content/StatBlock/StatBlock.stories.tsx +331 -0
- package/src/content/StatBlock/StatBlock.tsx +52 -0
- package/src/content/StatBlock/index.ts +2 -0
- package/src/feedback/Dialog/Dialog.scss +158 -0
- package/src/feedback/Dialog/Dialog.stories.tsx +286 -0
- package/src/feedback/Dialog/Dialog.tsx +120 -0
- package/src/feedback/Dialog/index.ts +1 -0
- package/src/feedback/PopupForm/PopupForm.scss +34 -0
- package/src/feedback/PopupForm/PopupForm.stories.tsx +341 -0
- package/src/feedback/PopupForm/PopupForm.tsx +90 -0
- package/src/feedback/PopupForm/index.ts +1 -0
- package/src/index.ts +61 -0
- package/src/layout/Container/Container.scss +40 -0
- package/src/layout/Container/Container.stories.tsx +153 -0
- package/src/layout/Container/Container.tsx +29 -0
- package/src/layout/Container/index.ts +2 -0
- package/src/layout/Divider/Divider.scss +117 -0
- package/src/layout/Divider/Divider.stories.tsx +204 -0
- package/src/layout/Divider/Divider.tsx +32 -0
- package/src/layout/Divider/index.ts +2 -0
- package/src/layout/Grid/Grid.scss +81 -0
- package/src/layout/Grid/Grid.stories.tsx +263 -0
- package/src/layout/Grid/Grid.tsx +75 -0
- package/src/layout/Grid/index.ts +2 -0
- package/src/layout/Section/Section.scss +74 -0
- package/src/layout/Section/Section.stories.tsx +173 -0
- package/src/layout/Section/Section.tsx +37 -0
- package/src/layout/Section/index.ts +2 -0
- package/src/layout/Stack/Stack.scss +61 -0
- package/src/layout/Stack/Stack.stories.tsx +342 -0
- package/src/layout/Stack/Stack.tsx +48 -0
- package/src/layout/Stack/index.ts +9 -0
- package/src/navigation/Footer/Footer.scss +233 -0
- package/src/navigation/Footer/Footer.stories.tsx +351 -0
- package/src/navigation/Footer/Footer.tsx +174 -0
- package/src/navigation/Footer/index.ts +2 -0
- package/src/navigation/Header/Header.scss +325 -0
- package/src/navigation/Header/Header.stories.tsx +346 -0
- package/src/navigation/Header/Header.tsx +185 -0
- package/src/navigation/Header/index.ts +2 -0
- package/src/primitives/Button/Button.scss +218 -0
- package/src/primitives/Button/Button.stories.tsx +300 -0
- package/src/primitives/Button/Button.tsx +120 -0
- package/src/primitives/Button/index.ts +2 -0
- package/src/primitives/Checkbox/Checkbox.scss +114 -0
- package/src/primitives/Checkbox/Checkbox.stories.tsx +204 -0
- package/src/primitives/Checkbox/Checkbox.tsx +75 -0
- package/src/primitives/Checkbox/index.ts +2 -0
- package/src/primitives/TextField/TextField.scss +93 -0
- package/src/primitives/TextField/TextField.stories.tsx +265 -0
- package/src/primitives/TextField/TextField.tsx +105 -0
- package/src/primitives/TextField/index.ts +2 -0
- package/src/styles/fonts.scss +27 -0
- package/src/styles/main.scss +36 -0
- package/src/styles/tokens.scss +301 -0
- 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
|
+
}
|