@pcoi/components 0.1.1 → 1.0.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/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/index.d.ts +826 -568
- package/dist/index.js +3517 -2
- package/dist/styles.css +2 -0
- package/package.json +36 -32
- package/dist/components.css +0 -1
- package/dist/index.mjs +0 -969
- package/src/Badge/Badge.css +0 -40
- package/src/Badge/Badge.tsx +0 -36
- package/src/Badge/index.ts +0 -2
- package/src/Button/Button.css +0 -93
- package/src/Button/Button.figma.tsx +0 -27
- package/src/Button/Button.test.tsx +0 -32
- package/src/Button/Button.tsx +0 -47
- package/src/Button/index.ts +0 -1
- package/src/Callout/Callout.css +0 -43
- package/src/Callout/Callout.figma.tsx +0 -25
- package/src/Callout/Callout.tsx +0 -39
- package/src/Callout/index.ts +0 -1
- package/src/Card/Card.css +0 -88
- package/src/Card/Card.figma.tsx +0 -28
- package/src/Card/Card.tsx +0 -60
- package/src/Card/index.ts +0 -1
- package/src/ChatInterface/ChatInterface.css +0 -49
- package/src/ChatInterface/ChatInterface.integration.test.tsx +0 -123
- package/src/ChatInterface/ChatInterface.tsx +0 -120
- package/src/ChatInterface/index.ts +0 -6
- package/src/ChatMessage/ChatMessage.css +0 -55
- package/src/ChatMessage/ChatMessage.tsx +0 -71
- package/src/ChatMessage/index.ts +0 -2
- package/src/ChatMessageList/ChatMessageList.css +0 -24
- package/src/ChatMessageList/ChatMessageList.test.tsx +0 -74
- package/src/ChatMessageList/ChatMessageList.tsx +0 -51
- package/src/ChatMessageList/index.ts +0 -2
- package/src/Checkbox/Checkbox.css +0 -97
- package/src/Checkbox/Checkbox.tsx +0 -70
- package/src/Checkbox/index.ts +0 -2
- package/src/CitationMark/CitationMark.css +0 -40
- package/src/CitationMark/CitationMark.tsx +0 -38
- package/src/CitationMark/index.ts +0 -2
- package/src/CitedExcerpt/CitedExcerpt.css +0 -75
- package/src/CitedExcerpt/CitedExcerpt.tsx +0 -51
- package/src/CitedExcerpt/index.ts +0 -2
- package/src/ComparisonTable/ComparisonTable.css +0 -66
- package/src/ComparisonTable/ComparisonTable.tsx +0 -48
- package/src/ComparisonTable/index.ts +0 -1
- package/src/ContactForm/ContactForm.css +0 -38
- package/src/ContactForm/ContactForm.tsx +0 -57
- package/src/ContactForm/index.ts +0 -1
- package/src/DataTable/DataTable.css +0 -56
- package/src/DataTable/DataTable.tsx +0 -104
- package/src/DataTable/index.ts +0 -2
- package/src/DocumentOverlay/DocumentOverlay.css +0 -57
- package/src/DocumentOverlay/DocumentOverlay.test.tsx +0 -95
- package/src/DocumentOverlay/DocumentOverlay.tsx +0 -86
- package/src/DocumentOverlay/index.ts +0 -2
- package/src/Footer/Footer.css +0 -72
- package/src/Footer/Footer.tsx +0 -56
- package/src/Footer/index.ts +0 -1
- package/src/FormField/FormField.css +0 -78
- package/src/FormField/FormField.figma.tsx +0 -28
- package/src/FormField/FormField.tsx +0 -103
- package/src/FormField/index.ts +0 -2
- package/src/HowStep/HowStep.css +0 -48
- package/src/HowStep/HowStep.figma.tsx +0 -23
- package/src/HowStep/HowStep.tsx +0 -38
- package/src/HowStep/index.ts +0 -1
- package/src/LogoMark/LogoMark.css +0 -16
- package/src/LogoMark/LogoMark.tsx +0 -24
- package/src/LogoMark/index.ts +0 -2
- package/src/Modal/Modal.css +0 -101
- package/src/Modal/Modal.figma.tsx +0 -28
- package/src/Modal/Modal.test.tsx +0 -46
- package/src/Modal/Modal.tsx +0 -139
- package/src/Modal/index.ts +0 -2
- package/src/Nav/Nav.css +0 -161
- package/src/Nav/Nav.tsx +0 -101
- package/src/Nav/index.ts +0 -1
- package/src/Panel/Panel.css +0 -35
- package/src/Panel/Panel.tsx +0 -61
- package/src/Panel/index.ts +0 -2
- package/src/PromptBar/PromptBar.css +0 -68
- package/src/PromptBar/PromptBar.figma.tsx +0 -25
- package/src/PromptBar/PromptBar.test.tsx +0 -83
- package/src/PromptBar/PromptBar.tsx +0 -93
- package/src/PromptBar/index.ts +0 -2
- package/src/RadioGroup/RadioGroup.css +0 -117
- package/src/RadioGroup/RadioGroup.tsx +0 -112
- package/src/RadioGroup/index.ts +0 -2
- package/src/SectionHeader/SectionHeader.css +0 -38
- package/src/SectionHeader/SectionHeader.figma.tsx +0 -23
- package/src/SectionHeader/SectionHeader.tsx +0 -55
- package/src/SectionHeader/index.ts +0 -1
- package/src/Select/Select.css +0 -90
- package/src/Select/Select.figma.tsx +0 -33
- package/src/Select/Select.tsx +0 -100
- package/src/Select/index.ts +0 -2
- package/src/SignalsPanel/SignalsPanel.css +0 -51
- package/src/SignalsPanel/SignalsPanel.tsx +0 -33
- package/src/SignalsPanel/index.ts +0 -1
- package/src/SuggestionCard/SuggestionCard.css +0 -51
- package/src/SuggestionCard/SuggestionCard.tsx +0 -34
- package/src/SuggestionCard/index.ts +0 -2
- package/src/SuggestionCards/SuggestionCards.css +0 -15
- package/src/SuggestionCards/SuggestionCards.test.tsx +0 -27
- package/src/SuggestionCards/SuggestionCards.tsx +0 -40
- package/src/SuggestionCards/index.ts +0 -2
- package/src/Toast/Toast.css +0 -85
- package/src/Toast/Toast.tsx +0 -77
- package/src/Toast/index.ts +0 -2
- package/src/Toggle/Toggle.css +0 -110
- package/src/Toggle/Toggle.figma.tsx +0 -24
- package/src/Toggle/Toggle.tsx +0 -73
- package/src/Toggle/index.ts +0 -2
- package/src/TypingIndicator/TypingIndicator.css +0 -70
- package/src/TypingIndicator/TypingIndicator.tsx +0 -37
- package/src/TypingIndicator/index.ts +0 -2
- package/src/index.ts +0 -37
- package/src/styles/utilities.css +0 -14
- package/src/styles.css +0 -32
- package/src/types.ts +0 -65
package/src/HowStep/HowStep.tsx
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import type { HeadingLevel } from "../types";
|
|
3
|
-
|
|
4
|
-
export interface HowStepProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
-
/** Step number displayed in circle (e.g., "01") */
|
|
6
|
-
number: string;
|
|
7
|
-
/** Step title */
|
|
8
|
-
title: string;
|
|
9
|
-
/** Step description */
|
|
10
|
-
description: string;
|
|
11
|
-
/** Whether this is the last step (no bottom border) */
|
|
12
|
-
isLast?: boolean;
|
|
13
|
-
/** Heading level for the title (default: "h3") */
|
|
14
|
-
headingLevel?: HeadingLevel;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* PCOI How-It-Works Step
|
|
19
|
-
* Tokens: spacing-32 (gap), spacing-40 (padding-y), border/default, radius-full (number circle)
|
|
20
|
-
*/
|
|
21
|
-
export const HowStep = React.forwardRef<HTMLDivElement, HowStepProps>(
|
|
22
|
-
({ number, title, description, isLast = false, headingLevel = "h3", className = "", ...rest }, ref) => {
|
|
23
|
-
const Heading = headingLevel;
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<div ref={ref} className={`pcoi-how-step ${isLast ? "pcoi-how-step--last" : ""} ${className}`} {...rest}>
|
|
27
|
-
<div className="pcoi-how-step__number">{number}</div>
|
|
28
|
-
<div className="pcoi-how-step__content">
|
|
29
|
-
<Heading className="pcoi-how-step__title">{title}</Heading>
|
|
30
|
-
<p className="pcoi-how-step__desc">{description}</p>
|
|
31
|
-
</div>
|
|
32
|
-
</div>
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
HowStep.displayName = "HowStep";
|
|
38
|
-
export default HowStep;
|
package/src/HowStep/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { HowStep, type HowStepProps } from "./HowStep";
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/* LogoMark — @pcoi/components */
|
|
2
|
-
|
|
3
|
-
.pcoi-logo {
|
|
4
|
-
font-family: var(--pcoi-semantic-type-body-font);
|
|
5
|
-
font-size: var(--pcoi-semantic-type-logo-size);
|
|
6
|
-
font-weight: var(--pcoi-semantic-type-emphasis-weight);
|
|
7
|
-
letter-spacing: var(--pcoi-semantic-type-logo-letter-spacing);
|
|
8
|
-
color: var(--pcoi-semantic-text-primary);
|
|
9
|
-
text-decoration: none;
|
|
10
|
-
display: inline-flex;
|
|
11
|
-
align-items: center;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.pcoi-logo__mark {
|
|
15
|
-
color: var(--pcoi-semantic-text-accent);
|
|
16
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
|
|
3
|
-
export type LogoMarkProps = React.AnchorHTMLAttributes<HTMLAnchorElement>;
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* PCOI Logo Mark — Atom
|
|
7
|
-
*
|
|
8
|
-
* Renders the brand mark: accent-colored "P" + "COI" text wrapped in an anchor.
|
|
9
|
-
* Single-sourced so Nav, Footer, and any future component share one implementation.
|
|
10
|
-
*
|
|
11
|
-
* Tokens consumed:
|
|
12
|
-
* - font-family/sans, font-size/logo, letterSpacing/logo
|
|
13
|
-
* - text/primary, text/accent
|
|
14
|
-
*/
|
|
15
|
-
export const LogoMark = React.forwardRef<HTMLAnchorElement, LogoMarkProps>(
|
|
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
|
|
19
|
-
</a>
|
|
20
|
-
)
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
LogoMark.displayName = "LogoMark";
|
|
24
|
-
export default LogoMark;
|
package/src/LogoMark/index.ts
DELETED
package/src/Modal/Modal.css
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/* Modal — @pcoi/components */
|
|
2
|
-
|
|
3
|
-
.pcoi-modal {
|
|
4
|
-
position: fixed;
|
|
5
|
-
inset: 0;
|
|
6
|
-
z-index: var(--pcoi-semantic-layout-z-modal, 500);
|
|
7
|
-
display: flex;
|
|
8
|
-
align-items: center;
|
|
9
|
-
justify-content: center;
|
|
10
|
-
background: var(--pcoi-semantic-bg-overlay);
|
|
11
|
-
padding: var(--pcoi-semantic-spacing-panel-padding);
|
|
12
|
-
animation: pcoi-modal-fade-in 0.2s ease;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
.pcoi-modal__dialog {
|
|
16
|
-
font-family: var(--pcoi-semantic-type-body-font);
|
|
17
|
-
background: var(--pcoi-semantic-surface-elevated);
|
|
18
|
-
border: 1px solid var(--pcoi-semantic-border-default);
|
|
19
|
-
border-radius: var(--pcoi-semantic-radius-panel);
|
|
20
|
-
box-shadow: var(--pcoi-effect-shadow-elevated);
|
|
21
|
-
width: 100%;
|
|
22
|
-
max-width: var(--pcoi-semantic-sizing-modal-width);
|
|
23
|
-
max-height: 85vh;
|
|
24
|
-
display: flex;
|
|
25
|
-
flex-direction: column;
|
|
26
|
-
animation: pcoi-modal-slide-up 0.2s ease;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.pcoi-modal__header {
|
|
30
|
-
display: flex;
|
|
31
|
-
align-items: center;
|
|
32
|
-
justify-content: space-between;
|
|
33
|
-
padding: var(--pcoi-semantic-spacing-card-gap) var(--pcoi-semantic-spacing-panel-padding);
|
|
34
|
-
border-bottom: 1px solid var(--pcoi-semantic-border-default);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.pcoi-modal__title {
|
|
38
|
-
font-size: var(--pcoi-semantic-type-heading-sm-size);
|
|
39
|
-
font-weight: var(--pcoi-semantic-type-emphasis-weight);
|
|
40
|
-
color: var(--pcoi-semantic-text-primary);
|
|
41
|
-
margin: 0;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
.pcoi-modal__close {
|
|
45
|
-
background: none;
|
|
46
|
-
border: none;
|
|
47
|
-
color: var(--pcoi-semantic-text-secondary);
|
|
48
|
-
font-size: var(--pcoi-semantic-type-close-lg-size);
|
|
49
|
-
line-height: var(--pcoi-semantic-type-none-line-height);
|
|
50
|
-
cursor: pointer;
|
|
51
|
-
padding: var(--pcoi-semantic-spacing-control-padding-2xs);
|
|
52
|
-
border-radius: var(--pcoi-semantic-radius-btn);
|
|
53
|
-
transition: color var(--pcoi-effect-transition-fast, 0.2s ease);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.pcoi-modal__close:hover {
|
|
57
|
-
color: var(--pcoi-semantic-text-primary);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.pcoi-modal__close:active {
|
|
61
|
-
transform: var(--pcoi-effect-transform-press-scale-icon);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
.pcoi-modal__close:focus-visible {
|
|
65
|
-
outline: none;
|
|
66
|
-
box-shadow: var(--pcoi-effect-shadow-focus-ring);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
.pcoi-modal__body {
|
|
70
|
-
padding: var(--pcoi-semantic-spacing-panel-padding);
|
|
71
|
-
overflow-y: auto;
|
|
72
|
-
flex: 1;
|
|
73
|
-
font-size: var(--pcoi-semantic-type-body-size);
|
|
74
|
-
color: var(--pcoi-semantic-text-secondary);
|
|
75
|
-
line-height: var(--pcoi-semantic-type-body-line-height);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.pcoi-modal__footer {
|
|
79
|
-
display: flex;
|
|
80
|
-
align-items: center;
|
|
81
|
-
justify-content: flex-end;
|
|
82
|
-
gap: var(--pcoi-semantic-spacing-panel-gap);
|
|
83
|
-
padding: var(--pcoi-semantic-spacing-panel-padding-sm) var(--pcoi-semantic-spacing-panel-padding);
|
|
84
|
-
border-top: 1px solid var(--pcoi-semantic-border-default);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/* ── Size variants ── */
|
|
88
|
-
.pcoi-modal__dialog--wide {
|
|
89
|
-
max-width: var(--pcoi-semantic-sizing-document-width, 900px);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/* ── Animations ── */
|
|
93
|
-
@keyframes pcoi-modal-fade-in {
|
|
94
|
-
from { opacity: 0; }
|
|
95
|
-
to { opacity: 1; }
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
@keyframes pcoi-modal-slide-up {
|
|
99
|
-
from { transform: translateY(16px); opacity: 0; }
|
|
100
|
-
to { transform: translateY(0); opacity: 1; }
|
|
101
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import figma from "@figma/code-connect";
|
|
2
|
-
import { Modal } from "./Modal";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Code Connect: Modal
|
|
6
|
-
* Maps Figma modal variants to React Modal props
|
|
7
|
-
*/
|
|
8
|
-
figma.connect(Modal, "https://www.figma.com/file/PCOIxDesignSystem/PCOI-Design-System?node-id=104-1", {
|
|
9
|
-
props: {
|
|
10
|
-
size: figma.enum("Size", {
|
|
11
|
-
Default: "default",
|
|
12
|
-
Wide: "wide",
|
|
13
|
-
}),
|
|
14
|
-
title: figma.string("Title"),
|
|
15
|
-
withFooter: figma.boolean("Footer"),
|
|
16
|
-
},
|
|
17
|
-
example: ({ size, title, withFooter }) => (
|
|
18
|
-
<Modal
|
|
19
|
-
open
|
|
20
|
-
onClose={() => undefined}
|
|
21
|
-
size={size}
|
|
22
|
-
title={title}
|
|
23
|
-
footer={withFooter ? <button type="button">Confirm</button> : undefined}
|
|
24
|
-
>
|
|
25
|
-
<p>Modal body content</p>
|
|
26
|
-
</Modal>
|
|
27
|
-
),
|
|
28
|
-
});
|
package/src/Modal/Modal.test.tsx
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
-
import { describe, expect, it, vi } from "vitest";
|
|
3
|
-
import { Modal } from "./Modal";
|
|
4
|
-
|
|
5
|
-
describe("Modal", () => {
|
|
6
|
-
it("does not render when closed", () => {
|
|
7
|
-
render(
|
|
8
|
-
<Modal open={false} onClose={vi.fn()} title="Dialog title">
|
|
9
|
-
Body
|
|
10
|
-
</Modal>
|
|
11
|
-
);
|
|
12
|
-
|
|
13
|
-
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("renders dialog content when open", () => {
|
|
17
|
-
render(
|
|
18
|
-
<Modal open onClose={vi.fn()} title="Dialog title">
|
|
19
|
-
Body content
|
|
20
|
-
</Modal>
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
24
|
-
expect(screen.getByText("Dialog title")).toBeInTheDocument();
|
|
25
|
-
expect(screen.getByText("Body content")).toBeInTheDocument();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("calls onClose for close button, Escape key, and backdrop click", () => {
|
|
29
|
-
const onClose = vi.fn();
|
|
30
|
-
|
|
31
|
-
render(
|
|
32
|
-
<Modal open onClose={onClose} title="Dialog title">
|
|
33
|
-
Body
|
|
34
|
-
</Modal>
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
fireEvent.click(screen.getByRole("button", { name: "Close modal" }));
|
|
38
|
-
fireEvent.keyDown(document, { key: "Escape" });
|
|
39
|
-
|
|
40
|
-
const backdrop = document.querySelector(".pcoi-modal");
|
|
41
|
-
if (!backdrop) throw new Error("Expected modal backdrop to exist");
|
|
42
|
-
fireEvent.click(backdrop);
|
|
43
|
-
|
|
44
|
-
expect(onClose).toHaveBeenCalledTimes(3);
|
|
45
|
-
});
|
|
46
|
-
});
|
package/src/Modal/Modal.tsx
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useRef, useCallback } from "react";
|
|
2
|
-
import { createPortal } from "react-dom";
|
|
3
|
-
import { CloseIcon } from "@pcoi/icons";
|
|
4
|
-
import type { HeadingLevel } from "../types";
|
|
5
|
-
|
|
6
|
-
export interface ModalProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
|
7
|
-
/** Whether the modal is visible */
|
|
8
|
-
open: boolean;
|
|
9
|
-
/** Called when the modal should close */
|
|
10
|
-
onClose: () => void;
|
|
11
|
-
/** Modal title */
|
|
12
|
-
title?: string;
|
|
13
|
-
/** Heading level for the title (default: "h2") */
|
|
14
|
-
headingLevel?: HeadingLevel;
|
|
15
|
-
/** Dialog width: "default" (520px) or "wide" (900px) */
|
|
16
|
-
size?: "default" | "wide";
|
|
17
|
-
/** Modal body content */
|
|
18
|
-
children?: React.ReactNode;
|
|
19
|
-
/** Footer content (e.g. action buttons) */
|
|
20
|
-
footer?: React.ReactNode;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* PCOI Modal — Dialog overlay with focus trap
|
|
25
|
-
* Tokens: bg/overlay, surface/elevated, border/default, shadow/elevated,
|
|
26
|
-
* zIndex/modal, radius-lg, text/primary, text/secondary
|
|
27
|
-
*/
|
|
28
|
-
export const Modal: React.FC<ModalProps> = ({
|
|
29
|
-
open,
|
|
30
|
-
onClose,
|
|
31
|
-
title,
|
|
32
|
-
headingLevel = "h2",
|
|
33
|
-
size = "default",
|
|
34
|
-
children,
|
|
35
|
-
footer,
|
|
36
|
-
className = "",
|
|
37
|
-
...rest
|
|
38
|
-
}) => {
|
|
39
|
-
const dialogRef = useRef<HTMLDivElement>(null);
|
|
40
|
-
const previousFocusRef = useRef<HTMLElement | null>(null);
|
|
41
|
-
|
|
42
|
-
const Heading = headingLevel;
|
|
43
|
-
const titleId = title ? "pcoi-modal-title" : undefined;
|
|
44
|
-
|
|
45
|
-
const handleKeyDown = useCallback(
|
|
46
|
-
(e: KeyboardEvent) => {
|
|
47
|
-
if (e.key === "Escape") {
|
|
48
|
-
onClose();
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (e.key === "Tab" && dialogRef.current) {
|
|
53
|
-
const focusable = dialogRef.current.querySelectorAll<HTMLElement>(
|
|
54
|
-
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
55
|
-
);
|
|
56
|
-
if (focusable.length === 0) return;
|
|
57
|
-
|
|
58
|
-
const first = focusable[0];
|
|
59
|
-
const last = focusable[focusable.length - 1];
|
|
60
|
-
|
|
61
|
-
if (e.shiftKey && document.activeElement === first) {
|
|
62
|
-
e.preventDefault();
|
|
63
|
-
last.focus();
|
|
64
|
-
} else if (!e.shiftKey && document.activeElement === last) {
|
|
65
|
-
e.preventDefault();
|
|
66
|
-
first.focus();
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
[onClose]
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
useEffect(() => {
|
|
74
|
-
if (open) {
|
|
75
|
-
previousFocusRef.current = document.activeElement as HTMLElement;
|
|
76
|
-
document.addEventListener("keydown", handleKeyDown);
|
|
77
|
-
document.body.style.overflow = "hidden";
|
|
78
|
-
|
|
79
|
-
// Focus the dialog after render
|
|
80
|
-
requestAnimationFrame(() => {
|
|
81
|
-
const focusable = dialogRef.current?.querySelector<HTMLElement>(
|
|
82
|
-
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
83
|
-
);
|
|
84
|
-
focusable?.focus();
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return () => {
|
|
89
|
-
document.removeEventListener("keydown", handleKeyDown);
|
|
90
|
-
document.body.style.overflow = "";
|
|
91
|
-
previousFocusRef.current?.focus();
|
|
92
|
-
};
|
|
93
|
-
}, [open, handleKeyDown]);
|
|
94
|
-
|
|
95
|
-
const handleBackdropClick = (e: React.MouseEvent) => {
|
|
96
|
-
if (e.target === e.currentTarget) {
|
|
97
|
-
onClose();
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
if (!open) return null;
|
|
102
|
-
|
|
103
|
-
const wrapperClasses = ["pcoi-modal", className].filter(Boolean).join(" ");
|
|
104
|
-
|
|
105
|
-
return createPortal(
|
|
106
|
-
<div className={wrapperClasses} onClick={handleBackdropClick}>
|
|
107
|
-
<div
|
|
108
|
-
ref={dialogRef}
|
|
109
|
-
className={`pcoi-modal__dialog${size === "wide" ? " pcoi-modal__dialog--wide" : ""}`}
|
|
110
|
-
role="dialog"
|
|
111
|
-
aria-modal="true"
|
|
112
|
-
aria-labelledby={titleId}
|
|
113
|
-
{...rest}
|
|
114
|
-
>
|
|
115
|
-
<div className="pcoi-modal__header">
|
|
116
|
-
{title && (
|
|
117
|
-
<Heading id={titleId} className="pcoi-modal__title">
|
|
118
|
-
{title}
|
|
119
|
-
</Heading>
|
|
120
|
-
)}
|
|
121
|
-
<button
|
|
122
|
-
type="button"
|
|
123
|
-
className="pcoi-modal__close"
|
|
124
|
-
onClick={onClose}
|
|
125
|
-
aria-label="Close modal"
|
|
126
|
-
>
|
|
127
|
-
<CloseIcon size={20} />
|
|
128
|
-
</button>
|
|
129
|
-
</div>
|
|
130
|
-
<div className="pcoi-modal__body">{children}</div>
|
|
131
|
-
{footer && <div className="pcoi-modal__footer">{footer}</div>}
|
|
132
|
-
</div>
|
|
133
|
-
</div>,
|
|
134
|
-
document.body
|
|
135
|
-
);
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
Modal.displayName = "Modal";
|
|
139
|
-
export default Modal;
|
package/src/Modal/index.ts
DELETED
package/src/Nav/Nav.css
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
/* Nav — @pcoi/components */
|
|
2
|
-
|
|
3
|
-
.pcoi-nav {
|
|
4
|
-
position: fixed;
|
|
5
|
-
top: 0;
|
|
6
|
-
left: 0;
|
|
7
|
-
right: 0;
|
|
8
|
-
z-index: var(--pcoi-semantic-layout-z-nav, 1000);
|
|
9
|
-
height: var(--pcoi-semantic-spacing-nav-height, 72px);
|
|
10
|
-
transition: background var(--pcoi-effect-transition-medium, 0.3s ease),
|
|
11
|
-
box-shadow var(--pcoi-effect-transition-medium, 0.3s ease);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.pcoi-nav--scrolled {
|
|
15
|
-
background: var(--pcoi-semantic-bg-nav-scrolled);
|
|
16
|
-
-webkit-backdrop-filter: blur(var(--pcoi-effect-blur-nav-backdrop, 20px));
|
|
17
|
-
backdrop-filter: blur(var(--pcoi-effect-blur-nav-backdrop, 20px));
|
|
18
|
-
box-shadow: var(--pcoi-effect-shadow-nav-scrolled);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
.pcoi-nav__inner {
|
|
22
|
-
max-width: var(--pcoi-semantic-sizing-container-max);
|
|
23
|
-
margin: 0 auto;
|
|
24
|
-
padding: 0 var(--pcoi-semantic-spacing-btn-x);
|
|
25
|
-
height: 100%;
|
|
26
|
-
display: flex;
|
|
27
|
-
align-items: center;
|
|
28
|
-
justify-content: space-between;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/* ── Desktop Links ── */
|
|
32
|
-
.pcoi-nav__links {
|
|
33
|
-
display: flex;
|
|
34
|
-
align-items: center;
|
|
35
|
-
gap: var(--pcoi-semantic-spacing-stack-lg);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
.pcoi-nav__links a {
|
|
39
|
-
font-size: var(--pcoi-semantic-type-nav-size);
|
|
40
|
-
font-weight: var(--pcoi-semantic-type-label-weight);
|
|
41
|
-
color: var(--pcoi-semantic-text-secondary);
|
|
42
|
-
text-decoration: none;
|
|
43
|
-
transition: color var(--pcoi-effect-transition-fast, 0.2s ease);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.pcoi-nav__links a:hover {
|
|
47
|
-
color: var(--pcoi-semantic-text-primary);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.pcoi-nav__links a:focus-visible {
|
|
51
|
-
outline: none;
|
|
52
|
-
box-shadow: var(--pcoi-effect-shadow-focus-ring);
|
|
53
|
-
border-radius: var(--pcoi-semantic-radius-btn);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.pcoi-nav__links a:active {
|
|
57
|
-
transform: var(--pcoi-effect-transform-press-scale);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/* ── Hamburger ── */
|
|
61
|
-
.pcoi-nav__hamburger {
|
|
62
|
-
display: none;
|
|
63
|
-
flex-direction: column;
|
|
64
|
-
justify-content: center;
|
|
65
|
-
gap: var(--pcoi-semantic-spacing-hamburger-gap);
|
|
66
|
-
width: var(--pcoi-semantic-sizing-hamburger-size);
|
|
67
|
-
height: var(--pcoi-semantic-sizing-hamburger-size);
|
|
68
|
-
background: none;
|
|
69
|
-
border: none;
|
|
70
|
-
cursor: pointer;
|
|
71
|
-
padding: 0;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.pcoi-nav__hamburger:hover {
|
|
75
|
-
color: var(--pcoi-semantic-text-primary);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.pcoi-nav__hamburger:focus-visible {
|
|
79
|
-
outline: none;
|
|
80
|
-
box-shadow: var(--pcoi-effect-shadow-focus-ring);
|
|
81
|
-
border-radius: var(--pcoi-semantic-radius-btn);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.pcoi-nav__hamburger:active {
|
|
85
|
-
transform: var(--pcoi-effect-transform-press-scale-icon);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.pcoi-nav__hamburger span {
|
|
89
|
-
display: block;
|
|
90
|
-
width: 100%;
|
|
91
|
-
height: 2px;
|
|
92
|
-
background: var(--pcoi-semantic-text-primary);
|
|
93
|
-
border-radius: 1px;
|
|
94
|
-
transition: transform var(--pcoi-effect-transition-fast, 0.2s ease),
|
|
95
|
-
opacity var(--pcoi-effect-transition-fast, 0.2s ease);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
.pcoi-nav__hamburger--active span:nth-child(1) {
|
|
99
|
-
transform: translateY(var(--pcoi-semantic-spacing-hamburger-bar-offset)) rotate(45deg);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
.pcoi-nav__hamburger--active span:nth-child(2) {
|
|
103
|
-
opacity: 0;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
.pcoi-nav__hamburger--active span:nth-child(3) {
|
|
107
|
-
transform: translateY(calc(-1 * var(--pcoi-semantic-spacing-hamburger-bar-offset))) rotate(-45deg);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/* ── Mobile Menu ── */
|
|
111
|
-
.pcoi-nav__mobile-menu {
|
|
112
|
-
display: none;
|
|
113
|
-
flex-direction: column;
|
|
114
|
-
padding: var(--pcoi-semantic-spacing-btn-x) var(--pcoi-semantic-spacing-btn-x) var(--pcoi-semantic-spacing-stack-lg);
|
|
115
|
-
background: var(--pcoi-semantic-bg-default);
|
|
116
|
-
box-shadow: var(--pcoi-effect-shadow-mobile-menu);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
.pcoi-nav__mobile-menu a {
|
|
120
|
-
font-size: var(--pcoi-semantic-type-body-lg-size);
|
|
121
|
-
font-weight: var(--pcoi-semantic-type-label-weight);
|
|
122
|
-
color: var(--pcoi-semantic-text-secondary);
|
|
123
|
-
text-decoration: none;
|
|
124
|
-
padding: var(--pcoi-semantic-spacing-input-y) 0;
|
|
125
|
-
transition: color var(--pcoi-effect-transition-fast, 0.2s ease);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
.pcoi-nav__mobile-menu a:hover {
|
|
129
|
-
color: var(--pcoi-semantic-text-primary);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
.pcoi-nav__mobile-menu a:focus-visible {
|
|
133
|
-
outline: none;
|
|
134
|
-
box-shadow: var(--pcoi-effect-shadow-focus-ring);
|
|
135
|
-
border-radius: var(--pcoi-semantic-radius-btn);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
.pcoi-nav__mobile-menu a:active {
|
|
139
|
-
transform: var(--pcoi-effect-transform-press-scale);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
.pcoi-nav__mobile-menu .pcoi-btn {
|
|
143
|
-
margin-top: var(--pcoi-semantic-spacing-stack-md);
|
|
144
|
-
width: 100%;
|
|
145
|
-
justify-content: center;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/* ── Responsive ── */
|
|
149
|
-
@media (max-width: 768px) {
|
|
150
|
-
.pcoi-nav__links {
|
|
151
|
-
display: none;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
.pcoi-nav__hamburger {
|
|
155
|
-
display: flex;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
.pcoi-nav--menu-open .pcoi-nav__mobile-menu {
|
|
159
|
-
display: flex;
|
|
160
|
-
}
|
|
161
|
-
}
|
package/src/Nav/Nav.tsx
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from "react";
|
|
2
|
-
import { Button } from "../Button";
|
|
3
|
-
import { LogoMark } from "../LogoMark";
|
|
4
|
-
import type { LinkItem } from "../types";
|
|
5
|
-
|
|
6
|
-
/** @deprecated Use `LinkItem` from `@pcoi/components` instead */
|
|
7
|
-
export type NavLink = LinkItem;
|
|
8
|
-
|
|
9
|
-
export interface NavProps extends React.HTMLAttributes<HTMLElement> {
|
|
10
|
-
links?: LinkItem[];
|
|
11
|
-
ctaLabel?: string;
|
|
12
|
-
ctaHref?: string;
|
|
13
|
-
onCtaClick?: () => void;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* PCOI Navigation Bar
|
|
18
|
-
*
|
|
19
|
-
* Fixed navbar with scroll-triggered backdrop, responsive hamburger menu.
|
|
20
|
-
* Tokens: bg/nav-scrolled, blur/nav-backdrop, border/nav, spacing-72 (height)
|
|
21
|
-
*/
|
|
22
|
-
export const Nav = React.forwardRef<HTMLElement, NavProps>(
|
|
23
|
-
(
|
|
24
|
-
{
|
|
25
|
-
links = [
|
|
26
|
-
{ label: "Problem", href: "#problem" },
|
|
27
|
-
{ label: "How It Works", href: "#how-it-works" },
|
|
28
|
-
{ label: "Difference", href: "#difference" },
|
|
29
|
-
{ label: "Who It's For", href: "#who" },
|
|
30
|
-
],
|
|
31
|
-
ctaLabel = "Start a Conversation",
|
|
32
|
-
ctaHref = "#contact",
|
|
33
|
-
onCtaClick,
|
|
34
|
-
className = "",
|
|
35
|
-
...rest
|
|
36
|
-
},
|
|
37
|
-
ref
|
|
38
|
-
) => {
|
|
39
|
-
const [scrolled, setScrolled] = useState(false);
|
|
40
|
-
const [menuOpen, setMenuOpen] = useState(false);
|
|
41
|
-
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
const handleScroll = () => setScrolled(window.scrollY > 40);
|
|
44
|
-
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
45
|
-
handleScroll();
|
|
46
|
-
return () => window.removeEventListener("scroll", handleScroll);
|
|
47
|
-
}, []);
|
|
48
|
-
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
if (!menuOpen) return;
|
|
51
|
-
const handleEscape = (e: KeyboardEvent) => {
|
|
52
|
-
if (e.key === "Escape") setMenuOpen(false);
|
|
53
|
-
};
|
|
54
|
-
document.addEventListener("keydown", handleEscape);
|
|
55
|
-
return () => document.removeEventListener("keydown", handleEscape);
|
|
56
|
-
}, [menuOpen]);
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<nav
|
|
60
|
-
ref={ref}
|
|
61
|
-
className={`pcoi-nav ${scrolled ? "pcoi-nav--scrolled" : ""} ${menuOpen ? "pcoi-nav--menu-open" : ""} ${className}`}
|
|
62
|
-
aria-label="Main"
|
|
63
|
-
{...rest}
|
|
64
|
-
>
|
|
65
|
-
<div className="pcoi-nav__inner">
|
|
66
|
-
<LogoMark />
|
|
67
|
-
<div className="pcoi-nav__links">
|
|
68
|
-
{links.map((link) => (
|
|
69
|
-
<a key={link.href} href={link.href} data-track-id={link.trackingId}>{link.label}</a>
|
|
70
|
-
))}
|
|
71
|
-
<a href={ctaHref}>
|
|
72
|
-
<Button variant="nav-cta" onClick={onCtaClick}>{ctaLabel}</Button>
|
|
73
|
-
</a>
|
|
74
|
-
</div>
|
|
75
|
-
<button
|
|
76
|
-
className={`pcoi-nav__hamburger ${menuOpen ? "pcoi-nav__hamburger--active" : ""}`}
|
|
77
|
-
onClick={() => setMenuOpen(!menuOpen)}
|
|
78
|
-
aria-label="Toggle menu"
|
|
79
|
-
aria-expanded={menuOpen}
|
|
80
|
-
aria-controls="pcoi-mobile-menu"
|
|
81
|
-
>
|
|
82
|
-
<span /><span /><span />
|
|
83
|
-
</button>
|
|
84
|
-
</div>
|
|
85
|
-
<div className="pcoi-nav__mobile-menu" id="pcoi-mobile-menu">
|
|
86
|
-
{links.map((link) => (
|
|
87
|
-
<a key={link.href} href={link.href} data-track-id={link.trackingId} onClick={() => setMenuOpen(false)}>
|
|
88
|
-
{link.label}
|
|
89
|
-
</a>
|
|
90
|
-
))}
|
|
91
|
-
<a href={ctaHref}>
|
|
92
|
-
<Button variant="nav-cta" onClick={() => { onCtaClick?.(); setMenuOpen(false); }}>{ctaLabel}</Button>
|
|
93
|
-
</a>
|
|
94
|
-
</div>
|
|
95
|
-
</nav>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
Nav.displayName = "Nav";
|
|
101
|
-
export default Nav;
|
package/src/Nav/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { Nav, type NavProps, type NavLink } from "./Nav";
|