@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
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { PromptBar } from "./PromptBar";
|
|
4
|
+
|
|
5
|
+
describe("PromptBar", () => {
|
|
6
|
+
it("calls onChange with textarea value", () => {
|
|
7
|
+
const onChange = vi.fn();
|
|
8
|
+
const onSubmit = vi.fn();
|
|
9
|
+
|
|
10
|
+
render(
|
|
11
|
+
<PromptBar
|
|
12
|
+
value=""
|
|
13
|
+
onChange={onChange}
|
|
14
|
+
onSubmit={onSubmit}
|
|
15
|
+
placeholder="Ask"
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
fireEvent.change(screen.getByRole("textbox", { name: "Ask" }), {
|
|
20
|
+
target: { value: "hello" },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(onChange).toHaveBeenCalledWith("hello");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("submits trimmed value on Enter", () => {
|
|
27
|
+
const onChange = vi.fn();
|
|
28
|
+
const onSubmit = vi.fn();
|
|
29
|
+
|
|
30
|
+
render(
|
|
31
|
+
<PromptBar
|
|
32
|
+
value=" hello world "
|
|
33
|
+
onChange={onChange}
|
|
34
|
+
onSubmit={onSubmit}
|
|
35
|
+
placeholder="Ask"
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
fireEvent.keyDown(screen.getByRole("textbox", { name: "Ask" }), {
|
|
40
|
+
key: "Enter",
|
|
41
|
+
code: "Enter",
|
|
42
|
+
shiftKey: false,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(onSubmit).toHaveBeenCalledTimes(1);
|
|
46
|
+
expect(onSubmit).toHaveBeenCalledWith("hello world");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("does not submit on Shift+Enter", () => {
|
|
50
|
+
const onChange = vi.fn();
|
|
51
|
+
const onSubmit = vi.fn();
|
|
52
|
+
|
|
53
|
+
render(
|
|
54
|
+
<PromptBar
|
|
55
|
+
value="hello"
|
|
56
|
+
onChange={onChange}
|
|
57
|
+
onSubmit={onSubmit}
|
|
58
|
+
placeholder="Ask"
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
fireEvent.keyDown(screen.getByRole("textbox", { name: "Ask" }), {
|
|
63
|
+
key: "Enter",
|
|
64
|
+
code: "Enter",
|
|
65
|
+
shiftKey: true,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("disables send button when value is empty/whitespace", () => {
|
|
72
|
+
render(
|
|
73
|
+
<PromptBar
|
|
74
|
+
value=" "
|
|
75
|
+
onChange={vi.fn()}
|
|
76
|
+
onSubmit={vi.fn()}
|
|
77
|
+
placeholder="Ask"
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
expect(screen.getByRole("button", { name: "Send message" })).toBeDisabled();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React, { useRef, useCallback } from "react";
|
|
2
2
|
import { Button } from "../Button/Button";
|
|
3
|
-
import { ArrowRightIcon } from "
|
|
3
|
+
import { ArrowRightIcon } from "@pcoi/icons";
|
|
4
4
|
|
|
5
|
-
export interface PromptBarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
export interface PromptBarProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange" | "onSubmit"> {
|
|
6
6
|
/** Current textarea value */
|
|
7
7
|
value: string;
|
|
8
8
|
/** Called when the textarea value changes */
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
margin: 0;
|
|
7
7
|
display: flex;
|
|
8
8
|
flex-direction: column;
|
|
9
|
-
gap: var(--pcoi-spacing-
|
|
9
|
+
gap: var(--pcoi-semantic-spacing-text-gap-sm);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
.pcoi-radio-group__legend {
|
|
@@ -15,24 +15,24 @@
|
|
|
15
15
|
letter-spacing: var(--pcoi-semantic-type-label-letter-spacing);
|
|
16
16
|
text-transform: uppercase;
|
|
17
17
|
color: var(--pcoi-semantic-text-secondary);
|
|
18
|
-
margin-bottom: var(--pcoi-spacing-
|
|
18
|
+
margin-bottom: var(--pcoi-semantic-spacing-inline-2xs);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
.pcoi-radio-group__required {
|
|
22
22
|
color: var(--pcoi-semantic-text-error);
|
|
23
|
-
margin-left: var(--pcoi-spacing-
|
|
23
|
+
margin-left: var(--pcoi-semantic-spacing-inline-2xs);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
.pcoi-radio-group__options {
|
|
27
27
|
display: flex;
|
|
28
28
|
flex-direction: column;
|
|
29
|
-
gap: var(--pcoi-spacing-
|
|
29
|
+
gap: var(--pcoi-semantic-spacing-panel-gap);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
.pcoi-radio-group__option {
|
|
33
33
|
display: inline-flex;
|
|
34
34
|
align-items: center;
|
|
35
|
-
gap: var(--pcoi-spacing-
|
|
35
|
+
gap: var(--pcoi-semantic-spacing-inline-sm);
|
|
36
36
|
cursor: pointer;
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -49,11 +49,11 @@
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
.pcoi-radio-group__circle {
|
|
52
|
-
width: var(--pcoi-
|
|
53
|
-
height: var(--pcoi-
|
|
52
|
+
width: var(--pcoi-semantic-sizing-control-box-size);
|
|
53
|
+
height: var(--pcoi-semantic-sizing-control-box-size);
|
|
54
54
|
flex-shrink: 0;
|
|
55
55
|
border: 1px solid var(--pcoi-semantic-border-default);
|
|
56
|
-
border-radius: var(--pcoi-radius-
|
|
56
|
+
border-radius: var(--pcoi-semantic-radius-badge);
|
|
57
57
|
background: var(--pcoi-semantic-bg-default);
|
|
58
58
|
position: relative;
|
|
59
59
|
transition: border-color var(--pcoi-effect-transition-fast, 0.2s ease);
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
top: 50%;
|
|
66
66
|
left: 50%;
|
|
67
67
|
transform: translate(-50%, -50%);
|
|
68
|
-
width: var(--pcoi-
|
|
69
|
-
height: var(--pcoi-
|
|
70
|
-
border-radius: var(--pcoi-radius-
|
|
68
|
+
width: var(--pcoi-semantic-sizing-radio-dot-size);
|
|
69
|
+
height: var(--pcoi-semantic-sizing-radio-dot-size);
|
|
70
|
+
border-radius: var(--pcoi-semantic-radius-badge);
|
|
71
71
|
background: var(--pcoi-semantic-action-primary-bg);
|
|
72
72
|
opacity: 0;
|
|
73
73
|
transition: opacity var(--pcoi-effect-transition-fast, 0.2s ease);
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
.pcoi-section-header {
|
|
4
4
|
text-align: center;
|
|
5
|
-
max-width: var(--pcoi-
|
|
6
|
-
margin: 0 auto var(--pcoi-spacing-
|
|
5
|
+
max-width: var(--pcoi-semantic-sizing-container-narrow);
|
|
6
|
+
margin: 0 auto var(--pcoi-semantic-spacing-section-header-margin);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
.pcoi-section-header__label {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
letter-spacing: var(--pcoi-semantic-type-section-label-letter-spacing);
|
|
15
15
|
text-transform: uppercase;
|
|
16
16
|
color: var(--pcoi-semantic-text-accent);
|
|
17
|
-
margin-bottom: var(--pcoi-spacing-
|
|
17
|
+
margin-bottom: var(--pcoi-semantic-spacing-stack-md);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
.pcoi-section-header__title {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
line-height: var(--pcoi-semantic-type-heading-line-height);
|
|
24
24
|
letter-spacing: var(--pcoi-semantic-type-heading-letter-spacing);
|
|
25
25
|
color: var(--pcoi-semantic-text-primary);
|
|
26
|
-
margin: 0 0 var(--pcoi-spacing-
|
|
26
|
+
margin: 0 0 var(--pcoi-semantic-spacing-stack-md) 0;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
.pcoi-section-header__title em {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import figma from "@figma/code-connect";
|
|
2
|
+
import { SectionHeader } from "./SectionHeader";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Code Connect: SectionHeader
|
|
6
|
+
* Maps Figma section header properties to React SectionHeader props
|
|
7
|
+
*/
|
|
8
|
+
figma.connect(SectionHeader, "https://www.figma.com/file/PCOIxDesignSystem/PCOI-Design-System?node-id=106-1", {
|
|
9
|
+
props: {
|
|
10
|
+
label: figma.string("Label"),
|
|
11
|
+
title: figma.string("Title"),
|
|
12
|
+
titleEmphasis: figma.string("Title Emphasis"),
|
|
13
|
+
description: figma.string("Description"),
|
|
14
|
+
},
|
|
15
|
+
example: ({ label, title, titleEmphasis, description }) => (
|
|
16
|
+
<SectionHeader
|
|
17
|
+
label={label}
|
|
18
|
+
title={title}
|
|
19
|
+
titleEmphasis={titleEmphasis || undefined}
|
|
20
|
+
description={description || undefined}
|
|
21
|
+
/>
|
|
22
|
+
),
|
|
23
|
+
});
|
package/src/Select/Select.css
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
.pcoi-select {
|
|
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-select__label {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
.pcoi-select__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-select__wrapper {
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
color: var(--pcoi-semantic-text-primary);
|
|
32
32
|
background: var(--pcoi-semantic-bg-default);
|
|
33
33
|
border: 1px solid var(--pcoi-semantic-border-default);
|
|
34
|
-
border-radius: var(--pcoi-radius-
|
|
35
|
-
padding: var(--pcoi-spacing-
|
|
34
|
+
border-radius: var(--pcoi-semantic-radius-input);
|
|
35
|
+
padding: var(--pcoi-semantic-spacing-input-y) var(--pcoi-semantic-spacing-btn-x-lg) var(--pcoi-semantic-spacing-input-y) var(--pcoi-semantic-spacing-input-x-compact);
|
|
36
36
|
cursor: pointer;
|
|
37
37
|
transition: border-color var(--pcoi-effect-transition-fast, 0.2s ease),
|
|
38
38
|
box-shadow var(--pcoi-effect-transition-fast, 0.2s ease);
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
|
|
60
60
|
.pcoi-select__chevron {
|
|
61
61
|
position: absolute;
|
|
62
|
-
right: var(--pcoi-spacing-
|
|
62
|
+
right: var(--pcoi-semantic-spacing-input-x-compact);
|
|
63
63
|
top: 50%;
|
|
64
64
|
transform: translateY(-50%);
|
|
65
65
|
pointer-events: none;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import figma from "@figma/code-connect";
|
|
2
|
+
import { Select } from "./Select";
|
|
3
|
+
|
|
4
|
+
const options = [
|
|
5
|
+
{ value: "one", label: "Option One" },
|
|
6
|
+
{ value: "two", label: "Option Two" },
|
|
7
|
+
{ value: "three", label: "Option Three" },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Code Connect: Select
|
|
12
|
+
* Maps Figma select field states to React Select props
|
|
13
|
+
*/
|
|
14
|
+
figma.connect(Select, "https://www.figma.com/file/PCOIxDesignSystem/PCOI-Design-System?node-id=109-1", {
|
|
15
|
+
props: {
|
|
16
|
+
label: figma.string("Label"),
|
|
17
|
+
placeholder: figma.string("Placeholder"),
|
|
18
|
+
required: figma.boolean("Required"),
|
|
19
|
+
disabled: figma.boolean("Disabled"),
|
|
20
|
+
hasError: figma.boolean("Error"),
|
|
21
|
+
},
|
|
22
|
+
example: ({ label, placeholder, required, disabled, hasError }) => (
|
|
23
|
+
<Select
|
|
24
|
+
name="mapped-select"
|
|
25
|
+
label={label}
|
|
26
|
+
options={options}
|
|
27
|
+
placeholder={placeholder || undefined}
|
|
28
|
+
required={required}
|
|
29
|
+
disabled={disabled}
|
|
30
|
+
error={hasError ? "Please select an option." : undefined}
|
|
31
|
+
/>
|
|
32
|
+
),
|
|
33
|
+
});
|
package/src/Select/Select.tsx
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
.pcoi-signals {
|
|
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-card);
|
|
7
|
+
padding: var(--pcoi-semantic-spacing-component-gap) var(--pcoi-semantic-spacing-inline-xl);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
.pcoi-signals__title {
|
|
11
11
|
font-size: var(--pcoi-semantic-type-heading-sm-size);
|
|
12
12
|
font-weight: var(--pcoi-semantic-type-emphasis-weight);
|
|
13
13
|
color: var(--pcoi-semantic-text-accent);
|
|
14
|
-
margin: 0 0 var(--pcoi-spacing-
|
|
14
|
+
margin: 0 0 var(--pcoi-semantic-spacing-panel-padding) 0;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
.pcoi-signals__list {
|
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
margin: 0;
|
|
21
21
|
display: flex;
|
|
22
22
|
flex-direction: column;
|
|
23
|
-
gap: var(--pcoi-spacing-
|
|
23
|
+
gap: var(--pcoi-semantic-spacing-stack-compact);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
.pcoi-signals__item {
|
|
27
27
|
position: relative;
|
|
28
|
-
padding-left: var(--pcoi-spacing-
|
|
28
|
+
padding-left: var(--pcoi-semantic-spacing-panel-padding);
|
|
29
29
|
font-size: var(--pcoi-semantic-type-body-compact-size);
|
|
30
30
|
color: var(--pcoi-semantic-text-secondary);
|
|
31
31
|
line-height: var(--pcoi-semantic-type-body-compact-line-height);
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
position: absolute;
|
|
37
37
|
left: 0;
|
|
38
38
|
top: 8px;
|
|
39
|
-
width: var(--pcoi-
|
|
40
|
-
height: var(--pcoi-
|
|
41
|
-
border-radius: var(--pcoi-radius-
|
|
39
|
+
width: var(--pcoi-semantic-sizing-bullet-size);
|
|
40
|
+
height: var(--pcoi-semantic-sizing-bullet-size);
|
|
41
|
+
border-radius: var(--pcoi-semantic-radius-badge);
|
|
42
42
|
background: var(--pcoi-semantic-text-accent);
|
|
43
43
|
opacity: 0.5;
|
|
44
44
|
}
|
|
@@ -46,6 +46,6 @@
|
|
|
46
46
|
/* ── Responsive ── */
|
|
47
47
|
@media (max-width: 768px) {
|
|
48
48
|
.pcoi-signals {
|
|
49
|
-
padding: var(--pcoi-spacing-
|
|
49
|
+
padding: var(--pcoi-semantic-spacing-panel-padding) var(--pcoi-semantic-spacing-inline-lg);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -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;
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
.pcoi-suggestion-card {
|
|
4
4
|
display: flex;
|
|
5
5
|
align-items: center;
|
|
6
|
-
gap: var(--pcoi-spacing-
|
|
6
|
+
gap: var(--pcoi-semantic-spacing-panel-gap);
|
|
7
7
|
width: 100%;
|
|
8
|
-
padding: var(--pcoi-spacing-
|
|
8
|
+
padding: var(--pcoi-semantic-spacing-panel-padding-sm) var(--pcoi-semantic-spacing-card-gap);
|
|
9
9
|
font-family: var(--pcoi-semantic-type-body-font);
|
|
10
10
|
font-size: var(--pcoi-semantic-type-body-size);
|
|
11
11
|
line-height: var(--pcoi-semantic-type-body-line-height);
|
|
12
12
|
color: var(--pcoi-semantic-text-secondary);
|
|
13
13
|
text-align: left;
|
|
14
|
-
background: var(--pcoi-
|
|
14
|
+
background: var(--pcoi-semantic-bg-card);
|
|
15
15
|
border: 1px solid var(--pcoi-semantic-border-card);
|
|
16
|
-
border-radius: var(--pcoi-radius-
|
|
16
|
+
border-radius: var(--pcoi-semantic-radius-card);
|
|
17
17
|
cursor: pointer;
|
|
18
18
|
transition:
|
|
19
19
|
background var(--pcoi-effect-transition-medium),
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
.pcoi-suggestion-card:hover {
|
|
26
|
-
background: var(--pcoi-
|
|
26
|
+
background: var(--pcoi-semantic-bg-card-hover);
|
|
27
27
|
border-color: var(--pcoi-semantic-border-card-hover);
|
|
28
28
|
transform: var(--pcoi-effect-transform-hover-lift-sm);
|
|
29
29
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import type { Suggestion } from "../types";
|
|
4
|
+
import { SuggestionCards } from "./SuggestionCards";
|
|
5
|
+
|
|
6
|
+
describe("SuggestionCards", () => {
|
|
7
|
+
it("renders suggestions and calls onSelect with clicked item", () => {
|
|
8
|
+
const suggestions: Suggestion[] = [
|
|
9
|
+
{ id: "s1", label: "What is PCOI?" },
|
|
10
|
+
{ id: "s2", label: "How does indexing work?" },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const onSelect = vi.fn();
|
|
14
|
+
|
|
15
|
+
render(<SuggestionCards suggestions={suggestions} onSelect={onSelect} />);
|
|
16
|
+
|
|
17
|
+
const first = screen.getByRole("button", { name: "What is PCOI?" });
|
|
18
|
+
const second = screen.getByRole("button", { name: "How does indexing work?" });
|
|
19
|
+
|
|
20
|
+
expect(first).toBeInTheDocument();
|
|
21
|
+
expect(second).toBeInTheDocument();
|
|
22
|
+
|
|
23
|
+
fireEvent.click(second);
|
|
24
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
25
|
+
expect(onSelect).toHaveBeenCalledWith(suggestions[1]);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -3,7 +3,7 @@ import { SuggestionCard } from "../SuggestionCard/SuggestionCard";
|
|
|
3
3
|
import type { Suggestion } from "../types";
|
|
4
4
|
|
|
5
5
|
export interface SuggestionCardsProps
|
|
6
|
-
extends React.HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect"> {
|
|
7
7
|
/** Array of suggestion prompts */
|
|
8
8
|
suggestions: Suggestion[];
|
|
9
9
|
/** Called when a suggestion card is clicked */
|
package/src/Toast/Toast.css
CHANGED
|
@@ -2,41 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
.pcoi-toast {
|
|
4
4
|
position: fixed;
|
|
5
|
-
bottom: var(--pcoi-spacing-
|
|
6
|
-
right: var(--pcoi-spacing-
|
|
7
|
-
z-index: var(--pcoi-layout-
|
|
5
|
+
bottom: var(--pcoi-semantic-spacing-btn-x);
|
|
6
|
+
right: var(--pcoi-semantic-spacing-btn-x);
|
|
7
|
+
z-index: var(--pcoi-semantic-layout-z-toast, 600);
|
|
8
8
|
animation: pcoi-toast-slide-in 0.3s ease;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
.pcoi-toast__content {
|
|
12
12
|
display: flex;
|
|
13
13
|
align-items: center;
|
|
14
|
-
gap: var(--pcoi-spacing-
|
|
14
|
+
gap: var(--pcoi-semantic-spacing-panel-gap);
|
|
15
15
|
font-family: var(--pcoi-semantic-type-body-font);
|
|
16
16
|
background: var(--pcoi-semantic-surface-elevated);
|
|
17
17
|
border: 1px solid var(--pcoi-semantic-border-default);
|
|
18
|
-
border-radius: var(--pcoi-radius-
|
|
18
|
+
border-radius: var(--pcoi-semantic-radius-card);
|
|
19
19
|
box-shadow: var(--pcoi-effect-shadow-elevated);
|
|
20
|
-
padding: var(--pcoi-spacing-
|
|
21
|
-
min-width: var(--pcoi-
|
|
22
|
-
max-width: var(--pcoi-
|
|
20
|
+
padding: var(--pcoi-semantic-spacing-btn-y-comfortable) var(--pcoi-semantic-spacing-card-gap);
|
|
21
|
+
min-width: var(--pcoi-semantic-sizing-toast-width-min);
|
|
22
|
+
max-width: var(--pcoi-semantic-sizing-toast-width-max);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/* ── Left accent border per variant ── */
|
|
26
26
|
.pcoi-toast--success .pcoi-toast__content {
|
|
27
|
-
border-left: var(--pcoi-
|
|
27
|
+
border-left: var(--pcoi-semantic-sizing-accent-border-width) solid var(--pcoi-semantic-border-success);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
.pcoi-toast--error .pcoi-toast__content {
|
|
31
|
-
border-left: var(--pcoi-
|
|
31
|
+
border-left: var(--pcoi-semantic-sizing-accent-border-width) solid var(--pcoi-semantic-border-error);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
.pcoi-toast--warning .pcoi-toast__content {
|
|
35
|
-
border-left: var(--pcoi-
|
|
35
|
+
border-left: var(--pcoi-semantic-sizing-accent-border-width) solid var(--pcoi-semantic-border-warning);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
.pcoi-toast--info .pcoi-toast__content {
|
|
39
|
-
border-left: var(--pcoi-
|
|
39
|
+
border-left: var(--pcoi-semantic-sizing-accent-border-width) solid var(--pcoi-semantic-border-info);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
.pcoi-toast__message {
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
font-size: var(--pcoi-semantic-type-close-sm-size);
|
|
54
54
|
line-height: var(--pcoi-semantic-type-none-line-height);
|
|
55
55
|
cursor: pointer;
|
|
56
|
-
padding: var(--pcoi-spacing-
|
|
57
|
-
border-radius: var(--pcoi-radius-
|
|
56
|
+
padding: var(--pcoi-semantic-spacing-control-padding-2xs);
|
|
57
|
+
border-radius: var(--pcoi-semantic-radius-btn);
|
|
58
58
|
flex-shrink: 0;
|
|
59
59
|
transition: color var(--pcoi-effect-transition-fast, 0.2s ease);
|
|
60
60
|
}
|
package/src/Toast/Toast.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useCallback } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
|
-
import { CloseIcon } from "
|
|
3
|
+
import { CloseIcon } from "@pcoi/icons";
|
|
4
4
|
|
|
5
5
|
export type ToastVariant = "success" | "error" | "warning" | "info";
|
|
6
6
|
|
|
@@ -23,55 +23,60 @@ export interface ToastProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
23
23
|
* text/primary, text/success|error|warning|info,
|
|
24
24
|
* shadow/elevated, zIndex/toast, radius-md
|
|
25
25
|
*/
|
|
26
|
-
export const Toast
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
export const Toast = React.forwardRef<HTMLDivElement, ToastProps>(
|
|
27
|
+
(
|
|
28
|
+
{
|
|
29
|
+
message,
|
|
30
|
+
variant = "info",
|
|
31
|
+
open,
|
|
32
|
+
onClose,
|
|
33
|
+
duration = 5000,
|
|
34
|
+
className = "",
|
|
35
|
+
...rest
|
|
36
|
+
},
|
|
37
|
+
ref
|
|
38
|
+
) => {
|
|
39
|
+
const handleClose = useCallback(() => {
|
|
40
|
+
onClose?.();
|
|
41
|
+
}, [onClose]);
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!open || duration === 0) return;
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
const timer = setTimeout(handleClose, duration);
|
|
47
|
+
return () => clearTimeout(timer);
|
|
48
|
+
}, [open, duration, handleClose]);
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
if (!open) return null;
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
const wrapperClasses = [
|
|
53
|
+
"pcoi-toast",
|
|
54
|
+
`pcoi-toast--${variant}`,
|
|
55
|
+
className,
|
|
56
|
+
]
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.join(" ");
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
60
|
+
return createPortal(
|
|
61
|
+
<div ref={ref} className={wrapperClasses} role="alert" {...rest}>
|
|
62
|
+
<div className="pcoi-toast__content">
|
|
63
|
+
<span className="pcoi-toast__message">{message}</span>
|
|
64
|
+
{onClose && (
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
className="pcoi-toast__close"
|
|
68
|
+
onClick={handleClose}
|
|
69
|
+
aria-label="Dismiss notification"
|
|
70
|
+
>
|
|
71
|
+
<CloseIcon size={16} />
|
|
72
|
+
</button>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
</div>,
|
|
76
|
+
document.body
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
);
|
|
75
80
|
|
|
76
81
|
Toast.displayName = "Toast";
|
|
77
82
|
export default Toast;
|