@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
@@ -23,7 +23,12 @@ export const ChatMessageList = React.forwardRef<
23
23
 
24
24
  const scrollToEnd = () => {
25
25
  requestAnimationFrame(() => {
26
- el.scrollTop = el.scrollHeight;
26
+ const lastChild = el.lastElementChild;
27
+ if (lastChild) {
28
+ lastChild.scrollIntoView({ block: "nearest" });
29
+ } else {
30
+ el.scrollTop = el.scrollHeight;
31
+ }
27
32
  });
28
33
  };
29
34
 
@@ -39,7 +44,7 @@ export const ChatMessageList = React.forwardRef<
39
44
  .join(" ");
40
45
 
41
46
  return (
42
- <div ref={ref} className={classes} {...rest}>
47
+ <div ref={ref} className={classes} role="log" aria-label="Conversation" {...rest}>
43
48
  <div ref={innerRef} className="pcoi-chat-message-list__inner">
44
49
  {children}
45
50
  </div>
@@ -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
 
@@ -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 {
@@ -29,6 +29,7 @@ export const CitedExcerpt = React.forwardRef<HTMLDivElement, CitedExcerptProps>(
29
29
  type="button"
30
30
  className="pcoi-cited-excerpt__source"
31
31
  onClick={onSourceClick}
32
+ aria-label={`View source: ${sourceTitle}`}
32
33
  >
33
34
  {sourceTitle}
34
35
  </button>
@@ -37,6 +38,7 @@ export const CitedExcerpt = React.forwardRef<HTMLDivElement, CitedExcerptProps>(
37
38
  type="button"
38
39
  className="pcoi-cited-excerpt__index"
39
40
  onClick={onSourceClick}
41
+ aria-label={`Citation ${index}, view source: ${sourceTitle}`}
40
42
  >
41
43
  [{index}]
42
44
  </button>
@@ -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
  }
@@ -1,14 +1,20 @@
1
1
  import React from "react";
2
2
 
3
3
  export interface ComparisonRow {
4
+ /** Feature label shown in the first column */
4
5
  label: string;
6
+ /** Competitor's value for this feature */
5
7
  competitor: string;
8
+ /** PCOI's value for this feature */
6
9
  pcoi: string;
7
10
  }
8
11
 
9
12
  export interface ComparisonTableProps extends React.HTMLAttributes<HTMLDivElement> {
13
+ /** Column header for the competitor (default: "Typical SaaS AI") */
10
14
  competitorName?: string;
15
+ /** Column header for PCOI (default: "PCOI") */
11
16
  pcoiName?: string;
17
+ /** Feature comparison rows */
12
18
  rows: ComparisonRow[];
13
19
  }
14
20
 
@@ -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,8 @@ 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
+ /** Called with form field values when the form is submitted */
6
7
  onSubmit?: (data: Record<string, string>) => void;
7
8
  }
8
9
 
@@ -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
+ });
@@ -65,6 +65,7 @@ export const DocumentOverlay = React.forwardRef<
65
65
  return React.createElement(
66
66
  Modal,
67
67
  {
68
+ ref,
68
69
  open,
69
70
  onClose,
70
71
  title,
@@ -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
  }
@@ -6,8 +6,11 @@ import type { LinkItem } from "../types";
6
6
  export type FooterLink = LinkItem;
7
7
 
8
8
  export interface FooterProps extends React.HTMLAttributes<HTMLElement> {
9
+ /** Navigation links rendered in the footer */
9
10
  links?: LinkItem[];
11
+ /** Brand tagline displayed alongside the logo */
10
12
  tagline?: string;
13
+ /** Copyright notice shown at the bottom */
11
14
  copyright?: string;
12
15
  }
13
16
 
@@ -39,11 +42,11 @@ export const Footer = React.forwardRef<HTMLElement, FooterProps>(
39
42
  <LogoMark className="pcoi-footer__logo" />
40
43
  <p className="pcoi-footer__tagline">{tagline}</p>
41
44
  </div>
42
- <div className="pcoi-footer__links">
45
+ <nav className="pcoi-footer__links" aria-label="Footer">
43
46
  {links.map((link) => (
44
47
  <a key={link.href} href={link.href} data-track-id={link.trackingId}>{link.label}</a>
45
48
  ))}
46
- </div>
49
+ </nav>
47
50
  </div>
48
51
  <div className="pcoi-footer__bottom">
49
52
  <p>{copyright}</p>
@@ -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
@@ -15,8 +14,8 @@ export interface LogoMarkProps extends React.AnchorHTMLAttributes<HTMLAnchorElem
15
14
  */
16
15
  export const LogoMark = React.forwardRef<HTMLAnchorElement, LogoMarkProps>(
17
16
  ({ href = "#", className = "", ...rest }, ref) => (
18
- <a ref={ref} href={href} className={`pcoi-logo ${className}`} {...rest}>
19
- <span className="pcoi-logo__mark">P</span>COI
17
+ <a ref={ref} href={href} className={`pcoi-logo ${className}`} aria-label="PCOI home" {...rest}>
18
+ <span className="pcoi-logo__mark" aria-hidden="true">P</span>COI
20
19
  </a>
21
20
  )
22
21
  );