@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.
- package/dist/components.css +1 -0
- package/dist/index.d.ts +667 -0
- package/dist/index.js +2 -0
- package/dist/index.mjs +1048 -0
- package/package.json +36 -0
- package/src/Badge/Badge.css +40 -0
- package/src/Badge/Badge.tsx +36 -0
- package/src/Badge/index.ts +2 -0
- package/src/Button/Button.css +93 -0
- package/src/Button/Button.figma.tsx +29 -0
- package/src/Button/Button.tsx +47 -0
- package/src/Button/index.ts +1 -0
- package/src/Callout/Callout.css +43 -0
- package/src/Callout/Callout.tsx +39 -0
- package/src/Callout/index.ts +1 -0
- package/src/Card/Card.css +88 -0
- package/src/Card/Card.tsx +60 -0
- package/src/Card/index.ts +1 -0
- package/src/ChatInterface/ChatInterface.css +49 -0
- package/src/ChatInterface/ChatInterface.tsx +120 -0
- package/src/ChatInterface/index.ts +6 -0
- package/src/ChatMessage/ChatMessage.css +55 -0
- package/src/ChatMessage/ChatMessage.tsx +71 -0
- package/src/ChatMessage/index.ts +2 -0
- package/src/ChatMessageList/ChatMessageList.css +24 -0
- package/src/ChatMessageList/ChatMessageList.tsx +51 -0
- package/src/ChatMessageList/index.ts +2 -0
- package/src/Checkbox/Checkbox.css +97 -0
- package/src/Checkbox/Checkbox.tsx +70 -0
- package/src/Checkbox/index.ts +2 -0
- package/src/CitationMark/CitationMark.css +40 -0
- package/src/CitationMark/CitationMark.tsx +38 -0
- package/src/CitationMark/index.ts +2 -0
- package/src/CitedExcerpt/CitedExcerpt.css +75 -0
- package/src/CitedExcerpt/CitedExcerpt.tsx +51 -0
- package/src/CitedExcerpt/index.ts +2 -0
- package/src/ComparisonTable/ComparisonTable.css +66 -0
- package/src/ComparisonTable/ComparisonTable.tsx +48 -0
- package/src/ComparisonTable/index.ts +1 -0
- package/src/ContactForm/ContactForm.css +38 -0
- package/src/ContactForm/ContactForm.tsx +57 -0
- package/src/ContactForm/index.ts +1 -0
- package/src/DataTable/DataTable.css +56 -0
- package/src/DataTable/DataTable.tsx +104 -0
- package/src/DataTable/index.ts +2 -0
- package/src/DocumentOverlay/DocumentOverlay.css +57 -0
- package/src/DocumentOverlay/DocumentOverlay.tsx +86 -0
- package/src/DocumentOverlay/index.ts +2 -0
- package/src/Footer/Footer.css +72 -0
- package/src/Footer/Footer.tsx +56 -0
- package/src/Footer/index.ts +1 -0
- package/src/FormField/FormField.css +78 -0
- package/src/FormField/FormField.tsx +103 -0
- package/src/FormField/index.ts +2 -0
- package/src/HowStep/HowStep.css +48 -0
- package/src/HowStep/HowStep.tsx +38 -0
- package/src/HowStep/index.ts +1 -0
- package/src/LogoMark/LogoMark.css +16 -0
- package/src/LogoMark/LogoMark.tsx +25 -0
- package/src/LogoMark/index.ts +2 -0
- package/src/Modal/Modal.css +101 -0
- package/src/Modal/Modal.tsx +141 -0
- package/src/Modal/index.ts +2 -0
- package/src/Nav/Nav.css +161 -0
- package/src/Nav/Nav.tsx +101 -0
- package/src/Nav/index.ts +1 -0
- package/src/Panel/Panel.css +35 -0
- package/src/Panel/Panel.tsx +61 -0
- package/src/Panel/index.ts +2 -0
- package/src/PromptBar/PromptBar.css +68 -0
- package/src/PromptBar/PromptBar.tsx +93 -0
- package/src/PromptBar/index.ts +2 -0
- package/src/RadioGroup/RadioGroup.css +117 -0
- package/src/RadioGroup/RadioGroup.tsx +112 -0
- package/src/RadioGroup/index.ts +2 -0
- package/src/SectionHeader/SectionHeader.css +38 -0
- package/src/SectionHeader/SectionHeader.tsx +55 -0
- package/src/SectionHeader/index.ts +1 -0
- package/src/Select/Select.css +90 -0
- package/src/Select/Select.tsx +100 -0
- package/src/Select/index.ts +2 -0
- package/src/SignalsPanel/SignalsPanel.css +51 -0
- package/src/SignalsPanel/SignalsPanel.tsx +33 -0
- package/src/SignalsPanel/index.ts +1 -0
- package/src/SuggestionCard/SuggestionCard.css +51 -0
- package/src/SuggestionCard/SuggestionCard.tsx +34 -0
- package/src/SuggestionCard/index.ts +2 -0
- package/src/SuggestionCards/SuggestionCards.css +15 -0
- package/src/SuggestionCards/SuggestionCards.tsx +40 -0
- package/src/SuggestionCards/index.ts +2 -0
- package/src/Toast/Toast.css +85 -0
- package/src/Toast/Toast.tsx +77 -0
- package/src/Toast/index.ts +2 -0
- package/src/Toggle/Toggle.css +110 -0
- package/src/Toggle/Toggle.tsx +73 -0
- package/src/Toggle/index.ts +2 -0
- package/src/TypingIndicator/TypingIndicator.css +70 -0
- package/src/TypingIndicator/TypingIndicator.tsx +37 -0
- package/src/TypingIndicator/index.ts +2 -0
- package/src/index.ts +37 -0
- package/src/styles/utilities.css +14 -0
- package/src/styles.css +32 -0
- 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,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,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,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
|
+
}
|