@buildcanada/components 0.1.0 → 0.2.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildcanada/components",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Build Canada design system components",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -0,0 +1,158 @@
1
+ @use "../../styles/tokens" as *;
2
+
3
+ /*******************************************************************************
4
+ * Dialog Component
5
+ *
6
+ * A non-modal floating panel that doesn't block interaction with the UI.
7
+ * Sizes to its contents and can be positioned in corners.
8
+ ******************************************************************************/
9
+
10
+ .bc-dialog {
11
+ position: fixed;
12
+ z-index: $z-popover;
13
+ background: $white;
14
+ border: 1px solid $border-default;
15
+ box-shadow: $shadow-lg;
16
+ max-width: calc(100vw - var(--bc-dialog-offset, 16px) * 2);
17
+ max-height: calc(100dvh - var(--bc-dialog-offset, 16px) * 2);
18
+
19
+ // Animation
20
+ animation: bc-dialog-enter $transition-base;
21
+ }
22
+
23
+ @keyframes bc-dialog-enter {
24
+ from {
25
+ opacity: 0;
26
+ transform: scale(0.95);
27
+ }
28
+ to {
29
+ opacity: 1;
30
+ transform: scale(1);
31
+ }
32
+ }
33
+
34
+ /*******************************************************************************
35
+ * Dialog Positions
36
+ ******************************************************************************/
37
+
38
+ .bc-dialog--top-left {
39
+ top: var(--bc-dialog-offset, 16px);
40
+ left: var(--bc-dialog-offset, 16px);
41
+ }
42
+
43
+ .bc-dialog--top-right {
44
+ top: var(--bc-dialog-offset, 16px);
45
+ right: var(--bc-dialog-offset, 16px);
46
+ }
47
+
48
+ .bc-dialog--bottom-left {
49
+ bottom: var(--bc-dialog-offset, 16px);
50
+ left: var(--bc-dialog-offset, 16px);
51
+ }
52
+
53
+ .bc-dialog--bottom-right {
54
+ bottom: var(--bc-dialog-offset, 16px);
55
+ right: var(--bc-dialog-offset, 16px);
56
+ }
57
+
58
+ .bc-dialog--center {
59
+ top: 50%;
60
+ left: 50%;
61
+ transform: translate(-50%, -50%);
62
+ animation: bc-dialog-enter-center $transition-base;
63
+ }
64
+
65
+ @keyframes bc-dialog-enter-center {
66
+ from {
67
+ opacity: 0;
68
+ transform: translate(-50%, -50%) scale(0.95);
69
+ }
70
+ to {
71
+ opacity: 1;
72
+ transform: translate(-50%, -50%) scale(1);
73
+ }
74
+ }
75
+
76
+ /*******************************************************************************
77
+ * Dialog Container
78
+ ******************************************************************************/
79
+
80
+ .bc-dialog__container {
81
+ display: flex;
82
+ flex-direction: column;
83
+ overflow: hidden;
84
+ }
85
+
86
+ /*******************************************************************************
87
+ * Dialog Header
88
+ ******************************************************************************/
89
+
90
+ .bc-dialog__header {
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: space-between;
94
+ gap: $space-2;
95
+ padding: $space-2 $space-2 0 $space-3;
96
+ flex-shrink: 0;
97
+ }
98
+
99
+ .bc-dialog__title {
100
+ font-family: "Soehne Kraftig", "Helvetica Neue", Helvetica, Arial, sans-serif;
101
+ font-size: 1.125rem;
102
+ font-weight: 500;
103
+ line-height: 1.4;
104
+ color: $charcoal;
105
+ margin: 0;
106
+ }
107
+
108
+ .bc-dialog__close {
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ width: 32px;
113
+ height: 32px;
114
+ padding: 0;
115
+ border: none;
116
+ background: transparent;
117
+ color: $gray-600;
118
+ cursor: pointer;
119
+ transition: background-color $transition-fast, color $transition-fast;
120
+ flex-shrink: 0;
121
+
122
+ &:hover {
123
+ background-color: $gray-100;
124
+ color: $charcoal;
125
+ }
126
+
127
+ &:focus-visible {
128
+ outline: 2px solid $auburn;
129
+ outline-offset: 2px;
130
+ }
131
+
132
+ svg {
133
+ width: 16px;
134
+ height: 16px;
135
+ }
136
+ }
137
+
138
+ /*******************************************************************************
139
+ * Dialog Description
140
+ ******************************************************************************/
141
+
142
+ .bc-dialog__description {
143
+ font-family: "Financier Text Regular", Georgia, "Times New Roman", serif;
144
+ font-size: 0.875rem;
145
+ line-height: 1.5;
146
+ color: $gray-600;
147
+ margin: 0;
148
+ padding: $space-1 $space-3 0;
149
+ }
150
+
151
+ /*******************************************************************************
152
+ * Dialog Content
153
+ ******************************************************************************/
154
+
155
+ .bc-dialog__content {
156
+ overflow-y: auto;
157
+ padding: $space-3;
158
+ }
@@ -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"
@@ -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
+ }
@@ -0,0 +1,90 @@
1
+ import cx from "classnames"
2
+
3
+ import { Dialog, type DialogPosition } from "../Dialog"
4
+ import { Button, type ButtonVariant } from "../../primitives/Button"
5
+
6
+ export interface PopupFormProps {
7
+ open: boolean
8
+ onClose: () => void
9
+ onSubmit: (e: React.FormEvent<HTMLFormElement>) => void
10
+ children: React.ReactNode
11
+ title?: string
12
+ description?: string
13
+ position?: DialogPosition
14
+ offset?: number
15
+ className?: string
16
+ submitText?: string
17
+ submitVariant?: ButtonVariant
18
+ cancelText?: string
19
+ cancelVariant?: ButtonVariant
20
+ showCancel?: boolean
21
+ isSubmitting?: boolean
22
+ submitDisabled?: boolean
23
+ closeOnEscape?: boolean
24
+ }
25
+
26
+ export function PopupForm({
27
+ open,
28
+ onClose,
29
+ onSubmit,
30
+ children,
31
+ title,
32
+ description,
33
+ position = "bottom-right",
34
+ offset,
35
+ className,
36
+ submitText = "Submit",
37
+ submitVariant = "solid-auburn",
38
+ cancelText = "Cancel",
39
+ cancelVariant = "outline-charcoal",
40
+ showCancel = true,
41
+ isSubmitting = false,
42
+ submitDisabled = false,
43
+ closeOnEscape = true,
44
+ }: PopupFormProps) {
45
+ const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
46
+ e.preventDefault()
47
+ onSubmit(e)
48
+ }
49
+
50
+ const classes = cx("bc-popup-form", className)
51
+
52
+ return (
53
+ <Dialog
54
+ open={open}
55
+ onClose={onClose}
56
+ title={title}
57
+ description={description}
58
+ position={position}
59
+ offset={offset}
60
+ closeOnEscape={closeOnEscape}
61
+ >
62
+ <form onSubmit={handleSubmit} className={classes}>
63
+ <div className="bc-popup-form__fields">
64
+ {children}
65
+ </div>
66
+ <div className="bc-popup-form__actions">
67
+ {showCancel && (
68
+ <Button
69
+ type="button"
70
+ text={cancelText}
71
+ variant={cancelVariant}
72
+ onClick={onClose}
73
+ disabled={isSubmitting}
74
+ icon={null}
75
+ />
76
+ )}
77
+ <Button
78
+ type="submit"
79
+ text={isSubmitting ? "Submitting..." : submitText}
80
+ variant={submitVariant}
81
+ disabled={submitDisabled || isSubmitting}
82
+ icon={null}
83
+ />
84
+ </div>
85
+ </form>
86
+ </Dialog>
87
+ )
88
+ }
89
+
90
+ export default PopupForm
@@ -0,0 +1 @@
1
+ export { PopupForm, type PopupFormProps } from "./PopupForm"
package/src/index.ts CHANGED
@@ -55,3 +55,7 @@ export { StatBlock, type StatBlockProps, type StatBlockSize, type StatBlockTrend
55
55
  // Navigation
56
56
  export { Header, type HeaderProps, type NavItem } from "./navigation/Header"
57
57
  export { Footer, type FooterProps, type FooterLink, type SocialLink } from "./navigation/Footer"
58
+
59
+ // Feedback
60
+ export { Dialog, type DialogProps, type DialogPosition } from "./feedback/Dialog"
61
+ export { PopupForm, type PopupFormProps } from "./feedback/PopupForm"
@@ -22,6 +22,8 @@
22
22
  @use "../content/StatBlock/StatBlock.scss";
23
23
  @use "../navigation/Header/Header.scss";
24
24
  @use "../navigation/Footer/Footer.scss";
25
+ @use "../feedback/Dialog/Dialog.scss";
26
+ @use "../feedback/PopupForm/PopupForm.scss";
25
27
 
26
28
  /*******************************************************************************
27
29
  * Base Styles