@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,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,2 @@
1
+ export { RadioGroup, type RadioGroupProps } from "./RadioGroup";
2
+ export { default } from "./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,2 @@
1
+ export { Select, type SelectProps } from "./Select";
2
+ export { default } from "./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,2 @@
1
+ export { SuggestionCard, type SuggestionCardProps } from "./SuggestionCard";
2
+ export { default } from "./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;
@@ -0,0 +1,2 @@
1
+ export { SuggestionCards, type SuggestionCardsProps } from "./SuggestionCards";
2
+ export { default } from "./SuggestionCards";