@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,120 @@
1
+ import cx from "classnames"
2
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
3
+ import { IconDefinition, faArrowRight } from "@fortawesome/free-solid-svg-icons"
4
+
5
+ export type ButtonVariant =
6
+ | "solid-auburn"
7
+ | "solid-charcoal"
8
+ | "solid-linen"
9
+ | "outline-auburn"
10
+ | "outline-charcoal"
11
+ | "outline-white"
12
+
13
+ export type ButtonSize = "sm" | "md" | "lg"
14
+
15
+ type ButtonCommonProps = {
16
+ children?: React.ReactNode
17
+ text?: string
18
+ className?: string
19
+ style?: React.CSSProperties
20
+ variant?: ButtonVariant
21
+ size?: ButtonSize
22
+ icon?: IconDefinition | null
23
+ iconPosition?: "left" | "right"
24
+ fullWidth?: boolean
25
+ disabled?: boolean
26
+ ariaLabel?: string
27
+ dataTrackNote?: string
28
+ }
29
+
30
+ type WithHrefProps = {
31
+ href: string
32
+ onClick?: never
33
+ type?: never
34
+ }
35
+
36
+ type WithOnClickProps = {
37
+ onClick?: () => void
38
+ href?: never
39
+ type?: "button" | "submit"
40
+ }
41
+
42
+ export type ButtonProps =
43
+ | (ButtonCommonProps & WithHrefProps)
44
+ | (ButtonCommonProps & WithOnClickProps)
45
+
46
+ export function Button({
47
+ variant = "solid-auburn",
48
+ size = "md",
49
+ className,
50
+ style,
51
+ href,
52
+ onClick,
53
+ text,
54
+ children,
55
+ ariaLabel,
56
+ type = "button",
57
+ icon = faArrowRight,
58
+ iconPosition = "right",
59
+ fullWidth = false,
60
+ dataTrackNote,
61
+ disabled,
62
+ }: ButtonProps) {
63
+ const classes = cx(
64
+ "bc-btn",
65
+ `bc-btn--${variant}`,
66
+ `bc-btn--${size}`,
67
+ className,
68
+ {
69
+ "bc-btn--icon-only": icon && !text && !children,
70
+ "bc-btn--full-width": fullWidth,
71
+ }
72
+ )
73
+
74
+ const content = (
75
+ <>
76
+ {iconPosition === "left" && icon && (
77
+ <FontAwesomeIcon
78
+ className={cx("bc-btn__icon", { "bc-btn__icon--left": text || children })}
79
+ icon={icon}
80
+ />
81
+ )}
82
+ {text && <span className="bc-btn__text">{text}</span>}
83
+ {children}
84
+ {iconPosition !== "left" && icon && (
85
+ <FontAwesomeIcon
86
+ className={cx("bc-btn__icon", { "bc-btn__icon--right": text || children })}
87
+ icon={icon}
88
+ />
89
+ )}
90
+ </>
91
+ )
92
+
93
+ if (href) {
94
+ const aProps = {
95
+ href: disabled ? undefined : href,
96
+ className: classes,
97
+ style,
98
+ "data-track-note": dataTrackNote,
99
+ onClick: disabled
100
+ ? (e: React.MouseEvent) => e.preventDefault()
101
+ : undefined,
102
+ "aria-label": ariaLabel,
103
+ "aria-disabled": disabled,
104
+ }
105
+ return <a {...aProps}>{content}</a>
106
+ }
107
+
108
+ const buttonProps = {
109
+ type,
110
+ className: classes,
111
+ style,
112
+ onClick,
113
+ "aria-label": ariaLabel,
114
+ "data-track-note": dataTrackNote,
115
+ disabled,
116
+ }
117
+ return <button {...buttonProps}>{content}</button>
118
+ }
119
+
120
+ export default Button
@@ -0,0 +1,2 @@
1
+ export { Button, type ButtonProps, type ButtonVariant, type ButtonSize } from "./Button.js"
2
+ export { default } from "./Button.js"
@@ -0,0 +1,114 @@
1
+ @use "../../styles/tokens" as *;
2
+ @use "../../styles/typography" as *;
3
+
4
+ /*******************************************************************************
5
+ * Checkbox Component
6
+ ******************************************************************************/
7
+
8
+ .bc-checkbox {
9
+ display: inline-flex;
10
+
11
+ /***************************************************************************
12
+ * Hidden native input
13
+ ***************************************************************************/
14
+
15
+ &__input {
16
+ position: absolute;
17
+ width: 1px;
18
+ height: 1px;
19
+ padding: 0;
20
+ margin: -1px;
21
+ overflow: hidden;
22
+ clip: rect(0, 0, 0, 0);
23
+ white-space: nowrap;
24
+ border: 0;
25
+ }
26
+
27
+ /***************************************************************************
28
+ * Label wrapper
29
+ ***************************************************************************/
30
+
31
+ &__label {
32
+ display: inline-flex;
33
+ align-items: center;
34
+ gap: $space-1;
35
+ cursor: pointer;
36
+ user-select: none;
37
+ }
38
+
39
+ /***************************************************************************
40
+ * Custom checkbox box
41
+ ***************************************************************************/
42
+
43
+ &__box {
44
+ display: inline-flex;
45
+ align-items: center;
46
+ justify-content: center;
47
+ width: 20px;
48
+ height: 20px;
49
+ border: 2px solid $border-default;
50
+ border-radius: $radius-none;
51
+ background-color: $white;
52
+ transition: all $transition-fast;
53
+ flex-shrink: 0;
54
+ }
55
+
56
+ &__check {
57
+ width: 14px;
58
+ height: 14px;
59
+ color: $white;
60
+ opacity: 0;
61
+ transform: scale(0.8);
62
+ transition: all $transition-fast;
63
+ }
64
+
65
+ &__text {
66
+ @include body-2;
67
+ color: $text-primary;
68
+ }
69
+
70
+ /***************************************************************************
71
+ * States
72
+ ***************************************************************************/
73
+
74
+ // Hover
75
+ &__label:hover &__box {
76
+ border-color: $gray-400;
77
+ }
78
+
79
+ // Focus
80
+ &__input:focus-visible + &__label &__box {
81
+ outline: 2px solid $auburn;
82
+ outline-offset: 2px;
83
+ }
84
+
85
+ // Checked
86
+ &__input:checked + &__label &__box {
87
+ background-color: $auburn;
88
+ border-color: $auburn;
89
+ }
90
+
91
+ &__input:checked + &__label &__check {
92
+ opacity: 1;
93
+ transform: scale(1);
94
+ }
95
+
96
+ // Disabled
97
+ &--disabled &__label {
98
+ cursor: not-allowed;
99
+ }
100
+
101
+ &--disabled &__box {
102
+ background-color: $gray-100;
103
+ border-color: $gray-300;
104
+ }
105
+
106
+ &--disabled &__text {
107
+ color: $text-muted;
108
+ }
109
+
110
+ &--disabled &__input:checked + &__label &__box {
111
+ background-color: $gray-400;
112
+ border-color: $gray-400;
113
+ }
114
+ }
@@ -0,0 +1,204 @@
1
+ import type { Meta, StoryObj } from "@storybook/react"
2
+ import { within, userEvent, expect, fn } from "@storybook/test"
3
+ import { useState } from "react"
4
+
5
+ import { Checkbox } from "./Checkbox"
6
+
7
+ const meta: Meta<typeof Checkbox> = {
8
+ title: "Components/Primitives/Checkbox",
9
+ component: Checkbox,
10
+ parameters: {
11
+ docs: {
12
+ description: {
13
+ component: `
14
+ A styled checkbox component with label support.
15
+
16
+ ## Usage
17
+
18
+ \`\`\`tsx
19
+ import { Checkbox } from "@buildcanada/components"
20
+
21
+ <Checkbox label="I agree to the terms" />
22
+ \`\`\`
23
+
24
+ ## Controlled vs Uncontrolled
25
+
26
+ Use \`defaultChecked\` for uncontrolled or \`checked\` with \`onChange\` for controlled:
27
+
28
+ \`\`\`tsx
29
+ // Uncontrolled
30
+ <Checkbox label="Option" defaultChecked />
31
+
32
+ // Controlled
33
+ <Checkbox
34
+ label="Option"
35
+ checked={isChecked}
36
+ onChange={(e) => setIsChecked(e.target.checked)}
37
+ />
38
+ \`\`\`
39
+ `,
40
+ },
41
+ },
42
+ },
43
+ argTypes: {
44
+ label: { description: "The label text displayed next to the checkbox" },
45
+ disabled: { description: "Whether the checkbox is disabled" },
46
+ defaultChecked: { description: "Initial checked state (uncontrolled)" },
47
+ checked: { description: "Controlled checked state" },
48
+ onChange: { description: "Callback when checkbox state changes" },
49
+ },
50
+ }
51
+
52
+ export default meta
53
+ type Story = StoryObj<typeof Checkbox>
54
+
55
+ export const Default: Story = {
56
+ args: {
57
+ label: "I agree to the terms and conditions",
58
+ },
59
+ }
60
+
61
+ export const Checked: Story = {
62
+ args: {
63
+ label: "Subscribe to newsletter",
64
+ defaultChecked: true,
65
+ },
66
+ }
67
+
68
+ export const Unchecked: Story = {
69
+ args: {
70
+ label: "Remember my preferences",
71
+ defaultChecked: false,
72
+ },
73
+ }
74
+
75
+ export const Disabled: Story = {
76
+ args: {
77
+ label: "This option is disabled",
78
+ disabled: true,
79
+ },
80
+ }
81
+
82
+ export const DisabledChecked: Story = {
83
+ args: {
84
+ label: "This option is checked and disabled",
85
+ defaultChecked: true,
86
+ disabled: true,
87
+ },
88
+ }
89
+
90
+ export const LongLabel: Story = {
91
+ args: {
92
+ label: "By checking this box, I confirm that I have read and understood the privacy policy, terms of service, and agree to receive communications from Build Canada.",
93
+ },
94
+ }
95
+
96
+ // Interactive test: Click to toggle
97
+ export const ClickTest: Story = {
98
+ args: {
99
+ label: "Click to toggle",
100
+ onChange: fn(),
101
+ },
102
+ play: async ({ canvasElement, args }) => {
103
+ const canvas = within(canvasElement)
104
+ const checkbox = canvas.getByRole("checkbox", { name: /click to toggle/i })
105
+
106
+ await expect(checkbox).not.toBeChecked()
107
+ await userEvent.click(checkbox)
108
+ await expect(checkbox).toBeChecked()
109
+ await expect(args.onChange).toHaveBeenCalled()
110
+ },
111
+ }
112
+
113
+ // Interactive test: Double click
114
+ export const DoubleClickTest: Story = {
115
+ args: {
116
+ label: "Double click test",
117
+ onChange: fn(),
118
+ },
119
+ play: async ({ canvasElement }) => {
120
+ const canvas = within(canvasElement)
121
+ const checkbox = canvas.getByRole("checkbox", { name: /double click/i })
122
+
123
+ await expect(checkbox).not.toBeChecked()
124
+ await userEvent.click(checkbox)
125
+ await expect(checkbox).toBeChecked()
126
+ await userEvent.click(checkbox)
127
+ await expect(checkbox).not.toBeChecked()
128
+ },
129
+ }
130
+
131
+ // Interactive test: Disabled should not toggle
132
+ export const DisabledClickTest: Story = {
133
+ args: {
134
+ label: "Disabled checkbox",
135
+ disabled: true,
136
+ onChange: fn(),
137
+ },
138
+ play: async ({ canvasElement, args }) => {
139
+ const canvas = within(canvasElement)
140
+ const checkbox = canvas.getByRole("checkbox", { name: /disabled checkbox/i })
141
+
142
+ await expect(checkbox).toBeDisabled()
143
+ await userEvent.click(checkbox)
144
+ await expect(args.onChange).not.toHaveBeenCalled()
145
+ },
146
+ }
147
+
148
+ // Interactive test: Keyboard interaction
149
+ export const KeyboardTest: Story = {
150
+ args: {
151
+ label: "Keyboard toggle",
152
+ onChange: fn(),
153
+ },
154
+ play: async ({ canvasElement }) => {
155
+ const canvas = within(canvasElement)
156
+ const checkbox = canvas.getByRole("checkbox", { name: /keyboard toggle/i })
157
+
158
+ checkbox.focus()
159
+ await expect(checkbox).toHaveFocus()
160
+ await userEvent.keyboard(" ")
161
+ await expect(checkbox).toBeChecked()
162
+ },
163
+ }
164
+
165
+ export const Controlled: Story = {
166
+ render: function ControlledCheckbox() {
167
+ const [checked, setChecked] = useState(false)
168
+ return (
169
+ <div>
170
+ <Checkbox
171
+ label={`Controlled checkbox (${checked ? "checked" : "unchecked"})`}
172
+ checked={checked}
173
+ onChange={(e) => setChecked(e.target.checked)}
174
+ />
175
+ <p style={{ marginTop: "16px", fontFamily: "sans-serif", fontSize: "14px" }}>
176
+ The checkbox is currently: <strong>{checked ? "checked" : "unchecked"}</strong>
177
+ </p>
178
+ </div>
179
+ )
180
+ },
181
+ }
182
+
183
+ export const MultipleCheckboxes: Story = {
184
+ render: () => (
185
+ <div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
186
+ <Checkbox label="Option A" defaultChecked />
187
+ <Checkbox label="Option B" />
188
+ <Checkbox label="Option C" />
189
+ <Checkbox label="Option D (disabled)" disabled />
190
+ </div>
191
+ ),
192
+ }
193
+
194
+ export const FormExample: Story = {
195
+ render: () => (
196
+ <div style={{ display: "flex", flexDirection: "column", gap: "16px", maxWidth: "500px" }}>
197
+ <h3 style={{ margin: 0, fontFamily: "sans-serif" }}>Communication Preferences</h3>
198
+ <Checkbox label="Email updates about new features" defaultChecked />
199
+ <Checkbox label="Weekly newsletter" defaultChecked />
200
+ <Checkbox label="Partner offers and promotions" />
201
+ <Checkbox label="Account activity alerts" defaultChecked />
202
+ </div>
203
+ ),
204
+ }
@@ -0,0 +1,75 @@
1
+ import cx from "classnames"
2
+ import { forwardRef, useId } from "react"
3
+
4
+ export interface CheckboxProps {
5
+ label: string
6
+ checked?: boolean
7
+ defaultChecked?: boolean
8
+ name?: string
9
+ id?: string
10
+ className?: string
11
+ disabled?: boolean
12
+ onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
13
+ }
14
+
15
+ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
16
+ function Checkbox(
17
+ {
18
+ label,
19
+ checked,
20
+ defaultChecked,
21
+ name,
22
+ id: providedId,
23
+ className,
24
+ disabled = false,
25
+ onChange,
26
+ },
27
+ ref
28
+ ) {
29
+ const generatedId = useId()
30
+ const id = providedId || generatedId
31
+
32
+ const classes = cx(
33
+ "bc-checkbox",
34
+ { "bc-checkbox--disabled": disabled },
35
+ className
36
+ )
37
+
38
+ return (
39
+ <div className={classes}>
40
+ <input
41
+ ref={ref}
42
+ type="checkbox"
43
+ id={id}
44
+ name={name}
45
+ checked={checked}
46
+ defaultChecked={defaultChecked}
47
+ disabled={disabled}
48
+ className="bc-checkbox__input"
49
+ onChange={onChange}
50
+ />
51
+ <label htmlFor={id} className="bc-checkbox__label">
52
+ <span className="bc-checkbox__box">
53
+ <svg
54
+ className="bc-checkbox__check"
55
+ viewBox="0 0 14 14"
56
+ fill="none"
57
+ xmlns="http://www.w3.org/2000/svg"
58
+ >
59
+ <path
60
+ d="M11.5 4L5.5 10L2.5 7"
61
+ stroke="currentColor"
62
+ strokeWidth="2"
63
+ strokeLinecap="square"
64
+ strokeLinejoin="round"
65
+ />
66
+ </svg>
67
+ </span>
68
+ <span className="bc-checkbox__text">{label}</span>
69
+ </label>
70
+ </div>
71
+ )
72
+ }
73
+ )
74
+
75
+ export default Checkbox
@@ -0,0 +1,2 @@
1
+ export { Checkbox, type CheckboxProps } from "./Checkbox.js"
2
+ export { default } from "./Checkbox.js"
@@ -0,0 +1,93 @@
1
+ @use "../../styles/tokens" as *;
2
+ @use "../../styles/typography" as *;
3
+
4
+ /*******************************************************************************
5
+ * TextField Component
6
+ ******************************************************************************/
7
+
8
+ .bc-textfield {
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: calc($space-1 / 2);
12
+
13
+ /***************************************************************************
14
+ * Label
15
+ ***************************************************************************/
16
+
17
+ &__label {
18
+ @include label;
19
+ color: $text-primary;
20
+ }
21
+
22
+ &__required {
23
+ color: $auburn;
24
+ margin-left: 2px;
25
+ }
26
+
27
+ /***************************************************************************
28
+ * Input
29
+ ***************************************************************************/
30
+
31
+ &__input {
32
+ @include body-2;
33
+ width: 100%;
34
+ padding: calc($space-1 + 4px) $space-2;
35
+ border: 2px solid $border-default;
36
+ border-radius: $radius-none;
37
+ background-color: $white;
38
+ color: $text-primary;
39
+ transition: border-color $transition-fast, box-shadow $transition-fast;
40
+
41
+ &::placeholder {
42
+ color: $text-muted;
43
+ }
44
+
45
+ &:hover:not(:disabled) {
46
+ border-color: $gray-400;
47
+ }
48
+
49
+ &:focus {
50
+ outline: none;
51
+ border-color: $auburn;
52
+ box-shadow: 0 0 0 3px rgba($auburn, 0.1);
53
+ }
54
+
55
+ &:disabled {
56
+ background-color: $gray-100;
57
+ color: $text-muted;
58
+ cursor: not-allowed;
59
+ }
60
+ }
61
+
62
+ /***************************************************************************
63
+ * Hint & Error
64
+ ***************************************************************************/
65
+
66
+ &__hint {
67
+ @include body-3;
68
+ color: $text-secondary;
69
+ margin: 0;
70
+ }
71
+
72
+ &__error {
73
+ @include body-3;
74
+ color: $auburn;
75
+ margin: 0;
76
+ }
77
+
78
+ /***************************************************************************
79
+ * States
80
+ ***************************************************************************/
81
+
82
+ &--error &__input {
83
+ border-color: $auburn;
84
+
85
+ &:focus {
86
+ box-shadow: 0 0 0 3px rgba($auburn, 0.15);
87
+ }
88
+ }
89
+
90
+ &--disabled &__label {
91
+ color: $text-muted;
92
+ }
93
+ }