@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pcoi/components",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "PCOI Design System — React UI components",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -11,9 +11,9 @@
11
11
  ],
12
12
  "exports": {
13
13
  ".": {
14
+ "types": "./dist/index.d.ts",
14
15
  "import": "./dist/index.mjs",
15
- "require": "./dist/index.js",
16
- "types": "./dist/index.d.ts"
16
+ "require": "./dist/index.js"
17
17
  },
18
18
  "./css": "./dist/components.css"
19
19
  },
@@ -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,11 +20,16 @@
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
+ padding: 0;
25
26
  font-style: italic;
26
27
  }
27
28
 
29
+ .pcoi-callout__quote p {
30
+ margin: 0;
31
+ }
32
+
28
33
  .pcoi-callout__attribution {
29
34
  font-size: var(--pcoi-semantic-type-body-sm-size);
30
35
  color: var(--pcoi-semantic-text-muted);
@@ -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
+ });
@@ -1,37 +1,41 @@
1
1
  import React from "react";
2
2
 
3
- export interface CalloutProps extends React.HTMLAttributes<HTMLDivElement> {
3
+ export interface CalloutProps extends React.HTMLAttributes<HTMLElement> {
4
4
  /** Quote text */
5
5
  quote: string;
6
6
  /** Attribution name */
7
7
  attribution?: string;
8
8
  /** Attribution title/context */
9
9
  attributionTitle?: string;
10
- /** Link URL */
10
+ /** Link URL for the source reference */
11
11
  sourceUrl?: string;
12
- /** Link label */
12
+ /** Display label for the source link (default: "Source") */
13
13
  sourceLabel?: string;
14
14
  }
15
15
 
16
16
  /**
17
17
  * PCOI Callout / Blockquote
18
+ *
19
+ * Renders a semantic `<figure>` with `<blockquote>` and optional `<figcaption>`.
18
20
  * Tokens: spacing-40 (padding), text/secondary, text/accent (link)
19
21
  */
20
- export const Callout = React.forwardRef<HTMLDivElement, CalloutProps>(
22
+ export const Callout = React.forwardRef<HTMLElement, CalloutProps>(
21
23
  ({ quote, attribution, attributionTitle, sourceUrl, sourceLabel, className = "", ...rest }, ref) => (
22
- <div ref={ref} className={`pcoi-callout ${className}`} {...rest}>
23
- <div className="pcoi-callout__line" />
24
- <p className="pcoi-callout__quote">{quote}</p>
24
+ <figure ref={ref} className={`pcoi-callout ${className}`} {...rest}>
25
+ <div className="pcoi-callout__line" aria-hidden="true" />
26
+ <blockquote className="pcoi-callout__quote">
27
+ <p>{quote}</p>
28
+ </blockquote>
25
29
  {(attribution || sourceUrl) && (
26
- <p className="pcoi-callout__attribution">
30
+ <figcaption className="pcoi-callout__attribution">
27
31
  {attributionTitle && <span>{attributionTitle} &nbsp;~&nbsp; </span>}
28
32
  {attribution}
29
33
  {sourceUrl && (
30
34
  <> &nbsp;|&nbsp; <a href={sourceUrl} target="_blank" rel="noopener noreferrer">{sourceLabel || "Source"}</a></>
31
35
  )}
32
- </p>
36
+ </figcaption>
33
37
  )}
34
- </div>
38
+ </figure>
35
39
  )
36
40
  );
37
41
 
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
+ });
@@ -3,8 +3,9 @@
3
3
  .pcoi-chat {
4
4
  display: flex;
5
5
  flex-direction: column;
6
+ width: 100%;
6
7
  height: 100%;
7
- max-width: var(--pcoi-layout-container-narrow, 800px);
8
+ max-width: var(--pcoi-semantic-sizing-container-narrow, 800px);
8
9
  margin: 0 auto;
9
10
  }
10
11
 
@@ -18,7 +19,7 @@
18
19
  display: flex;
19
20
  flex-direction: column;
20
21
  justify-content: center;
21
- padding: var(--pcoi-spacing-16);
22
+ padding: var(--pcoi-semantic-spacing-panel-padding-sm);
22
23
  }
23
24
 
24
25
  .pcoi-chat__suggestions {
@@ -34,16 +35,16 @@
34
35
  /* ── Prompt ── */
35
36
  .pcoi-chat__prompt {
36
37
  flex-shrink: 0;
37
- padding: var(--pcoi-spacing-16);
38
+ padding: var(--pcoi-semantic-spacing-panel-padding-sm);
38
39
  }
39
40
 
40
41
  /* ── Responsive ── */
41
42
  @media (min-width: 768px) {
42
43
  .pcoi-chat__empty-state {
43
- padding: var(--pcoi-spacing-24);
44
+ padding: var(--pcoi-semantic-spacing-panel-padding);
44
45
  }
45
46
 
46
47
  .pcoi-chat__prompt {
47
- padding: var(--pcoi-spacing-24);
48
+ padding: var(--pcoi-semantic-spacing-panel-padding);
48
49
  }
49
50
  }
@@ -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: "View source: 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
+ });
@@ -7,10 +7,15 @@ import { TypingIndicator } from "../TypingIndicator/TypingIndicator";
7
7
  import type { ChatMessageRole, Citation, Suggestion } from "../types";
8
8
 
9
9
  export interface ChatInterfaceMessage {
10
+ /** Unique message identifier */
10
11
  id: string;
12
+ /** Sender role: "user" or "assistant" */
11
13
  role: ChatMessageRole;
14
+ /** Message body content */
12
15
  content: React.ReactNode;
16
+ /** Citations referenced in the message */
13
17
  citations?: Citation[];
18
+ /** ISO timestamp displayed below the message */
14
19
  timestamp?: string;
15
20
  }
16
21
 
@@ -85,7 +90,7 @@ export const ChatInterface = React.forwardRef<
85
90
  )}
86
91
  </div>
87
92
  ) : (
88
- <ChatMessageList className="pcoi-chat__messages">
93
+ <ChatMessageList className="pcoi-chat__messages" aria-live="polite" aria-relevant="additions">
89
94
  {messages.map((msg) => (
90
95
  <ChatMessage
91
96
  key={msg.id}
@@ -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,70 @@
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: "View source: Knowledge Report" }));
34
+ fireEvent.click(screen.getByRole("button", { name: "Citation 1, view source: Knowledge Report" }));
35
+
36
+ expect(onCitationClick).toHaveBeenCalledTimes(2);
37
+ expect(onCitationClick).toHaveBeenCalledWith(citation);
38
+ });
39
+
40
+ it("auto-scrolls last message into view when new messages are added", async () => {
41
+ const scrollIntoViewMock = vi.fn();
42
+ Element.prototype.scrollIntoView = scrollIntoViewMock;
43
+
44
+ vi.spyOn(window, "requestAnimationFrame").mockImplementation(
45
+ (cb: FrameRequestCallback) => {
46
+ cb(0);
47
+ return 1;
48
+ }
49
+ );
50
+
51
+ const { rerender } = render(
52
+ <ChatMessageList>
53
+ <div>Message 1</div>
54
+ </ChatMessageList>
55
+ );
56
+
57
+ scrollIntoViewMock.mockClear();
58
+
59
+ rerender(
60
+ <ChatMessageList>
61
+ <div>Message 1</div>
62
+ <div>Message 2</div>
63
+ </ChatMessageList>
64
+ );
65
+
66
+ await waitFor(() => {
67
+ expect(scrollIntoViewMock).toHaveBeenCalledWith({ block: "nearest" });
68
+ });
69
+ });
70
+ });