@pcoi/components 0.1.0

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 (103) hide show
  1. package/dist/components.css +1 -0
  2. package/dist/index.d.ts +667 -0
  3. package/dist/index.js +2 -0
  4. package/dist/index.mjs +1048 -0
  5. package/package.json +36 -0
  6. package/src/Badge/Badge.css +40 -0
  7. package/src/Badge/Badge.tsx +36 -0
  8. package/src/Badge/index.ts +2 -0
  9. package/src/Button/Button.css +93 -0
  10. package/src/Button/Button.figma.tsx +29 -0
  11. package/src/Button/Button.tsx +47 -0
  12. package/src/Button/index.ts +1 -0
  13. package/src/Callout/Callout.css +43 -0
  14. package/src/Callout/Callout.tsx +39 -0
  15. package/src/Callout/index.ts +1 -0
  16. package/src/Card/Card.css +88 -0
  17. package/src/Card/Card.tsx +60 -0
  18. package/src/Card/index.ts +1 -0
  19. package/src/ChatInterface/ChatInterface.css +49 -0
  20. package/src/ChatInterface/ChatInterface.tsx +120 -0
  21. package/src/ChatInterface/index.ts +6 -0
  22. package/src/ChatMessage/ChatMessage.css +55 -0
  23. package/src/ChatMessage/ChatMessage.tsx +71 -0
  24. package/src/ChatMessage/index.ts +2 -0
  25. package/src/ChatMessageList/ChatMessageList.css +24 -0
  26. package/src/ChatMessageList/ChatMessageList.tsx +51 -0
  27. package/src/ChatMessageList/index.ts +2 -0
  28. package/src/Checkbox/Checkbox.css +97 -0
  29. package/src/Checkbox/Checkbox.tsx +70 -0
  30. package/src/Checkbox/index.ts +2 -0
  31. package/src/CitationMark/CitationMark.css +40 -0
  32. package/src/CitationMark/CitationMark.tsx +38 -0
  33. package/src/CitationMark/index.ts +2 -0
  34. package/src/CitedExcerpt/CitedExcerpt.css +75 -0
  35. package/src/CitedExcerpt/CitedExcerpt.tsx +51 -0
  36. package/src/CitedExcerpt/index.ts +2 -0
  37. package/src/ComparisonTable/ComparisonTable.css +66 -0
  38. package/src/ComparisonTable/ComparisonTable.tsx +48 -0
  39. package/src/ComparisonTable/index.ts +1 -0
  40. package/src/ContactForm/ContactForm.css +38 -0
  41. package/src/ContactForm/ContactForm.tsx +57 -0
  42. package/src/ContactForm/index.ts +1 -0
  43. package/src/DataTable/DataTable.css +56 -0
  44. package/src/DataTable/DataTable.tsx +104 -0
  45. package/src/DataTable/index.ts +2 -0
  46. package/src/DocumentOverlay/DocumentOverlay.css +57 -0
  47. package/src/DocumentOverlay/DocumentOverlay.tsx +86 -0
  48. package/src/DocumentOverlay/index.ts +2 -0
  49. package/src/Footer/Footer.css +72 -0
  50. package/src/Footer/Footer.tsx +56 -0
  51. package/src/Footer/index.ts +1 -0
  52. package/src/FormField/FormField.css +78 -0
  53. package/src/FormField/FormField.tsx +103 -0
  54. package/src/FormField/index.ts +2 -0
  55. package/src/HowStep/HowStep.css +48 -0
  56. package/src/HowStep/HowStep.tsx +38 -0
  57. package/src/HowStep/index.ts +1 -0
  58. package/src/LogoMark/LogoMark.css +16 -0
  59. package/src/LogoMark/LogoMark.tsx +25 -0
  60. package/src/LogoMark/index.ts +2 -0
  61. package/src/Modal/Modal.css +101 -0
  62. package/src/Modal/Modal.tsx +141 -0
  63. package/src/Modal/index.ts +2 -0
  64. package/src/Nav/Nav.css +161 -0
  65. package/src/Nav/Nav.tsx +101 -0
  66. package/src/Nav/index.ts +1 -0
  67. package/src/Panel/Panel.css +35 -0
  68. package/src/Panel/Panel.tsx +61 -0
  69. package/src/Panel/index.ts +2 -0
  70. package/src/PromptBar/PromptBar.css +68 -0
  71. package/src/PromptBar/PromptBar.tsx +93 -0
  72. package/src/PromptBar/index.ts +2 -0
  73. package/src/RadioGroup/RadioGroup.css +117 -0
  74. package/src/RadioGroup/RadioGroup.tsx +112 -0
  75. package/src/RadioGroup/index.ts +2 -0
  76. package/src/SectionHeader/SectionHeader.css +38 -0
  77. package/src/SectionHeader/SectionHeader.tsx +55 -0
  78. package/src/SectionHeader/index.ts +1 -0
  79. package/src/Select/Select.css +90 -0
  80. package/src/Select/Select.tsx +100 -0
  81. package/src/Select/index.ts +2 -0
  82. package/src/SignalsPanel/SignalsPanel.css +51 -0
  83. package/src/SignalsPanel/SignalsPanel.tsx +33 -0
  84. package/src/SignalsPanel/index.ts +1 -0
  85. package/src/SuggestionCard/SuggestionCard.css +51 -0
  86. package/src/SuggestionCard/SuggestionCard.tsx +34 -0
  87. package/src/SuggestionCard/index.ts +2 -0
  88. package/src/SuggestionCards/SuggestionCards.css +15 -0
  89. package/src/SuggestionCards/SuggestionCards.tsx +40 -0
  90. package/src/SuggestionCards/index.ts +2 -0
  91. package/src/Toast/Toast.css +85 -0
  92. package/src/Toast/Toast.tsx +77 -0
  93. package/src/Toast/index.ts +2 -0
  94. package/src/Toggle/Toggle.css +110 -0
  95. package/src/Toggle/Toggle.tsx +73 -0
  96. package/src/Toggle/index.ts +2 -0
  97. package/src/TypingIndicator/TypingIndicator.css +70 -0
  98. package/src/TypingIndicator/TypingIndicator.tsx +37 -0
  99. package/src/TypingIndicator/index.ts +2 -0
  100. package/src/index.ts +37 -0
  101. package/src/styles/utilities.css +14 -0
  102. package/src/styles.css +32 -0
  103. package/src/types.ts +65 -0
@@ -0,0 +1,51 @@
1
+ import React from "react";
2
+
3
+ export interface CitedExcerptProps
4
+ extends React.HTMLAttributes<HTMLDivElement> {
5
+ /** 1-based citation index */
6
+ index: number;
7
+ /** Short excerpt from the source document */
8
+ excerpt: string;
9
+ /** Source document title */
10
+ sourceTitle: string;
11
+ /** Called when the source link is clicked */
12
+ onSourceClick?: () => void;
13
+ }
14
+
15
+ /**
16
+ * PCOI CitedExcerpt — Blockquote with excerpt text above, clickable index + source below
17
+ * Tokens: surface/accent-dim, border/accent-dim, accent-border-w,
18
+ * radius-sm, text/secondary, text/accent, type/body-compact-size
19
+ */
20
+ export const CitedExcerpt = React.forwardRef<HTMLDivElement, CitedExcerptProps>(
21
+ ({ index, excerpt, sourceTitle, onSourceClick, className = "", ...rest }, ref) => {
22
+ const classes = ["pcoi-cited-excerpt", className]
23
+ .filter(Boolean)
24
+ .join(" ");
25
+
26
+ return (
27
+ <div ref={ref} className={classes} {...rest}>
28
+ <button
29
+ type="button"
30
+ className="pcoi-cited-excerpt__source"
31
+ onClick={onSourceClick}
32
+ >
33
+ {sourceTitle}
34
+ </button>
35
+ <div className="pcoi-cited-excerpt__body">
36
+ <button
37
+ type="button"
38
+ className="pcoi-cited-excerpt__index"
39
+ onClick={onSourceClick}
40
+ >
41
+ [{index}]
42
+ </button>
43
+ <p className="pcoi-cited-excerpt__text">{excerpt}</p>
44
+ </div>
45
+ </div>
46
+ );
47
+ }
48
+ );
49
+
50
+ CitedExcerpt.displayName = "CitedExcerpt";
51
+ export default CitedExcerpt;
@@ -0,0 +1,2 @@
1
+ export { CitedExcerpt, type CitedExcerptProps } from "./CitedExcerpt";
2
+ export { default } from "./CitedExcerpt";
@@ -0,0 +1,66 @@
1
+ /* ComparisonTable — @pcoi/components */
2
+
3
+ .pcoi-comparison {
4
+ max-width: var(--pcoi-layout-container-compare, 900px);
5
+ border: 1px solid var(--pcoi-semantic-border-card);
6
+ border-radius: var(--pcoi-radius-md);
7
+ overflow: hidden;
8
+ }
9
+
10
+ .pcoi-comparison__table {
11
+ width: 100%;
12
+ border-collapse: collapse;
13
+ }
14
+
15
+ .pcoi-comparison__header .pcoi-comparison__col {
16
+ font-family: var(--pcoi-semantic-type-mono-font);
17
+ font-size: var(--pcoi-semantic-type-body-sm-size);
18
+ font-weight: var(--pcoi-semantic-type-emphasis-weight);
19
+ letter-spacing: var(--pcoi-semantic-type-label-letter-spacing);
20
+ text-transform: uppercase;
21
+ color: var(--pcoi-semantic-text-secondary);
22
+ padding: var(--pcoi-spacing-16) var(--pcoi-spacing-20);
23
+ background: var(--pcoi-semantic-bg-alt);
24
+ text-align: left;
25
+ }
26
+
27
+ .pcoi-comparison__row {
28
+ border-top: 1px solid var(--pcoi-semantic-border-card);
29
+ }
30
+
31
+ .pcoi-comparison__col {
32
+ padding: var(--pcoi-spacing-16) var(--pcoi-spacing-20);
33
+ font-size: var(--pcoi-semantic-type-body-compact-size);
34
+ color: var(--pcoi-semantic-text-secondary);
35
+ line-height: var(--pcoi-semantic-type-body-compact-line-height);
36
+ }
37
+
38
+ .pcoi-comparison__col--label {
39
+ font-weight: var(--pcoi-semantic-type-label-weight);
40
+ color: var(--pcoi-semantic-text-primary);
41
+ text-align: left;
42
+ }
43
+
44
+ .pcoi-comparison__col--highlight {
45
+ background: var(--pcoi-semantic-surface-highlight);
46
+ color: var(--pcoi-semantic-text-accent);
47
+ font-weight: var(--pcoi-semantic-type-label-weight);
48
+ }
49
+
50
+ .pcoi-comparison__header .pcoi-comparison__col--highlight {
51
+ color: var(--pcoi-semantic-text-accent);
52
+ font-weight: var(--pcoi-semantic-type-emphasis-weight);
53
+ }
54
+
55
+ /* ── Responsive ── */
56
+ /* 768px = --pcoi-layout-breakpoint-mobile (CSS vars not supported in @media) */
57
+ @media (max-width: 768px) {
58
+ .pcoi-comparison {
59
+ overflow-x: auto;
60
+ -webkit-overflow-scrolling: touch;
61
+ }
62
+
63
+ .pcoi-comparison__table {
64
+ min-width: var(--pcoi-layout-container-compare-scroll);
65
+ }
66
+ }
@@ -0,0 +1,48 @@
1
+ import React from "react";
2
+
3
+ export interface ComparisonRow {
4
+ label: string;
5
+ competitor: string;
6
+ pcoi: string;
7
+ }
8
+
9
+ export interface ComparisonTableProps extends React.HTMLAttributes<HTMLDivElement> {
10
+ competitorName?: string;
11
+ pcoiName?: string;
12
+ rows: ComparisonRow[];
13
+ }
14
+
15
+ /**
16
+ * PCOI Comparison Table
17
+ * Semantic <table> with 3 columns: label | competitor | PCOI (highlighted)
18
+ * Tokens: radius-md, border/default, bg/alt, surface/accent-dim, text/accent
19
+ */
20
+ export const ComparisonTable = React.forwardRef<HTMLDivElement, ComparisonTableProps>(
21
+ ({ competitorName = "Typical SaaS AI", pcoiName = "PCOI", rows, className = "", ...rest }, ref) => (
22
+ <div ref={ref} className={`pcoi-comparison ${className}`} {...rest}>
23
+ <table className="pcoi-comparison__table">
24
+ <thead>
25
+ <tr className="pcoi-comparison__header">
26
+ <th className="pcoi-comparison__col" scope="col">
27
+ <span className="pcoi-sr-only">Feature</span>
28
+ </th>
29
+ <th className="pcoi-comparison__col" scope="col">{competitorName}</th>
30
+ <th className="pcoi-comparison__col pcoi-comparison__col--highlight" scope="col">{pcoiName}</th>
31
+ </tr>
32
+ </thead>
33
+ <tbody>
34
+ {rows.map((row, i) => (
35
+ <tr key={i} className="pcoi-comparison__row">
36
+ <th className="pcoi-comparison__col pcoi-comparison__col--label" scope="row">{row.label}</th>
37
+ <td className="pcoi-comparison__col">{row.competitor}</td>
38
+ <td className="pcoi-comparison__col pcoi-comparison__col--highlight">{row.pcoi}</td>
39
+ </tr>
40
+ ))}
41
+ </tbody>
42
+ </table>
43
+ </div>
44
+ )
45
+ );
46
+
47
+ ComparisonTable.displayName = "ComparisonTable";
48
+ export default ComparisonTable;
@@ -0,0 +1 @@
1
+ export { ComparisonTable, type ComparisonTableProps, type ComparisonRow } from "./ComparisonTable";
@@ -0,0 +1,38 @@
1
+ /* ContactForm — @pcoi/components */
2
+
3
+ .pcoi-form {
4
+ background: var(--pcoi-semantic-bg-surface);
5
+ border: 1px solid var(--pcoi-semantic-border-default);
6
+ border-radius: var(--pcoi-radius-lg);
7
+ padding: var(--pcoi-spacing-40);
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: var(--pcoi-spacing-20);
11
+ }
12
+
13
+ .pcoi-form__row {
14
+ display: grid;
15
+ grid-template-columns: 1fr 1fr;
16
+ gap: var(--pcoi-spacing-20);
17
+ }
18
+
19
+ .pcoi-form__full {
20
+ grid-column: 1 / -1;
21
+ }
22
+
23
+ .pcoi-form__note {
24
+ font-size: var(--pcoi-semantic-type-label-size);
25
+ color: var(--pcoi-semantic-text-muted);
26
+ margin: 0;
27
+ }
28
+
29
+ /* ── Responsive ── */
30
+ @media (max-width: 768px) {
31
+ .pcoi-form {
32
+ padding: var(--pcoi-spacing-24);
33
+ }
34
+
35
+ .pcoi-form__row {
36
+ grid-template-columns: 1fr;
37
+ }
38
+ }
@@ -0,0 +1,57 @@
1
+ import React, { useState } from "react";
2
+ import { Button } from "../Button";
3
+ import { FormField } from "../FormField";
4
+
5
+ export interface ContactFormProps extends React.FormHTMLAttributes<HTMLFormElement> {
6
+ onSubmit?: (data: Record<string, string>) => void;
7
+ }
8
+
9
+ /**
10
+ * PCOI Contact Form
11
+ * Composes: FormField (atom), Button (atom)
12
+ * Tokens: bg/surface, border/default, radius-lg, spacing-40 (padding), spacing-20 (gap)
13
+ */
14
+ export const ContactForm = React.forwardRef<HTMLFormElement, ContactFormProps>(
15
+ ({ onSubmit, className = "", ...rest }, ref) => {
16
+ const [submitted, setSubmitted] = useState(false);
17
+
18
+ const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
19
+ e.preventDefault();
20
+ const form = e.currentTarget;
21
+ const formData = new FormData(form);
22
+ const data = Object.fromEntries(formData.entries()) as Record<string, string>;
23
+ onSubmit?.(data);
24
+ setSubmitted(true);
25
+ form.reset();
26
+ setTimeout(() => setSubmitted(false), 3000);
27
+ };
28
+
29
+ return (
30
+ <form ref={ref} className={`pcoi-form ${className}`} onSubmit={handleSubmit} aria-label="Contact form" {...rest}>
31
+ <div className="pcoi-form__row">
32
+ <FormField label="Name" name="name" placeholder="Your name" required />
33
+ <FormField label="Company" name="company" placeholder="Company name" required />
34
+ </div>
35
+ <div className="pcoi-form__row">
36
+ <FormField label="Email" name="email" type="email" placeholder="you@company.com" required />
37
+ <FormField label="Your Role" name="role" placeholder="CEO, COO, VP Ops, etc." />
38
+ </div>
39
+ <FormField
40
+ label="What problem are you trying to solve?"
41
+ name="context"
42
+ multiline
43
+ rows={3}
44
+ placeholder="Tell us about the knowledge challenges in your organization..."
45
+ className="pcoi-form__full"
46
+ />
47
+ <Button type="submit" variant="primary" size="large" disabled={submitted}>
48
+ {submitted ? "Message Received" : "Start the Conversation"}
49
+ </Button>
50
+ <p className="pcoi-form__note">No sales automation. A real person reads this and responds personally.</p>
51
+ </form>
52
+ );
53
+ }
54
+ );
55
+
56
+ ContactForm.displayName = "ContactForm";
57
+ export default ContactForm;
@@ -0,0 +1 @@
1
+ export { ContactForm, type ContactFormProps } from "./ContactForm";
@@ -0,0 +1,56 @@
1
+ /* DataTable — @pcoi/components */
2
+
3
+ .pcoi-data-table {
4
+ width: 100%;
5
+ overflow: hidden;
6
+ overflow-x: auto;
7
+ border: 1px solid var(--pcoi-semantic-border-card);
8
+ border-radius: var(--pcoi-radius-md);
9
+ }
10
+
11
+ .pcoi-data-table__table {
12
+ width: 100%;
13
+ border-collapse: collapse;
14
+ }
15
+
16
+ .pcoi-data-table__th {
17
+ font-family: var(--pcoi-semantic-type-mono-font);
18
+ font-size: var(--pcoi-semantic-type-body-sm-size);
19
+ font-weight: var(--pcoi-semantic-type-emphasis-weight);
20
+ letter-spacing: var(--pcoi-semantic-type-label-letter-spacing);
21
+ text-transform: uppercase;
22
+ color: var(--pcoi-semantic-text-secondary);
23
+ text-align: left;
24
+ padding: var(--pcoi-spacing-16) var(--pcoi-spacing-20);
25
+ background: var(--pcoi-semantic-bg-alt);
26
+ }
27
+
28
+ .pcoi-data-table__th--center { text-align: center; }
29
+ .pcoi-data-table__th--right { text-align: right; }
30
+
31
+ .pcoi-data-table__td {
32
+ font-size: var(--pcoi-semantic-type-body-compact-size);
33
+ color: var(--pcoi-semantic-text-secondary);
34
+ line-height: var(--pcoi-semantic-type-body-compact-line-height);
35
+ text-align: left;
36
+ padding: var(--pcoi-spacing-16) var(--pcoi-spacing-20);
37
+ }
38
+
39
+ .pcoi-data-table__td--center { text-align: center; }
40
+ .pcoi-data-table__td--right { text-align: right; }
41
+
42
+ .pcoi-data-table__row {
43
+ border-top: 1px solid var(--pcoi-semantic-border-card);
44
+ transition: background var(--pcoi-effect-transition-fast, 0.2s ease);
45
+ }
46
+
47
+ .pcoi-data-table__row:hover {
48
+ background: var(--pcoi-semantic-bg-card-hover);
49
+ }
50
+
51
+ .pcoi-data-table__empty {
52
+ font-size: var(--pcoi-semantic-type-body-size);
53
+ color: var(--pcoi-semantic-text-muted);
54
+ text-align: center;
55
+ padding: var(--pcoi-spacing-40) var(--pcoi-spacing-16);
56
+ }
@@ -0,0 +1,104 @@
1
+ import React from "react";
2
+
3
+ export interface DataTableColumn<T = Record<string, unknown>> {
4
+ /** Unique key matching a property on the row data */
5
+ key: string;
6
+ /** Column header text */
7
+ header: string;
8
+ /** Text alignment (default: "left") */
9
+ align?: "left" | "center" | "right";
10
+ /** Custom cell renderer */
11
+ render?: (value: unknown, row: T) => React.ReactNode;
12
+ }
13
+
14
+ export interface DataTableProps<T = Record<string, unknown>> extends React.HTMLAttributes<HTMLDivElement> {
15
+ /** Column definitions */
16
+ columns: DataTableColumn<T>[];
17
+ /** Row data */
18
+ rows: T[];
19
+ /** Key extractor for row identity */
20
+ rowKey?: keyof T | ((row: T, index: number) => string);
21
+ /** Empty state message */
22
+ emptyText?: string;
23
+ /** Accessible label for the table (describes its content for screen readers) */
24
+ ariaLabel?: string;
25
+ }
26
+
27
+ /**
28
+ * PCOI DataTable — Presentational data table for dashboards
29
+ * Tokens: bg/card-hover, border/default, text/primary, text/secondary,
30
+ * font-size/caption/body-default, spacing-12/16
31
+ */
32
+ export const DataTable = React.forwardRef<HTMLDivElement, DataTableProps>(
33
+ (
34
+ {
35
+ columns,
36
+ rows,
37
+ rowKey,
38
+ emptyText = "No data available",
39
+ ariaLabel,
40
+ className = "",
41
+ ...rest
42
+ },
43
+ ref
44
+ ) => {
45
+ const getRowKey = (row: Record<string, unknown>, index: number): string => {
46
+ if (!rowKey) return String(index);
47
+ if (typeof rowKey === "function") return rowKey(row, index);
48
+ return String(row[rowKey as string]);
49
+ };
50
+
51
+ return (
52
+ <div ref={ref} className={`pcoi-data-table ${className}`} {...rest}>
53
+ <table className="pcoi-data-table__table" aria-label={ariaLabel}>
54
+ <thead className="pcoi-data-table__head">
55
+ <tr className="pcoi-data-table__head-row">
56
+ {columns.map((col) => (
57
+ <th
58
+ key={col.key}
59
+ className={`pcoi-data-table__th${col.align ? ` pcoi-data-table__th--${col.align}` : ""}`}
60
+ >
61
+ {col.header}
62
+ </th>
63
+ ))}
64
+ </tr>
65
+ </thead>
66
+ <tbody className="pcoi-data-table__body">
67
+ {rows.length === 0 ? (
68
+ <tr>
69
+ <td
70
+ colSpan={columns.length}
71
+ className="pcoi-data-table__empty"
72
+ >
73
+ {emptyText}
74
+ </td>
75
+ </tr>
76
+ ) : (
77
+ rows.map((row, index) => (
78
+ <tr
79
+ key={getRowKey(row as Record<string, unknown>, index)}
80
+ className="pcoi-data-table__row"
81
+ >
82
+ {columns.map((col) => {
83
+ const value = (row as Record<string, unknown>)[col.key];
84
+ return (
85
+ <td
86
+ key={col.key}
87
+ className={`pcoi-data-table__td${col.align ? ` pcoi-data-table__td--${col.align}` : ""}`}
88
+ >
89
+ {col.render ? col.render(value, row) : String(value ?? "")}
90
+ </td>
91
+ );
92
+ })}
93
+ </tr>
94
+ ))
95
+ )}
96
+ </tbody>
97
+ </table>
98
+ </div>
99
+ );
100
+ }
101
+ );
102
+
103
+ DataTable.displayName = "DataTable";
104
+ export default DataTable;
@@ -0,0 +1,2 @@
1
+ export { DataTable, type DataTableProps, type DataTableColumn } from "./DataTable";
2
+ export { default } from "./DataTable";
@@ -0,0 +1,57 @@
1
+ /* DocumentOverlay — @pcoi/components */
2
+
3
+ /* ── Gold modal title ── */
4
+ .pcoi-doc-overlay .pcoi-modal__title {
5
+ color: var(--pcoi-semantic-text-accent);
6
+ }
7
+
8
+ .pcoi-doc-overlay__content {
9
+ font-family: var(--pcoi-semantic-type-body-font);
10
+ font-size: var(--pcoi-semantic-type-body-size);
11
+ line-height: var(--pcoi-semantic-type-body-line-height);
12
+ color: var(--pcoi-semantic-text-primary);
13
+ }
14
+
15
+ .pcoi-doc-overlay__content p {
16
+ margin: 0 0 var(--pcoi-spacing-12);
17
+ }
18
+
19
+ .pcoi-doc-overlay__content p:last-child {
20
+ margin-bottom: 0;
21
+ }
22
+
23
+ .pcoi-doc-overlay__highlight {
24
+ display: flex;
25
+ align-items: baseline;
26
+ gap: var(--pcoi-spacing-6);
27
+ background: var(--pcoi-semantic-surface-highlight);
28
+ border-left: var(--pcoi-layout-component-accent-border-w) solid
29
+ var(--pcoi-semantic-border-accent-subtle);
30
+ padding: var(--pcoi-spacing-8) var(--pcoi-spacing-12);
31
+ border-radius: var(--pcoi-radius-sm);
32
+ animation: pcoi-doc-highlight-fade 0.4s ease;
33
+ }
34
+
35
+ .pcoi-doc-overlay__highlight[data-highlight-index]::before {
36
+ content: attr(data-highlight-index);
37
+ flex-shrink: 0;
38
+ font-family: var(--pcoi-semantic-type-mono-font);
39
+ font-size: var(--pcoi-semantic-type-body-compact-size);
40
+ font-weight: var(--pcoi-semantic-type-label-weight);
41
+ color: var(--pcoi-semantic-text-accent);
42
+ }
43
+
44
+ .pcoi-doc-overlay__source {
45
+ display: flex;
46
+ align-items: center;
47
+ }
48
+
49
+ /* ── Animation ── */
50
+ @keyframes pcoi-doc-highlight-fade {
51
+ from {
52
+ background: var(--pcoi-semantic-surface-accent-dim);
53
+ }
54
+ to {
55
+ background: var(--pcoi-semantic-surface-highlight);
56
+ }
57
+ }
@@ -0,0 +1,86 @@
1
+ import React, { useEffect, useRef } from "react";
2
+ import { Modal } from "../Modal/Modal";
3
+ import { Badge } from "../Badge/Badge";
4
+
5
+ export interface DocumentOverlayProps
6
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
7
+ /** Whether the overlay is visible */
8
+ open: boolean;
9
+ /** Called when the overlay should close */
10
+ onClose: () => void;
11
+ /** Document title shown in the header */
12
+ title: string;
13
+ /** Optional source label shown as a badge */
14
+ sourceLabel?: string;
15
+ /** Document body content */
16
+ children: React.ReactNode;
17
+ /** Element ID to scroll to and highlight on open */
18
+ highlightId?: string;
19
+ /** Citation index to display beside the highlighted element */
20
+ highlightIndex?: number;
21
+ }
22
+
23
+ /**
24
+ * PCOI DocumentOverlay — Wide modal for viewing source documents with highlighted citations
25
+ * Composes Modal (size="wide") + Badge
26
+ * Tokens: surface/highlight, border/accent-subtle, container/document
27
+ */
28
+ export const DocumentOverlay = React.forwardRef<
29
+ HTMLDivElement,
30
+ DocumentOverlayProps
31
+ >(
32
+ (
33
+ { open, onClose, title, sourceLabel, children, highlightId, highlightIndex, className = "", ...rest },
34
+ ref
35
+ ) => {
36
+ const contentRef = useRef<HTMLDivElement>(null);
37
+
38
+ useEffect(() => {
39
+ if (open && highlightId && contentRef.current) {
40
+ requestAnimationFrame(() => {
41
+ const el = contentRef.current?.querySelector(`#${CSS.escape(highlightId)}`);
42
+ if (el) {
43
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
44
+ el.classList.add("pcoi-doc-overlay__highlight");
45
+ if (highlightIndex !== undefined) {
46
+ el.setAttribute("data-highlight-index", `[${highlightIndex}]`);
47
+ }
48
+ }
49
+ });
50
+ }
51
+ }, [open, highlightId, highlightIndex]);
52
+
53
+ const classes = ["pcoi-doc-overlay", className]
54
+ .filter(Boolean)
55
+ .join(" ");
56
+
57
+ const footer = sourceLabel
58
+ ? React.createElement(
59
+ "div",
60
+ { className: "pcoi-doc-overlay__source" },
61
+ React.createElement(Badge, null, sourceLabel)
62
+ )
63
+ : undefined;
64
+
65
+ return React.createElement(
66
+ Modal,
67
+ {
68
+ open,
69
+ onClose,
70
+ title,
71
+ size: "wide",
72
+ footer,
73
+ className: classes,
74
+ ...rest,
75
+ },
76
+ React.createElement(
77
+ "div",
78
+ { ref: contentRef, className: "pcoi-doc-overlay__content" },
79
+ children
80
+ )
81
+ );
82
+ }
83
+ );
84
+
85
+ DocumentOverlay.displayName = "DocumentOverlay";
86
+ export default DocumentOverlay;
@@ -0,0 +1,2 @@
1
+ export { DocumentOverlay, type DocumentOverlayProps } from "./DocumentOverlay";
2
+ export { default } from "./DocumentOverlay";
@@ -0,0 +1,72 @@
1
+ /* Footer — @pcoi/components */
2
+
3
+ .pcoi-footer {
4
+ border-top: 1px solid var(--pcoi-semantic-border-default);
5
+ padding: var(--pcoi-spacing-60) var(--pcoi-spacing-24) var(--pcoi-spacing-40);
6
+ }
7
+
8
+ .pcoi-footer__inner {
9
+ max-width: var(--pcoi-layout-container-max);
10
+ margin: 0 auto;
11
+ display: flex;
12
+ justify-content: space-between;
13
+ align-items: flex-start;
14
+ }
15
+
16
+ /* ── Brand (LogoMark overrides) ── */
17
+ .pcoi-footer__logo {
18
+ margin-bottom: var(--pcoi-spacing-8);
19
+ }
20
+
21
+ .pcoi-footer__tagline {
22
+ font-size: var(--pcoi-semantic-type-body-sm-size);
23
+ color: var(--pcoi-semantic-text-secondary);
24
+ line-height: var(--pcoi-semantic-type-body-compact-line-height);
25
+ margin: 0;
26
+ white-space: pre-line;
27
+ }
28
+
29
+ /* ── Links ── */
30
+ .pcoi-footer__links {
31
+ display: flex;
32
+ flex-wrap: wrap;
33
+ gap: var(--pcoi-spacing-28);
34
+ }
35
+
36
+ .pcoi-footer__links a {
37
+ font-size: var(--pcoi-semantic-type-nav-size);
38
+ color: var(--pcoi-semantic-text-secondary);
39
+ text-decoration: none;
40
+ transition: color var(--pcoi-effect-transition-fast, 0.2s ease);
41
+ }
42
+
43
+ .pcoi-footer__links a:hover {
44
+ color: var(--pcoi-semantic-text-primary);
45
+ }
46
+
47
+ /* ── Bottom ── */
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);
52
+ border-top: 1px solid var(--pcoi-semantic-border-default);
53
+ }
54
+
55
+ .pcoi-footer__bottom p {
56
+ font-size: var(--pcoi-semantic-type-label-size);
57
+ color: var(--pcoi-semantic-text-muted);
58
+ margin: 0;
59
+ }
60
+
61
+ /* ── Responsive ── */
62
+ @media (max-width: 768px) {
63
+ .pcoi-footer__inner {
64
+ flex-direction: column;
65
+ gap: var(--pcoi-spacing-32);
66
+ }
67
+
68
+ .pcoi-footer__links {
69
+ flex-direction: column;
70
+ gap: var(--pcoi-spacing-14);
71
+ }
72
+ }