@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.
Files changed (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -0
  3. package/dist/index.d.ts +826 -568
  4. package/dist/index.js +3517 -2
  5. package/dist/styles.css +2 -0
  6. package/package.json +36 -32
  7. package/dist/components.css +0 -1
  8. package/dist/index.mjs +0 -969
  9. package/src/Badge/Badge.css +0 -40
  10. package/src/Badge/Badge.tsx +0 -36
  11. package/src/Badge/index.ts +0 -2
  12. package/src/Button/Button.css +0 -93
  13. package/src/Button/Button.figma.tsx +0 -27
  14. package/src/Button/Button.test.tsx +0 -32
  15. package/src/Button/Button.tsx +0 -47
  16. package/src/Button/index.ts +0 -1
  17. package/src/Callout/Callout.css +0 -43
  18. package/src/Callout/Callout.figma.tsx +0 -25
  19. package/src/Callout/Callout.tsx +0 -39
  20. package/src/Callout/index.ts +0 -1
  21. package/src/Card/Card.css +0 -88
  22. package/src/Card/Card.figma.tsx +0 -28
  23. package/src/Card/Card.tsx +0 -60
  24. package/src/Card/index.ts +0 -1
  25. package/src/ChatInterface/ChatInterface.css +0 -49
  26. package/src/ChatInterface/ChatInterface.integration.test.tsx +0 -123
  27. package/src/ChatInterface/ChatInterface.tsx +0 -120
  28. package/src/ChatInterface/index.ts +0 -6
  29. package/src/ChatMessage/ChatMessage.css +0 -55
  30. package/src/ChatMessage/ChatMessage.tsx +0 -71
  31. package/src/ChatMessage/index.ts +0 -2
  32. package/src/ChatMessageList/ChatMessageList.css +0 -24
  33. package/src/ChatMessageList/ChatMessageList.test.tsx +0 -74
  34. package/src/ChatMessageList/ChatMessageList.tsx +0 -51
  35. package/src/ChatMessageList/index.ts +0 -2
  36. package/src/Checkbox/Checkbox.css +0 -97
  37. package/src/Checkbox/Checkbox.tsx +0 -70
  38. package/src/Checkbox/index.ts +0 -2
  39. package/src/CitationMark/CitationMark.css +0 -40
  40. package/src/CitationMark/CitationMark.tsx +0 -38
  41. package/src/CitationMark/index.ts +0 -2
  42. package/src/CitedExcerpt/CitedExcerpt.css +0 -75
  43. package/src/CitedExcerpt/CitedExcerpt.tsx +0 -51
  44. package/src/CitedExcerpt/index.ts +0 -2
  45. package/src/ComparisonTable/ComparisonTable.css +0 -66
  46. package/src/ComparisonTable/ComparisonTable.tsx +0 -48
  47. package/src/ComparisonTable/index.ts +0 -1
  48. package/src/ContactForm/ContactForm.css +0 -38
  49. package/src/ContactForm/ContactForm.tsx +0 -57
  50. package/src/ContactForm/index.ts +0 -1
  51. package/src/DataTable/DataTable.css +0 -56
  52. package/src/DataTable/DataTable.tsx +0 -104
  53. package/src/DataTable/index.ts +0 -2
  54. package/src/DocumentOverlay/DocumentOverlay.css +0 -57
  55. package/src/DocumentOverlay/DocumentOverlay.test.tsx +0 -95
  56. package/src/DocumentOverlay/DocumentOverlay.tsx +0 -86
  57. package/src/DocumentOverlay/index.ts +0 -2
  58. package/src/Footer/Footer.css +0 -72
  59. package/src/Footer/Footer.tsx +0 -56
  60. package/src/Footer/index.ts +0 -1
  61. package/src/FormField/FormField.css +0 -78
  62. package/src/FormField/FormField.figma.tsx +0 -28
  63. package/src/FormField/FormField.tsx +0 -103
  64. package/src/FormField/index.ts +0 -2
  65. package/src/HowStep/HowStep.css +0 -48
  66. package/src/HowStep/HowStep.figma.tsx +0 -23
  67. package/src/HowStep/HowStep.tsx +0 -38
  68. package/src/HowStep/index.ts +0 -1
  69. package/src/LogoMark/LogoMark.css +0 -16
  70. package/src/LogoMark/LogoMark.tsx +0 -24
  71. package/src/LogoMark/index.ts +0 -2
  72. package/src/Modal/Modal.css +0 -101
  73. package/src/Modal/Modal.figma.tsx +0 -28
  74. package/src/Modal/Modal.test.tsx +0 -46
  75. package/src/Modal/Modal.tsx +0 -139
  76. package/src/Modal/index.ts +0 -2
  77. package/src/Nav/Nav.css +0 -161
  78. package/src/Nav/Nav.tsx +0 -101
  79. package/src/Nav/index.ts +0 -1
  80. package/src/Panel/Panel.css +0 -35
  81. package/src/Panel/Panel.tsx +0 -61
  82. package/src/Panel/index.ts +0 -2
  83. package/src/PromptBar/PromptBar.css +0 -68
  84. package/src/PromptBar/PromptBar.figma.tsx +0 -25
  85. package/src/PromptBar/PromptBar.test.tsx +0 -83
  86. package/src/PromptBar/PromptBar.tsx +0 -93
  87. package/src/PromptBar/index.ts +0 -2
  88. package/src/RadioGroup/RadioGroup.css +0 -117
  89. package/src/RadioGroup/RadioGroup.tsx +0 -112
  90. package/src/RadioGroup/index.ts +0 -2
  91. package/src/SectionHeader/SectionHeader.css +0 -38
  92. package/src/SectionHeader/SectionHeader.figma.tsx +0 -23
  93. package/src/SectionHeader/SectionHeader.tsx +0 -55
  94. package/src/SectionHeader/index.ts +0 -1
  95. package/src/Select/Select.css +0 -90
  96. package/src/Select/Select.figma.tsx +0 -33
  97. package/src/Select/Select.tsx +0 -100
  98. package/src/Select/index.ts +0 -2
  99. package/src/SignalsPanel/SignalsPanel.css +0 -51
  100. package/src/SignalsPanel/SignalsPanel.tsx +0 -33
  101. package/src/SignalsPanel/index.ts +0 -1
  102. package/src/SuggestionCard/SuggestionCard.css +0 -51
  103. package/src/SuggestionCard/SuggestionCard.tsx +0 -34
  104. package/src/SuggestionCard/index.ts +0 -2
  105. package/src/SuggestionCards/SuggestionCards.css +0 -15
  106. package/src/SuggestionCards/SuggestionCards.test.tsx +0 -27
  107. package/src/SuggestionCards/SuggestionCards.tsx +0 -40
  108. package/src/SuggestionCards/index.ts +0 -2
  109. package/src/Toast/Toast.css +0 -85
  110. package/src/Toast/Toast.tsx +0 -77
  111. package/src/Toast/index.ts +0 -2
  112. package/src/Toggle/Toggle.css +0 -110
  113. package/src/Toggle/Toggle.figma.tsx +0 -24
  114. package/src/Toggle/Toggle.tsx +0 -73
  115. package/src/Toggle/index.ts +0 -2
  116. package/src/TypingIndicator/TypingIndicator.css +0 -70
  117. package/src/TypingIndicator/TypingIndicator.tsx +0 -37
  118. package/src/TypingIndicator/index.ts +0 -2
  119. package/src/index.ts +0 -37
  120. package/src/styles/utilities.css +0 -14
  121. package/src/styles.css +0 -32
  122. package/src/types.ts +0 -65
@@ -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;
@@ -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;
@@ -1,2 +0,0 @@
1
- export { LogoMark, type LogoMarkProps } from "./LogoMark";
2
- export { default } from "./LogoMark";
@@ -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
- });
@@ -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
- });
@@ -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;
@@ -1,2 +0,0 @@
1
- export { Modal, type ModalProps } from "./Modal";
2
- export { default } from "./Modal";
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";