@pcoi/components 0.1.0 → 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 +50 -13
- package/dist/index.js +2 -2
- package/dist/index.mjs +499 -553
- package/package.json +14 -7
- package/src/Badge/Badge.css +2 -2
- package/src/Button/Button.css +4 -4
- package/src/Button/Button.figma.tsx +3 -5
- package/src/Button/Button.test.tsx +32 -0
- package/src/Callout/Callout.css +10 -5
- package/src/Callout/Callout.figma.tsx +25 -0
- package/src/Callout/Callout.tsx +14 -10
- package/src/Card/Card.css +8 -8
- package/src/Card/Card.figma.tsx +28 -0
- package/src/ChatInterface/ChatInterface.css +6 -5
- package/src/ChatInterface/ChatInterface.integration.test.tsx +123 -0
- package/src/ChatInterface/ChatInterface.tsx +6 -1
- package/src/ChatMessage/ChatMessage.css +8 -8
- package/src/ChatMessageList/ChatMessageList.css +4 -4
- package/src/ChatMessageList/ChatMessageList.test.tsx +70 -0
- package/src/ChatMessageList/ChatMessageList.tsx +7 -2
- package/src/Checkbox/Checkbox.css +6 -6
- package/src/CitationMark/CitationMark.css +3 -3
- package/src/CitedExcerpt/CitedExcerpt.css +7 -7
- package/src/CitedExcerpt/CitedExcerpt.tsx +2 -0
- package/src/ComparisonTable/ComparisonTable.css +6 -6
- package/src/ComparisonTable/ComparisonTable.tsx +6 -0
- package/src/ContactForm/ContactForm.css +5 -5
- package/src/ContactForm/ContactForm.tsx +2 -1
- package/src/DataTable/DataTable.css +4 -4
- package/src/DocumentOverlay/DocumentOverlay.css +5 -5
- package/src/DocumentOverlay/DocumentOverlay.test.tsx +95 -0
- package/src/DocumentOverlay/DocumentOverlay.tsx +1 -0
- package/src/Footer/Footer.css +9 -9
- package/src/Footer/Footer.tsx +5 -2
- package/src/FormField/FormField.css +4 -4
- package/src/FormField/FormField.figma.tsx +28 -0
- package/src/HowStep/HowStep.css +4 -4
- package/src/HowStep/HowStep.figma.tsx +23 -0
- package/src/LogoMark/LogoMark.tsx +3 -4
- package/src/Modal/Modal.css +11 -11
- package/src/Modal/Modal.figma.tsx +28 -0
- package/src/Modal/Modal.test.tsx +46 -0
- package/src/Modal/Modal.tsx +88 -85
- package/src/Nav/Nav.css +16 -16
- package/src/Nav/Nav.tsx +6 -2
- package/src/Panel/Panel.css +3 -3
- package/src/PromptBar/PromptBar.css +10 -10
- package/src/PromptBar/PromptBar.figma.tsx +25 -0
- package/src/PromptBar/PromptBar.test.tsx +83 -0
- package/src/PromptBar/PromptBar.tsx +2 -2
- package/src/RadioGroup/RadioGroup.css +11 -11
- package/src/SectionHeader/SectionHeader.css +4 -4
- package/src/SectionHeader/SectionHeader.figma.tsx +23 -0
- package/src/Select/Select.css +5 -5
- package/src/Select/Select.figma.tsx +33 -0
- package/src/Select/Select.tsx +1 -1
- package/src/SignalsPanel/SignalsPanel.css +9 -9
- package/src/SignalsPanel/SignalsPanel.tsx +2 -0
- package/src/SuggestionCard/SuggestionCard.css +5 -5
- package/src/SuggestionCards/SuggestionCards.css +1 -1
- package/src/SuggestionCards/SuggestionCards.test.tsx +27 -0
- package/src/SuggestionCards/SuggestionCards.tsx +1 -1
- package/src/Toast/Toast.css +14 -14
- package/src/Toast/Toast.tsx +50 -45
- package/src/Toggle/Toggle.css +15 -15
- package/src/Toggle/Toggle.figma.tsx +24 -0
- package/src/TypingIndicator/TypingIndicator.css +6 -6
- 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
|
@@ -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>
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
.pcoi-checkbox {
|
|
4
4
|
display: flex;
|
|
5
5
|
flex-direction: column;
|
|
6
|
-
gap: var(--pcoi-spacing-
|
|
6
|
+
gap: var(--pcoi-semantic-spacing-form-gap-compact);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
.pcoi-checkbox__control {
|
|
10
10
|
display: inline-flex;
|
|
11
11
|
align-items: center;
|
|
12
|
-
gap: var(--pcoi-spacing-
|
|
12
|
+
gap: var(--pcoi-semantic-spacing-inline-sm);
|
|
13
13
|
cursor: pointer;
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
.pcoi-checkbox__box {
|
|
29
|
-
width: var(--pcoi-
|
|
30
|
-
height: var(--pcoi-
|
|
29
|
+
width: var(--pcoi-semantic-sizing-control-box-size);
|
|
30
|
+
height: var(--pcoi-semantic-sizing-control-box-size);
|
|
31
31
|
flex-shrink: 0;
|
|
32
32
|
border: 1px solid var(--pcoi-semantic-border-default);
|
|
33
|
-
border-radius: var(--pcoi-radius-
|
|
33
|
+
border-radius: var(--pcoi-semantic-radius-input);
|
|
34
34
|
background: var(--pcoi-semantic-bg-default);
|
|
35
35
|
transition: background var(--pcoi-effect-transition-fast, 0.2s ease),
|
|
36
36
|
border-color var(--pcoi-effect-transition-fast, 0.2s ease);
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
font-size: var(--pcoi-semantic-type-label-size);
|
|
88
88
|
color: var(--pcoi-semantic-text-error);
|
|
89
89
|
margin: 0;
|
|
90
|
-
padding-left: calc(var(--pcoi-
|
|
90
|
+
padding-left: calc(var(--pcoi-semantic-sizing-control-box-size) + var(--pcoi-semantic-spacing-inline-sm));
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/* ── Disabled state ── */
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
justify-content: center;
|
|
7
7
|
min-width: 32px;
|
|
8
8
|
min-height: 32px;
|
|
9
|
-
padding: var(--pcoi-spacing-
|
|
9
|
+
padding: var(--pcoi-semantic-spacing-inline-2xs) var(--pcoi-semantic-spacing-chip-x);
|
|
10
10
|
font-family: var(--pcoi-semantic-type-mono-font);
|
|
11
11
|
font-size: var(--pcoi-semantic-type-body-compact-size);
|
|
12
12
|
font-weight: var(--pcoi-semantic-type-label-weight);
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
color: var(--pcoi-semantic-text-accent);
|
|
15
15
|
background: var(--pcoi-semantic-surface-accent-dim);
|
|
16
16
|
border: 1px solid var(--pcoi-semantic-border-accent-dim);
|
|
17
|
-
border-radius: var(--pcoi-radius-
|
|
17
|
+
border-radius: var(--pcoi-semantic-radius-badge);
|
|
18
18
|
cursor: pointer;
|
|
19
19
|
vertical-align: middle;
|
|
20
20
|
transition:
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
.pcoi-citation-mark:hover {
|
|
28
28
|
color: var(--pcoi-semantic-text-accent-hover);
|
|
29
|
-
background: var(--pcoi-
|
|
29
|
+
background: var(--pcoi-semantic-surface-accent-dim);
|
|
30
30
|
border-color: var(--pcoi-semantic-border-accent-subtle);
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
.pcoi-cited-excerpt {
|
|
4
4
|
display: flex;
|
|
5
5
|
flex-direction: column;
|
|
6
|
-
gap: var(--pcoi-spacing-
|
|
7
|
-
padding: var(--pcoi-spacing-
|
|
6
|
+
gap: var(--pcoi-semantic-spacing-form-gap-compact);
|
|
7
|
+
padding: var(--pcoi-semantic-spacing-input-y);
|
|
8
8
|
background: var(--pcoi-semantic-surface-accent-dim);
|
|
9
|
-
border-left: var(--pcoi-
|
|
9
|
+
border-left: var(--pcoi-semantic-sizing-accent-border-width) solid
|
|
10
10
|
var(--pcoi-semantic-border-accent-dim);
|
|
11
|
-
border-radius: var(--pcoi-radius-
|
|
11
|
+
border-radius: var(--pcoi-semantic-radius-input);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
.pcoi-cited-excerpt__source {
|
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
.pcoi-cited-excerpt__source:focus-visible {
|
|
35
35
|
outline: none;
|
|
36
36
|
box-shadow: var(--pcoi-effect-shadow-focus-ring);
|
|
37
|
-
border-radius: var(--pcoi-radius-
|
|
37
|
+
border-radius: var(--pcoi-semantic-radius-btn);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
.pcoi-cited-excerpt__body {
|
|
41
41
|
display: flex;
|
|
42
42
|
align-items: baseline;
|
|
43
|
-
gap: var(--pcoi-spacing-
|
|
43
|
+
gap: var(--pcoi-semantic-spacing-form-gap-compact);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
.pcoi-cited-excerpt__index {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
.pcoi-cited-excerpt__index:focus-visible {
|
|
64
64
|
outline: none;
|
|
65
65
|
box-shadow: var(--pcoi-effect-shadow-focus-ring);
|
|
66
|
-
border-radius: var(--pcoi-radius-
|
|
66
|
+
border-radius: var(--pcoi-semantic-radius-btn);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
.pcoi-cited-excerpt__text {
|
|
@@ -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,9 +1,9 @@
|
|
|
1
1
|
/* ComparisonTable — @pcoi/components */
|
|
2
2
|
|
|
3
3
|
.pcoi-comparison {
|
|
4
|
-
max-width: var(--pcoi-
|
|
4
|
+
max-width: var(--pcoi-semantic-sizing-comparison-width, 900px);
|
|
5
5
|
border: 1px solid var(--pcoi-semantic-border-card);
|
|
6
|
-
border-radius: var(--pcoi-radius-
|
|
6
|
+
border-radius: var(--pcoi-semantic-radius-card);
|
|
7
7
|
overflow: hidden;
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
letter-spacing: var(--pcoi-semantic-type-label-letter-spacing);
|
|
20
20
|
text-transform: uppercase;
|
|
21
21
|
color: var(--pcoi-semantic-text-secondary);
|
|
22
|
-
padding: var(--pcoi-spacing-
|
|
22
|
+
padding: var(--pcoi-semantic-spacing-stack-md) var(--pcoi-semantic-spacing-card-gap);
|
|
23
23
|
background: var(--pcoi-semantic-bg-alt);
|
|
24
24
|
text-align: left;
|
|
25
25
|
}
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
.pcoi-comparison__col {
|
|
32
|
-
padding: var(--pcoi-spacing-
|
|
32
|
+
padding: var(--pcoi-semantic-spacing-stack-md) var(--pcoi-semantic-spacing-card-gap);
|
|
33
33
|
font-size: var(--pcoi-semantic-type-body-compact-size);
|
|
34
34
|
color: var(--pcoi-semantic-text-secondary);
|
|
35
35
|
line-height: var(--pcoi-semantic-type-body-compact-line-height);
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/* ── Responsive ── */
|
|
56
|
-
/* 768px =
|
|
56
|
+
/* 768px = mobile breakpoint (CSS vars are not supported in @media) */
|
|
57
57
|
@media (max-width: 768px) {
|
|
58
58
|
.pcoi-comparison {
|
|
59
59
|
overflow-x: auto;
|
|
@@ -61,6 +61,6 @@
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
.pcoi-comparison__table {
|
|
64
|
-
min-width: var(--pcoi-
|
|
64
|
+
min-width: var(--pcoi-semantic-sizing-comparison-scroll-width);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
@@ -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,17 +3,17 @@
|
|
|
3
3
|
.pcoi-form {
|
|
4
4
|
background: var(--pcoi-semantic-bg-surface);
|
|
5
5
|
border: 1px solid var(--pcoi-semantic-border-default);
|
|
6
|
-
border-radius: var(--pcoi-radius-
|
|
7
|
-
padding: var(--pcoi-spacing-
|
|
6
|
+
border-radius: var(--pcoi-semantic-radius-panel);
|
|
7
|
+
padding: var(--pcoi-semantic-spacing-form-padding);
|
|
8
8
|
display: flex;
|
|
9
9
|
flex-direction: column;
|
|
10
|
-
gap: var(--pcoi-spacing-
|
|
10
|
+
gap: var(--pcoi-semantic-spacing-form-gap);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
.pcoi-form__row {
|
|
14
14
|
display: grid;
|
|
15
15
|
grid-template-columns: 1fr 1fr;
|
|
16
|
-
gap: var(--pcoi-spacing-
|
|
16
|
+
gap: var(--pcoi-semantic-spacing-form-gap);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
.pcoi-form__full {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
/* ── Responsive ── */
|
|
30
30
|
@media (max-width: 768px) {
|
|
31
31
|
.pcoi-form {
|
|
32
|
-
padding: var(--pcoi-spacing-
|
|
32
|
+
padding: var(--pcoi-semantic-spacing-panel-padding);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
.pcoi-form__row {
|
|
@@ -2,7 +2,8 @@ import React, { useState } from "react";
|
|
|
2
2
|
import { Button } from "../Button";
|
|
3
3
|
import { FormField } from "../FormField";
|
|
4
4
|
|
|
5
|
-
export interface ContactFormProps extends React.FormHTMLAttributes<HTMLFormElement> {
|
|
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
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
overflow: hidden;
|
|
6
6
|
overflow-x: auto;
|
|
7
7
|
border: 1px solid var(--pcoi-semantic-border-card);
|
|
8
|
-
border-radius: var(--pcoi-radius-
|
|
8
|
+
border-radius: var(--pcoi-semantic-radius-card);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
.pcoi-data-table__table {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
text-transform: uppercase;
|
|
22
22
|
color: var(--pcoi-semantic-text-secondary);
|
|
23
23
|
text-align: left;
|
|
24
|
-
padding: var(--pcoi-spacing-
|
|
24
|
+
padding: var(--pcoi-semantic-spacing-stack-md) var(--pcoi-semantic-spacing-card-gap);
|
|
25
25
|
background: var(--pcoi-semantic-bg-alt);
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
color: var(--pcoi-semantic-text-secondary);
|
|
34
34
|
line-height: var(--pcoi-semantic-type-body-compact-line-height);
|
|
35
35
|
text-align: left;
|
|
36
|
-
padding: var(--pcoi-spacing-
|
|
36
|
+
padding: var(--pcoi-semantic-spacing-stack-md) var(--pcoi-semantic-spacing-card-gap);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
.pcoi-data-table__td--center { text-align: center; }
|
|
@@ -52,5 +52,5 @@
|
|
|
52
52
|
font-size: var(--pcoi-semantic-type-body-size);
|
|
53
53
|
color: var(--pcoi-semantic-text-muted);
|
|
54
54
|
text-align: center;
|
|
55
|
-
padding: var(--pcoi-spacing-
|
|
55
|
+
padding: var(--pcoi-semantic-spacing-component-gap) var(--pcoi-semantic-spacing-stack-md);
|
|
56
56
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
.pcoi-doc-overlay__content p {
|
|
16
|
-
margin: 0 0 var(--pcoi-spacing-
|
|
16
|
+
margin: 0 0 var(--pcoi-semantic-spacing-panel-gap);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
.pcoi-doc-overlay__content p:last-child {
|
|
@@ -23,12 +23,12 @@
|
|
|
23
23
|
.pcoi-doc-overlay__highlight {
|
|
24
24
|
display: flex;
|
|
25
25
|
align-items: baseline;
|
|
26
|
-
gap: var(--pcoi-spacing-
|
|
26
|
+
gap: var(--pcoi-semantic-spacing-form-gap-compact);
|
|
27
27
|
background: var(--pcoi-semantic-surface-highlight);
|
|
28
|
-
border-left: var(--pcoi-
|
|
28
|
+
border-left: var(--pcoi-semantic-sizing-accent-border-width) solid
|
|
29
29
|
var(--pcoi-semantic-border-accent-subtle);
|
|
30
|
-
padding: var(--pcoi-spacing-
|
|
31
|
-
border-radius: var(--pcoi-radius-
|
|
30
|
+
padding: var(--pcoi-semantic-spacing-inline-sm) var(--pcoi-semantic-spacing-input-y);
|
|
31
|
+
border-radius: var(--pcoi-semantic-radius-input);
|
|
32
32
|
animation: pcoi-doc-highlight-fade 0.4s ease;
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { DocumentOverlay } from "./DocumentOverlay";
|
|
4
|
+
|
|
5
|
+
describe("DocumentOverlay", () => {
|
|
6
|
+
it("does not render when closed", () => {
|
|
7
|
+
render(
|
|
8
|
+
<DocumentOverlay open={false} onClose={vi.fn()} title="Source doc">
|
|
9
|
+
<p>Body content</p>
|
|
10
|
+
</DocumentOverlay>
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("renders title, body, and source label when open", () => {
|
|
17
|
+
render(
|
|
18
|
+
<DocumentOverlay
|
|
19
|
+
open
|
|
20
|
+
onClose={vi.fn()}
|
|
21
|
+
title="Source doc"
|
|
22
|
+
sourceLabel="Internal Research"
|
|
23
|
+
>
|
|
24
|
+
<p>Body content</p>
|
|
25
|
+
</DocumentOverlay>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
29
|
+
expect(screen.getByText("Source doc")).toBeInTheDocument();
|
|
30
|
+
expect(screen.getByText("Body content")).toBeInTheDocument();
|
|
31
|
+
expect(screen.getByText("Internal Research")).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("calls onClose from close button, Escape key, and backdrop click", () => {
|
|
35
|
+
const onClose = vi.fn();
|
|
36
|
+
|
|
37
|
+
render(
|
|
38
|
+
<DocumentOverlay open onClose={onClose} title="Source doc">
|
|
39
|
+
<p>Body content</p>
|
|
40
|
+
</DocumentOverlay>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
fireEvent.click(screen.getByRole("button", { name: "Close modal" }));
|
|
44
|
+
fireEvent.keyDown(document, { key: "Escape" });
|
|
45
|
+
|
|
46
|
+
const backdrop = document.querySelector(".pcoi-modal");
|
|
47
|
+
if (!backdrop) throw new Error("Expected modal backdrop to exist");
|
|
48
|
+
fireEvent.click(backdrop);
|
|
49
|
+
|
|
50
|
+
expect(onClose).toHaveBeenCalledTimes(3);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("highlights and indexes cited content when highlight props are provided", () => {
|
|
54
|
+
// JSDOM may not provide CSS.escape by default.
|
|
55
|
+
if (!(globalThis.CSS && typeof globalThis.CSS.escape === "function")) {
|
|
56
|
+
const existing = globalThis.CSS ?? ({} as CSS);
|
|
57
|
+
globalThis.CSS = {
|
|
58
|
+
...existing,
|
|
59
|
+
escape: (value: string) => value,
|
|
60
|
+
} as CSS;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const raf = vi
|
|
64
|
+
.spyOn(window, "requestAnimationFrame")
|
|
65
|
+
.mockImplementation((cb: FrameRequestCallback) => {
|
|
66
|
+
cb(0);
|
|
67
|
+
return 1;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const scrollIntoView = vi.fn();
|
|
71
|
+
Object.defineProperty(window.HTMLElement.prototype, "scrollIntoView", {
|
|
72
|
+
configurable: true,
|
|
73
|
+
value: scrollIntoView,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
render(
|
|
77
|
+
<DocumentOverlay
|
|
78
|
+
open
|
|
79
|
+
onClose={vi.fn()}
|
|
80
|
+
title="Source doc"
|
|
81
|
+
highlightId="cite-1"
|
|
82
|
+
highlightIndex={2}
|
|
83
|
+
>
|
|
84
|
+
<p id="cite-1">Cited paragraph</p>
|
|
85
|
+
</DocumentOverlay>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const cited = screen.getByText("Cited paragraph");
|
|
89
|
+
expect(cited).toHaveClass("pcoi-doc-overlay__highlight");
|
|
90
|
+
expect(cited).toHaveAttribute("data-highlight-index", "[2]");
|
|
91
|
+
expect(scrollIntoView).toHaveBeenCalled();
|
|
92
|
+
|
|
93
|
+
raf.mockRestore();
|
|
94
|
+
});
|
|
95
|
+
});
|
package/src/Footer/Footer.css
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
.pcoi-footer {
|
|
4
4
|
border-top: 1px solid var(--pcoi-semantic-border-default);
|
|
5
|
-
padding: var(--pcoi-spacing-
|
|
5
|
+
padding: var(--pcoi-semantic-spacing-section-y-md) var(--pcoi-semantic-spacing-btn-x) var(--pcoi-semantic-spacing-component-gap);
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
.pcoi-footer__inner {
|
|
9
|
-
max-width: var(--pcoi-
|
|
9
|
+
max-width: var(--pcoi-semantic-sizing-container-max);
|
|
10
10
|
margin: 0 auto;
|
|
11
11
|
display: flex;
|
|
12
12
|
justify-content: space-between;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
/* ── Brand (LogoMark overrides) ── */
|
|
17
17
|
.pcoi-footer__logo {
|
|
18
|
-
margin-bottom: var(--pcoi-spacing-
|
|
18
|
+
margin-bottom: var(--pcoi-semantic-spacing-stack-sm);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
.pcoi-footer__tagline {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
.pcoi-footer__links {
|
|
31
31
|
display: flex;
|
|
32
32
|
flex-wrap: wrap;
|
|
33
|
-
gap: var(--pcoi-spacing-
|
|
33
|
+
gap: var(--pcoi-semantic-spacing-inline-lg);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
.pcoi-footer__links a {
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
|
|
47
47
|
/* ── Bottom ── */
|
|
48
48
|
.pcoi-footer__bottom {
|
|
49
|
-
max-width: var(--pcoi-
|
|
50
|
-
margin: var(--pcoi-spacing-
|
|
51
|
-
padding-top: var(--pcoi-spacing-
|
|
49
|
+
max-width: var(--pcoi-semantic-sizing-container-max);
|
|
50
|
+
margin: var(--pcoi-semantic-spacing-component-gap) auto 0;
|
|
51
|
+
padding-top: var(--pcoi-semantic-spacing-panel-padding);
|
|
52
52
|
border-top: 1px solid var(--pcoi-semantic-border-default);
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -62,11 +62,11 @@
|
|
|
62
62
|
@media (max-width: 768px) {
|
|
63
63
|
.pcoi-footer__inner {
|
|
64
64
|
flex-direction: column;
|
|
65
|
-
gap: var(--pcoi-spacing-
|
|
65
|
+
gap: var(--pcoi-semantic-spacing-stack-lg);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
.pcoi-footer__links {
|
|
69
69
|
flex-direction: column;
|
|
70
|
-
gap: var(--pcoi-spacing-
|
|
70
|
+
gap: var(--pcoi-semantic-spacing-stack-compact);
|
|
71
71
|
}
|
|
72
72
|
}
|
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>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
.pcoi-field {
|
|
4
4
|
display: flex;
|
|
5
5
|
flex-direction: column;
|
|
6
|
-
gap: var(--pcoi-spacing-
|
|
6
|
+
gap: var(--pcoi-semantic-spacing-form-gap-compact);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
.pcoi-field__label {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
.pcoi-field__required {
|
|
18
18
|
color: var(--pcoi-semantic-text-error);
|
|
19
|
-
margin-left: var(--pcoi-spacing-
|
|
19
|
+
margin-left: var(--pcoi-semantic-spacing-inline-2xs);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
.pcoi-field__input,
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
color: var(--pcoi-semantic-text-primary);
|
|
27
27
|
background: var(--pcoi-semantic-bg-default);
|
|
28
28
|
border: 1px solid var(--pcoi-semantic-border-default);
|
|
29
|
-
border-radius: var(--pcoi-radius-
|
|
30
|
-
padding: var(--pcoi-spacing-
|
|
29
|
+
border-radius: var(--pcoi-semantic-radius-input);
|
|
30
|
+
padding: var(--pcoi-semantic-spacing-input-y) var(--pcoi-semantic-spacing-input-x-compact);
|
|
31
31
|
transition: border-color var(--pcoi-effect-transition-fast, 0.2s ease),
|
|
32
32
|
box-shadow var(--pcoi-effect-transition-fast, 0.2s ease);
|
|
33
33
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import figma from "@figma/code-connect";
|
|
2
|
+
import { FormField } from "./FormField";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Code Connect: FormField
|
|
6
|
+
* Maps Figma form field properties to React FormField props
|
|
7
|
+
*/
|
|
8
|
+
figma.connect(FormField, "https://www.figma.com/file/PCOIxDesignSystem/PCOI-Design-System?node-id=103-1", {
|
|
9
|
+
props: {
|
|
10
|
+
label: figma.string("Label"),
|
|
11
|
+
placeholder: figma.string("Placeholder"),
|
|
12
|
+
multiline: figma.boolean("Multiline"),
|
|
13
|
+
required: figma.boolean("Required"),
|
|
14
|
+
disabled: figma.boolean("Disabled"),
|
|
15
|
+
hasError: figma.boolean("Error"),
|
|
16
|
+
},
|
|
17
|
+
example: ({ label, placeholder, multiline, required, disabled, hasError }) => (
|
|
18
|
+
<FormField
|
|
19
|
+
name="field"
|
|
20
|
+
label={label}
|
|
21
|
+
placeholder={placeholder}
|
|
22
|
+
multiline={multiline}
|
|
23
|
+
required={required}
|
|
24
|
+
disabled={disabled}
|
|
25
|
+
error={hasError ? "This field is required." : undefined}
|
|
26
|
+
/>
|
|
27
|
+
),
|
|
28
|
+
});
|
package/src/HowStep/HowStep.css
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
.pcoi-how-step {
|
|
4
4
|
display: flex;
|
|
5
5
|
align-items: flex-start;
|
|
6
|
-
gap: var(--pcoi-spacing-
|
|
7
|
-
padding: var(--pcoi-spacing-
|
|
6
|
+
gap: var(--pcoi-semantic-spacing-stack-lg);
|
|
7
|
+
padding: var(--pcoi-semantic-spacing-component-gap) 0;
|
|
8
8
|
border-bottom: 1px solid var(--pcoi-semantic-border-default);
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
flex-shrink: 0;
|
|
17
17
|
width: var(--pcoi-semantic-sizing-step-number);
|
|
18
18
|
height: var(--pcoi-semantic-sizing-step-number);
|
|
19
|
-
border-radius: var(--pcoi-radius-
|
|
19
|
+
border-radius: var(--pcoi-semantic-radius-avatar);
|
|
20
20
|
background: var(--pcoi-semantic-surface-accent-dim);
|
|
21
21
|
border: 1px solid var(--pcoi-semantic-border-accent-dim);
|
|
22
22
|
display: flex;
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
font-size: var(--pcoi-semantic-type-step-title-size);
|
|
38
38
|
font-weight: var(--pcoi-semantic-type-emphasis-weight);
|
|
39
39
|
color: var(--pcoi-semantic-text-primary);
|
|
40
|
-
margin: 0 0 var(--pcoi-spacing-
|
|
40
|
+
margin: 0 0 var(--pcoi-semantic-spacing-stack-sm) 0;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
.pcoi-how-step__desc {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import figma from "@figma/code-connect";
|
|
2
|
+
import { HowStep } from "./HowStep";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Code Connect: HowStep
|
|
6
|
+
* Maps Figma step properties to React HowStep props
|
|
7
|
+
*/
|
|
8
|
+
figma.connect(HowStep, "https://www.figma.com/file/PCOIxDesignSystem/PCOI-Design-System?node-id=107-1", {
|
|
9
|
+
props: {
|
|
10
|
+
number: figma.string("Step Number"),
|
|
11
|
+
title: figma.string("Title"),
|
|
12
|
+
description: figma.string("Description"),
|
|
13
|
+
isLast: figma.boolean("Last Step"),
|
|
14
|
+
},
|
|
15
|
+
example: ({ number, title, description, isLast }) => (
|
|
16
|
+
<HowStep
|
|
17
|
+
number={number}
|
|
18
|
+
title={title}
|
|
19
|
+
description={description}
|
|
20
|
+
isLast={isLast}
|
|
21
|
+
/>
|
|
22
|
+
),
|
|
23
|
+
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
-
export
|
|
4
|
-
}
|
|
3
|
+
export type LogoMarkProps = React.AnchorHTMLAttributes<HTMLAnchorElement>;
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* PCOI Logo Mark — Atom
|
|
@@ -15,8 +14,8 @@ export interface LogoMarkProps extends React.AnchorHTMLAttributes<HTMLAnchorElem
|
|
|
15
14
|
*/
|
|
16
15
|
export const LogoMark = React.forwardRef<HTMLAnchorElement, LogoMarkProps>(
|
|
17
16
|
({ href = "#", className = "", ...rest }, ref) => (
|
|
18
|
-
<a ref={ref} href={href} className={`pcoi-logo ${className}`} {...rest}>
|
|
19
|
-
<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
|
|
20
19
|
</a>
|
|
21
20
|
)
|
|
22
21
|
);
|