@pcoi/components 0.1.1 → 0.1.2
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/dist/components.css +1 -1
- package/dist/index.d.ts +45 -7
- package/dist/index.js +2 -2
- package/dist/index.mjs +495 -470
- package/package.json +3 -3
- package/src/Callout/Callout.css +5 -0
- package/src/Callout/Callout.tsx +14 -10
- package/src/ChatInterface/ChatInterface.css +1 -0
- package/src/ChatInterface/ChatInterface.integration.test.tsx +1 -1
- package/src/ChatInterface/ChatInterface.tsx +6 -1
- package/src/ChatMessageList/ChatMessageList.test.tsx +9 -13
- package/src/ChatMessageList/ChatMessageList.tsx +7 -2
- package/src/CitedExcerpt/CitedExcerpt.tsx +2 -0
- package/src/ComparisonTable/ComparisonTable.tsx +6 -0
- package/src/ContactForm/ContactForm.tsx +1 -0
- package/src/DocumentOverlay/DocumentOverlay.tsx +2 -1
- package/src/Footer/Footer.tsx +5 -2
- package/src/LogoMark/LogoMark.tsx +2 -2
- package/src/Modal/Modal.tsx +100 -95
- package/src/Nav/Nav.tsx +6 -2
- package/src/SignalsPanel/SignalsPanel.tsx +2 -0
- package/src/Toast/Toast.tsx +49 -44
- package/src/TypingIndicator/TypingIndicator.tsx +2 -2
- package/src/index.ts +2 -0
- package/src/styles.css +1 -0
- package/src/types.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pcoi/components",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "PCOI Design System — React UI components",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
],
|
|
12
12
|
"exports": {
|
|
13
13
|
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
14
15
|
"import": "./dist/index.mjs",
|
|
15
|
-
"require": "./dist/index.js"
|
|
16
|
-
"types": "./dist/index.d.ts"
|
|
16
|
+
"require": "./dist/index.js"
|
|
17
17
|
},
|
|
18
18
|
"./css": "./dist/components.css"
|
|
19
19
|
},
|
package/src/Callout/Callout.css
CHANGED
|
@@ -22,9 +22,14 @@
|
|
|
22
22
|
line-height: var(--pcoi-semantic-type-body-compact-line-height);
|
|
23
23
|
max-width: var(--pcoi-semantic-sizing-callout-width, 640px);
|
|
24
24
|
margin: 0 0 var(--pcoi-semantic-spacing-panel-padding) 0;
|
|
25
|
+
padding: 0;
|
|
25
26
|
font-style: italic;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
.pcoi-callout__quote p {
|
|
30
|
+
margin: 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
.pcoi-callout__attribution {
|
|
29
34
|
font-size: var(--pcoi-semantic-type-body-sm-size);
|
|
30
35
|
color: var(--pcoi-semantic-text-muted);
|
package/src/Callout/Callout.tsx
CHANGED
|
@@ -1,37 +1,41 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
-
export interface CalloutProps extends React.HTMLAttributes<
|
|
3
|
+
export interface CalloutProps extends React.HTMLAttributes<HTMLElement> {
|
|
4
4
|
/** Quote text */
|
|
5
5
|
quote: string;
|
|
6
6
|
/** Attribution name */
|
|
7
7
|
attribution?: string;
|
|
8
8
|
/** Attribution title/context */
|
|
9
9
|
attributionTitle?: string;
|
|
10
|
-
/** Link URL */
|
|
10
|
+
/** Link URL for the source reference */
|
|
11
11
|
sourceUrl?: string;
|
|
12
|
-
/**
|
|
12
|
+
/** Display label for the source link (default: "Source") */
|
|
13
13
|
sourceLabel?: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* PCOI Callout / Blockquote
|
|
18
|
+
*
|
|
19
|
+
* Renders a semantic `<figure>` with `<blockquote>` and optional `<figcaption>`.
|
|
18
20
|
* Tokens: spacing-40 (padding), text/secondary, text/accent (link)
|
|
19
21
|
*/
|
|
20
|
-
export const Callout = React.forwardRef<
|
|
22
|
+
export const Callout = React.forwardRef<HTMLElement, CalloutProps>(
|
|
21
23
|
({ quote, attribution, attributionTitle, sourceUrl, sourceLabel, className = "", ...rest }, ref) => (
|
|
22
|
-
<
|
|
23
|
-
<div className="pcoi-callout__line" />
|
|
24
|
-
<
|
|
24
|
+
<figure ref={ref} className={`pcoi-callout ${className}`} {...rest}>
|
|
25
|
+
<div className="pcoi-callout__line" aria-hidden="true" />
|
|
26
|
+
<blockquote className="pcoi-callout__quote">
|
|
27
|
+
<p>{quote}</p>
|
|
28
|
+
</blockquote>
|
|
25
29
|
{(attribution || sourceUrl) && (
|
|
26
|
-
<
|
|
30
|
+
<figcaption className="pcoi-callout__attribution">
|
|
27
31
|
{attributionTitle && <span>{attributionTitle} ~ </span>}
|
|
28
32
|
{attribution}
|
|
29
33
|
{sourceUrl && (
|
|
30
34
|
<> | <a href={sourceUrl} target="_blank" rel="noopener noreferrer">{sourceLabel || "Source"}</a></>
|
|
31
35
|
)}
|
|
32
|
-
</
|
|
36
|
+
</figcaption>
|
|
33
37
|
)}
|
|
34
|
-
</
|
|
38
|
+
</figure>
|
|
35
39
|
)
|
|
36
40
|
);
|
|
37
41
|
|
|
@@ -98,7 +98,7 @@ describe("ChatInterface integration", () => {
|
|
|
98
98
|
|
|
99
99
|
render(<ChatCitationHarness />);
|
|
100
100
|
|
|
101
|
-
fireEvent.click(screen.getByRole("button", { name: "Knowledge Report" }));
|
|
101
|
+
fireEvent.click(screen.getByRole("button", { name: "View source: Knowledge Report" }));
|
|
102
102
|
|
|
103
103
|
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
104
104
|
expect(screen.getByText("Knowledge Retention Report")).toBeInTheDocument();
|
|
@@ -7,10 +7,15 @@ import { TypingIndicator } from "../TypingIndicator/TypingIndicator";
|
|
|
7
7
|
import type { ChatMessageRole, Citation, Suggestion } from "../types";
|
|
8
8
|
|
|
9
9
|
export interface ChatInterfaceMessage {
|
|
10
|
+
/** Unique message identifier */
|
|
10
11
|
id: string;
|
|
12
|
+
/** Sender role: "user" or "assistant" */
|
|
11
13
|
role: ChatMessageRole;
|
|
14
|
+
/** Message body content */
|
|
12
15
|
content: React.ReactNode;
|
|
16
|
+
/** Citations referenced in the message */
|
|
13
17
|
citations?: Citation[];
|
|
18
|
+
/** ISO timestamp displayed below the message */
|
|
14
19
|
timestamp?: string;
|
|
15
20
|
}
|
|
16
21
|
|
|
@@ -85,7 +90,7 @@ export const ChatInterface = React.forwardRef<
|
|
|
85
90
|
)}
|
|
86
91
|
</div>
|
|
87
92
|
) : (
|
|
88
|
-
<ChatMessageList className="pcoi-chat__messages">
|
|
93
|
+
<ChatMessageList className="pcoi-chat__messages" aria-live="polite" aria-relevant="additions">
|
|
89
94
|
{messages.map((msg) => (
|
|
90
95
|
<ChatMessage
|
|
91
96
|
key={msg.id}
|
|
@@ -30,14 +30,17 @@ describe("ChatMessageList", () => {
|
|
|
30
30
|
|
|
31
31
|
expect(screen.getByText("Assistant response")).toBeInTheDocument();
|
|
32
32
|
|
|
33
|
-
fireEvent.click(screen.getByRole("button", { name: "Knowledge Report" }));
|
|
34
|
-
fireEvent.click(screen.getByRole("button", { name: "
|
|
33
|
+
fireEvent.click(screen.getByRole("button", { name: "View source: Knowledge Report" }));
|
|
34
|
+
fireEvent.click(screen.getByRole("button", { name: "Citation 1, view source: Knowledge Report" }));
|
|
35
35
|
|
|
36
36
|
expect(onCitationClick).toHaveBeenCalledTimes(2);
|
|
37
37
|
expect(onCitationClick).toHaveBeenCalledWith(citation);
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
-
it("auto-scrolls
|
|
40
|
+
it("auto-scrolls last message into view when new messages are added", async () => {
|
|
41
|
+
const scrollIntoViewMock = vi.fn();
|
|
42
|
+
Element.prototype.scrollIntoView = scrollIntoViewMock;
|
|
43
|
+
|
|
41
44
|
vi.spyOn(window, "requestAnimationFrame").mockImplementation(
|
|
42
45
|
(cb: FrameRequestCallback) => {
|
|
43
46
|
cb(0);
|
|
@@ -45,20 +48,13 @@ describe("ChatMessageList", () => {
|
|
|
45
48
|
}
|
|
46
49
|
);
|
|
47
50
|
|
|
48
|
-
const {
|
|
51
|
+
const { rerender } = render(
|
|
49
52
|
<ChatMessageList>
|
|
50
53
|
<div>Message 1</div>
|
|
51
54
|
</ChatMessageList>
|
|
52
55
|
);
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
".pcoi-chat-message-list__inner"
|
|
56
|
-
) as HTMLDivElement;
|
|
57
|
-
|
|
58
|
-
Object.defineProperty(inner, "scrollHeight", {
|
|
59
|
-
configurable: true,
|
|
60
|
-
get: () => 480,
|
|
61
|
-
});
|
|
57
|
+
scrollIntoViewMock.mockClear();
|
|
62
58
|
|
|
63
59
|
rerender(
|
|
64
60
|
<ChatMessageList>
|
|
@@ -68,7 +64,7 @@ describe("ChatMessageList", () => {
|
|
|
68
64
|
);
|
|
69
65
|
|
|
70
66
|
await waitFor(() => {
|
|
71
|
-
expect(
|
|
67
|
+
expect(scrollIntoViewMock).toHaveBeenCalledWith({ block: "nearest" });
|
|
72
68
|
});
|
|
73
69
|
});
|
|
74
70
|
});
|
|
@@ -23,7 +23,12 @@ export const ChatMessageList = React.forwardRef<
|
|
|
23
23
|
|
|
24
24
|
const scrollToEnd = () => {
|
|
25
25
|
requestAnimationFrame(() => {
|
|
26
|
-
|
|
26
|
+
const lastChild = el.lastElementChild;
|
|
27
|
+
if (lastChild) {
|
|
28
|
+
lastChild.scrollIntoView({ block: "nearest" });
|
|
29
|
+
} else {
|
|
30
|
+
el.scrollTop = el.scrollHeight;
|
|
31
|
+
}
|
|
27
32
|
});
|
|
28
33
|
};
|
|
29
34
|
|
|
@@ -39,7 +44,7 @@ export const ChatMessageList = React.forwardRef<
|
|
|
39
44
|
.join(" ");
|
|
40
45
|
|
|
41
46
|
return (
|
|
42
|
-
<div ref={ref} className={classes} {...rest}>
|
|
47
|
+
<div ref={ref} className={classes} role="log" aria-label="Conversation" {...rest}>
|
|
43
48
|
<div ref={innerRef} className="pcoi-chat-message-list__inner">
|
|
44
49
|
{children}
|
|
45
50
|
</div>
|
|
@@ -29,6 +29,7 @@ export const CitedExcerpt = React.forwardRef<HTMLDivElement, CitedExcerptProps>(
|
|
|
29
29
|
type="button"
|
|
30
30
|
className="pcoi-cited-excerpt__source"
|
|
31
31
|
onClick={onSourceClick}
|
|
32
|
+
aria-label={`View source: ${sourceTitle}`}
|
|
32
33
|
>
|
|
33
34
|
{sourceTitle}
|
|
34
35
|
</button>
|
|
@@ -37,6 +38,7 @@ export const CitedExcerpt = React.forwardRef<HTMLDivElement, CitedExcerptProps>(
|
|
|
37
38
|
type="button"
|
|
38
39
|
className="pcoi-cited-excerpt__index"
|
|
39
40
|
onClick={onSourceClick}
|
|
41
|
+
aria-label={`Citation ${index}, view source: ${sourceTitle}`}
|
|
40
42
|
>
|
|
41
43
|
[{index}]
|
|
42
44
|
</button>
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
3
|
export interface ComparisonRow {
|
|
4
|
+
/** Feature label shown in the first column */
|
|
4
5
|
label: string;
|
|
6
|
+
/** Competitor's value for this feature */
|
|
5
7
|
competitor: string;
|
|
8
|
+
/** PCOI's value for this feature */
|
|
6
9
|
pcoi: string;
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
export interface ComparisonTableProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
13
|
+
/** Column header for the competitor (default: "Typical SaaS AI") */
|
|
10
14
|
competitorName?: string;
|
|
15
|
+
/** Column header for PCOI (default: "PCOI") */
|
|
11
16
|
pcoiName?: string;
|
|
17
|
+
/** Feature comparison rows */
|
|
12
18
|
rows: ComparisonRow[];
|
|
13
19
|
}
|
|
14
20
|
|
|
@@ -3,6 +3,7 @@ import { Button } from "../Button";
|
|
|
3
3
|
import { FormField } from "../FormField";
|
|
4
4
|
|
|
5
5
|
export interface ContactFormProps extends Omit<React.FormHTMLAttributes<HTMLFormElement>, "onSubmit"> {
|
|
6
|
+
/** Called with form field values when the form is submitted */
|
|
6
7
|
onSubmit?: (data: Record<string, string>) => void;
|
|
7
8
|
}
|
|
8
9
|
|
|
@@ -31,7 +31,7 @@ export const DocumentOverlay = React.forwardRef<
|
|
|
31
31
|
>(
|
|
32
32
|
(
|
|
33
33
|
{ open, onClose, title, sourceLabel, children, highlightId, highlightIndex, className = "", ...rest },
|
|
34
|
-
|
|
34
|
+
ref
|
|
35
35
|
) => {
|
|
36
36
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
37
37
|
|
|
@@ -65,6 +65,7 @@ export const DocumentOverlay = React.forwardRef<
|
|
|
65
65
|
return React.createElement(
|
|
66
66
|
Modal,
|
|
67
67
|
{
|
|
68
|
+
ref,
|
|
68
69
|
open,
|
|
69
70
|
onClose,
|
|
70
71
|
title,
|
package/src/Footer/Footer.tsx
CHANGED
|
@@ -6,8 +6,11 @@ import type { LinkItem } from "../types";
|
|
|
6
6
|
export type FooterLink = LinkItem;
|
|
7
7
|
|
|
8
8
|
export interface FooterProps extends React.HTMLAttributes<HTMLElement> {
|
|
9
|
+
/** Navigation links rendered in the footer */
|
|
9
10
|
links?: LinkItem[];
|
|
11
|
+
/** Brand tagline displayed alongside the logo */
|
|
10
12
|
tagline?: string;
|
|
13
|
+
/** Copyright notice shown at the bottom */
|
|
11
14
|
copyright?: string;
|
|
12
15
|
}
|
|
13
16
|
|
|
@@ -39,11 +42,11 @@ export const Footer = React.forwardRef<HTMLElement, FooterProps>(
|
|
|
39
42
|
<LogoMark className="pcoi-footer__logo" />
|
|
40
43
|
<p className="pcoi-footer__tagline">{tagline}</p>
|
|
41
44
|
</div>
|
|
42
|
-
<
|
|
45
|
+
<nav className="pcoi-footer__links" aria-label="Footer">
|
|
43
46
|
{links.map((link) => (
|
|
44
47
|
<a key={link.href} href={link.href} data-track-id={link.trackingId}>{link.label}</a>
|
|
45
48
|
))}
|
|
46
|
-
</
|
|
49
|
+
</nav>
|
|
47
50
|
</div>
|
|
48
51
|
<div className="pcoi-footer__bottom">
|
|
49
52
|
<p>{copyright}</p>
|
|
@@ -14,8 +14,8 @@ export type LogoMarkProps = React.AnchorHTMLAttributes<HTMLAnchorElement>;
|
|
|
14
14
|
*/
|
|
15
15
|
export const LogoMark = React.forwardRef<HTMLAnchorElement, LogoMarkProps>(
|
|
16
16
|
({ href = "#", className = "", ...rest }, ref) => (
|
|
17
|
-
<a ref={ref} href={href} className={`pcoi-logo ${className}`} {...rest}>
|
|
18
|
-
<span className="pcoi-logo__mark">P</span>COI
|
|
17
|
+
<a ref={ref} href={href} className={`pcoi-logo ${className}`} aria-label="PCOI home" {...rest}>
|
|
18
|
+
<span className="pcoi-logo__mark" aria-hidden="true">P</span>COI
|
|
19
19
|
</a>
|
|
20
20
|
)
|
|
21
21
|
);
|
package/src/Modal/Modal.tsx
CHANGED
|
@@ -25,115 +25,120 @@ export interface ModalProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "
|
|
|
25
25
|
* Tokens: bg/overlay, surface/elevated, border/default, shadow/elevated,
|
|
26
26
|
* zIndex/modal, radius-lg, text/primary, text/secondary
|
|
27
27
|
*/
|
|
28
|
-
export const Modal
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
28
|
+
export const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
|
29
|
+
(
|
|
30
|
+
{
|
|
31
|
+
open,
|
|
32
|
+
onClose,
|
|
33
|
+
title,
|
|
34
|
+
headingLevel = "h2",
|
|
35
|
+
size = "default",
|
|
36
|
+
children,
|
|
37
|
+
footer,
|
|
38
|
+
className = "",
|
|
39
|
+
...rest
|
|
40
|
+
},
|
|
41
|
+
_ref
|
|
42
|
+
) => {
|
|
43
|
+
const dialogRef = useRef<HTMLDivElement>(null);
|
|
44
|
+
const previousFocusRef = useRef<HTMLElement | null>(null);
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
const Heading = headingLevel;
|
|
47
|
+
const titleId = title ? "pcoi-modal-title" : undefined;
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
const handleKeyDown = useCallback(
|
|
50
|
+
(e: KeyboardEvent) => {
|
|
51
|
+
if (e.key === "Escape") {
|
|
52
|
+
onClose();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
if (e.key === "Tab" && dialogRef.current) {
|
|
57
|
+
const focusable = dialogRef.current.querySelectorAll<HTMLElement>(
|
|
58
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
59
|
+
);
|
|
60
|
+
if (focusable.length === 0) return;
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
const first = focusable[0];
|
|
63
|
+
const last = focusable[focusable.length - 1];
|
|
60
64
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
last.focus();
|
|
68
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
first.focus();
|
|
71
|
+
}
|
|
67
72
|
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
);
|
|
73
|
+
},
|
|
74
|
+
[onClose]
|
|
75
|
+
);
|
|
72
76
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (open) {
|
|
79
|
+
previousFocusRef.current = document.activeElement as HTMLElement;
|
|
80
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
81
|
+
document.body.style.overflow = "hidden";
|
|
78
82
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
// Focus the dialog after render
|
|
84
|
+
requestAnimationFrame(() => {
|
|
85
|
+
const focusable = dialogRef.current?.querySelector<HTMLElement>(
|
|
86
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
87
|
+
);
|
|
88
|
+
focusable?.focus();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
87
91
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
return () => {
|
|
93
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
94
|
+
document.body.style.overflow = "";
|
|
95
|
+
previousFocusRef.current?.focus();
|
|
96
|
+
};
|
|
97
|
+
}, [open, handleKeyDown]);
|
|
94
98
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
const handleBackdropClick = (e: React.MouseEvent) => {
|
|
100
|
+
if (e.target === e.currentTarget) {
|
|
101
|
+
onClose();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
100
104
|
|
|
101
|
-
|
|
105
|
+
if (!open) return null;
|
|
102
106
|
|
|
103
|
-
|
|
107
|
+
const wrapperClasses = ["pcoi-modal", className].filter(Boolean).join(" ");
|
|
104
108
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
109
|
+
return createPortal(
|
|
110
|
+
<div className={wrapperClasses} onClick={handleBackdropClick}>
|
|
111
|
+
<div
|
|
112
|
+
ref={dialogRef}
|
|
113
|
+
className={`pcoi-modal__dialog${size === "wide" ? " pcoi-modal__dialog--wide" : ""}`}
|
|
114
|
+
role="dialog"
|
|
115
|
+
aria-modal="true"
|
|
116
|
+
aria-labelledby={titleId}
|
|
117
|
+
{...rest}
|
|
118
|
+
>
|
|
119
|
+
<div className="pcoi-modal__header">
|
|
120
|
+
{title && (
|
|
121
|
+
<Heading id={titleId} className="pcoi-modal__title">
|
|
122
|
+
{title}
|
|
123
|
+
</Heading>
|
|
124
|
+
)}
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
className="pcoi-modal__close"
|
|
128
|
+
onClick={onClose}
|
|
129
|
+
aria-label="Close modal"
|
|
130
|
+
>
|
|
131
|
+
<CloseIcon size={20} />
|
|
132
|
+
</button>
|
|
133
|
+
</div>
|
|
134
|
+
<div className="pcoi-modal__body">{children}</div>
|
|
135
|
+
{footer && <div className="pcoi-modal__footer">{footer}</div>}
|
|
129
136
|
</div>
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
);
|
|
136
|
-
};
|
|
137
|
+
</div>,
|
|
138
|
+
document.body
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
);
|
|
137
142
|
|
|
138
143
|
Modal.displayName = "Modal";
|
|
139
144
|
export default Modal;
|
package/src/Nav/Nav.tsx
CHANGED
|
@@ -7,9 +7,13 @@ import type { LinkItem } from "../types";
|
|
|
7
7
|
export type NavLink = LinkItem;
|
|
8
8
|
|
|
9
9
|
export interface NavProps extends React.HTMLAttributes<HTMLElement> {
|
|
10
|
+
/** Navigation links rendered in the navbar */
|
|
10
11
|
links?: LinkItem[];
|
|
12
|
+
/** Label text for the call-to-action button */
|
|
11
13
|
ctaLabel?: string;
|
|
14
|
+
/** URL the CTA button links to */
|
|
12
15
|
ctaHref?: string;
|
|
16
|
+
/** Called when the CTA button is clicked */
|
|
13
17
|
onCtaClick?: () => void;
|
|
14
18
|
}
|
|
15
19
|
|
|
@@ -82,9 +86,9 @@ export const Nav = React.forwardRef<HTMLElement, NavProps>(
|
|
|
82
86
|
<span /><span /><span />
|
|
83
87
|
</button>
|
|
84
88
|
</div>
|
|
85
|
-
<div className="pcoi-nav__mobile-menu" id="pcoi-mobile-menu">
|
|
89
|
+
<div className="pcoi-nav__mobile-menu" id="pcoi-mobile-menu" role="menu" aria-hidden={!menuOpen}>
|
|
86
90
|
{links.map((link) => (
|
|
87
|
-
<a key={link.href} href={link.href} data-track-id={link.trackingId} onClick={() => setMenuOpen(false)}>
|
|
91
|
+
<a key={link.href} href={link.href} data-track-id={link.trackingId} role="menuitem" tabIndex={menuOpen ? 0 : -1} onClick={() => setMenuOpen(false)}>
|
|
88
92
|
{link.label}
|
|
89
93
|
</a>
|
|
90
94
|
))}
|
|
@@ -2,7 +2,9 @@ import React from "react";
|
|
|
2
2
|
import type { HeadingLevel } from "../types";
|
|
3
3
|
|
|
4
4
|
export interface SignalsPanelProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
/** Panel heading text */
|
|
5
6
|
title?: string;
|
|
7
|
+
/** List of signal statements displayed as bullet points */
|
|
6
8
|
signals: string[];
|
|
7
9
|
/** Heading level for the title (default: "h3") */
|
|
8
10
|
headingLevel?: HeadingLevel;
|