@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.
Files changed (72) hide show
  1. package/dist/components.css +1 -1
  2. package/dist/index.d.ts +50 -13
  3. package/dist/index.js +2 -2
  4. package/dist/index.mjs +499 -553
  5. package/package.json +14 -7
  6. package/src/Badge/Badge.css +2 -2
  7. package/src/Button/Button.css +4 -4
  8. package/src/Button/Button.figma.tsx +3 -5
  9. package/src/Button/Button.test.tsx +32 -0
  10. package/src/Callout/Callout.css +10 -5
  11. package/src/Callout/Callout.figma.tsx +25 -0
  12. package/src/Callout/Callout.tsx +14 -10
  13. package/src/Card/Card.css +8 -8
  14. package/src/Card/Card.figma.tsx +28 -0
  15. package/src/ChatInterface/ChatInterface.css +6 -5
  16. package/src/ChatInterface/ChatInterface.integration.test.tsx +123 -0
  17. package/src/ChatInterface/ChatInterface.tsx +6 -1
  18. package/src/ChatMessage/ChatMessage.css +8 -8
  19. package/src/ChatMessageList/ChatMessageList.css +4 -4
  20. package/src/ChatMessageList/ChatMessageList.test.tsx +70 -0
  21. package/src/ChatMessageList/ChatMessageList.tsx +7 -2
  22. package/src/Checkbox/Checkbox.css +6 -6
  23. package/src/CitationMark/CitationMark.css +3 -3
  24. package/src/CitedExcerpt/CitedExcerpt.css +7 -7
  25. package/src/CitedExcerpt/CitedExcerpt.tsx +2 -0
  26. package/src/ComparisonTable/ComparisonTable.css +6 -6
  27. package/src/ComparisonTable/ComparisonTable.tsx +6 -0
  28. package/src/ContactForm/ContactForm.css +5 -5
  29. package/src/ContactForm/ContactForm.tsx +2 -1
  30. package/src/DataTable/DataTable.css +4 -4
  31. package/src/DocumentOverlay/DocumentOverlay.css +5 -5
  32. package/src/DocumentOverlay/DocumentOverlay.test.tsx +95 -0
  33. package/src/DocumentOverlay/DocumentOverlay.tsx +1 -0
  34. package/src/Footer/Footer.css +9 -9
  35. package/src/Footer/Footer.tsx +5 -2
  36. package/src/FormField/FormField.css +4 -4
  37. package/src/FormField/FormField.figma.tsx +28 -0
  38. package/src/HowStep/HowStep.css +4 -4
  39. package/src/HowStep/HowStep.figma.tsx +23 -0
  40. package/src/LogoMark/LogoMark.tsx +3 -4
  41. package/src/Modal/Modal.css +11 -11
  42. package/src/Modal/Modal.figma.tsx +28 -0
  43. package/src/Modal/Modal.test.tsx +46 -0
  44. package/src/Modal/Modal.tsx +88 -85
  45. package/src/Nav/Nav.css +16 -16
  46. package/src/Nav/Nav.tsx +6 -2
  47. package/src/Panel/Panel.css +3 -3
  48. package/src/PromptBar/PromptBar.css +10 -10
  49. package/src/PromptBar/PromptBar.figma.tsx +25 -0
  50. package/src/PromptBar/PromptBar.test.tsx +83 -0
  51. package/src/PromptBar/PromptBar.tsx +2 -2
  52. package/src/RadioGroup/RadioGroup.css +11 -11
  53. package/src/SectionHeader/SectionHeader.css +4 -4
  54. package/src/SectionHeader/SectionHeader.figma.tsx +23 -0
  55. package/src/Select/Select.css +5 -5
  56. package/src/Select/Select.figma.tsx +33 -0
  57. package/src/Select/Select.tsx +1 -1
  58. package/src/SignalsPanel/SignalsPanel.css +9 -9
  59. package/src/SignalsPanel/SignalsPanel.tsx +2 -0
  60. package/src/SuggestionCard/SuggestionCard.css +5 -5
  61. package/src/SuggestionCards/SuggestionCards.css +1 -1
  62. package/src/SuggestionCards/SuggestionCards.test.tsx +27 -0
  63. package/src/SuggestionCards/SuggestionCards.tsx +1 -1
  64. package/src/Toast/Toast.css +14 -14
  65. package/src/Toast/Toast.tsx +50 -45
  66. package/src/Toggle/Toggle.css +15 -15
  67. package/src/Toggle/Toggle.figma.tsx +24 -0
  68. package/src/TypingIndicator/TypingIndicator.css +6 -6
  69. package/src/TypingIndicator/TypingIndicator.tsx +2 -2
  70. package/src/index.ts +2 -0
  71. package/src/styles.css +1 -0
  72. 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 "../../../icons/src/react/ArrowRightIcon";
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-10);
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-4);
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-4);
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-12);
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-8);
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-layout-component-control-box);
53
- height: var(--pcoi-layout-component-control-box);
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-full);
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-layout-component-radio-dot);
69
- height: var(--pcoi-layout-component-radio-dot);
70
- border-radius: var(--pcoi-radius-full);
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-layout-container-narrow);
6
- margin: 0 auto var(--pcoi-spacing-64);
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-16);
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-16) 0;
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
+ });
@@ -3,7 +3,7 @@
3
3
  .pcoi-select {
4
4
  display: flex;
5
5
  flex-direction: column;
6
- gap: var(--pcoi-spacing-6);
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-4);
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-sm);
35
- padding: var(--pcoi-spacing-12) var(--pcoi-spacing-36) var(--pcoi-spacing-12) var(--pcoi-spacing-14);
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-14);
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
+ });
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { ChevronDownIcon } from "../../../icons/src/react/ChevronDownIcon";
2
+ import { ChevronDownIcon } from "@pcoi/icons";
3
3
  import type { OptionItem } from "../types";
4
4
 
5
5
  export interface SelectProps
@@ -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-md);
7
- padding: var(--pcoi-spacing-40) var(--pcoi-spacing-48);
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-24) 0;
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-14);
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-24);
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-layout-component-bullet);
40
- height: var(--pcoi-layout-component-bullet);
41
- border-radius: var(--pcoi-radius-full);
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-24) var(--pcoi-spacing-28);
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-12);
6
+ gap: var(--pcoi-semantic-spacing-panel-gap);
7
7
  width: 100%;
8
- padding: var(--pcoi-spacing-16) var(--pcoi-spacing-20);
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-color-bg-card);
14
+ background: var(--pcoi-semantic-bg-card);
15
15
  border: 1px solid var(--pcoi-semantic-border-card);
16
- border-radius: var(--pcoi-radius-md);
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-color-bg-card-hover);
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
  }
@@ -3,7 +3,7 @@
3
3
  .pcoi-suggestion-cards {
4
4
  display: flex;
5
5
  flex-direction: column;
6
- gap: var(--pcoi-spacing-16);
6
+ gap: var(--pcoi-semantic-spacing-stack-md);
7
7
  }
8
8
 
9
9
  /* ── Responsive: 3-col grid on tablet and up ── */
@@ -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 */
@@ -2,41 +2,41 @@
2
2
 
3
3
  .pcoi-toast {
4
4
  position: fixed;
5
- bottom: var(--pcoi-spacing-24);
6
- right: var(--pcoi-spacing-24);
7
- z-index: var(--pcoi-layout-zIndex-toast, 600);
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-12);
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-md);
18
+ border-radius: var(--pcoi-semantic-radius-card);
19
19
  box-shadow: var(--pcoi-effect-shadow-elevated);
20
- padding: var(--pcoi-spacing-14) var(--pcoi-spacing-20);
21
- min-width: var(--pcoi-layout-container-toast-min);
22
- max-width: var(--pcoi-layout-container-toast-max);
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-layout-component-accent-border-w) solid var(--pcoi-semantic-border-success);
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-layout-component-accent-border-w) solid var(--pcoi-semantic-border-error);
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-layout-component-accent-border-w) solid var(--pcoi-semantic-border-warning);
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-layout-component-accent-border-w) solid var(--pcoi-semantic-border-info);
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-4);
57
- border-radius: var(--pcoi-radius-sm);
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
  }
@@ -1,6 +1,6 @@
1
1
  import React, { useEffect, useCallback } from "react";
2
2
  import { createPortal } from "react-dom";
3
- import { CloseIcon } from "../../../icons/src/react/CloseIcon";
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: React.FC<ToastProps> = ({
27
- message,
28
- variant = "info",
29
- open,
30
- onClose,
31
- duration = 5000,
32
- className = "",
33
- ...rest
34
- }) => {
35
- const handleClose = useCallback(() => {
36
- onClose?.();
37
- }, [onClose]);
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
- useEffect(() => {
40
- if (!open || duration === 0) return;
43
+ useEffect(() => {
44
+ if (!open || duration === 0) return;
41
45
 
42
- const timer = setTimeout(handleClose, duration);
43
- return () => clearTimeout(timer);
44
- }, [open, duration, handleClose]);
46
+ const timer = setTimeout(handleClose, duration);
47
+ return () => clearTimeout(timer);
48
+ }, [open, duration, handleClose]);
45
49
 
46
- if (!open) return null;
50
+ if (!open) return null;
47
51
 
48
- const wrapperClasses = [
49
- "pcoi-toast",
50
- `pcoi-toast--${variant}`,
51
- className,
52
- ]
53
- .filter(Boolean)
54
- .join(" ");
52
+ const wrapperClasses = [
53
+ "pcoi-toast",
54
+ `pcoi-toast--${variant}`,
55
+ className,
56
+ ]
57
+ .filter(Boolean)
58
+ .join(" ");
55
59
 
56
- return createPortal(
57
- <div className={wrapperClasses} role="alert" {...rest}>
58
- <div className="pcoi-toast__content">
59
- <span className="pcoi-toast__message">{message}</span>
60
- {onClose && (
61
- <button
62
- type="button"
63
- className="pcoi-toast__close"
64
- onClick={handleClose}
65
- aria-label="Dismiss notification"
66
- >
67
- <CloseIcon size={16} />
68
- </button>
69
- )}
70
- </div>
71
- </div>,
72
- document.body
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;