@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,89 @@
|
|
|
1
|
+
import Button from "../inputs/Button.jsx";
|
|
2
|
+
import IconButton from "../inputs/IconButton.jsx";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Toolbar component - Application toolbar with actions
|
|
6
|
+
*
|
|
7
|
+
* Styles are in neptune-components.css. Only pass `style` for truly dynamic overrides.
|
|
8
|
+
*
|
|
9
|
+
* Displays a labeled toolbar with optional badge and action buttons.
|
|
10
|
+
* Uses Button and IconButton from the design system for all actions.
|
|
11
|
+
*
|
|
12
|
+
* @component
|
|
13
|
+
* @param {Object} props - Component props
|
|
14
|
+
* @param {string} [props.label="Application"] - Toolbar label text
|
|
15
|
+
* @param {Object} [props.badge] - Badge object with {text} properties
|
|
16
|
+
* @param {string} [props.badge.text] - Badge text content
|
|
17
|
+
* @param {Array} [props.actions=[]] - Array of action objects
|
|
18
|
+
* @param {string} [props.actions[].label] - Action button label (if absent, renders icon-only)
|
|
19
|
+
* @param {React.ReactNode} [props.actions[].icon] - Icon component for button
|
|
20
|
+
* @param {string} [props.actions[].variant] - Button variant: 'primary' | 'secondary' | 'tertiary' | 'icon' | 'icon-outline'
|
|
21
|
+
* @param {Function} [props.actions[].onClick] - Click handler for action
|
|
22
|
+
* @param {React.ReactNode} [props.left] - Custom left-side content (overrides label+badge)
|
|
23
|
+
* @param {React.ReactNode} [props.right] - Custom right-side content (overrides actions)
|
|
24
|
+
* @param {React.ReactNode} [props.children] - Additional content
|
|
25
|
+
* @returns {JSX.Element} Toolbar component
|
|
26
|
+
*/
|
|
27
|
+
export default function Toolbar({
|
|
28
|
+
label = "Application",
|
|
29
|
+
badge,
|
|
30
|
+
actions = [],
|
|
31
|
+
left,
|
|
32
|
+
right,
|
|
33
|
+
children,
|
|
34
|
+
}) {
|
|
35
|
+
return (
|
|
36
|
+
<div className="neptune-toolbar">
|
|
37
|
+
{/* Left side */}
|
|
38
|
+
{left || (
|
|
39
|
+
<div className="neptune-toolbar__left">
|
|
40
|
+
<span className="neptune-toolbar__label">
|
|
41
|
+
{label}
|
|
42
|
+
</span>
|
|
43
|
+
{badge && (
|
|
44
|
+
<span className="neptune-toolbar__badge">
|
|
45
|
+
{badge.text}
|
|
46
|
+
</span>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
|
|
51
|
+
{/* Middle */}
|
|
52
|
+
{children}
|
|
53
|
+
|
|
54
|
+
{/* Right side */}
|
|
55
|
+
{right || (
|
|
56
|
+
<div className="neptune-toolbar__right">
|
|
57
|
+
{actions.map((action, index) => {
|
|
58
|
+
const r6 = { borderRadius: "6px" };
|
|
59
|
+
if (action.variant === "icon" || action.variant === "icon-outline") {
|
|
60
|
+
return (
|
|
61
|
+
<IconButton
|
|
62
|
+
key={index}
|
|
63
|
+
variant={action.variant === "icon-outline" ? "outline" : "subtle"}
|
|
64
|
+
icon={action.icon}
|
|
65
|
+
title={action.label}
|
|
66
|
+
size="sm"
|
|
67
|
+
onClick={action.onClick}
|
|
68
|
+
style={r6}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return (
|
|
73
|
+
<Button
|
|
74
|
+
key={index}
|
|
75
|
+
variant={action.variant || "secondary"}
|
|
76
|
+
size="sm"
|
|
77
|
+
icon={action.icon}
|
|
78
|
+
onClick={action.onClick}
|
|
79
|
+
style={r6}
|
|
80
|
+
>
|
|
81
|
+
{action.label}
|
|
82
|
+
</Button>
|
|
83
|
+
);
|
|
84
|
+
})}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Breadcrumb — Navigation breadcrumb trail
|
|
3
|
+
*
|
|
4
|
+
* Styles are in neptune-components.css. Only pass `style` for truly dynamic overrides.
|
|
5
|
+
*
|
|
6
|
+
* @component
|
|
7
|
+
* @param {Object} props
|
|
8
|
+
* @param {Array<{label: string, icon?: React.ReactNode, onClick?: Function}>} props.items - Breadcrumb items, last one is active
|
|
9
|
+
* @param {string} [props.className]
|
|
10
|
+
* @param {Object} [props.style]
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* <Breadcrumb items={[
|
|
14
|
+
* { label: "Cockpit", onClick: () => navigate("/") },
|
|
15
|
+
* { label: "Application", icon: <FolderOpen size={13} /> },
|
|
16
|
+
* ]} />
|
|
17
|
+
*/
|
|
18
|
+
export default function Breadcrumb({
|
|
19
|
+
items = [],
|
|
20
|
+
className = "",
|
|
21
|
+
style = {},
|
|
22
|
+
}) {
|
|
23
|
+
return (
|
|
24
|
+
<nav className={`neptune-breadcrumb ${className}`} style={style} aria-label="Breadcrumb">
|
|
25
|
+
{items.map((item, i) => {
|
|
26
|
+
const isLast = i === items.length - 1;
|
|
27
|
+
return (
|
|
28
|
+
<span key={i} style={{ display: "contents" }}>
|
|
29
|
+
{i > 0 && <span className="neptune-breadcrumb__separator">/</span>}
|
|
30
|
+
<button
|
|
31
|
+
className={`neptune-breadcrumb__item ${isLast ? "neptune-breadcrumb__item--active" : ""}`}
|
|
32
|
+
onClick={!isLast ? item.onClick : undefined}
|
|
33
|
+
aria-current={isLast ? "page" : undefined}
|
|
34
|
+
>
|
|
35
|
+
{item.icon}
|
|
36
|
+
<span>{item.label}</span>
|
|
37
|
+
</button>
|
|
38
|
+
</span>
|
|
39
|
+
);
|
|
40
|
+
})}
|
|
41
|
+
</nav>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Check } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dropdown component for menu selection with check mark on active item
|
|
5
|
+
* Displays a list of selectable items with visual indication of the selected item.
|
|
6
|
+
* Supports both simple string items and rich object items with subtitles and extras.
|
|
7
|
+
*
|
|
8
|
+
* Styles are in neptune-components.css. Only pass `style` for truly dynamic overrides.
|
|
9
|
+
*
|
|
10
|
+
* @component
|
|
11
|
+
* @param {Object} props - Component props
|
|
12
|
+
* @param {Array<string | {id: string, label: string, subtitle?: string, extra?: React.ReactNode}>} props.items - Array of item labels (strings) or rich item objects
|
|
13
|
+
* @param {number} [props.selectedIndex=0] - Index of the currently selected item
|
|
14
|
+
* @param {Function} props.onSelect - Callback fired when an item is selected, receives the item index
|
|
15
|
+
* @param {Function} [props.renderItem] - Custom render function: (item, index, isSelected) => ReactNode. Overrides default rendering when provided.
|
|
16
|
+
* @param {boolean} [props.open=true] - Whether the dropdown is visible
|
|
17
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
18
|
+
* @param {Object} [props.style] - Inline styles
|
|
19
|
+
* @returns {JSX.Element} Styled dropdown menu
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // Basic dropdown with strings (backward compatible)
|
|
23
|
+
* const [selected, setSelected] = useState(0);
|
|
24
|
+
* <Dropdown
|
|
25
|
+
* items={['Option A', 'Option B', 'Option C']}
|
|
26
|
+
* selectedIndex={selected}
|
|
27
|
+
* onSelect={setSelected}
|
|
28
|
+
* />
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* // Rich items with subtitles
|
|
32
|
+
* <Dropdown
|
|
33
|
+
* items={[
|
|
34
|
+
* { id: 'a', label: 'Option A', subtitle: 'Description of A' },
|
|
35
|
+
* { id: 'b', label: 'Option B', subtitle: 'Description of B', extra: <Badge label="New" /> },
|
|
36
|
+
* ]}
|
|
37
|
+
* selectedIndex={0}
|
|
38
|
+
* onSelect={setSelected}
|
|
39
|
+
* />
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* // Custom renderItem
|
|
43
|
+
* <Dropdown
|
|
44
|
+
* items={myItems}
|
|
45
|
+
* selectedIndex={0}
|
|
46
|
+
* onSelect={setSelected}
|
|
47
|
+
* renderItem={(item, index, isSelected) => (
|
|
48
|
+
* <div>{item.label} {isSelected && '(selected)'}</div>
|
|
49
|
+
* )}
|
|
50
|
+
* />
|
|
51
|
+
*/
|
|
52
|
+
export default function Dropdown({
|
|
53
|
+
items = [],
|
|
54
|
+
selectedIndex = 0,
|
|
55
|
+
onSelect,
|
|
56
|
+
renderItem,
|
|
57
|
+
open = true,
|
|
58
|
+
className = "",
|
|
59
|
+
style = {},
|
|
60
|
+
...props
|
|
61
|
+
}) {
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
className={`neptune-dropdown ${!open ? "neptune-dropdown--closed" : ""} ${className}`}
|
|
65
|
+
style={style}
|
|
66
|
+
{...props}
|
|
67
|
+
>
|
|
68
|
+
{items.map((item, index) => {
|
|
69
|
+
const isSelected = index === selectedIndex;
|
|
70
|
+
const isString = typeof item === "string";
|
|
71
|
+
const label = isString ? item : item.label;
|
|
72
|
+
const subtitle = isString ? undefined : item.subtitle;
|
|
73
|
+
const extra = isString ? undefined : item.extra;
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<button
|
|
77
|
+
key={isString ? index : (item.id || index)}
|
|
78
|
+
className={`neptune-dropdown__item ${isSelected ? "neptune-dropdown__item--selected" : ""}`}
|
|
79
|
+
onClick={() => onSelect && onSelect(index)}
|
|
80
|
+
>
|
|
81
|
+
{renderItem ? (
|
|
82
|
+
renderItem(item, index, isSelected)
|
|
83
|
+
) : (
|
|
84
|
+
<>
|
|
85
|
+
<div className="neptune-dropdown__item-content">
|
|
86
|
+
<span className="neptune-dropdown__item-label">{label}</span>
|
|
87
|
+
{subtitle && <span className="neptune-dropdown__item-subtitle">{subtitle}</span>}
|
|
88
|
+
</div>
|
|
89
|
+
<div className="neptune-dropdown__item-right">
|
|
90
|
+
{extra && <span className="neptune-dropdown__item-extra">{extra}</span>}
|
|
91
|
+
{isSelected && (
|
|
92
|
+
<span className="neptune-dropdown__item-check">
|
|
93
|
+
<Check size={16} />
|
|
94
|
+
</span>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
</>
|
|
98
|
+
)}
|
|
99
|
+
</button>
|
|
100
|
+
);
|
|
101
|
+
})}
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SidebarNav component for main navigation sidebar with expanded and collapsed modes
|
|
3
|
+
* Supports both icon + label display (expanded) and icon-only (collapsed) layouts.
|
|
4
|
+
* Items can include dividers via `{type: "divider"}` to visually separate nav sections.
|
|
5
|
+
* Optional header and footer props for logo/branding and bottom-anchored controls.
|
|
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 {Array<{label: string, icon: React.ReactNode, active?: boolean} | {type: "divider"}>} props.items - Navigation items with labels and icons, or divider objects
|
|
12
|
+
* @param {boolean} [props.collapsed=false] - Whether sidebar is in collapsed mode (icon-only)
|
|
13
|
+
* @param {Function} [props.onItemClick] - Callback fired when a nav item is clicked, receives (item, index)
|
|
14
|
+
* @param {React.ReactNode} [props.header] - Content rendered at the top of the nav (e.g. logo area)
|
|
15
|
+
* @param {React.ReactNode} [props.footer] - Content rendered at the bottom of the nav (e.g. settings, collapse toggle)
|
|
16
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
17
|
+
* @param {Object} [props.style] - Inline styles
|
|
18
|
+
* @returns {JSX.Element} Styled sidebar navigation component
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // With header and footer
|
|
22
|
+
* <SidebarNav
|
|
23
|
+
* items={[
|
|
24
|
+
* { label: 'Home', icon: <HomeIcon />, active: true },
|
|
25
|
+
* { label: 'Projects', icon: <AppWindowIcon /> },
|
|
26
|
+
* { type: 'divider' },
|
|
27
|
+
* { label: 'Settings', icon: <SettingsIcon /> },
|
|
28
|
+
* ]}
|
|
29
|
+
* header={<div className="logo-area">Neptune</div>}
|
|
30
|
+
* footer={<button>Collapse</button>}
|
|
31
|
+
* collapsed={false}
|
|
32
|
+
* onItemClick={(item, index) => console.log(item.label)}
|
|
33
|
+
* />
|
|
34
|
+
*/
|
|
35
|
+
export default function SidebarNav({
|
|
36
|
+
items = [],
|
|
37
|
+
collapsed = false,
|
|
38
|
+
onItemClick,
|
|
39
|
+
header,
|
|
40
|
+
footer,
|
|
41
|
+
className = "",
|
|
42
|
+
style = {},
|
|
43
|
+
...props
|
|
44
|
+
}) {
|
|
45
|
+
return (
|
|
46
|
+
<nav
|
|
47
|
+
className={`neptune-sidebar-nav ${collapsed ? "neptune-sidebar-nav--collapsed" : ""} ${className}`}
|
|
48
|
+
style={style}
|
|
49
|
+
{...props}
|
|
50
|
+
>
|
|
51
|
+
{header && (
|
|
52
|
+
<div className="neptune-sidebar-nav__header">{header}</div>
|
|
53
|
+
)}
|
|
54
|
+
<div className="neptune-sidebar-nav__items">
|
|
55
|
+
{items.map((item, index) => {
|
|
56
|
+
if (item.type === "divider") {
|
|
57
|
+
return <div key={`divider-${index}`} className="neptune-sidebar-nav__divider" />;
|
|
58
|
+
}
|
|
59
|
+
const isActive = item.active ?? false;
|
|
60
|
+
return (
|
|
61
|
+
<button
|
|
62
|
+
key={index}
|
|
63
|
+
className={`neptune-sidebar-nav__item ${isActive ? "neptune-sidebar-nav__item--active" : ""}`}
|
|
64
|
+
onClick={() => onItemClick && onItemClick(item, index)}
|
|
65
|
+
title={collapsed ? item.label : undefined}
|
|
66
|
+
>
|
|
67
|
+
<span className="neptune-sidebar-nav__icon">
|
|
68
|
+
{item.icon}
|
|
69
|
+
</span>
|
|
70
|
+
{!collapsed && (
|
|
71
|
+
<span className="neptune-sidebar-nav__label">{item.label}</span>
|
|
72
|
+
)}
|
|
73
|
+
</button>
|
|
74
|
+
);
|
|
75
|
+
})}
|
|
76
|
+
</div>
|
|
77
|
+
{footer && (
|
|
78
|
+
<div className="neptune-sidebar-nav__footer">{footer}</div>
|
|
79
|
+
)}
|
|
80
|
+
</nav>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SidebarTabs component for tab switching within sidebar panels
|
|
3
|
+
* Displays tabs in horizontal pill-style layout when expanded,
|
|
4
|
+
* and as a vertical icon column when collapsed.
|
|
5
|
+
* Tabs can be simple strings or objects with icon, label, activeColor, and badge.
|
|
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 {Array<string | {label: string, icon?: React.ReactNode, activeColor?: string, badge?: React.ReactNode}>} props.tabs
|
|
12
|
+
* Array of tab labels (strings) or objects with label, optional icon, optional activeColor, optional badge
|
|
13
|
+
* @param {number} [props.activeIndex=0] - Index of the currently active tab
|
|
14
|
+
* @param {Function} props.onChange - Callback fired when a tab is clicked, receives the new index
|
|
15
|
+
* @param {boolean} [props.collapsed=false] - Whether sidebar is in collapsed mode (icon-only vertical)
|
|
16
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
17
|
+
* @param {Object} [props.style] - Inline styles
|
|
18
|
+
* @returns {JSX.Element} Styled sidebar tab switcher
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // String tabs (backward compatible)
|
|
22
|
+
* const [activeTab, setActiveTab] = useState(0);
|
|
23
|
+
* <SidebarTabs
|
|
24
|
+
* tabs={['Files', 'Pages', 'Components']}
|
|
25
|
+
* activeIndex={activeTab}
|
|
26
|
+
* onChange={setActiveTab}
|
|
27
|
+
* collapsed={false}
|
|
28
|
+
* />
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* // Tabs with icons, custom active color, and badges
|
|
32
|
+
* <SidebarTabs
|
|
33
|
+
* tabs={[
|
|
34
|
+
* { label: 'Naia', icon: <NaiaLogo size={11} />, activeColor: 'var(--npt-accent-secondary)' },
|
|
35
|
+
* { label: 'More Agents', icon: <BotIcon size={11} /> },
|
|
36
|
+
* { label: 'Versions', icon: <HistoryIcon size={11} />, badge: '5' },
|
|
37
|
+
* ]}
|
|
38
|
+
* activeIndex={activeTab}
|
|
39
|
+
* onChange={setActiveTab}
|
|
40
|
+
* />
|
|
41
|
+
*/
|
|
42
|
+
export default function SidebarTabs({
|
|
43
|
+
tabs = [],
|
|
44
|
+
activeIndex = 0,
|
|
45
|
+
onChange,
|
|
46
|
+
collapsed = false,
|
|
47
|
+
className = "",
|
|
48
|
+
style = {},
|
|
49
|
+
...props
|
|
50
|
+
}) {
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
className={`neptune-sidebar-tabs ${collapsed ? "neptune-sidebar-tabs--collapsed" : ""} ${className}`}
|
|
54
|
+
style={style}
|
|
55
|
+
{...props}
|
|
56
|
+
>
|
|
57
|
+
{tabs.map((tab, index) => {
|
|
58
|
+
const isString = typeof tab === "string";
|
|
59
|
+
const label = isString ? tab : tab.label;
|
|
60
|
+
const icon = isString ? undefined : tab.icon;
|
|
61
|
+
const activeColor = isString ? undefined : tab.activeColor;
|
|
62
|
+
const badge = isString ? undefined : tab.badge;
|
|
63
|
+
const isActive = index === activeIndex;
|
|
64
|
+
|
|
65
|
+
// If this tab has a custom activeColor and is active, apply it to the bottom border
|
|
66
|
+
const tabStyle = isActive && activeColor
|
|
67
|
+
? { borderBottomColor: activeColor }
|
|
68
|
+
: undefined;
|
|
69
|
+
|
|
70
|
+
// For the icon, apply activeColor when active
|
|
71
|
+
const iconStyle = isActive && activeColor
|
|
72
|
+
? { color: activeColor }
|
|
73
|
+
: undefined;
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<button
|
|
77
|
+
key={index}
|
|
78
|
+
className={`neptune-sidebar-tabs__tab ${isActive ? "neptune-sidebar-tabs__tab--active" : ""}`}
|
|
79
|
+
style={tabStyle}
|
|
80
|
+
onClick={() => onChange && onChange(index)}
|
|
81
|
+
title={collapsed ? label : undefined}
|
|
82
|
+
>
|
|
83
|
+
{collapsed ? (
|
|
84
|
+
icon || label.charAt(0).toUpperCase()
|
|
85
|
+
) : (
|
|
86
|
+
<>
|
|
87
|
+
{icon && <span className="neptune-sidebar-tabs__icon" style={iconStyle}>{icon}</span>}
|
|
88
|
+
{label}
|
|
89
|
+
{badge != null && (
|
|
90
|
+
<span className="neptune-sidebar-tabs__badge">{badge}</span>
|
|
91
|
+
)}
|
|
92
|
+
</>
|
|
93
|
+
)}
|
|
94
|
+
</button>
|
|
95
|
+
);
|
|
96
|
+
})}
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TabBar component for horizontal tab navigation with underline active indicator
|
|
3
|
+
* Displays a row of tabs with visual indication of the active tab.
|
|
4
|
+
* Tabs can be simple strings or objects with icon and label.
|
|
5
|
+
*
|
|
6
|
+
* Styles are in neptune-components.css. Only pass `style` for truly dynamic overrides.
|
|
7
|
+
*
|
|
8
|
+
* @component
|
|
9
|
+
* @param {Object} props - Component props
|
|
10
|
+
* @param {Array<string | {label: string, icon?: React.ReactNode}>} props.tabs - Array of tab labels (strings) or objects with label and optional icon
|
|
11
|
+
* @param {number} [props.activeIndex=0] - Index of the currently active tab
|
|
12
|
+
* @param {Function} props.onChange - Callback fired when a tab is clicked, receives the new index
|
|
13
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
14
|
+
* @param {Object} [props.style] - Inline styles
|
|
15
|
+
* @returns {JSX.Element} Styled horizontal tab bar with underline indicator
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // Basic usage with strings (backward compatible)
|
|
19
|
+
* const [activeTab, setActiveTab] = useState(0);
|
|
20
|
+
* <TabBar tabs={['Overview', 'Details', 'Settings']} activeIndex={activeTab} onChange={setActiveTab} />
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // Tabs with icons
|
|
24
|
+
* <TabBar
|
|
25
|
+
* tabs={[
|
|
26
|
+
* { label: 'Pages', icon: <LayoutIcon size={13} /> },
|
|
27
|
+
* { label: 'Scripts', icon: <CodeIcon size={13} /> },
|
|
28
|
+
* ]}
|
|
29
|
+
* activeIndex={activeTab}
|
|
30
|
+
* onChange={setActiveTab}
|
|
31
|
+
* />
|
|
32
|
+
*/
|
|
33
|
+
export default function TabBar({
|
|
34
|
+
tabs = [],
|
|
35
|
+
activeIndex = 0,
|
|
36
|
+
onChange,
|
|
37
|
+
className = "",
|
|
38
|
+
style = {},
|
|
39
|
+
...props
|
|
40
|
+
}) {
|
|
41
|
+
return (
|
|
42
|
+
<div className={`neptune-tabbar ${className}`} style={style} {...props}>
|
|
43
|
+
{tabs.map((tab, index) => {
|
|
44
|
+
const isString = typeof tab === "string";
|
|
45
|
+
const label = isString ? tab : tab.label;
|
|
46
|
+
const icon = isString ? undefined : tab.icon;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<button
|
|
50
|
+
key={index}
|
|
51
|
+
className={`neptune-tabbar__tab ${index === activeIndex ? "neptune-tabbar__tab--active" : ""}`}
|
|
52
|
+
onClick={() => onChange && onChange(index)}
|
|
53
|
+
>
|
|
54
|
+
{icon && <span className="neptune-tabbar__icon">{icon}</span>}
|
|
55
|
+
{label}
|
|
56
|
+
</button>
|
|
57
|
+
);
|
|
58
|
+
})}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useEffect, useCallback } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Modal — General-purpose modal overlay
|
|
5
|
+
*
|
|
6
|
+
* Styles are in neptune-components.css. Only pass `style` for truly dynamic overrides.
|
|
7
|
+
*
|
|
8
|
+
* A centered overlay dialog with a backdrop, rounded panel, header with title
|
|
9
|
+
* and close button. Closes on Escape key press and backdrop click.
|
|
10
|
+
* Uses surface-overlay for background, border-default for the border, and
|
|
11
|
+
* shadow-xl for elevation.
|
|
12
|
+
*
|
|
13
|
+
* @component
|
|
14
|
+
* @param {Object} props - Component props
|
|
15
|
+
* @param {boolean} [props.open=false] - Whether the modal is visible
|
|
16
|
+
* @param {Function} [props.onClose] - Called when the modal should close (Escape, backdrop, close button)
|
|
17
|
+
* @param {string} [props.title] - Header title text
|
|
18
|
+
* @param {React.ReactNode} [props.children] - Modal body content
|
|
19
|
+
* @param {string} [props.width='480px'] - Panel width (CSS value)
|
|
20
|
+
* @param {string} [props.className] - Additional CSS classes for the panel
|
|
21
|
+
* @param {React.CSSProperties} [props.style] - Additional inline styles for the panel
|
|
22
|
+
* @returns {JSX.Element|null} Modal component or null when closed
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* <Modal open={isOpen} onClose={() => setIsOpen(false)} title="Confirm">
|
|
26
|
+
* <p>Are you sure?</p>
|
|
27
|
+
* </Modal>
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* <Modal open={true} onClose={handleClose} title="Settings" width="600px">
|
|
31
|
+
* <SettingsForm />
|
|
32
|
+
* </Modal>
|
|
33
|
+
*/
|
|
34
|
+
export default function Modal({
|
|
35
|
+
open = false,
|
|
36
|
+
onClose,
|
|
37
|
+
title,
|
|
38
|
+
children,
|
|
39
|
+
width = "480px",
|
|
40
|
+
className = "",
|
|
41
|
+
style = {},
|
|
42
|
+
}) {
|
|
43
|
+
const handleKeyDown = useCallback(
|
|
44
|
+
(e) => {
|
|
45
|
+
if (e.key === "Escape" && onClose) {
|
|
46
|
+
onClose();
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
[onClose]
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (open) {
|
|
54
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
55
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
56
|
+
}
|
|
57
|
+
}, [open, handleKeyDown]);
|
|
58
|
+
|
|
59
|
+
if (!open) return null;
|
|
60
|
+
|
|
61
|
+
const handleBackdropClick = (e) => {
|
|
62
|
+
if (e.target === e.currentTarget && onClose) {
|
|
63
|
+
onClose();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
className="neptune-modal-backdrop"
|
|
70
|
+
onClick={handleBackdropClick}
|
|
71
|
+
role="dialog"
|
|
72
|
+
aria-modal="true"
|
|
73
|
+
aria-label={title || "Dialog"}
|
|
74
|
+
>
|
|
75
|
+
<div
|
|
76
|
+
className={`neptune-modal ${className}`}
|
|
77
|
+
style={{ width, ...style }}
|
|
78
|
+
>
|
|
79
|
+
{title && (
|
|
80
|
+
<div className="neptune-modal__header">
|
|
81
|
+
<h2 className="neptune-modal__title">{title}</h2>
|
|
82
|
+
<button
|
|
83
|
+
className="neptune-modal__close"
|
|
84
|
+
onClick={onClose}
|
|
85
|
+
aria-label="Close dialog"
|
|
86
|
+
title="Close"
|
|
87
|
+
>
|
|
88
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
89
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
90
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
91
|
+
</svg>
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
<div className="neptune-modal__body">
|
|
96
|
+
{children}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|