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