@pcoi/components 0.1.0 → 0.1.1

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 (60) hide show
  1. package/dist/components.css +1 -1
  2. package/dist/index.d.ts +5 -6
  3. package/dist/index.js +2 -2
  4. package/dist/index.mjs +296 -375
  5. package/package.json +12 -5
  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 +5 -5
  11. package/src/Callout/Callout.figma.tsx +25 -0
  12. package/src/Card/Card.css +8 -8
  13. package/src/Card/Card.figma.tsx +28 -0
  14. package/src/ChatInterface/ChatInterface.css +5 -5
  15. package/src/ChatInterface/ChatInterface.integration.test.tsx +123 -0
  16. package/src/ChatMessage/ChatMessage.css +8 -8
  17. package/src/ChatMessageList/ChatMessageList.css +4 -4
  18. package/src/ChatMessageList/ChatMessageList.test.tsx +74 -0
  19. package/src/Checkbox/Checkbox.css +6 -6
  20. package/src/CitationMark/CitationMark.css +3 -3
  21. package/src/CitedExcerpt/CitedExcerpt.css +7 -7
  22. package/src/ComparisonTable/ComparisonTable.css +6 -6
  23. package/src/ContactForm/ContactForm.css +5 -5
  24. package/src/ContactForm/ContactForm.tsx +1 -1
  25. package/src/DataTable/DataTable.css +4 -4
  26. package/src/DocumentOverlay/DocumentOverlay.css +5 -5
  27. package/src/DocumentOverlay/DocumentOverlay.test.tsx +95 -0
  28. package/src/DocumentOverlay/DocumentOverlay.tsx +1 -1
  29. package/src/Footer/Footer.css +9 -9
  30. package/src/FormField/FormField.css +4 -4
  31. package/src/FormField/FormField.figma.tsx +28 -0
  32. package/src/HowStep/HowStep.css +4 -4
  33. package/src/HowStep/HowStep.figma.tsx +23 -0
  34. package/src/LogoMark/LogoMark.tsx +1 -2
  35. package/src/Modal/Modal.css +11 -11
  36. package/src/Modal/Modal.figma.tsx +28 -0
  37. package/src/Modal/Modal.test.tsx +46 -0
  38. package/src/Modal/Modal.tsx +17 -19
  39. package/src/Nav/Nav.css +16 -16
  40. package/src/Panel/Panel.css +3 -3
  41. package/src/PromptBar/PromptBar.css +10 -10
  42. package/src/PromptBar/PromptBar.figma.tsx +25 -0
  43. package/src/PromptBar/PromptBar.test.tsx +83 -0
  44. package/src/PromptBar/PromptBar.tsx +2 -2
  45. package/src/RadioGroup/RadioGroup.css +11 -11
  46. package/src/SectionHeader/SectionHeader.css +4 -4
  47. package/src/SectionHeader/SectionHeader.figma.tsx +23 -0
  48. package/src/Select/Select.css +5 -5
  49. package/src/Select/Select.figma.tsx +33 -0
  50. package/src/Select/Select.tsx +1 -1
  51. package/src/SignalsPanel/SignalsPanel.css +9 -9
  52. package/src/SuggestionCard/SuggestionCard.css +5 -5
  53. package/src/SuggestionCards/SuggestionCards.css +1 -1
  54. package/src/SuggestionCards/SuggestionCards.test.tsx +27 -0
  55. package/src/SuggestionCards/SuggestionCards.tsx +1 -1
  56. package/src/Toast/Toast.css +14 -14
  57. package/src/Toast/Toast.tsx +1 -1
  58. package/src/Toggle/Toggle.css +15 -15
  59. package/src/Toggle/Toggle.figma.tsx +24 -0
  60. package/src/TypingIndicator/TypingIndicator.css +6 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pcoi/components",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "PCOI Design System — React UI components",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -27,10 +27,17 @@
27
27
  "react-dom": ">=18.0.0"
28
28
  },
29
29
  "dependencies": {
30
- "@pcoi/tokens": "^0.1.0",
31
- "@pcoi/icons": "^0.1.0"
30
+ "@pcoi/tokens": "^0.1.1",
31
+ "@pcoi/icons": "^0.1.1"
32
32
  },
33
- "keywords": ["components", "pcoi", "design-system", "react"],
33
+ "keywords": [
34
+ "components",
35
+ "pcoi",
36
+ "design-system",
37
+ "react"
38
+ ],
34
39
  "license": "MIT",
35
- "sideEffects": ["*.css"]
40
+ "sideEffects": [
41
+ "*.css"
42
+ ]
36
43
  }
@@ -8,8 +8,8 @@
8
8
  font-weight: var(--pcoi-semantic-type-label-weight);
9
9
  letter-spacing: var(--pcoi-semantic-type-badge-letter-spacing);
10
10
  text-transform: uppercase;
11
- padding: var(--pcoi-spacing-4) var(--pcoi-spacing-10);
12
- border-radius: var(--pcoi-radius-full);
11
+ padding: var(--pcoi-semantic-spacing-inline-2xs) var(--pcoi-semantic-spacing-chip-x);
12
+ border-radius: var(--pcoi-semantic-radius-badge);
13
13
  line-height: var(--pcoi-semantic-type-none-line-height);
14
14
  }
15
15
 
@@ -5,11 +5,11 @@
5
5
  display: inline-flex;
6
6
  align-items: center;
7
7
  justify-content: center;
8
- padding: var(--pcoi-spacing-14) var(--pcoi-spacing-28);
8
+ padding: var(--pcoi-semantic-spacing-btn-y-comfortable) var(--pcoi-semantic-spacing-btn-x-comfortable);
9
9
  font-size: var(--pcoi-semantic-type-body-size); /* 0.95rem */
10
10
  font-weight: var(--pcoi-semantic-type-emphasis-weight);
11
11
  font-family: var(--pcoi-semantic-type-body-font);
12
- border-radius: var(--pcoi-radius-sm);
12
+ border-radius: var(--pcoi-semantic-radius-btn);
13
13
  border: none;
14
14
  cursor: pointer;
15
15
  transition: all var(--pcoi-effect-transition-fast, 0.2s ease);
@@ -47,7 +47,7 @@
47
47
  .pcoi-btn--nav-cta {
48
48
  background: var(--pcoi-semantic-action-primary-bg);
49
49
  color: var(--pcoi-semantic-action-primary-text);
50
- padding: var(--pcoi-spacing-10) var(--pcoi-spacing-22);
50
+ padding: var(--pcoi-semantic-spacing-btn-y) var(--pcoi-semantic-spacing-btn-x-nav);
51
51
  font-weight: var(--pcoi-semantic-type-emphasis-weight);
52
52
  }
53
53
 
@@ -88,6 +88,6 @@
88
88
 
89
89
  /* ── Size: Large ── */
90
90
  .pcoi-btn--large {
91
- padding: var(--pcoi-spacing-16) var(--pcoi-spacing-36);
91
+ padding: var(--pcoi-semantic-spacing-btn-y-lg) var(--pcoi-semantic-spacing-btn-x-lg);
92
92
  font-size: var(--pcoi-semantic-type-body-lg-size); /* 1rem */
93
93
  }
@@ -5,9 +5,7 @@ import { Button } from "./Button";
5
5
  * Code Connect: Button/Primary
6
6
  * Maps Figma Button component properties to React Button props
7
7
  */
8
- // TODO: Replace with your Figma component URL from the Figma file
9
- // e.g. "https://www.figma.com/file/FILEID/NAME?node-id=NODEID"
10
- figma.connect(Button, "https://figma.com/REPLACE_WITH_BUTTON_COMPONENT_URL", {
8
+ figma.connect(Button, "https://www.figma.com/file/PCOIxDesignSystem/PCOI-Design-System?node-id=101-1", {
11
9
  props: {
12
10
  variant: figma.enum("Variant", {
13
11
  Primary: "primary",
@@ -21,8 +19,8 @@ figma.connect(Button, "https://figma.com/REPLACE_WITH_BUTTON_COMPONENT_URL", {
21
19
  label: figma.string("Label"),
22
20
  disabled: figma.boolean("Disabled"),
23
21
  },
24
- example: ({ variant, size, label }) => (
25
- <Button variant={variant} size={size}>
22
+ example: ({ variant, size, label, disabled }) => (
23
+ <Button variant={variant} size={size} disabled={disabled}>
26
24
  {label}
27
25
  </Button>
28
26
  ),
@@ -0,0 +1,32 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { Button } from "./Button";
4
+
5
+ describe("Button", () => {
6
+ it("renders children with default variant classes", () => {
7
+ render(<Button>Send</Button>);
8
+
9
+ const button = screen.getByRole("button", { name: "Send" });
10
+ expect(button).toHaveClass("pcoi-btn", "pcoi-btn--primary");
11
+ expect(button).not.toHaveClass("pcoi-btn--large");
12
+ });
13
+
14
+ it("applies variant and size classes", () => {
15
+ render(
16
+ <Button variant="ghost" size="large">
17
+ Cancel
18
+ </Button>
19
+ );
20
+
21
+ const button = screen.getByRole("button", { name: "Cancel" });
22
+ expect(button).toHaveClass("pcoi-btn--ghost", "pcoi-btn--large");
23
+ });
24
+
25
+ it("invokes onClick when enabled", () => {
26
+ const onClick = vi.fn();
27
+ render(<Button onClick={onClick}>Click</Button>);
28
+
29
+ fireEvent.click(screen.getByRole("button", { name: "Click" }));
30
+ expect(onClick).toHaveBeenCalledTimes(1);
31
+ });
32
+ });
@@ -2,17 +2,17 @@
2
2
 
3
3
  .pcoi-callout {
4
4
  text-align: center;
5
- padding: var(--pcoi-spacing-40) var(--pcoi-spacing-24);
5
+ padding: var(--pcoi-semantic-spacing-component-gap) var(--pcoi-semantic-spacing-btn-x);
6
6
  display: flex;
7
7
  flex-direction: column;
8
8
  align-items: center;
9
9
  }
10
10
 
11
11
  .pcoi-callout__line {
12
- width: var(--pcoi-layout-component-accent-line-w);
12
+ width: var(--pcoi-semantic-sizing-accent-line-width);
13
13
  height: 2px;
14
14
  background: var(--pcoi-semantic-text-accent);
15
- margin-bottom: var(--pcoi-spacing-32);
15
+ margin-bottom: var(--pcoi-semantic-spacing-stack-lg);
16
16
  }
17
17
 
18
18
  .pcoi-callout__quote {
@@ -20,8 +20,8 @@
20
20
  font-weight: var(--pcoi-semantic-type-callout-weight);
21
21
  color: var(--pcoi-semantic-text-secondary);
22
22
  line-height: var(--pcoi-semantic-type-body-compact-line-height);
23
- max-width: var(--pcoi-layout-container-chamath, 640px);
24
- margin: 0 0 var(--pcoi-spacing-24) 0;
23
+ max-width: var(--pcoi-semantic-sizing-callout-width, 640px);
24
+ margin: 0 0 var(--pcoi-semantic-spacing-panel-padding) 0;
25
25
  font-style: italic;
26
26
  }
27
27
 
@@ -0,0 +1,25 @@
1
+ import figma from "@figma/code-connect";
2
+ import { Callout } from "./Callout";
3
+
4
+ /**
5
+ * Code Connect: Callout
6
+ * Maps Figma callout content properties to React Callout props
7
+ */
8
+ figma.connect(Callout, "https://www.figma.com/file/PCOIxDesignSystem/PCOI-Design-System?node-id=108-1", {
9
+ props: {
10
+ quote: figma.string("Quote"),
11
+ attribution: figma.string("Attribution"),
12
+ attributionTitle: figma.string("Attribution Title"),
13
+ sourceUrl: figma.string("Source URL"),
14
+ sourceLabel: figma.string("Source Label"),
15
+ },
16
+ example: ({ quote, attribution, attributionTitle, sourceUrl, sourceLabel }) => (
17
+ <Callout
18
+ quote={quote}
19
+ attribution={attribution || undefined}
20
+ attributionTitle={attributionTitle || undefined}
21
+ sourceUrl={sourceUrl || undefined}
22
+ sourceLabel={sourceLabel || undefined}
23
+ />
24
+ ),
25
+ });
package/src/Card/Card.css CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  .pcoi-card {
4
4
  border: 1px solid var(--pcoi-semantic-border-card);
5
- border-radius: var(--pcoi-radius-md);
6
- padding: var(--pcoi-spacing-36);
5
+ border-radius: var(--pcoi-semantic-radius-card);
6
+ padding: var(--pcoi-semantic-spacing-card-padding);
7
7
  transition: border-color var(--pcoi-effect-transition-medium, 0.3s ease),
8
8
  transform var(--pcoi-effect-transition-medium, 0.3s ease);
9
9
  }
@@ -21,12 +21,12 @@
21
21
  .pcoi-card--problem .pcoi-card__icon {
22
22
  width: var(--pcoi-semantic-sizing-icon-box);
23
23
  height: var(--pcoi-semantic-sizing-icon-box);
24
- border-radius: var(--pcoi-radius-sm);
24
+ border-radius: var(--pcoi-semantic-radius-btn);
25
25
  background: var(--pcoi-semantic-surface-accent-dim);
26
26
  display: flex;
27
27
  align-items: center;
28
28
  justify-content: center;
29
- margin-bottom: var(--pcoi-spacing-20);
29
+ margin-bottom: var(--pcoi-semantic-spacing-card-gap);
30
30
  color: var(--pcoi-semantic-text-accent);
31
31
  }
32
32
 
@@ -34,7 +34,7 @@
34
34
  font-size: var(--pcoi-semantic-type-heading-md-size);
35
35
  font-weight: var(--pcoi-semantic-type-emphasis-weight);
36
36
  color: var(--pcoi-semantic-text-primary);
37
- margin: 0 0 var(--pcoi-spacing-10) 0;
37
+ margin: 0 0 var(--pcoi-semantic-spacing-text-gap-sm) 0;
38
38
  }
39
39
 
40
40
  .pcoi-card--problem .pcoi-card__description {
@@ -52,7 +52,7 @@
52
52
  font-size: var(--pcoi-semantic-type-heading-sm-size);
53
53
  font-weight: var(--pcoi-semantic-type-emphasis-weight);
54
54
  color: var(--pcoi-semantic-text-accent);
55
- margin: 0 0 var(--pcoi-spacing-10) 0;
55
+ margin: 0 0 var(--pcoi-semantic-spacing-text-gap-sm) 0;
56
56
  }
57
57
 
58
58
  .pcoi-card--who .pcoi-card__description {
@@ -71,14 +71,14 @@
71
71
  font-size: var(--pcoi-semantic-type-body-sm-size);
72
72
  font-weight: var(--pcoi-semantic-type-emphasis-weight);
73
73
  color: var(--pcoi-semantic-text-accent);
74
- margin-bottom: var(--pcoi-spacing-16);
74
+ margin-bottom: var(--pcoi-semantic-spacing-stack-md);
75
75
  }
76
76
 
77
77
  .pcoi-card--principle .pcoi-card__title {
78
78
  font-size: var(--pcoi-semantic-type-card-subtitle-size);
79
79
  font-weight: var(--pcoi-semantic-type-emphasis-weight);
80
80
  color: var(--pcoi-semantic-text-primary);
81
- margin: 0 0 var(--pcoi-spacing-10) 0;
81
+ margin: 0 0 var(--pcoi-semantic-spacing-text-gap-sm) 0;
82
82
  }
83
83
 
84
84
  .pcoi-card--principle .pcoi-card__description {
@@ -0,0 +1,28 @@
1
+ import figma from "@figma/code-connect";
2
+ import { Card } from "./Card";
3
+
4
+ /**
5
+ * Code Connect: Card
6
+ * Maps Figma Card component properties to React Card props
7
+ */
8
+ figma.connect(Card, "https://www.figma.com/file/PCOIxDesignSystem/PCOI-Design-System?node-id=102-1", {
9
+ props: {
10
+ variant: figma.enum("Variant", {
11
+ Problem: "problem",
12
+ Who: "who",
13
+ Principle: "principle",
14
+ }),
15
+ title: figma.string("Title"),
16
+ description: figma.string("Description"),
17
+ number: figma.string("Number"),
18
+ },
19
+ example: ({ variant, title, description, number }) => (
20
+ <Card
21
+ variant={variant}
22
+ title={title}
23
+ description={description}
24
+ number={number}
25
+ icon={variant === "problem" ? <span aria-hidden="true">!</span> : undefined}
26
+ />
27
+ ),
28
+ });
@@ -4,7 +4,7 @@
4
4
  display: flex;
5
5
  flex-direction: column;
6
6
  height: 100%;
7
- max-width: var(--pcoi-layout-container-narrow, 800px);
7
+ max-width: var(--pcoi-semantic-sizing-container-narrow, 800px);
8
8
  margin: 0 auto;
9
9
  }
10
10
 
@@ -18,7 +18,7 @@
18
18
  display: flex;
19
19
  flex-direction: column;
20
20
  justify-content: center;
21
- padding: var(--pcoi-spacing-16);
21
+ padding: var(--pcoi-semantic-spacing-panel-padding-sm);
22
22
  }
23
23
 
24
24
  .pcoi-chat__suggestions {
@@ -34,16 +34,16 @@
34
34
  /* ── Prompt ── */
35
35
  .pcoi-chat__prompt {
36
36
  flex-shrink: 0;
37
- padding: var(--pcoi-spacing-16);
37
+ padding: var(--pcoi-semantic-spacing-panel-padding-sm);
38
38
  }
39
39
 
40
40
  /* ── Responsive ── */
41
41
  @media (min-width: 768px) {
42
42
  .pcoi-chat__empty-state {
43
- padding: var(--pcoi-spacing-24);
43
+ padding: var(--pcoi-semantic-spacing-panel-padding);
44
44
  }
45
45
 
46
46
  .pcoi-chat__prompt {
47
- padding: var(--pcoi-spacing-24);
47
+ padding: var(--pcoi-semantic-spacing-panel-padding);
48
48
  }
49
49
  }
@@ -0,0 +1,123 @@
1
+ import React, { useState } from "react";
2
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import type { Citation } from "../types";
5
+ import { DocumentOverlay } from "../DocumentOverlay/DocumentOverlay";
6
+ import { ChatInterface, type ChatInterfaceMessage } from "./ChatInterface";
7
+
8
+ const citation: Citation = {
9
+ index: 1,
10
+ excerpt: "Structured capture improves onboarding outcomes.",
11
+ sourceTitle: "Knowledge Report",
12
+ sourceId: "doc-1",
13
+ };
14
+
15
+ const messages: ChatInterfaceMessage[] = [
16
+ {
17
+ id: "m-1",
18
+ role: "assistant",
19
+ content: "Structured capture improves onboarding outcomes.",
20
+ citations: [citation],
21
+ timestamp: "2:35 PM",
22
+ },
23
+ ];
24
+
25
+ function ChatCitationHarness() {
26
+ const [overlayOpen, setOverlayOpen] = useState(false);
27
+ const [activeCitation, setActiveCitation] = useState<Citation | null>(null);
28
+
29
+ const handleCitationClick = (clickedCitation: Citation) => {
30
+ setActiveCitation(clickedCitation);
31
+ setOverlayOpen(true);
32
+ };
33
+
34
+ const doc = activeCitation
35
+ ? {
36
+ title: "Knowledge Retention Report",
37
+ sourceLabel: "Internal Research",
38
+ highlightId: "cite-1",
39
+ content: (
40
+ <>
41
+ <p id="cite-1">Structured capture improves onboarding outcomes.</p>
42
+ <p>Additional context.</p>
43
+ </>
44
+ ),
45
+ }
46
+ : null;
47
+
48
+ return (
49
+ <>
50
+ <ChatInterface
51
+ messages={messages}
52
+ promptValue=""
53
+ onPromptChange={() => {}}
54
+ onPromptSubmit={() => {}}
55
+ onCitationClick={handleCitationClick}
56
+ />
57
+ {doc && (
58
+ <DocumentOverlay
59
+ open={overlayOpen}
60
+ onClose={() => {
61
+ setOverlayOpen(false);
62
+ setActiveCitation(null);
63
+ }}
64
+ title={doc.title}
65
+ sourceLabel={doc.sourceLabel}
66
+ highlightId={doc.highlightId}
67
+ highlightIndex={activeCitation?.index}
68
+ >
69
+ {doc.content}
70
+ </DocumentOverlay>
71
+ )}
72
+ </>
73
+ );
74
+ }
75
+
76
+ describe("ChatInterface integration", () => {
77
+ it("opens document overlay with highlighted citation content", async () => {
78
+ if (!(globalThis.CSS && typeof globalThis.CSS.escape === "function")) {
79
+ const existing = globalThis.CSS ?? ({} as CSS);
80
+ globalThis.CSS = {
81
+ ...existing,
82
+ escape: (value: string) => value,
83
+ } as CSS;
84
+ }
85
+
86
+ const raf = vi
87
+ .spyOn(window, "requestAnimationFrame")
88
+ .mockImplementation((cb: FrameRequestCallback) => {
89
+ cb(0);
90
+ return 1;
91
+ });
92
+
93
+ const scrollIntoView = vi.fn();
94
+ Object.defineProperty(window.HTMLElement.prototype, "scrollIntoView", {
95
+ configurable: true,
96
+ value: scrollIntoView,
97
+ });
98
+
99
+ render(<ChatCitationHarness />);
100
+
101
+ fireEvent.click(screen.getByRole("button", { name: "Knowledge Report" }));
102
+
103
+ expect(screen.getByRole("dialog")).toBeInTheDocument();
104
+ expect(screen.getByText("Knowledge Retention Report")).toBeInTheDocument();
105
+ expect(screen.getByText("Internal Research")).toBeInTheDocument();
106
+
107
+ const highlighted = document.querySelector("#cite-1");
108
+ if (!highlighted) {
109
+ throw new Error("Expected highlighted citation element to exist");
110
+ }
111
+
112
+ await waitFor(() => {
113
+ expect(highlighted).toHaveClass("pcoi-doc-overlay__highlight");
114
+ expect(highlighted).toHaveAttribute("data-highlight-index", "[1]");
115
+ expect(scrollIntoView).toHaveBeenCalled();
116
+ });
117
+
118
+ fireEvent.click(screen.getByRole("button", { name: "Close modal" }));
119
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
120
+
121
+ raf.mockRestore();
122
+ });
123
+ });
@@ -3,15 +3,15 @@
3
3
  .pcoi-chat-message {
4
4
  display: flex;
5
5
  flex-direction: column;
6
- gap: var(--pcoi-spacing-8);
7
- padding: var(--pcoi-spacing-16);
8
- border-radius: var(--pcoi-radius-md);
6
+ gap: var(--pcoi-semantic-spacing-inline-sm);
7
+ padding: var(--pcoi-semantic-spacing-stack-md);
8
+ border-radius: var(--pcoi-semantic-radius-card);
9
9
  border: 1px solid var(--pcoi-semantic-border-card);
10
10
  }
11
11
 
12
12
  /* ── Role variants ── */
13
13
  .pcoi-chat-message--user {
14
- background: var(--pcoi-color-surface);
14
+ background: var(--pcoi-semantic-bg-surface);
15
15
  }
16
16
 
17
17
  .pcoi-chat-message--assistant {
@@ -33,8 +33,8 @@
33
33
  .pcoi-chat-message__citations {
34
34
  display: flex;
35
35
  flex-direction: column;
36
- gap: var(--pcoi-spacing-8);
37
- margin-top: var(--pcoi-spacing-4);
36
+ gap: var(--pcoi-semantic-spacing-inline-sm);
37
+ margin-top: var(--pcoi-semantic-spacing-inline-2xs);
38
38
  }
39
39
 
40
40
  .pcoi-chat-message__timestamp {
@@ -46,10 +46,10 @@
46
46
  /* ── Responsive: offset margins on desktop ── */
47
47
  @media (min-width: 1025px) {
48
48
  .pcoi-chat-message--user {
49
- margin-left: var(--pcoi-spacing-48);
49
+ margin-left: var(--pcoi-semantic-spacing-inline-xl);
50
50
  }
51
51
 
52
52
  .pcoi-chat-message--assistant {
53
- margin-right: var(--pcoi-spacing-48);
53
+ margin-right: var(--pcoi-semantic-spacing-inline-xl);
54
54
  }
55
55
  }
@@ -11,14 +11,14 @@
11
11
  overflow-y: auto;
12
12
  display: flex;
13
13
  flex-direction: column;
14
- gap: var(--pcoi-spacing-16);
15
- padding: var(--pcoi-spacing-16);
14
+ gap: var(--pcoi-semantic-spacing-stack-md);
15
+ padding: var(--pcoi-semantic-spacing-panel-padding-sm);
16
16
  }
17
17
 
18
18
  /* ── Responsive: wider spacing on tablet+ ── */
19
19
  @media (min-width: 768px) {
20
20
  .pcoi-chat-message-list__inner {
21
- gap: var(--pcoi-spacing-20);
22
- padding: var(--pcoi-spacing-24);
21
+ gap: var(--pcoi-semantic-spacing-card-gap);
22
+ padding: var(--pcoi-semantic-spacing-panel-padding);
23
23
  }
24
24
  }
@@ -0,0 +1,74 @@
1
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { ChatMessage } from "../ChatMessage";
4
+ import type { Citation } from "../types";
5
+ import { ChatMessageList } from "./ChatMessageList";
6
+
7
+ describe("ChatMessageList", () => {
8
+ it("renders children and relays citation clicks", () => {
9
+ const citation: Citation = {
10
+ index: 1,
11
+ excerpt: "Referenced excerpt.",
12
+ sourceTitle: "Knowledge Report",
13
+ sourceId: "doc-1",
14
+ };
15
+
16
+ const onCitationClick = vi.fn();
17
+
18
+ render(
19
+ <ChatMessageList>
20
+ <ChatMessage
21
+ role="assistant"
22
+ timestamp="2:35 PM"
23
+ citations={[citation]}
24
+ onCitationClick={onCitationClick}
25
+ >
26
+ Assistant response
27
+ </ChatMessage>
28
+ </ChatMessageList>
29
+ );
30
+
31
+ expect(screen.getByText("Assistant response")).toBeInTheDocument();
32
+
33
+ fireEvent.click(screen.getByRole("button", { name: "Knowledge Report" }));
34
+ fireEvent.click(screen.getByRole("button", { name: "[1]" }));
35
+
36
+ expect(onCitationClick).toHaveBeenCalledTimes(2);
37
+ expect(onCitationClick).toHaveBeenCalledWith(citation);
38
+ });
39
+
40
+ it("auto-scrolls to bottom when new messages are added", async () => {
41
+ vi.spyOn(window, "requestAnimationFrame").mockImplementation(
42
+ (cb: FrameRequestCallback) => {
43
+ cb(0);
44
+ return 1;
45
+ }
46
+ );
47
+
48
+ const { container, rerender } = render(
49
+ <ChatMessageList>
50
+ <div>Message 1</div>
51
+ </ChatMessageList>
52
+ );
53
+
54
+ const inner = container.querySelector(
55
+ ".pcoi-chat-message-list__inner"
56
+ ) as HTMLDivElement;
57
+
58
+ Object.defineProperty(inner, "scrollHeight", {
59
+ configurable: true,
60
+ get: () => 480,
61
+ });
62
+
63
+ rerender(
64
+ <ChatMessageList>
65
+ <div>Message 1</div>
66
+ <div>Message 2</div>
67
+ </ChatMessageList>
68
+ );
69
+
70
+ await waitFor(() => {
71
+ expect(inner.scrollTop).toBe(480);
72
+ });
73
+ });
74
+ });
@@ -3,13 +3,13 @@
3
3
  .pcoi-checkbox {
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-checkbox__control {
10
10
  display: inline-flex;
11
11
  align-items: center;
12
- gap: var(--pcoi-spacing-8);
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-layout-component-control-box);
30
- height: var(--pcoi-layout-component-control-box);
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-sm);
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-layout-component-control-box) + var(--pcoi-spacing-8));
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-4) var(--pcoi-spacing-10);
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-full);
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-color-accent-dim);
29
+ background: var(--pcoi-semantic-surface-accent-dim);
30
30
  border-color: var(--pcoi-semantic-border-accent-subtle);
31
31
  }
32
32