@inversestudio/neptune-components 1.0.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/README.md +2 -0
- package/components/data-display/AppPreview.jsx +150 -0
- package/components/data-display/DataTable.jsx +65 -0
- package/components/data-display/FileTree.jsx +123 -0
- package/components/data-display/KpiCard.jsx +57 -0
- package/components/data-display/VersionRow.jsx +103 -0
- package/components/feedback/Avatar.jsx +28 -0
- package/components/feedback/Badge.jsx +32 -0
- package/components/feedback/ChatMessage.jsx +42 -0
- package/components/feedback/StatusDot.jsx +55 -0
- package/components/feedback/StatusIndicator.jsx +40 -0
- package/components/inputs/Button.jsx +48 -0
- package/components/inputs/Checkbox.jsx +90 -0
- package/components/inputs/FilterBar.jsx +64 -0
- package/components/inputs/IconButton.jsx +43 -0
- package/components/inputs/IconToggle.jsx +44 -0
- package/components/inputs/NaiaChatInput.jsx +173 -0
- package/components/inputs/NaiaSendButton.jsx +36 -0
- package/components/inputs/PillSelect.jsx +175 -0
- package/components/inputs/PropertyField.jsx +58 -0
- package/components/inputs/SuggestionPill.jsx +28 -0
- package/components/inputs/TextInput.jsx +96 -0
- package/components/inputs/Toggle.jsx +73 -0
- package/components/layout/AppHeader.jsx +56 -0
- package/components/layout/BottomBar.jsx +81 -0
- package/components/layout/Card.jsx +57 -0
- package/components/layout/Panel.jsx +26 -0
- package/components/layout/Toolbar.jsx +89 -0
- package/components/navigation/Breadcrumb.jsx +43 -0
- package/components/navigation/Dropdown.jsx +104 -0
- package/components/navigation/SidebarNav.jsx +82 -0
- package/components/navigation/SidebarTabs.jsx +99 -0
- package/components/navigation/TabBar.jsx +61 -0
- package/components/overlays/Modal.jsx +101 -0
- package/components/shared/index.jsx +112 -0
- package/index.css +3 -0
- package/index.js +50 -0
- package/neptune-components.css +1771 -0
- package/package.json +45 -0
- package/registry.json +1215 -0
- package/tokens/neptune-design-tokens.css +730 -0
- package/tokens/neptune-design-tokens.json +191 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatusIndicator component - Animated status dot with label
|
|
3
|
+
*
|
|
4
|
+
* Styles are in neptune-components.css. Only pass `style` for truly dynamic overrides.
|
|
5
|
+
*
|
|
6
|
+
* Displays a colored dot with an optional pulse animation and accompanying label.
|
|
7
|
+
* Used to indicate status of processes, services, or system components.
|
|
8
|
+
*
|
|
9
|
+
* @component
|
|
10
|
+
* @param {Object} props - Component props
|
|
11
|
+
* @param {string} props.label - Text label describing the status
|
|
12
|
+
* @param {string} [props.status='running'] - Status state: 'running', 'paused', 'error', 'stopped'
|
|
13
|
+
* @param {boolean} [props.pulse=true] - Whether to show pulse animation (default true for running status)
|
|
14
|
+
* @returns {JSX.Element} Styled status indicator with dot and label
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* <StatusIndicator label="API Server" status="running" pulse={true} />
|
|
18
|
+
* <StatusIndicator label="Database" status="error" pulse={false} />
|
|
19
|
+
*/
|
|
20
|
+
export default function StatusIndicator({
|
|
21
|
+
label,
|
|
22
|
+
status = "running",
|
|
23
|
+
pulse,
|
|
24
|
+
...props
|
|
25
|
+
}) {
|
|
26
|
+
// Default pulse behavior: true for running, false for others
|
|
27
|
+
const showPulse = pulse !== undefined ? pulse : status === "running";
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
className={`neptune-status-indicator neptune-status-indicator--${status} ${showPulse ? "neptune-status-indicator--pulse" : ""}`}
|
|
32
|
+
{...props}
|
|
33
|
+
>
|
|
34
|
+
<div className="neptune-status-indicator__dot" />
|
|
35
|
+
<span className="neptune-status-indicator__label">
|
|
36
|
+
{label}
|
|
37
|
+
</span>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Button component for user interactions
|
|
3
|
+
*
|
|
4
|
+
* Supports multiple variants (primary, secondary, tertiary, ghost, danger) and
|
|
5
|
+
* sizes (sm, default, lg). Can include an icon alongside text via the icon prop.
|
|
6
|
+
*
|
|
7
|
+
* Theme-aware: when rendered inside a data-theme="naia" container, accent
|
|
8
|
+
* colors automatically switch from orange to purple — no variant change needed.
|
|
9
|
+
*
|
|
10
|
+
* Styles are in neptune-components.css. Only pass `style` for truly dynamic overrides.
|
|
11
|
+
*
|
|
12
|
+
* @component
|
|
13
|
+
* @param {Object} props - Component props
|
|
14
|
+
* @param {string} [props.variant='primary'] - 'primary' | 'secondary' | 'tertiary' | 'ghost' | 'danger'
|
|
15
|
+
* @param {string} [props.size='default'] - 'sm' | 'default' | 'lg'
|
|
16
|
+
* @param {boolean} [props.disabled=false]
|
|
17
|
+
* @param {React.ReactNode} [props.icon] - Optional icon before text
|
|
18
|
+
* @param {React.ReactNode} [props.children] - Button text
|
|
19
|
+
* @param {Function} [props.onClick]
|
|
20
|
+
* @param {string} [props.className]
|
|
21
|
+
* @param {Object} [props.style] - Dynamic inline overrides only
|
|
22
|
+
*/
|
|
23
|
+
export default function Button({
|
|
24
|
+
variant = "primary",
|
|
25
|
+
size = "default",
|
|
26
|
+
disabled = false,
|
|
27
|
+
icon,
|
|
28
|
+
children,
|
|
29
|
+
onClick,
|
|
30
|
+
className = "",
|
|
31
|
+
style = {},
|
|
32
|
+
...props
|
|
33
|
+
}) {
|
|
34
|
+
return (
|
|
35
|
+
<button
|
|
36
|
+
className={`neptune-button neptune-button--${variant} neptune-button--${size} ${
|
|
37
|
+
disabled ? "neptune-button--disabled" : ""
|
|
38
|
+
} ${className}`}
|
|
39
|
+
style={style}
|
|
40
|
+
disabled={disabled}
|
|
41
|
+
onClick={onClick}
|
|
42
|
+
{...props}
|
|
43
|
+
>
|
|
44
|
+
{icon && <span className="neptune-button__icon">{icon}</span>}
|
|
45
|
+
{children && <span className="neptune-button__text">{children}</span>}
|
|
46
|
+
</button>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Check } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checkbox component for boolean selection
|
|
5
|
+
* Displays a checkmark icon when checked, supports label
|
|
6
|
+
*
|
|
7
|
+
* Styles are in neptune-components.css. Only pass `style` for truly dynamic overrides.
|
|
8
|
+
*
|
|
9
|
+
* @component
|
|
10
|
+
* @param {Object} props - Component props
|
|
11
|
+
* @param {boolean} [props.checked=false] - Whether the checkbox is in the checked state
|
|
12
|
+
* @param {Function} [props.onChange] - Change handler that receives the new boolean value
|
|
13
|
+
* @param {string} [props.label] - Optional label text displayed next to checkbox
|
|
14
|
+
* @param {boolean} [props.disabled=false] - Whether the checkbox is disabled
|
|
15
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
16
|
+
* @param {Object} [props.style] - Inline styles
|
|
17
|
+
* @returns {JSX.Element} Styled checkbox element with optional label
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // Basic checkbox
|
|
21
|
+
* <Checkbox
|
|
22
|
+
* checked={isChecked}
|
|
23
|
+
* onChange={(checked) => setIsChecked(checked)}
|
|
24
|
+
* />
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* // Checkbox with label
|
|
28
|
+
* <Checkbox
|
|
29
|
+
* label="I agree to the terms"
|
|
30
|
+
* checked={agreed}
|
|
31
|
+
* onChange={(checked) => setAgreed(checked)}
|
|
32
|
+
* />
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // Disabled checkbox
|
|
36
|
+
* <Checkbox
|
|
37
|
+
* label="Unavailable option"
|
|
38
|
+
* checked={false}
|
|
39
|
+
* disabled={true}
|
|
40
|
+
* />
|
|
41
|
+
*/
|
|
42
|
+
export default function Checkbox({
|
|
43
|
+
checked = false,
|
|
44
|
+
onChange,
|
|
45
|
+
label,
|
|
46
|
+
disabled = false,
|
|
47
|
+
className = "",
|
|
48
|
+
style = {},
|
|
49
|
+
...props
|
|
50
|
+
}) {
|
|
51
|
+
const handleChange = () => {
|
|
52
|
+
if (!disabled && onChange) {
|
|
53
|
+
onChange(!checked);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<label
|
|
59
|
+
className={`neptune-checkbox ${checked ? "neptune-checkbox--checked" : ""} ${
|
|
60
|
+
disabled ? "neptune-checkbox--disabled" : ""
|
|
61
|
+
} ${className}`}
|
|
62
|
+
style={style}
|
|
63
|
+
{...props}
|
|
64
|
+
>
|
|
65
|
+
<div className="neptune-checkbox__box">
|
|
66
|
+
{checked && (
|
|
67
|
+
<Check
|
|
68
|
+
size={16}
|
|
69
|
+
strokeWidth={3}
|
|
70
|
+
className="neptune-checkbox__icon"
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{label && (
|
|
76
|
+
<span className="neptune-checkbox__label">
|
|
77
|
+
{label}
|
|
78
|
+
</span>
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
<input
|
|
82
|
+
type="checkbox"
|
|
83
|
+
checked={checked}
|
|
84
|
+
onChange={handleChange}
|
|
85
|
+
disabled={disabled}
|
|
86
|
+
className="neptune-checkbox__input"
|
|
87
|
+
/>
|
|
88
|
+
</label>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import Button from "./Button.jsx";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FilterBar — Horizontal row of filter pill buttons
|
|
5
|
+
*
|
|
6
|
+
* Styles are in neptune-components.css. Only pass `style` for truly dynamic overrides.
|
|
7
|
+
*
|
|
8
|
+
* Renders an array of filter labels as Button components. The currently active
|
|
9
|
+
* filter uses the "secondary" variant; inactive filters use "tertiary". All
|
|
10
|
+
* buttons are size="sm". Calls `onChange` with the selected filter string
|
|
11
|
+
* when clicked.
|
|
12
|
+
*
|
|
13
|
+
* Theme-aware: inherits theme from parent container via Button's theme support.
|
|
14
|
+
*
|
|
15
|
+
* @component
|
|
16
|
+
* @param {Object} props - Component props
|
|
17
|
+
* @param {string[]} [props.filters=[]] - Array of filter label strings
|
|
18
|
+
* @param {string} [props.active] - Currently active filter value
|
|
19
|
+
* @param {Function} [props.onChange] - Called with the selected filter string
|
|
20
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
21
|
+
* @param {React.CSSProperties} [props.style] - Additional inline styles
|
|
22
|
+
* @returns {JSX.Element} FilterBar component
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* <FilterBar
|
|
26
|
+
* filters={["All", "Published", "Draft", "Archived"]}
|
|
27
|
+
* active="All"
|
|
28
|
+
* onChange={(filter) => console.log("Selected:", filter)}
|
|
29
|
+
* />
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // Inside a Naia-themed container
|
|
33
|
+
* <div data-theme="naia">
|
|
34
|
+
* <FilterBar filters={["Recent", "Starred"]} active="Recent" onChange={setFilter} />
|
|
35
|
+
* </div>
|
|
36
|
+
*/
|
|
37
|
+
export default function FilterBar({
|
|
38
|
+
filters = [],
|
|
39
|
+
active,
|
|
40
|
+
onChange,
|
|
41
|
+
className = "",
|
|
42
|
+
style = {},
|
|
43
|
+
}) {
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
className={`neptune-filter-bar ${className}`}
|
|
47
|
+
style={style}
|
|
48
|
+
role="group"
|
|
49
|
+
aria-label="Filters"
|
|
50
|
+
>
|
|
51
|
+
{filters.map((filter) => (
|
|
52
|
+
<Button
|
|
53
|
+
key={filter}
|
|
54
|
+
variant={filter === active ? "secondary" : "tertiary"}
|
|
55
|
+
size="sm"
|
|
56
|
+
onClick={() => onChange && onChange(filter)}
|
|
57
|
+
aria-pressed={filter === active}
|
|
58
|
+
>
|
|
59
|
+
{filter}
|
|
60
|
+
</Button>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IconButton — Icon-only button for toolbars, headers, and compact actions
|
|
3
|
+
*
|
|
4
|
+
* Styles are in neptune-components.css. Only pass `style` for truly dynamic overrides.
|
|
5
|
+
*
|
|
6
|
+
* @component
|
|
7
|
+
* @param {Object} props
|
|
8
|
+
* @param {React.ReactNode} props.icon - Icon element (required)
|
|
9
|
+
* @param {string} [props.variant='subtle'] - 'subtle' | 'outline' | 'secondary' | 'ghost'
|
|
10
|
+
* @param {string} [props.size='default'] - 'sm' (24px) | 'default' (32px)
|
|
11
|
+
* @param {boolean} [props.disabled=false]
|
|
12
|
+
* @param {string} [props.title] - Tooltip / aria-label
|
|
13
|
+
* @param {Function} [props.onClick]
|
|
14
|
+
* @param {string} [props.className]
|
|
15
|
+
* @param {Object} [props.style] - Dynamic inline overrides only
|
|
16
|
+
*/
|
|
17
|
+
export default function IconButton({
|
|
18
|
+
icon,
|
|
19
|
+
variant = "subtle",
|
|
20
|
+
size = "default",
|
|
21
|
+
disabled = false,
|
|
22
|
+
title,
|
|
23
|
+
onClick,
|
|
24
|
+
className = "",
|
|
25
|
+
style = {},
|
|
26
|
+
...props
|
|
27
|
+
}) {
|
|
28
|
+
return (
|
|
29
|
+
<button
|
|
30
|
+
className={`neptune-icon-button neptune-icon-button--${variant} neptune-icon-button--${size} ${
|
|
31
|
+
disabled ? "neptune-icon-button--disabled" : ""
|
|
32
|
+
} ${className}`}
|
|
33
|
+
style={style}
|
|
34
|
+
disabled={disabled}
|
|
35
|
+
title={title}
|
|
36
|
+
onClick={onClick}
|
|
37
|
+
aria-label={title}
|
|
38
|
+
{...props}
|
|
39
|
+
>
|
|
40
|
+
{icon}
|
|
41
|
+
</button>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IconToggle — Stateful icon button that toggles between pressed/unpressed
|
|
3
|
+
*
|
|
4
|
+
* Styles are in neptune-components.css. Only pass `style` for truly dynamic overrides.
|
|
5
|
+
*
|
|
6
|
+
* @component
|
|
7
|
+
* @param {Object} props
|
|
8
|
+
* @param {React.ReactNode} props.icon - Icon element (required)
|
|
9
|
+
* @param {boolean} [props.pressed=false] - Whether pressed/active
|
|
10
|
+
* @param {Function} [props.onToggle] - Toggle handler, receives new boolean
|
|
11
|
+
* @param {string} [props.size='default'] - 'sm' (24px) | 'default' (32px)
|
|
12
|
+
* @param {boolean} [props.disabled=false]
|
|
13
|
+
* @param {string} [props.title] - Tooltip / aria-label
|
|
14
|
+
* @param {string} [props.className]
|
|
15
|
+
* @param {Object} [props.style] - Dynamic inline overrides only
|
|
16
|
+
*/
|
|
17
|
+
export default function IconToggle({
|
|
18
|
+
icon,
|
|
19
|
+
pressed = false,
|
|
20
|
+
onToggle,
|
|
21
|
+
size = "default",
|
|
22
|
+
disabled = false,
|
|
23
|
+
title,
|
|
24
|
+
className = "",
|
|
25
|
+
style = {},
|
|
26
|
+
...props
|
|
27
|
+
}) {
|
|
28
|
+
return (
|
|
29
|
+
<button
|
|
30
|
+
className={`neptune-icon-toggle neptune-icon-toggle--${size} ${pressed ? "neptune-icon-toggle--pressed" : ""} ${
|
|
31
|
+
disabled ? "neptune-icon-toggle--disabled" : ""
|
|
32
|
+
} ${className}`}
|
|
33
|
+
style={style}
|
|
34
|
+
disabled={disabled}
|
|
35
|
+
title={title}
|
|
36
|
+
onClick={() => !disabled && onToggle && onToggle(!pressed)}
|
|
37
|
+
aria-pressed={pressed}
|
|
38
|
+
aria-label={title}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
{icon}
|
|
42
|
+
</button>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from "react";
|
|
2
|
+
import PillSelect from "./PillSelect.jsx";
|
|
3
|
+
import NaiaSendButton from "./NaiaSendButton.jsx";
|
|
4
|
+
import SuggestionPill from "./SuggestionPill.jsx";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* NaiaChatInput — Full chat input area with suggestions, pill selects, and send button
|
|
8
|
+
*
|
|
9
|
+
* Styles are in neptune-components.css. Only pass `style` for truly dynamic overrides.
|
|
10
|
+
*
|
|
11
|
+
* A themed chat input that composes PillSelect, SuggestionPill, and NaiaSendButton
|
|
12
|
+
* into a cohesive input area for Naia AI panels. The textarea auto-resizes as the
|
|
13
|
+
* user types. Suggestion pills render above the container. Mode, model, and autonomy
|
|
14
|
+
* PillSelects render in the footer alongside the send button.
|
|
15
|
+
*
|
|
16
|
+
* Theme-aware: accent colors adapt automatically in Naia context.
|
|
17
|
+
*
|
|
18
|
+
* @component
|
|
19
|
+
* @param {Object} props
|
|
20
|
+
* @param {string} [props.value=''] - Controlled textarea value
|
|
21
|
+
* @param {Function} [props.onChange] - Called with new string value on input
|
|
22
|
+
* @param {Function} [props.onSend] - Called with trimmed string when user sends (Enter or button)
|
|
23
|
+
* @param {string} [props.placeholder='Ask Naia...'] - Placeholder text
|
|
24
|
+
* @param {string[]} [props.suggestions] - Suggestion pill labels shown above the input
|
|
25
|
+
* @param {Function} [props.onSuggestionClick] - Called with the suggestion string when clicked
|
|
26
|
+
* @param {{value: string, options: Array, onChange: Function}} [props.mode] - Mode PillSelect config
|
|
27
|
+
* @param {{value: string, options: Array, onChange: Function}} [props.model] - Model PillSelect config
|
|
28
|
+
* @param {{value: string, options: Array, onChange: Function}} [props.autonomy] - Autonomy PillSelect config
|
|
29
|
+
* @param {React.ReactNode} [props.modeIcon] - Icon for the mode PillSelect trigger (e.g. NaiaLogo)
|
|
30
|
+
* @param {string} [props.className]
|
|
31
|
+
* @param {React.CSSProperties} [props.style]
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* <NaiaChatInput
|
|
35
|
+
* value={msg}
|
|
36
|
+
* onChange={setMsg}
|
|
37
|
+
* onSend={(text) => send(text)}
|
|
38
|
+
* placeholder="Plan, Build, / for commands..."
|
|
39
|
+
* suggestions={["Create a Fiori app", "Check agent errors"]}
|
|
40
|
+
* onSuggestionClick={(s) => send(s)}
|
|
41
|
+
* mode={{ value: "code", options: modeOpts, onChange: setMode }}
|
|
42
|
+
* model={{ value: "opus", options: modelOpts, onChange: setModel }}
|
|
43
|
+
* autonomy={{ value: "balanced", options: autoOpts, onChange: setAutonomy }}
|
|
44
|
+
* modeIcon={<NaiaLogo size={12} />}
|
|
45
|
+
* />
|
|
46
|
+
*/
|
|
47
|
+
export default function NaiaChatInput({
|
|
48
|
+
value: controlledValue,
|
|
49
|
+
onChange,
|
|
50
|
+
onSend,
|
|
51
|
+
placeholder = "Ask Naia...",
|
|
52
|
+
suggestions,
|
|
53
|
+
onSuggestionClick,
|
|
54
|
+
mode,
|
|
55
|
+
model,
|
|
56
|
+
autonomy,
|
|
57
|
+
modeIcon,
|
|
58
|
+
className = "",
|
|
59
|
+
style = {},
|
|
60
|
+
}) {
|
|
61
|
+
const [internalValue, setInternalValue] = useState("");
|
|
62
|
+
const [focused, setFocused] = useState(false);
|
|
63
|
+
const textareaRef = useRef(null);
|
|
64
|
+
|
|
65
|
+
const value = controlledValue !== undefined ? controlledValue : internalValue;
|
|
66
|
+
|
|
67
|
+
const updateValue = useCallback(
|
|
68
|
+
(newVal) => {
|
|
69
|
+
if (onChange) onChange(newVal);
|
|
70
|
+
else setInternalValue(newVal);
|
|
71
|
+
},
|
|
72
|
+
[onChange]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Auto-resize textarea (truly dynamic — must stay as inline style)
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const el = textareaRef.current;
|
|
78
|
+
if (el) {
|
|
79
|
+
el.style.height = "auto";
|
|
80
|
+
el.style.height = Math.min(el.scrollHeight, 140) + "px";
|
|
81
|
+
}
|
|
82
|
+
}, [value]);
|
|
83
|
+
|
|
84
|
+
const handleKeyDown = (e) => {
|
|
85
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
handleSend();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleSend = () => {
|
|
92
|
+
const trimmed = value.trim();
|
|
93
|
+
if (trimmed && onSend) {
|
|
94
|
+
onSend(trimmed);
|
|
95
|
+
updateValue("");
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const hasContent = value.trim().length > 0;
|
|
100
|
+
const hasSuggestions = suggestions && suggestions.length > 0;
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div data-theme="naia" className={`neptune-naia-chat-input-area ${className}`}>
|
|
104
|
+
{/* Suggestion pills row */}
|
|
105
|
+
{hasSuggestions && (
|
|
106
|
+
<div className="neptune-naia-chat-input__suggestions">
|
|
107
|
+
{suggestions.map((label, i) => (
|
|
108
|
+
<SuggestionPill
|
|
109
|
+
key={i}
|
|
110
|
+
onClick={() => onSuggestionClick && onSuggestionClick(label)}
|
|
111
|
+
>
|
|
112
|
+
{label}
|
|
113
|
+
</SuggestionPill>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{/* Rounded container */}
|
|
119
|
+
<div
|
|
120
|
+
className={`neptune-naia-chat-input__container ${focused ? "neptune-naia-chat-input__container--focused" : ""}`}
|
|
121
|
+
style={style}
|
|
122
|
+
>
|
|
123
|
+
{/* Textarea */}
|
|
124
|
+
<div className="neptune-naia-chat-input__textarea-wrap">
|
|
125
|
+
<textarea
|
|
126
|
+
ref={textareaRef}
|
|
127
|
+
value={value}
|
|
128
|
+
onChange={(e) => updateValue(e.target.value)}
|
|
129
|
+
onKeyDown={handleKeyDown}
|
|
130
|
+
onFocus={() => setFocused(true)}
|
|
131
|
+
onBlur={() => setFocused(false)}
|
|
132
|
+
placeholder={placeholder}
|
|
133
|
+
rows={1}
|
|
134
|
+
className="neptune-naia-chat-input__textarea"
|
|
135
|
+
aria-label={placeholder}
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{/* Footer row: pill selects + send button */}
|
|
140
|
+
<div className="neptune-naia-chat-input__footer">
|
|
141
|
+
{mode && (
|
|
142
|
+
<PillSelect
|
|
143
|
+
value={mode.value}
|
|
144
|
+
options={mode.options}
|
|
145
|
+
onChange={mode.onChange}
|
|
146
|
+
icon={modeIcon}
|
|
147
|
+
/>
|
|
148
|
+
)}
|
|
149
|
+
{model && (
|
|
150
|
+
<PillSelect
|
|
151
|
+
value={model.value}
|
|
152
|
+
options={model.options}
|
|
153
|
+
onChange={model.onChange}
|
|
154
|
+
/>
|
|
155
|
+
)}
|
|
156
|
+
{autonomy && (
|
|
157
|
+
<PillSelect
|
|
158
|
+
value={autonomy.value}
|
|
159
|
+
options={autonomy.options}
|
|
160
|
+
onChange={autonomy.onChange}
|
|
161
|
+
/>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
<NaiaSendButton
|
|
165
|
+
onClick={handleSend}
|
|
166
|
+
disabled={!hasContent}
|
|
167
|
+
style={{ marginLeft: "auto" }}
|
|
168
|
+
/>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Send } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* NaiaSendButton — Circular send button for chat inputs
|
|
5
|
+
*
|
|
6
|
+
* Styles are in neptune-components.css. The `size` prop is dynamic (arbitrary px)
|
|
7
|
+
* so it remains as an inline style.
|
|
8
|
+
*
|
|
9
|
+
* @component
|
|
10
|
+
* @param {Object} props
|
|
11
|
+
* @param {Function} [props.onClick]
|
|
12
|
+
* @param {boolean} [props.disabled=false]
|
|
13
|
+
* @param {number} [props.size=28] - Button diameter in px
|
|
14
|
+
* @param {string} [props.className]
|
|
15
|
+
* @param {Object} [props.style] - Dynamic inline overrides only
|
|
16
|
+
*/
|
|
17
|
+
export default function NaiaSendButton({
|
|
18
|
+
onClick,
|
|
19
|
+
disabled = false,
|
|
20
|
+
size = 28,
|
|
21
|
+
className = "",
|
|
22
|
+
style = {},
|
|
23
|
+
}) {
|
|
24
|
+
return (
|
|
25
|
+
<button
|
|
26
|
+
className={`neptune-naia-send-button ${className}`}
|
|
27
|
+
style={{ width: `${size}px`, height: `${size}px`, ...style }}
|
|
28
|
+
onClick={!disabled ? onClick : undefined}
|
|
29
|
+
disabled={disabled}
|
|
30
|
+
aria-label="Send message"
|
|
31
|
+
title="Send message"
|
|
32
|
+
>
|
|
33
|
+
<Send size={13} />
|
|
34
|
+
</button>
|
|
35
|
+
);
|
|
36
|
+
}
|