@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,112 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { OptionItem } from "../types";
|
|
3
|
+
|
|
4
|
+
export interface RadioGroupProps extends Omit<React.FieldsetHTMLAttributes<HTMLFieldSetElement>, "onChange"> {
|
|
5
|
+
/** Group label rendered as a fieldset legend */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Shared name for all radio inputs */
|
|
8
|
+
name: string;
|
|
9
|
+
/** List of radio options */
|
|
10
|
+
options: OptionItem[];
|
|
11
|
+
/** Pre-selected value */
|
|
12
|
+
defaultValue?: string;
|
|
13
|
+
/** Error message — when truthy, group enters error state */
|
|
14
|
+
error?: string;
|
|
15
|
+
/** Marks the group as required */
|
|
16
|
+
required?: boolean;
|
|
17
|
+
/** Disables all radio inputs */
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
/** Called with the selected value when it changes */
|
|
20
|
+
onChange?: (value: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* PCOI RadioGroup — Accessible radio button group using fieldset/legend
|
|
25
|
+
* Tokens: bg/default, border/default, border/error, focus/border, focus/glow,
|
|
26
|
+
* action/primary-bg, text/primary, text/secondary, text/error, radius-full
|
|
27
|
+
*/
|
|
28
|
+
export const RadioGroup = React.forwardRef<HTMLFieldSetElement, RadioGroupProps>(
|
|
29
|
+
(
|
|
30
|
+
{
|
|
31
|
+
label,
|
|
32
|
+
name,
|
|
33
|
+
options,
|
|
34
|
+
defaultValue,
|
|
35
|
+
error,
|
|
36
|
+
required = false,
|
|
37
|
+
disabled = false,
|
|
38
|
+
onChange,
|
|
39
|
+
className = "",
|
|
40
|
+
...rest
|
|
41
|
+
},
|
|
42
|
+
ref
|
|
43
|
+
) => {
|
|
44
|
+
const errorId = error ? `pcoi-field-${name}-error` : undefined;
|
|
45
|
+
|
|
46
|
+
const wrapperClasses = [
|
|
47
|
+
"pcoi-radio-group",
|
|
48
|
+
error ? "pcoi-radio-group--error" : "",
|
|
49
|
+
disabled ? "pcoi-radio-group--disabled" : "",
|
|
50
|
+
className,
|
|
51
|
+
]
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.join(" ");
|
|
54
|
+
|
|
55
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
56
|
+
onChange?.(e.target.value);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<fieldset
|
|
61
|
+
ref={ref}
|
|
62
|
+
className={wrapperClasses}
|
|
63
|
+
aria-invalid={!!error}
|
|
64
|
+
aria-describedby={errorId}
|
|
65
|
+
{...rest}
|
|
66
|
+
>
|
|
67
|
+
<legend className="pcoi-radio-group__legend">
|
|
68
|
+
{label}
|
|
69
|
+
{required && (
|
|
70
|
+
<span className="pcoi-radio-group__required" aria-hidden="true">
|
|
71
|
+
*
|
|
72
|
+
</span>
|
|
73
|
+
)}
|
|
74
|
+
</legend>
|
|
75
|
+
<div className="pcoi-radio-group__options">
|
|
76
|
+
{options.map((opt) => {
|
|
77
|
+
const optionId = `pcoi-field-${name}-${opt.value}`;
|
|
78
|
+
return (
|
|
79
|
+
<label
|
|
80
|
+
key={opt.value}
|
|
81
|
+
htmlFor={optionId}
|
|
82
|
+
className="pcoi-radio-group__option"
|
|
83
|
+
>
|
|
84
|
+
<input
|
|
85
|
+
type="radio"
|
|
86
|
+
id={optionId}
|
|
87
|
+
name={name}
|
|
88
|
+
value={opt.value}
|
|
89
|
+
defaultChecked={defaultValue === opt.value}
|
|
90
|
+
required={required}
|
|
91
|
+
disabled={disabled || opt.disabled}
|
|
92
|
+
onChange={handleChange}
|
|
93
|
+
className="pcoi-radio-group__input"
|
|
94
|
+
/>
|
|
95
|
+
<span className="pcoi-radio-group__circle" aria-hidden="true" />
|
|
96
|
+
<span className="pcoi-radio-group__label">{opt.label}</span>
|
|
97
|
+
</label>
|
|
98
|
+
);
|
|
99
|
+
})}
|
|
100
|
+
</div>
|
|
101
|
+
{error && (
|
|
102
|
+
<span id={errorId} className="pcoi-radio-group__error" role="alert">
|
|
103
|
+
{error}
|
|
104
|
+
</span>
|
|
105
|
+
)}
|
|
106
|
+
</fieldset>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
RadioGroup.displayName = "RadioGroup";
|
|
112
|
+
export default RadioGroup;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* SectionHeader — @pcoi/components */
|
|
2
|
+
|
|
3
|
+
.pcoi-section-header {
|
|
4
|
+
text-align: center;
|
|
5
|
+
max-width: var(--pcoi-layout-container-narrow);
|
|
6
|
+
margin: 0 auto var(--pcoi-spacing-64);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.pcoi-section-header__label {
|
|
10
|
+
display: inline-block;
|
|
11
|
+
font-family: var(--pcoi-semantic-type-mono-font);
|
|
12
|
+
font-size: var(--pcoi-semantic-type-section-label-size);
|
|
13
|
+
font-weight: var(--pcoi-semantic-type-label-weight);
|
|
14
|
+
letter-spacing: var(--pcoi-semantic-type-section-label-letter-spacing);
|
|
15
|
+
text-transform: uppercase;
|
|
16
|
+
color: var(--pcoi-semantic-text-accent);
|
|
17
|
+
margin-bottom: var(--pcoi-spacing-16);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.pcoi-section-header__title {
|
|
21
|
+
font-size: var(--pcoi-semantic-type-section-title-size);
|
|
22
|
+
font-weight: var(--pcoi-semantic-type-heading-weight);
|
|
23
|
+
line-height: var(--pcoi-semantic-type-heading-line-height);
|
|
24
|
+
letter-spacing: var(--pcoi-semantic-type-heading-letter-spacing);
|
|
25
|
+
color: var(--pcoi-semantic-text-primary);
|
|
26
|
+
margin: 0 0 var(--pcoi-spacing-16) 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.pcoi-section-header__title em {
|
|
30
|
+
font-style: normal;
|
|
31
|
+
color: var(--pcoi-semantic-text-accent);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.pcoi-section-header__desc {
|
|
35
|
+
font-size: var(--pcoi-semantic-type-heading-sm-size);
|
|
36
|
+
color: var(--pcoi-semantic-text-secondary);
|
|
37
|
+
line-height: var(--pcoi-semantic-type-relaxed-line-height);
|
|
38
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { HeadingLevel } from "../types";
|
|
3
|
+
|
|
4
|
+
export interface SectionHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
/** Section label (mono, uppercase, accent) e.g. "The Problem" */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Main heading */
|
|
8
|
+
title: string;
|
|
9
|
+
/** Emphasized portion of title (renders in accent color) */
|
|
10
|
+
titleEmphasis?: string;
|
|
11
|
+
/** Optional subtitle/description */
|
|
12
|
+
description?: string;
|
|
13
|
+
/** Heading level for the title (default: "h2") */
|
|
14
|
+
headingLevel?: HeadingLevel;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* PCOI Section Header
|
|
19
|
+
*
|
|
20
|
+
* Centered header pattern used across all major sections.
|
|
21
|
+
* Label + H2 + optional description, max-width 800px.
|
|
22
|
+
*
|
|
23
|
+
* Tokens consumed:
|
|
24
|
+
* - text/accent (label), text/primary (h2), text/secondary (desc)
|
|
25
|
+
* - font-mono (label), font-sans (h2, desc)
|
|
26
|
+
* - spacing-16 (label mb, h2 mb), spacing-64 (section mb)
|
|
27
|
+
* - container-narrow (800px max-width)
|
|
28
|
+
*/
|
|
29
|
+
export const SectionHeader = React.forwardRef<HTMLDivElement, SectionHeaderProps>(
|
|
30
|
+
({ label, title, titleEmphasis, description, headingLevel = "h2", className = "", ...rest }, ref) => {
|
|
31
|
+
const Heading = headingLevel;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div ref={ref} className={`pcoi-section-header ${className}`} {...rest}>
|
|
35
|
+
<span className="pcoi-section-header__label">{label}</span>
|
|
36
|
+
<Heading className="pcoi-section-header__title">
|
|
37
|
+
{titleEmphasis ? (
|
|
38
|
+
<>
|
|
39
|
+
{title.replace(titleEmphasis, "")}
|
|
40
|
+
<em>{titleEmphasis}</em>
|
|
41
|
+
</>
|
|
42
|
+
) : (
|
|
43
|
+
title
|
|
44
|
+
)}
|
|
45
|
+
</Heading>
|
|
46
|
+
{description && (
|
|
47
|
+
<p className="pcoi-section-header__desc">{description}</p>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
SectionHeader.displayName = "SectionHeader";
|
|
55
|
+
export default SectionHeader;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SectionHeader, type SectionHeaderProps } from "./SectionHeader";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/* Select — @pcoi/components */
|
|
2
|
+
|
|
3
|
+
.pcoi-select {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: var(--pcoi-spacing-6);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.pcoi-select__label {
|
|
10
|
+
font-size: var(--pcoi-semantic-type-label-size);
|
|
11
|
+
font-weight: var(--pcoi-semantic-type-label-weight);
|
|
12
|
+
letter-spacing: var(--pcoi-semantic-type-label-letter-spacing);
|
|
13
|
+
text-transform: uppercase;
|
|
14
|
+
color: var(--pcoi-semantic-text-secondary);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.pcoi-select__required {
|
|
18
|
+
color: var(--pcoi-semantic-text-error);
|
|
19
|
+
margin-left: var(--pcoi-spacing-4);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.pcoi-select__wrapper {
|
|
23
|
+
position: relative;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.pcoi-select__native {
|
|
27
|
+
appearance: none;
|
|
28
|
+
width: 100%;
|
|
29
|
+
font-family: var(--pcoi-semantic-type-body-font);
|
|
30
|
+
font-size: var(--pcoi-semantic-type-body-size);
|
|
31
|
+
color: var(--pcoi-semantic-text-primary);
|
|
32
|
+
background: var(--pcoi-semantic-bg-default);
|
|
33
|
+
border: 1px solid var(--pcoi-semantic-border-default);
|
|
34
|
+
border-radius: var(--pcoi-radius-sm);
|
|
35
|
+
padding: var(--pcoi-spacing-12) var(--pcoi-spacing-36) var(--pcoi-spacing-12) var(--pcoi-spacing-14);
|
|
36
|
+
cursor: pointer;
|
|
37
|
+
transition: border-color var(--pcoi-effect-transition-fast, 0.2s ease),
|
|
38
|
+
box-shadow var(--pcoi-effect-transition-fast, 0.2s ease);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.pcoi-select__native:hover {
|
|
42
|
+
border-color: var(--pcoi-semantic-border-input-hover);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.pcoi-select__native:active {
|
|
46
|
+
transform: var(--pcoi-effect-transform-press-scale-input);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.pcoi-select__native:focus {
|
|
50
|
+
outline: none;
|
|
51
|
+
border-color: var(--pcoi-semantic-focus-border);
|
|
52
|
+
box-shadow: var(--pcoi-effect-shadow-focus-ring);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.pcoi-select__native option {
|
|
56
|
+
background: var(--pcoi-semantic-bg-default);
|
|
57
|
+
color: var(--pcoi-semantic-text-primary);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.pcoi-select__chevron {
|
|
61
|
+
position: absolute;
|
|
62
|
+
right: var(--pcoi-spacing-14);
|
|
63
|
+
top: 50%;
|
|
64
|
+
transform: translateY(-50%);
|
|
65
|
+
pointer-events: none;
|
|
66
|
+
color: var(--pcoi-semantic-text-secondary);
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* ── Error state ── */
|
|
72
|
+
.pcoi-select--error .pcoi-select__native {
|
|
73
|
+
border-color: var(--pcoi-semantic-border-error);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.pcoi-select--error .pcoi-select__native:focus {
|
|
77
|
+
box-shadow: var(--pcoi-effect-shadow-focus-ring-error);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.pcoi-select__error {
|
|
81
|
+
font-size: var(--pcoi-semantic-type-label-size);
|
|
82
|
+
color: var(--pcoi-semantic-text-error);
|
|
83
|
+
margin: 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* ── Disabled state ── */
|
|
87
|
+
.pcoi-select--disabled {
|
|
88
|
+
opacity: var(--pcoi-effect-opacity-disabled);
|
|
89
|
+
pointer-events: none;
|
|
90
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ChevronDownIcon } from "../../../icons/src/react/ChevronDownIcon";
|
|
3
|
+
import type { OptionItem } from "../types";
|
|
4
|
+
|
|
5
|
+
export interface SelectProps
|
|
6
|
+
extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "size"> {
|
|
7
|
+
/** Visible label text */
|
|
8
|
+
label: string;
|
|
9
|
+
/** Unique field name, also used to generate id */
|
|
10
|
+
name: string;
|
|
11
|
+
/** List of options */
|
|
12
|
+
options: OptionItem[];
|
|
13
|
+
/** Placeholder text shown as first disabled option */
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
/** Error message — when truthy, field enters error state */
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* PCOI Select — Native dropdown with custom styling
|
|
21
|
+
* Tokens: bg/default, border/default, border/error, focus/border, focus/glow,
|
|
22
|
+
* text/primary, text/secondary, text/muted, text/error, radius-sm
|
|
23
|
+
*/
|
|
24
|
+
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
|
25
|
+
(
|
|
26
|
+
{
|
|
27
|
+
label,
|
|
28
|
+
name,
|
|
29
|
+
options,
|
|
30
|
+
placeholder,
|
|
31
|
+
error,
|
|
32
|
+
required = false,
|
|
33
|
+
disabled = false,
|
|
34
|
+
className = "",
|
|
35
|
+
...props
|
|
36
|
+
},
|
|
37
|
+
ref
|
|
38
|
+
) => {
|
|
39
|
+
const fieldId = `pcoi-field-${name}`;
|
|
40
|
+
const errorId = error ? `${fieldId}-error` : undefined;
|
|
41
|
+
|
|
42
|
+
const wrapperClasses = [
|
|
43
|
+
"pcoi-select",
|
|
44
|
+
error ? "pcoi-select--error" : "",
|
|
45
|
+
disabled ? "pcoi-select--disabled" : "",
|
|
46
|
+
className,
|
|
47
|
+
]
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.join(" ");
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className={wrapperClasses}>
|
|
53
|
+
<label htmlFor={fieldId} className="pcoi-select__label">
|
|
54
|
+
{label}
|
|
55
|
+
{required && (
|
|
56
|
+
<span className="pcoi-select__required" aria-hidden="true">
|
|
57
|
+
*
|
|
58
|
+
</span>
|
|
59
|
+
)}
|
|
60
|
+
</label>
|
|
61
|
+
<div className="pcoi-select__wrapper">
|
|
62
|
+
<select
|
|
63
|
+
ref={ref}
|
|
64
|
+
id={fieldId}
|
|
65
|
+
name={name}
|
|
66
|
+
required={required}
|
|
67
|
+
disabled={disabled}
|
|
68
|
+
aria-invalid={!!error}
|
|
69
|
+
aria-describedby={errorId}
|
|
70
|
+
className="pcoi-select__native"
|
|
71
|
+
defaultValue={placeholder ? "" : undefined}
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
{placeholder && (
|
|
75
|
+
<option value="" disabled hidden>
|
|
76
|
+
{placeholder}
|
|
77
|
+
</option>
|
|
78
|
+
)}
|
|
79
|
+
{options.map((opt) => (
|
|
80
|
+
<option key={opt.value} value={opt.value} disabled={opt.disabled}>
|
|
81
|
+
{opt.label}
|
|
82
|
+
</option>
|
|
83
|
+
))}
|
|
84
|
+
</select>
|
|
85
|
+
<span className="pcoi-select__chevron" aria-hidden="true">
|
|
86
|
+
<ChevronDownIcon size={16} />
|
|
87
|
+
</span>
|
|
88
|
+
</div>
|
|
89
|
+
{error && (
|
|
90
|
+
<span id={errorId} className="pcoi-select__error" role="alert">
|
|
91
|
+
{error}
|
|
92
|
+
</span>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
Select.displayName = "Select";
|
|
100
|
+
export default Select;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* SignalsPanel — @pcoi/components */
|
|
2
|
+
|
|
3
|
+
.pcoi-signals {
|
|
4
|
+
background: var(--pcoi-semantic-bg-surface);
|
|
5
|
+
border: 1px solid var(--pcoi-semantic-border-default);
|
|
6
|
+
border-radius: var(--pcoi-radius-md);
|
|
7
|
+
padding: var(--pcoi-spacing-40) var(--pcoi-spacing-48);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.pcoi-signals__title {
|
|
11
|
+
font-size: var(--pcoi-semantic-type-heading-sm-size);
|
|
12
|
+
font-weight: var(--pcoi-semantic-type-emphasis-weight);
|
|
13
|
+
color: var(--pcoi-semantic-text-accent);
|
|
14
|
+
margin: 0 0 var(--pcoi-spacing-24) 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.pcoi-signals__list {
|
|
18
|
+
list-style: none;
|
|
19
|
+
padding: 0;
|
|
20
|
+
margin: 0;
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
gap: var(--pcoi-spacing-14);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.pcoi-signals__item {
|
|
27
|
+
position: relative;
|
|
28
|
+
padding-left: var(--pcoi-spacing-24);
|
|
29
|
+
font-size: var(--pcoi-semantic-type-body-compact-size);
|
|
30
|
+
color: var(--pcoi-semantic-text-secondary);
|
|
31
|
+
line-height: var(--pcoi-semantic-type-body-compact-line-height);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.pcoi-signals__item::before {
|
|
35
|
+
content: "";
|
|
36
|
+
position: absolute;
|
|
37
|
+
left: 0;
|
|
38
|
+
top: 8px;
|
|
39
|
+
width: var(--pcoi-layout-component-bullet);
|
|
40
|
+
height: var(--pcoi-layout-component-bullet);
|
|
41
|
+
border-radius: var(--pcoi-radius-full);
|
|
42
|
+
background: var(--pcoi-semantic-text-accent);
|
|
43
|
+
opacity: 0.5;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* ── Responsive ── */
|
|
47
|
+
@media (max-width: 768px) {
|
|
48
|
+
.pcoi-signals {
|
|
49
|
+
padding: var(--pcoi-spacing-24) var(--pcoi-spacing-28);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { HeadingLevel } from "../types";
|
|
3
|
+
|
|
4
|
+
export interface SignalsPanelProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
title?: string;
|
|
6
|
+
signals: string[];
|
|
7
|
+
/** Heading level for the title (default: "h3") */
|
|
8
|
+
headingLevel?: HeadingLevel;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* PCOI Signals Panel — "You'll know this is for you if:" section
|
|
13
|
+
* Tokens: bg/surface, border/default, radius-md, spacing-40/48, text/accent
|
|
14
|
+
*/
|
|
15
|
+
export const SignalsPanel = React.forwardRef<HTMLDivElement, SignalsPanelProps>(
|
|
16
|
+
({ title = "You'll know this is for you if:", signals, headingLevel = "h3", className = "", ...rest }, ref) => {
|
|
17
|
+
const Heading = headingLevel;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div ref={ref} className={`pcoi-signals ${className}`} {...rest}>
|
|
21
|
+
<Heading className="pcoi-signals__title">{title}</Heading>
|
|
22
|
+
<ul className="pcoi-signals__list">
|
|
23
|
+
{signals.map((signal, i) => (
|
|
24
|
+
<li key={i} className="pcoi-signals__item">{signal}</li>
|
|
25
|
+
))}
|
|
26
|
+
</ul>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
SignalsPanel.displayName = "SignalsPanel";
|
|
33
|
+
export default SignalsPanel;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SignalsPanel, type SignalsPanelProps } from "./SignalsPanel";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* SuggestionCard — @pcoi/components */
|
|
2
|
+
|
|
3
|
+
.pcoi-suggestion-card {
|
|
4
|
+
display: flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
gap: var(--pcoi-spacing-12);
|
|
7
|
+
width: 100%;
|
|
8
|
+
padding: var(--pcoi-spacing-16) var(--pcoi-spacing-20);
|
|
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-secondary);
|
|
13
|
+
text-align: left;
|
|
14
|
+
background: var(--pcoi-color-bg-card);
|
|
15
|
+
border: 1px solid var(--pcoi-semantic-border-card);
|
|
16
|
+
border-radius: var(--pcoi-radius-md);
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
transition:
|
|
19
|
+
background var(--pcoi-effect-transition-medium),
|
|
20
|
+
border-color var(--pcoi-effect-transition-medium),
|
|
21
|
+
transform var(--pcoi-effect-transition-medium),
|
|
22
|
+
box-shadow var(--pcoi-effect-transition-medium);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.pcoi-suggestion-card:hover {
|
|
26
|
+
background: var(--pcoi-color-bg-card-hover);
|
|
27
|
+
border-color: var(--pcoi-semantic-border-card-hover);
|
|
28
|
+
transform: var(--pcoi-effect-transform-hover-lift-sm);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.pcoi-suggestion-card:active {
|
|
32
|
+
transform: var(--pcoi-effect-transform-press-scale);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.pcoi-suggestion-card:focus-visible {
|
|
36
|
+
outline: none;
|
|
37
|
+
box-shadow: var(--pcoi-effect-shadow-focus-ring);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.pcoi-suggestion-card__icon {
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
flex-shrink: 0;
|
|
45
|
+
color: var(--pcoi-semantic-text-accent);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.pcoi-suggestion-card__label {
|
|
49
|
+
flex: 1;
|
|
50
|
+
min-width: 0;
|
|
51
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface SuggestionCardProps
|
|
4
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
5
|
+
/** Display text */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Optional icon rendered before the label */
|
|
8
|
+
icon?: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* PCOI SuggestionCard — Clickable FAQ card for chat empty state
|
|
13
|
+
* Tokens: bg/card, bg/card-hover, border/card, border/card-hover,
|
|
14
|
+
* text/secondary, text/accent, radius-md, transition-medium,
|
|
15
|
+
* transform/hover-lift-sm, transform/press-scale, shadow/focus-ring
|
|
16
|
+
*/
|
|
17
|
+
export const SuggestionCard = React.forwardRef<
|
|
18
|
+
HTMLButtonElement,
|
|
19
|
+
SuggestionCardProps
|
|
20
|
+
>(({ label, icon, className = "", ...rest }, ref) => {
|
|
21
|
+
const classes = ["pcoi-suggestion-card", className]
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.join(" ");
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<button ref={ref} type="button" className={classes} {...rest}>
|
|
27
|
+
{icon && <span className="pcoi-suggestion-card__icon">{icon}</span>}
|
|
28
|
+
<span className="pcoi-suggestion-card__label">{label}</span>
|
|
29
|
+
</button>
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
SuggestionCard.displayName = "SuggestionCard";
|
|
34
|
+
export default SuggestionCard;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* SuggestionCards — @pcoi/components */
|
|
2
|
+
|
|
3
|
+
.pcoi-suggestion-cards {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: var(--pcoi-spacing-16);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/* ── Responsive: 3-col grid on tablet and up ── */
|
|
10
|
+
@media (min-width: 768px) {
|
|
11
|
+
.pcoi-suggestion-cards {
|
|
12
|
+
display: grid;
|
|
13
|
+
grid-template-columns: repeat(3, 1fr);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { SuggestionCard } from "../SuggestionCard/SuggestionCard";
|
|
3
|
+
import type { Suggestion } from "../types";
|
|
4
|
+
|
|
5
|
+
export interface SuggestionCardsProps
|
|
6
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/** Array of suggestion prompts */
|
|
8
|
+
suggestions: Suggestion[];
|
|
9
|
+
/** Called when a suggestion card is clicked */
|
|
10
|
+
onSelect: (suggestion: Suggestion) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* PCOI SuggestionCards — 3-column grid of suggestion cards for chat empty state
|
|
15
|
+
* Tokens: spacing-16 (gap), grid/suggestions
|
|
16
|
+
*/
|
|
17
|
+
export const SuggestionCards = React.forwardRef<
|
|
18
|
+
HTMLDivElement,
|
|
19
|
+
SuggestionCardsProps
|
|
20
|
+
>(({ suggestions, onSelect, className = "", ...rest }, ref) => {
|
|
21
|
+
const classes = ["pcoi-suggestion-cards", className]
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.join(" ");
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div ref={ref} className={classes} {...rest}>
|
|
27
|
+
{suggestions.map((s) => (
|
|
28
|
+
<SuggestionCard
|
|
29
|
+
key={s.id}
|
|
30
|
+
label={s.label}
|
|
31
|
+
icon={s.icon}
|
|
32
|
+
onClick={() => onSelect(s)}
|
|
33
|
+
/>
|
|
34
|
+
))}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
SuggestionCards.displayName = "SuggestionCards";
|
|
40
|
+
export default SuggestionCards;
|