@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,265 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
2
|
+
import { within, userEvent, expect, fn } from "@storybook/test"
|
|
3
|
+
|
|
4
|
+
import { TextField } from "./TextField"
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof TextField> = {
|
|
7
|
+
title: "Components/Primitives/TextField",
|
|
8
|
+
component: TextField,
|
|
9
|
+
parameters: {
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component: `
|
|
13
|
+
A form text input component with label, hint, and error states.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
\`\`\`tsx
|
|
18
|
+
import { TextField } from "@buildcanada/components"
|
|
19
|
+
|
|
20
|
+
<TextField
|
|
21
|
+
label="Email Address"
|
|
22
|
+
type="email"
|
|
23
|
+
placeholder="you@example.com"
|
|
24
|
+
/>
|
|
25
|
+
\`\`\`
|
|
26
|
+
|
|
27
|
+
## Input Types
|
|
28
|
+
|
|
29
|
+
Supports standard HTML input types: text, email, password, number, tel, url.
|
|
30
|
+
|
|
31
|
+
## Validation States
|
|
32
|
+
|
|
33
|
+
\`\`\`tsx
|
|
34
|
+
// With hint text
|
|
35
|
+
<TextField label="Password" hint="Must be 8+ characters" />
|
|
36
|
+
|
|
37
|
+
// With error
|
|
38
|
+
<TextField label="Email" error="Invalid email format" />
|
|
39
|
+
|
|
40
|
+
// Required field
|
|
41
|
+
<TextField label="Name" required />
|
|
42
|
+
\`\`\`
|
|
43
|
+
`,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
argTypes: {
|
|
48
|
+
type: {
|
|
49
|
+
control: "select",
|
|
50
|
+
options: ["text", "email", "password", "number", "tel", "url"],
|
|
51
|
+
description: "HTML input type",
|
|
52
|
+
},
|
|
53
|
+
label: { description: "Label text above the input" },
|
|
54
|
+
placeholder: { description: "Placeholder text" },
|
|
55
|
+
hint: { description: "Helper text below the input" },
|
|
56
|
+
error: { description: "Error message (shows error state when set)" },
|
|
57
|
+
required: { description: "Whether the field is required" },
|
|
58
|
+
disabled: { description: "Whether the input is disabled" },
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default meta
|
|
63
|
+
type Story = StoryObj<typeof TextField>
|
|
64
|
+
|
|
65
|
+
export const Default: Story = {
|
|
66
|
+
args: {
|
|
67
|
+
label: "Full Name",
|
|
68
|
+
placeholder: "Enter your full name",
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const WithValue: Story = {
|
|
73
|
+
args: {
|
|
74
|
+
label: "Email Address",
|
|
75
|
+
type: "email",
|
|
76
|
+
value: "contact@buildcanada.com",
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const WithPlaceholder: Story = {
|
|
81
|
+
args: {
|
|
82
|
+
label: "Company",
|
|
83
|
+
placeholder: "Your company name",
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const WithHint: Story = {
|
|
88
|
+
args: {
|
|
89
|
+
label: "Password",
|
|
90
|
+
type: "password",
|
|
91
|
+
placeholder: "Enter your password",
|
|
92
|
+
hint: "Must be at least 8 characters long",
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const WithError: Story = {
|
|
97
|
+
args: {
|
|
98
|
+
label: "Email Address",
|
|
99
|
+
type: "email",
|
|
100
|
+
value: "invalid-email",
|
|
101
|
+
error: "Please enter a valid email address",
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const Required: Story = {
|
|
106
|
+
args: {
|
|
107
|
+
label: "Email Address",
|
|
108
|
+
type: "email",
|
|
109
|
+
placeholder: "you@example.com",
|
|
110
|
+
required: true,
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const Disabled: Story = {
|
|
115
|
+
args: {
|
|
116
|
+
label: "Organization",
|
|
117
|
+
value: "Build Canada",
|
|
118
|
+
disabled: true,
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const EmailType: Story = {
|
|
123
|
+
args: {
|
|
124
|
+
label: "Email",
|
|
125
|
+
type: "email",
|
|
126
|
+
placeholder: "you@example.com",
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const PasswordType: Story = {
|
|
131
|
+
args: {
|
|
132
|
+
label: "Password",
|
|
133
|
+
type: "password",
|
|
134
|
+
placeholder: "Enter your password",
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const NumberType: Story = {
|
|
139
|
+
args: {
|
|
140
|
+
label: "Amount (CAD)",
|
|
141
|
+
type: "number",
|
|
142
|
+
placeholder: "0.00",
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const TelType: Story = {
|
|
147
|
+
args: {
|
|
148
|
+
label: "Phone Number",
|
|
149
|
+
type: "tel",
|
|
150
|
+
placeholder: "+1 (555) 123-4567",
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const NoLabel: Story = {
|
|
155
|
+
args: {
|
|
156
|
+
placeholder: "Search...",
|
|
157
|
+
type: "text",
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Interactive test: Type in the field
|
|
162
|
+
export const TypeTest: Story = {
|
|
163
|
+
args: {
|
|
164
|
+
label: "Username",
|
|
165
|
+
placeholder: "Enter username",
|
|
166
|
+
onChange: fn(),
|
|
167
|
+
},
|
|
168
|
+
play: async ({ canvasElement, args }) => {
|
|
169
|
+
const canvas = within(canvasElement)
|
|
170
|
+
const input = canvas.getByRole("textbox", { name: /username/i })
|
|
171
|
+
|
|
172
|
+
await expect(input).toBeInTheDocument()
|
|
173
|
+
await userEvent.type(input, "testuser")
|
|
174
|
+
await expect(input).toHaveValue("testuser")
|
|
175
|
+
await expect(args.onChange).toHaveBeenCalled()
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Interactive test: Focus and blur
|
|
180
|
+
export const FocusBlurTest: Story = {
|
|
181
|
+
args: {
|
|
182
|
+
label: "Email",
|
|
183
|
+
placeholder: "Enter email",
|
|
184
|
+
onFocus: fn(),
|
|
185
|
+
onBlur: fn(),
|
|
186
|
+
},
|
|
187
|
+
play: async ({ canvasElement, args }) => {
|
|
188
|
+
const canvas = within(canvasElement)
|
|
189
|
+
const input = canvas.getByRole("textbox", { name: /email/i })
|
|
190
|
+
|
|
191
|
+
await userEvent.click(input)
|
|
192
|
+
await expect(args.onFocus).toHaveBeenCalled()
|
|
193
|
+
|
|
194
|
+
await userEvent.tab()
|
|
195
|
+
await expect(args.onBlur).toHaveBeenCalled()
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Interactive test: Disabled field
|
|
200
|
+
export const DisabledTest: Story = {
|
|
201
|
+
args: {
|
|
202
|
+
label: "Locked Field",
|
|
203
|
+
value: "Cannot edit this",
|
|
204
|
+
disabled: true,
|
|
205
|
+
},
|
|
206
|
+
play: async ({ canvasElement }) => {
|
|
207
|
+
const canvas = within(canvasElement)
|
|
208
|
+
const input = canvas.getByRole("textbox", { name: /locked field/i })
|
|
209
|
+
|
|
210
|
+
await expect(input).toBeDisabled()
|
|
211
|
+
await expect(input).toHaveValue("Cannot edit this")
|
|
212
|
+
},
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Interactive test: Error state
|
|
216
|
+
export const ErrorStateTest: Story = {
|
|
217
|
+
args: {
|
|
218
|
+
label: "Email",
|
|
219
|
+
value: "invalid",
|
|
220
|
+
error: "Invalid email format",
|
|
221
|
+
},
|
|
222
|
+
play: async ({ canvasElement }) => {
|
|
223
|
+
const canvas = within(canvasElement)
|
|
224
|
+
const input = canvas.getByRole("textbox", { name: /email/i })
|
|
225
|
+
const errorMessage = canvas.getByText("Invalid email format")
|
|
226
|
+
|
|
227
|
+
await expect(input).toHaveAttribute("aria-invalid", "true")
|
|
228
|
+
await expect(errorMessage).toBeInTheDocument()
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export const AllStates: Story = {
|
|
233
|
+
render: () => (
|
|
234
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "24px", maxWidth: "400px" }}>
|
|
235
|
+
<TextField
|
|
236
|
+
label="Default"
|
|
237
|
+
placeholder="Enter text..."
|
|
238
|
+
/>
|
|
239
|
+
<TextField
|
|
240
|
+
label="With Value"
|
|
241
|
+
value="Some content"
|
|
242
|
+
/>
|
|
243
|
+
<TextField
|
|
244
|
+
label="With Hint"
|
|
245
|
+
placeholder="Enter text..."
|
|
246
|
+
hint="This is a helpful hint"
|
|
247
|
+
/>
|
|
248
|
+
<TextField
|
|
249
|
+
label="With Error"
|
|
250
|
+
value="Invalid input"
|
|
251
|
+
error="This field has an error"
|
|
252
|
+
/>
|
|
253
|
+
<TextField
|
|
254
|
+
label="Required Field"
|
|
255
|
+
placeholder="This field is required"
|
|
256
|
+
required
|
|
257
|
+
/>
|
|
258
|
+
<TextField
|
|
259
|
+
label="Disabled"
|
|
260
|
+
value="Cannot edit"
|
|
261
|
+
disabled
|
|
262
|
+
/>
|
|
263
|
+
</div>
|
|
264
|
+
),
|
|
265
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import cx from "classnames"
|
|
2
|
+
import { forwardRef, useId } from "react"
|
|
3
|
+
|
|
4
|
+
export type TextFieldType = "text" | "email" | "password" | "number" | "tel" | "url"
|
|
5
|
+
|
|
6
|
+
export interface TextFieldProps {
|
|
7
|
+
label?: string
|
|
8
|
+
placeholder?: string
|
|
9
|
+
value?: string
|
|
10
|
+
defaultValue?: string
|
|
11
|
+
type?: TextFieldType
|
|
12
|
+
name?: string
|
|
13
|
+
id?: string
|
|
14
|
+
className?: string
|
|
15
|
+
error?: string
|
|
16
|
+
hint?: string
|
|
17
|
+
disabled?: boolean
|
|
18
|
+
required?: boolean
|
|
19
|
+
autoComplete?: string
|
|
20
|
+
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
|
21
|
+
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void
|
|
22
|
+
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
26
|
+
function TextField(
|
|
27
|
+
{
|
|
28
|
+
label,
|
|
29
|
+
placeholder,
|
|
30
|
+
value,
|
|
31
|
+
defaultValue,
|
|
32
|
+
type = "text",
|
|
33
|
+
name,
|
|
34
|
+
id: providedId,
|
|
35
|
+
className,
|
|
36
|
+
error,
|
|
37
|
+
hint,
|
|
38
|
+
disabled = false,
|
|
39
|
+
required = false,
|
|
40
|
+
autoComplete,
|
|
41
|
+
onChange,
|
|
42
|
+
onBlur,
|
|
43
|
+
onFocus,
|
|
44
|
+
},
|
|
45
|
+
ref
|
|
46
|
+
) {
|
|
47
|
+
const generatedId = useId()
|
|
48
|
+
const id = providedId || generatedId
|
|
49
|
+
const errorId = `${id}-error`
|
|
50
|
+
const hintId = `${id}-hint`
|
|
51
|
+
|
|
52
|
+
const hasError = Boolean(error)
|
|
53
|
+
|
|
54
|
+
const classes = cx(
|
|
55
|
+
"bc-textfield",
|
|
56
|
+
{ "bc-textfield--error": hasError },
|
|
57
|
+
{ "bc-textfield--disabled": disabled },
|
|
58
|
+
className
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className={classes}>
|
|
63
|
+
{label && (
|
|
64
|
+
<label htmlFor={id} className="bc-textfield__label">
|
|
65
|
+
{label}
|
|
66
|
+
{required && <span className="bc-textfield__required">*</span>}
|
|
67
|
+
</label>
|
|
68
|
+
)}
|
|
69
|
+
<input
|
|
70
|
+
ref={ref}
|
|
71
|
+
type={type}
|
|
72
|
+
id={id}
|
|
73
|
+
name={name}
|
|
74
|
+
value={value}
|
|
75
|
+
defaultValue={defaultValue}
|
|
76
|
+
placeholder={placeholder}
|
|
77
|
+
disabled={disabled}
|
|
78
|
+
required={required}
|
|
79
|
+
autoComplete={autoComplete}
|
|
80
|
+
className="bc-textfield__input"
|
|
81
|
+
aria-invalid={hasError}
|
|
82
|
+
aria-describedby={
|
|
83
|
+
[error && errorId, hint && hintId].filter(Boolean).join(" ") ||
|
|
84
|
+
undefined
|
|
85
|
+
}
|
|
86
|
+
onChange={onChange}
|
|
87
|
+
onBlur={onBlur}
|
|
88
|
+
onFocus={onFocus}
|
|
89
|
+
/>
|
|
90
|
+
{hint && !error && (
|
|
91
|
+
<p id={hintId} className="bc-textfield__hint">
|
|
92
|
+
{hint}
|
|
93
|
+
</p>
|
|
94
|
+
)}
|
|
95
|
+
{error && (
|
|
96
|
+
<p id={errorId} className="bc-textfield__error" role="alert">
|
|
97
|
+
{error}
|
|
98
|
+
</p>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
export default TextField
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*******************************************************************************
|
|
2
|
+
* Build Canada Font Definitions
|
|
3
|
+
******************************************************************************/
|
|
4
|
+
|
|
5
|
+
@font-face {
|
|
6
|
+
font-family: 'Soehne Kraftig';
|
|
7
|
+
src: url('/assets/fonts/soehne-kraftig.woff2') format('woff2');
|
|
8
|
+
font-weight: 500;
|
|
9
|
+
font-style: normal;
|
|
10
|
+
font-display: swap;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@font-face {
|
|
14
|
+
font-family: 'Financier Text';
|
|
15
|
+
src: url('/assets/fonts/financier-text-regular.woff2') format('woff2');
|
|
16
|
+
font-weight: 400;
|
|
17
|
+
font-style: normal;
|
|
18
|
+
font-display: swap;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@font-face {
|
|
22
|
+
font-family: 'Founders Grotesk Mono';
|
|
23
|
+
src: url('/assets/fonts/founders-grotesk-mono-regular.woff2') format('woff2');
|
|
24
|
+
font-weight: 400;
|
|
25
|
+
font-style: normal;
|
|
26
|
+
font-display: swap;
|
|
27
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*******************************************************************************
|
|
2
|
+
* Build Canada Components - Main Stylesheet
|
|
3
|
+
*
|
|
4
|
+
* All @use statements must come first before any other rules
|
|
5
|
+
******************************************************************************/
|
|
6
|
+
|
|
7
|
+
@use "fonts";
|
|
8
|
+
@use "tokens";
|
|
9
|
+
@use "typography";
|
|
10
|
+
|
|
11
|
+
// Component stylesheets (using @use to include their CSS output)
|
|
12
|
+
@use "../primitives/Button/Button.scss";
|
|
13
|
+
@use "../primitives/TextField/TextField.scss";
|
|
14
|
+
@use "../primitives/Checkbox/Checkbox.scss";
|
|
15
|
+
@use "../layout/Container/Container.scss";
|
|
16
|
+
@use "../layout/Section/Section.scss";
|
|
17
|
+
@use "../layout/Grid/Grid.scss";
|
|
18
|
+
@use "../layout/Stack/Stack.scss";
|
|
19
|
+
@use "../layout/Divider/Divider.scss";
|
|
20
|
+
@use "../content/Card/Card.scss";
|
|
21
|
+
@use "../content/Hero/Hero.scss";
|
|
22
|
+
@use "../content/StatBlock/StatBlock.scss";
|
|
23
|
+
@use "../navigation/Header/Header.scss";
|
|
24
|
+
@use "../navigation/Footer/Footer.scss";
|
|
25
|
+
@use "../feedback/Dialog/Dialog.scss";
|
|
26
|
+
@use "../feedback/PopupForm/PopupForm.scss";
|
|
27
|
+
|
|
28
|
+
/*******************************************************************************
|
|
29
|
+
* Base Styles
|
|
30
|
+
******************************************************************************/
|
|
31
|
+
|
|
32
|
+
*,
|
|
33
|
+
*::before,
|
|
34
|
+
*::after {
|
|
35
|
+
box-sizing: border-box;
|
|
36
|
+
}
|