@khal-os/ui 1.0.0 → 1.0.2

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 (59) hide show
  1. package/LICENSE +94 -0
  2. package/README.md +25 -0
  3. package/dist/index.cjs +2661 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +926 -0
  6. package/dist/index.d.ts +926 -0
  7. package/dist/index.js +2510 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +59 -40
  10. package/tokens.css +260 -238
  11. package/src/components/ContextMenu.tsx +0 -130
  12. package/src/components/avatar.tsx +0 -71
  13. package/src/components/badge.tsx +0 -39
  14. package/src/components/button.tsx +0 -102
  15. package/src/components/command.tsx +0 -165
  16. package/src/components/cost-counter.tsx +0 -75
  17. package/src/components/data-row.tsx +0 -97
  18. package/src/components/dropdown-menu.tsx +0 -233
  19. package/src/components/glass-card.tsx +0 -74
  20. package/src/components/input.tsx +0 -48
  21. package/src/components/khal-logo.tsx +0 -73
  22. package/src/components/live-feed.tsx +0 -109
  23. package/src/components/mesh-gradient.tsx +0 -57
  24. package/src/components/metric-display.tsx +0 -93
  25. package/src/components/note.tsx +0 -55
  26. package/src/components/number-flow.tsx +0 -25
  27. package/src/components/pill-badge.tsx +0 -65
  28. package/src/components/progress-bar.tsx +0 -70
  29. package/src/components/section-card.tsx +0 -76
  30. package/src/components/separator.tsx +0 -25
  31. package/src/components/spinner.tsx +0 -42
  32. package/src/components/status-dot.tsx +0 -90
  33. package/src/components/switch.tsx +0 -36
  34. package/src/components/theme-provider.tsx +0 -58
  35. package/src/components/theme-switcher.tsx +0 -59
  36. package/src/components/ticker-bar.tsx +0 -41
  37. package/src/components/tooltip.tsx +0 -62
  38. package/src/components/window-minimized-context.tsx +0 -29
  39. package/src/hooks/useReducedMotion.ts +0 -21
  40. package/src/index.ts +0 -58
  41. package/src/lib/animations.ts +0 -50
  42. package/src/primitives/collapsible-sidebar.tsx +0 -226
  43. package/src/primitives/dialog.tsx +0 -76
  44. package/src/primitives/empty-state.tsx +0 -43
  45. package/src/primitives/index.ts +0 -22
  46. package/src/primitives/list-view.tsx +0 -155
  47. package/src/primitives/property-panel.tsx +0 -108
  48. package/src/primitives/section-header.tsx +0 -19
  49. package/src/primitives/sidebar-nav.tsx +0 -110
  50. package/src/primitives/split-pane.tsx +0 -146
  51. package/src/primitives/status-badge.tsx +0 -10
  52. package/src/primitives/status-bar.tsx +0 -100
  53. package/src/primitives/toolbar.tsx +0 -152
  54. package/src/server.ts +0 -4
  55. package/src/stores/notification-store.ts +0 -271
  56. package/src/stores/theme-store.ts +0 -33
  57. package/src/tokens/lp-tokens.ts +0 -36
  58. package/src/utils.ts +0 -6
  59. package/tsconfig.json +0 -17
@@ -1,155 +0,0 @@
1
- 'use client';
2
-
3
- import { type KeyboardEvent, type ReactNode, useCallback, useEffect, useRef, useState } from 'react';
4
-
5
- // ---------------------------------------------------------------------------
6
- // ListView — selectable list with keyboard navigation.
7
- //
8
- // Usage:
9
- // <ListView
10
- // items={files}
11
- // selected={selectedId}
12
- // onSelect={setSelectedId}
13
- // onActivate={(item) => openFile(item)}
14
- // renderItem={(item, { selected, focused }) => (
15
- // <div className="flex items-center gap-2">
16
- // <FileIcon name={item.name} />
17
- // <span>{item.name}</span>
18
- // </div>
19
- // )}
20
- // getKey={(item) => item.id}
21
- // />
22
- // ---------------------------------------------------------------------------
23
-
24
- interface ListViewProps<T> {
25
- items: T[];
26
- selected?: string | string[] | null;
27
- onSelect?: (key: string | null) => void;
28
- onActivate?: (item: T) => void;
29
- renderItem: (item: T, state: { selected: boolean; focused: boolean; index: number }) => ReactNode;
30
- getKey: (item: T) => string;
31
- multiSelect?: boolean;
32
- emptyMessage?: string;
33
- className?: string;
34
- }
35
-
36
- export function ListView<T>({
37
- items,
38
- selected,
39
- onSelect,
40
- onActivate,
41
- renderItem,
42
- getKey,
43
- multiSelect = false,
44
- emptyMessage = 'No items',
45
- className = '',
46
- }: ListViewProps<T>) {
47
- const [focusIndex, setFocusIndex] = useState(0);
48
- const listRef = useRef<HTMLDivElement>(null);
49
-
50
- const selectedSet = new Set(selected == null ? [] : Array.isArray(selected) ? selected : [selected]);
51
-
52
- const clamp = useCallback((i: number) => Math.max(0, Math.min(items.length - 1, i)), [items.length]);
53
-
54
- useEffect(() => {
55
- setFocusIndex((prev) => clamp(prev));
56
- }, [items.length, clamp]);
57
-
58
- const scrollToIndex = useCallback((i: number) => {
59
- const el = listRef.current?.children[i] as HTMLElement | undefined;
60
- el?.scrollIntoView({ block: 'nearest' });
61
- }, []);
62
-
63
- const handleKeyDown = useCallback(
64
- (e: KeyboardEvent) => {
65
- switch (e.key) {
66
- case 'ArrowDown': {
67
- e.preventDefault();
68
- const next = clamp(focusIndex + 1);
69
- setFocusIndex(next);
70
- scrollToIndex(next);
71
- if (!multiSelect) onSelect?.(getKey(items[next]));
72
- break;
73
- }
74
- case 'ArrowUp': {
75
- e.preventDefault();
76
- const prev = clamp(focusIndex - 1);
77
- setFocusIndex(prev);
78
- scrollToIndex(prev);
79
- if (!multiSelect) onSelect?.(getKey(items[prev]));
80
- break;
81
- }
82
- case 'Home': {
83
- e.preventDefault();
84
- setFocusIndex(0);
85
- scrollToIndex(0);
86
- if (!multiSelect && items.length > 0) onSelect?.(getKey(items[0]));
87
- break;
88
- }
89
- case 'End': {
90
- e.preventDefault();
91
- const last = items.length - 1;
92
- setFocusIndex(last);
93
- scrollToIndex(last);
94
- if (!multiSelect && items.length > 0) onSelect?.(getKey(items[last]));
95
- break;
96
- }
97
- case 'Enter':
98
- case ' ': {
99
- e.preventDefault();
100
- const item = items[focusIndex];
101
- if (item) {
102
- onSelect?.(getKey(item));
103
- if (e.key === 'Enter') onActivate?.(item);
104
- }
105
- break;
106
- }
107
- }
108
- },
109
- [focusIndex, items, clamp, scrollToIndex, onSelect, onActivate, getKey, multiSelect]
110
- );
111
-
112
- if (items.length === 0) {
113
- return (
114
- <div className={`flex h-full items-center justify-center text-label-13 text-gray-800 ${className}`}>
115
- {emptyMessage}
116
- </div>
117
- );
118
- }
119
-
120
- return (
121
- <div
122
- ref={listRef}
123
- className={`overflow-y-auto outline-none ${className}`}
124
- role="listbox"
125
- tabIndex={0}
126
- onKeyDown={handleKeyDown}
127
- aria-multiselectable={multiSelect}
128
- >
129
- {items.map((item, i) => {
130
- const key = getKey(item);
131
- const isSelected = selectedSet.has(key);
132
- const isFocused = i === focusIndex;
133
-
134
- return (
135
- <div
136
- key={key}
137
- role="option"
138
- aria-selected={isSelected}
139
- className={`cursor-default select-none px-2 py-1 transition-colors
140
- ${isSelected ? 'bg-blue-700/15 text-gray-1000' : 'text-gray-1000'}
141
- ${isFocused && !isSelected ? 'bg-gray-alpha-100' : ''}
142
- hover:bg-gray-alpha-200`}
143
- onClick={() => {
144
- setFocusIndex(i);
145
- onSelect?.(key);
146
- }}
147
- onDoubleClick={() => onActivate?.(item)}
148
- >
149
- {renderItem(item, { selected: isSelected, focused: isFocused, index: i })}
150
- </div>
151
- );
152
- })}
153
- </div>
154
- );
155
- }
@@ -1,108 +0,0 @@
1
- 'use client';
2
-
3
- import { type ReactNode } from 'react';
4
- import { Separator } from '../components/separator';
5
-
6
- // ---------------------------------------------------------------------------
7
- // PropertyPanel — key-value inspector panel (like Finder's "Get Info").
8
- //
9
- // Usage:
10
- // <PropertyPanel title="File Info">
11
- // <PropertyPanel.Section title="General">
12
- // <PropertyPanel.Row label="Name" value="document.pdf" />
13
- // <PropertyPanel.Row label="Size" value="2.4 MB" />
14
- // <PropertyPanel.Row label="Modified" value="Feb 10, 2026" />
15
- // </PropertyPanel.Section>
16
- // <PropertyPanel.Section title="Permissions">
17
- // <PropertyPanel.Row label="Owner" value="vercel-sandbox" />
18
- // <PropertyPanel.Row label="Group" value="users" />
19
- // </PropertyPanel.Section>
20
- // </PropertyPanel>
21
- // ---------------------------------------------------------------------------
22
-
23
- interface PropertyPanelProps {
24
- title?: string;
25
- children: ReactNode;
26
- className?: string;
27
- }
28
-
29
- function PropertyPanelRoot({ title, children, className = '' }: PropertyPanelProps) {
30
- return (
31
- <div className={`flex flex-col overflow-y-auto ${className}`}>
32
- {title && (
33
- <div className="sticky top-0 z-10 border-b border-gray-alpha-200 bg-background-100 px-3 py-2">
34
- <h3 className="text-label-13 font-medium text-gray-1000">{title}</h3>
35
- </div>
36
- )}
37
- <div className="flex flex-col">{children}</div>
38
- </div>
39
- );
40
- }
41
-
42
- // ---------------------------------------------------------------------------
43
- // PropertyPanel.Section
44
- // ---------------------------------------------------------------------------
45
-
46
- function PropertySection({
47
- title,
48
- children,
49
- collapsible: _collapsible = false,
50
- defaultOpen: _defaultOpen = true,
51
- }: {
52
- title?: string;
53
- children: ReactNode;
54
- collapsible?: boolean;
55
- defaultOpen?: boolean;
56
- }) {
57
- return (
58
- <div className="border-b border-gray-alpha-200 last:border-b-0">
59
- {title && (
60
- <div className="px-3 pt-3 pb-1">
61
- <span className="text-label-13 font-medium text-gray-800">{title}</span>
62
- </div>
63
- )}
64
- <div className={`px-3 pb-2 ${title ? '' : 'pt-2'}`}>{children}</div>
65
- </div>
66
- );
67
- }
68
-
69
- // ---------------------------------------------------------------------------
70
- // PropertyPanel.Row
71
- // ---------------------------------------------------------------------------
72
-
73
- interface PropertyRowProps {
74
- label: string;
75
- value?: ReactNode;
76
- children?: ReactNode;
77
- mono?: boolean;
78
- copyable?: boolean;
79
- }
80
-
81
- function PropertyRow({ label, value, children, mono }: PropertyRowProps) {
82
- return (
83
- <div className="flex items-baseline justify-between gap-4 py-1">
84
- <dt className="shrink-0 text-label-13 text-gray-800">{label}</dt>
85
- <dd className={`min-w-0 truncate text-right text-label-13 text-gray-1000 ${mono ? 'font-mono' : ''}`}>
86
- {children ?? value}
87
- </dd>
88
- </div>
89
- );
90
- }
91
-
92
- // ---------------------------------------------------------------------------
93
- // PropertyPanel.Separator
94
- // ---------------------------------------------------------------------------
95
-
96
- function PropertySeparator() {
97
- return <Separator className="my-1" />;
98
- }
99
-
100
- // ---------------------------------------------------------------------------
101
- // Export
102
- // ---------------------------------------------------------------------------
103
-
104
- export const PropertyPanel = Object.assign(PropertyPanelRoot, {
105
- Section: PropertySection,
106
- Row: PropertyRow,
107
- Separator: PropertySeparator,
108
- });
@@ -1,19 +0,0 @@
1
- export function SectionHeader({
2
- title,
3
- description,
4
- children,
5
- }: {
6
- title: string;
7
- description?: string;
8
- children?: React.ReactNode;
9
- }) {
10
- return (
11
- <div className="mb-4 flex items-start justify-between">
12
- <div>
13
- <h2 className="text-copy-13 font-medium text-gray-1000">{title}</h2>
14
- {description && <p className="mt-0.5 text-copy-13 text-gray-900">{description}</p>}
15
- </div>
16
- {children}
17
- </div>
18
- );
19
- }
@@ -1,110 +0,0 @@
1
- 'use client';
2
-
3
- import type { ReactNode } from 'react';
4
-
5
- // ---------------------------------------------------------------------------
6
- // SidebarNav — reusable sidebar navigation for app shells.
7
- //
8
- // Extracts the repeated sidebar + tab-button pattern used across Settings,
9
- // AppStore, etc. Designed to sit inside a SplitPane.Panel.
10
- //
11
- // Usage:
12
- // <SidebarNav label="Settings" title="Settings">
13
- // <SidebarNav.Item active={tab === "a"} onClick={() => setTab("a")} icon={<Icon />}>
14
- // Appearance
15
- // </SidebarNav.Item>
16
- // </SidebarNav>
17
- //
18
- // // Multiple sections:
19
- // <SidebarNav label="App Store">
20
- // <SidebarNav.Group title="Packages">
21
- // <SidebarNav.Item ...>GUI Apps</SidebarNav.Item>
22
- // </SidebarNav.Group>
23
- // <SidebarNav.Group title="Repos">
24
- // ...
25
- // </SidebarNav.Group>
26
- // </SidebarNav>
27
- // ---------------------------------------------------------------------------
28
-
29
- interface SidebarNavProps {
30
- children: ReactNode;
31
- /** Accessible label for the <nav> element */
32
- label: string;
33
- /** Optional heading displayed at the top of the sidebar */
34
- title?: string;
35
- className?: string;
36
- }
37
-
38
- function SidebarNavRoot({ children, label, title, className = '' }: SidebarNavProps) {
39
- return (
40
- <nav className={`flex flex-col py-2 ${className}`} aria-label={label}>
41
- {title && (
42
- <div className="px-3 pb-1">
43
- <span className="text-copy-13 font-medium text-gray-800">{title}</span>
44
- </div>
45
- )}
46
- {children}
47
- </nav>
48
- );
49
- }
50
-
51
- // ---------------------------------------------------------------------------
52
- // SidebarNav.Group — optional section divider with a title
53
- // ---------------------------------------------------------------------------
54
-
55
- interface SidebarNavGroupProps {
56
- children: ReactNode;
57
- title?: string;
58
- className?: string;
59
- }
60
-
61
- function SidebarNavGroup({ children, title, className = '' }: SidebarNavGroupProps) {
62
- return (
63
- <div className={className}>
64
- {title && (
65
- <div className="px-3 pt-4 pb-1">
66
- <span className="text-copy-13 font-medium text-gray-800">{title}</span>
67
- </div>
68
- )}
69
- {children}
70
- </div>
71
- );
72
- }
73
-
74
- // ---------------------------------------------------------------------------
75
- // SidebarNav.Item — a single navigation button
76
- // ---------------------------------------------------------------------------
77
-
78
- interface SidebarNavItemProps {
79
- children: ReactNode;
80
- active?: boolean;
81
- onClick?: () => void;
82
- icon?: ReactNode;
83
- /** Trailing content (e.g. a count badge) */
84
- suffix?: ReactNode;
85
- className?: string;
86
- }
87
-
88
- function SidebarNavItem({ children, active, onClick, icon, suffix, className = '' }: SidebarNavItemProps) {
89
- return (
90
- <button
91
- onClick={onClick}
92
- className={`mx-1 flex items-center gap-2 rounded-md px-2 py-1 text-copy-13 transition-colors ${
93
- active ? 'bg-gray-alpha-200 text-gray-1000' : 'text-gray-900 hover:bg-gray-alpha-100 hover:text-gray-1000'
94
- } ${className}`}
95
- >
96
- {icon && <span className="shrink-0 text-gray-800 [&>svg]:h-3.5 [&>svg]:w-3.5">{icon}</span>}
97
- <span className="min-w-0 truncate">{children}</span>
98
- {suffix && <span className="ml-auto font-mono text-copy-13 tabular-nums text-gray-700">{suffix}</span>}
99
- </button>
100
- );
101
- }
102
-
103
- // ---------------------------------------------------------------------------
104
- // Export
105
- // ---------------------------------------------------------------------------
106
-
107
- export const SidebarNav = Object.assign(SidebarNavRoot, {
108
- Group: SidebarNavGroup,
109
- Item: SidebarNavItem,
110
- });
@@ -1,146 +0,0 @@
1
- 'use client';
2
-
3
- import { type CSSProperties, type ReactNode, useCallback, useRef, useState, useSyncExternalStore } from 'react';
4
-
5
- // ---------------------------------------------------------------------------
6
- // SplitPane — resizable two-panel layout (horizontal or vertical).
7
- //
8
- // Usage:
9
- // <SplitPane defaultSize={240} min={120} max={400}>
10
- // <SplitPane.Panel>Sidebar</SplitPane.Panel>
11
- // <SplitPane.Panel>Main</SplitPane.Panel>
12
- // </SplitPane>
13
- //
14
- // <SplitPane direction="vertical" defaultSize={200} min={100}>
15
- // <SplitPane.Panel>Top</SplitPane.Panel>
16
- // <SplitPane.Panel>Bottom</SplitPane.Panel>
17
- // </SplitPane>
18
- // ---------------------------------------------------------------------------
19
-
20
- interface SplitPaneProps {
21
- children: [ReactNode, ReactNode];
22
- direction?: 'horizontal' | 'vertical';
23
- defaultSize?: number;
24
- min?: number;
25
- max?: number;
26
- /** On narrow viewports, stack panels vertically and hide the resize handle */
27
- collapseBelow?: number;
28
- onResize?: (size: number) => void;
29
- className?: string;
30
- }
31
-
32
- function useMediaQuery(query: string) {
33
- const subscribe = useCallback(
34
- (callback: () => void) => {
35
- const mql = window.matchMedia(query);
36
- mql.addEventListener('change', callback);
37
- return () => mql.removeEventListener('change', callback);
38
- },
39
- [query]
40
- );
41
- const getSnapshot = useCallback(() => window.matchMedia(query).matches, [query]);
42
- const getServerSnapshot = useCallback(() => false, []);
43
- return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
44
- }
45
-
46
- function SplitPaneRoot({
47
- children,
48
- direction = 'horizontal',
49
- defaultSize = 240,
50
- min = 100,
51
- max = 600,
52
- collapseBelow = 640,
53
- onResize,
54
- className = '',
55
- }: SplitPaneProps) {
56
- const [size, setSize] = useState(defaultSize);
57
- const containerRef = useRef<HTMLDivElement>(null);
58
- const dragging = useRef(false);
59
- const startPos = useRef(0);
60
- const startSize = useRef(0);
61
- const isMobile = useMediaQuery(`(max-width: ${collapseBelow}px)`);
62
-
63
- const isHorizontal = direction === 'horizontal' && !isMobile;
64
- const [first, second] = children;
65
-
66
- const onPointerDown = useCallback(
67
- (e: React.PointerEvent) => {
68
- e.preventDefault();
69
- dragging.current = true;
70
- startPos.current = isHorizontal ? e.clientX : e.clientY;
71
- startSize.current = size;
72
- (e.target as HTMLElement).setPointerCapture(e.pointerId);
73
- },
74
- [isHorizontal, size]
75
- );
76
-
77
- const onPointerMove = useCallback(
78
- (e: React.PointerEvent) => {
79
- if (!dragging.current) return;
80
- const delta = (isHorizontal ? e.clientX : e.clientY) - startPos.current;
81
- const next = Math.min(max, Math.max(min, startSize.current + delta));
82
- setSize(next);
83
- onResize?.(next);
84
- },
85
- [isHorizontal, min, max, onResize]
86
- );
87
-
88
- const onPointerUp = useCallback(() => {
89
- dragging.current = false;
90
- }, []);
91
-
92
- // On mobile, stack vertically with no resize handle
93
- if (isMobile) {
94
- return (
95
- <div ref={containerRef} className={`flex flex-col h-full w-full overflow-hidden ${className}`}>
96
- <div className="shrink-0 overflow-auto border-b border-gray-alpha-200">{first}</div>
97
- <div className="flex-1 overflow-hidden">{second}</div>
98
- </div>
99
- );
100
- }
101
-
102
- const firstStyle: CSSProperties = isHorizontal
103
- ? { width: size, minWidth: min, maxWidth: max, flexShrink: 0 }
104
- : { height: size, minHeight: min, maxHeight: max, flexShrink: 0 };
105
-
106
- return (
107
- <div
108
- ref={containerRef}
109
- className={`flex ${isHorizontal ? 'flex-row' : 'flex-col'} h-full w-full overflow-hidden ${className}`}
110
- >
111
- <div style={firstStyle} className="overflow-hidden">
112
- {first}
113
- </div>
114
- <div
115
- className={`shrink-0 ${
116
- isHorizontal
117
- ? 'w-px cursor-col-resize hover:w-0.5 hover:bg-blue-700/50 active:w-0.5 active:bg-blue-700'
118
- : 'h-px cursor-row-resize hover:h-0.5 hover:bg-blue-700/50 active:h-0.5 active:bg-blue-700'
119
- } bg-gray-alpha-200 transition-colors`}
120
- onPointerDown={onPointerDown}
121
- onPointerMove={onPointerMove}
122
- onPointerUp={onPointerUp}
123
- role="separator"
124
- aria-orientation={isHorizontal ? 'vertical' : 'horizontal'}
125
- tabIndex={0}
126
- />
127
- <div className="flex-1 overflow-hidden">{second}</div>
128
- </div>
129
- );
130
- }
131
-
132
- // ---------------------------------------------------------------------------
133
- // SplitPane.Panel — thin wrapper for semantic clarity
134
- // ---------------------------------------------------------------------------
135
-
136
- function Panel({ children, className = '' }: { children: ReactNode; className?: string }) {
137
- return <div className={`h-full w-full overflow-auto ${className}`}>{children}</div>;
138
- }
139
-
140
- // ---------------------------------------------------------------------------
141
- // Export as compound component
142
- // ---------------------------------------------------------------------------
143
-
144
- export const SplitPane = Object.assign(SplitPaneRoot, {
145
- Panel,
146
- });
@@ -1,10 +0,0 @@
1
- import { Badge } from '../components/badge';
2
-
3
- export function StatusBadge({ status }: { status: string }) {
4
- const variant = status === 'active' ? 'green' : status === 'creating' ? 'amber' : status === 'error' ? 'red' : 'gray';
5
- return (
6
- <Badge variant={variant} size="sm" contrast="low">
7
- {status}
8
- </Badge>
9
- );
10
- }
@@ -1,100 +0,0 @@
1
- 'use client';
2
-
3
- import { type ReactNode } from 'react';
4
- import { Separator } from '../components/separator';
5
-
6
- // ---------------------------------------------------------------------------
7
- // StatusBar — bottom bar for app-level status, breadcrumbs, and indicators.
8
- //
9
- // Usage:
10
- // <StatusBar>
11
- // <StatusBar.Item>Ln 42, Col 18</StatusBar.Item>
12
- // <StatusBar.Separator />
13
- // <StatusBar.Item>UTF-8</StatusBar.Item>
14
- // <StatusBar.Spacer />
15
- // <StatusBar.Item icon={<CheckCircle />} variant="success">Saved</StatusBar.Item>
16
- // </StatusBar>
17
- // ---------------------------------------------------------------------------
18
-
19
- interface StatusBarProps {
20
- children: ReactNode;
21
- className?: string;
22
- }
23
-
24
- function StatusBarRoot({ children, className = '' }: StatusBarProps) {
25
- return (
26
- <div
27
- className={`flex h-6 shrink-0 items-center gap-0 border-t border-gray-alpha-200 bg-background-100 px-2 font-mono text-label-13 text-gray-900 ${className}`}
28
- role="status"
29
- >
30
- {children}
31
- </div>
32
- );
33
- }
34
-
35
- // ---------------------------------------------------------------------------
36
- // StatusBar.Item
37
- // ---------------------------------------------------------------------------
38
-
39
- interface StatusBarItemProps {
40
- children: ReactNode;
41
- icon?: ReactNode;
42
- variant?: 'default' | 'success' | 'warning' | 'error';
43
- onClick?: () => void;
44
- className?: string;
45
- }
46
-
47
- const variantColors = {
48
- default: 'text-gray-900',
49
- success: 'text-green-900',
50
- warning: 'text-amber-900',
51
- error: 'text-red-900',
52
- };
53
-
54
- function StatusBarItem({ children, icon, variant = 'default', onClick, className = '' }: StatusBarItemProps) {
55
- const classes = `inline-flex items-center gap-1 px-1.5 py-0.5 rounded-sm ${variantColors[variant]} ${
56
- onClick ? 'cursor-pointer hover:bg-gray-alpha-200 transition-colors' : ''
57
- } ${className}`;
58
-
59
- if (onClick) {
60
- return (
61
- <button className={classes} onClick={onClick}>
62
- {icon}
63
- {children}
64
- </button>
65
- );
66
- }
67
-
68
- return (
69
- <span className={classes}>
70
- {icon}
71
- {children}
72
- </span>
73
- );
74
- }
75
-
76
- // ---------------------------------------------------------------------------
77
- // StatusBar.Separator
78
- // ---------------------------------------------------------------------------
79
-
80
- function StatusBarSeparator() {
81
- return <Separator orientation="vertical" className="mx-0.5 h-3" />;
82
- }
83
-
84
- // ---------------------------------------------------------------------------
85
- // StatusBar.Spacer
86
- // ---------------------------------------------------------------------------
87
-
88
- function StatusBarSpacer() {
89
- return <div className="flex-1" />;
90
- }
91
-
92
- // ---------------------------------------------------------------------------
93
- // Export
94
- // ---------------------------------------------------------------------------
95
-
96
- export const StatusBar = Object.assign(StatusBarRoot, {
97
- Item: StatusBarItem,
98
- Separator: StatusBarSeparator,
99
- Spacer: StatusBarSpacer,
100
- });