@khal-os/ui 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.
Files changed (51) hide show
  1. package/package.json +41 -0
  2. package/src/components/ContextMenu.tsx +130 -0
  3. package/src/components/avatar.tsx +71 -0
  4. package/src/components/badge.tsx +39 -0
  5. package/src/components/button.tsx +102 -0
  6. package/src/components/command.tsx +165 -0
  7. package/src/components/cost-counter.tsx +75 -0
  8. package/src/components/data-row.tsx +97 -0
  9. package/src/components/dropdown-menu.tsx +233 -0
  10. package/src/components/glass-card.tsx +74 -0
  11. package/src/components/input.tsx +48 -0
  12. package/src/components/khal-logo.tsx +73 -0
  13. package/src/components/live-feed.tsx +109 -0
  14. package/src/components/mesh-gradient.tsx +57 -0
  15. package/src/components/metric-display.tsx +93 -0
  16. package/src/components/note.tsx +55 -0
  17. package/src/components/number-flow.tsx +25 -0
  18. package/src/components/pill-badge.tsx +65 -0
  19. package/src/components/progress-bar.tsx +70 -0
  20. package/src/components/section-card.tsx +76 -0
  21. package/src/components/separator.tsx +25 -0
  22. package/src/components/spinner.tsx +42 -0
  23. package/src/components/status-dot.tsx +90 -0
  24. package/src/components/switch.tsx +36 -0
  25. package/src/components/theme-provider.tsx +58 -0
  26. package/src/components/theme-switcher.tsx +59 -0
  27. package/src/components/ticker-bar.tsx +41 -0
  28. package/src/components/tooltip.tsx +62 -0
  29. package/src/components/window-minimized-context.tsx +29 -0
  30. package/src/hooks/useReducedMotion.ts +21 -0
  31. package/src/index.ts +58 -0
  32. package/src/lib/animations.ts +50 -0
  33. package/src/primitives/collapsible-sidebar.tsx +226 -0
  34. package/src/primitives/dialog.tsx +76 -0
  35. package/src/primitives/empty-state.tsx +43 -0
  36. package/src/primitives/index.ts +22 -0
  37. package/src/primitives/list-view.tsx +155 -0
  38. package/src/primitives/property-panel.tsx +108 -0
  39. package/src/primitives/section-header.tsx +19 -0
  40. package/src/primitives/sidebar-nav.tsx +110 -0
  41. package/src/primitives/split-pane.tsx +146 -0
  42. package/src/primitives/status-badge.tsx +10 -0
  43. package/src/primitives/status-bar.tsx +100 -0
  44. package/src/primitives/toolbar.tsx +152 -0
  45. package/src/server.ts +4 -0
  46. package/src/stores/notification-store.ts +271 -0
  47. package/src/stores/theme-store.ts +33 -0
  48. package/src/tokens/lp-tokens.ts +36 -0
  49. package/src/utils.ts +6 -0
  50. package/tokens.css +295 -0
  51. package/tsconfig.json +17 -0
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import { Monitor, Moon, Sun } from 'lucide-react';
4
+ import { useTheme } from 'next-themes';
5
+ import { useEffect, useState } from 'react';
6
+ import { cn } from '../utils';
7
+
8
+ const themes = [
9
+ { value: 'system', label: 'System', icon: Monitor },
10
+ { value: 'light', label: 'Light', icon: Sun },
11
+ { value: 'dark', label: 'Dark', icon: Moon },
12
+ ] as const;
13
+
14
+ interface ThemeSwitcherProps {
15
+ small?: boolean;
16
+ className?: string;
17
+ onThemeSwitch?: (theme: string) => void;
18
+ disabled?: boolean;
19
+ }
20
+
21
+ function ThemeSwitcher({ small, className, onThemeSwitch, disabled }: ThemeSwitcherProps) {
22
+ const { theme, setTheme } = useTheme();
23
+ const [mounted, setMounted] = useState(false);
24
+ useEffect(() => setMounted(true), []);
25
+
26
+ // Avoid hydration mismatch: useTheme() returns different values on server vs client
27
+ const resolvedTheme = mounted ? theme : undefined;
28
+
29
+ return (
30
+ <fieldset
31
+ className={cn('inline-flex items-center gap-1 rounded-lg bg-gray-alpha-100 p-1', className)}
32
+ disabled={disabled}
33
+ >
34
+ {themes.map(({ value, label, icon: Icon }) => (
35
+ <button
36
+ key={value}
37
+ type="button"
38
+ role="radio"
39
+ aria-checked={resolvedTheme === value}
40
+ onClick={() => {
41
+ setTheme(value);
42
+ onThemeSwitch?.(value);
43
+ }}
44
+ className={cn(
45
+ 'inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-label-12 transition-colors cursor-pointer',
46
+ resolvedTheme === value
47
+ ? 'bg-background-100 text-gray-1000 shadow-sm'
48
+ : 'text-gray-700 hover:text-gray-1000'
49
+ )}
50
+ >
51
+ <Icon className={small ? 'h-3.5 w-3.5' : 'h-4 w-4'} />
52
+ {!small && label}
53
+ </button>
54
+ ))}
55
+ </fieldset>
56
+ );
57
+ }
58
+
59
+ export { ThemeSwitcher };
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import { cn } from '../utils';
5
+
6
+ interface TickerBarProps {
7
+ /** Items to scroll — rendered twice for seamless looping */
8
+ children: ReactNode;
9
+ /** Animation duration in seconds (default 30) */
10
+ duration?: number;
11
+ /** Pause on hover */
12
+ pauseOnHover?: boolean;
13
+ className?: string;
14
+ }
15
+
16
+ function TickerBar({ children, duration = 30, pauseOnHover = true, className }: TickerBarProps) {
17
+ return (
18
+ <div
19
+ className={cn('relative w-full overflow-hidden', className)}
20
+ style={{
21
+ maskImage: 'linear-gradient(to right, transparent, black 10%, black 90%, transparent)',
22
+ WebkitMaskImage: 'linear-gradient(to right, transparent, black 10%, black 90%, transparent)',
23
+ }}
24
+ >
25
+ <div
26
+ className={cn('flex w-max', pauseOnHover && 'hover:[animation-play-state:paused]')}
27
+ style={{
28
+ animation: `khal-ticker ${duration}s linear infinite`,
29
+ }}
30
+ >
31
+ <div className="flex shrink-0 items-center">{children}</div>
32
+ <div className="flex shrink-0 items-center" aria-hidden>
33
+ {children}
34
+ </div>
35
+ </div>
36
+ </div>
37
+ );
38
+ }
39
+
40
+ export type { TickerBarProps };
41
+ export { TickerBar };
@@ -0,0 +1,62 @@
1
+ 'use client';
2
+
3
+ import * as TooltipPrimitive from '@radix-ui/react-tooltip';
4
+ import * as React from 'react';
5
+ import { cn } from '../utils';
6
+
7
+ const TooltipProvider = TooltipPrimitive.Provider;
8
+ const TooltipRoot = TooltipPrimitive.Root;
9
+ const TooltipTrigger = TooltipPrimitive.Trigger;
10
+
11
+ const TooltipContent = React.forwardRef<
12
+ React.ComponentRef<typeof TooltipPrimitive.Content>,
13
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
14
+ >(({ className, sideOffset = 4, style, ...props }, ref) => (
15
+ <TooltipPrimitive.Portal>
16
+ <TooltipPrimitive.Content
17
+ ref={ref}
18
+ sideOffset={sideOffset}
19
+ className={cn(
20
+ 'z-[9999] overflow-hidden rounded-md px-2.5 py-1 text-label-12',
21
+ 'animate-in fade-in-0 zoom-in-95',
22
+ 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
23
+ 'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
24
+ className
25
+ )}
26
+ style={{
27
+ background: 'var(--khal-text-primary, var(--ds-gray-1000))',
28
+ color: 'var(--khal-text-inverse, var(--ds-background-100))',
29
+ ...style,
30
+ }}
31
+ {...props}
32
+ />
33
+ </TooltipPrimitive.Portal>
34
+ ));
35
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
36
+
37
+ interface SimpleTooltipProps {
38
+ text: React.ReactNode;
39
+ children: React.ReactNode;
40
+ position?: 'top' | 'bottom' | 'left' | 'right';
41
+ delay?: boolean;
42
+ delayTime?: number;
43
+ desktopOnly?: boolean;
44
+ className?: string;
45
+ }
46
+
47
+ function Tooltip({ text, children, position = 'top', delay, delayTime, desktopOnly, className }: SimpleTooltipProps) {
48
+ const delayDuration = delayTime ?? (delay ? 400 : 200);
49
+
50
+ return (
51
+ <TooltipProvider delayDuration={delayDuration}>
52
+ <TooltipRoot>
53
+ <TooltipTrigger asChild>{children}</TooltipTrigger>
54
+ <TooltipContent side={position} className={cn(desktopOnly && 'max-md:hidden', className)}>
55
+ {text}
56
+ </TooltipContent>
57
+ </TooltipRoot>
58
+ </TooltipProvider>
59
+ );
60
+ }
61
+
62
+ export { Tooltip, TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger };
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext } from 'react';
4
+
5
+ /**
6
+ * Exposes the parent window's minimized state to deeply nested child components.
7
+ * Terminal panes use this to dispose WebGL contexts when minimized (GPU savings)
8
+ * and re-attach them on restore.
9
+ */
10
+ const WindowMinimizedContext = createContext(false);
11
+
12
+ export const WindowMinimizedProvider = WindowMinimizedContext.Provider;
13
+
14
+ export function useWindowMinimized(): boolean {
15
+ return useContext(WindowMinimizedContext);
16
+ }
17
+
18
+ /**
19
+ * Exposes the parent window's focused/active state to child components.
20
+ * Apps use this to pause polling, animations, and heavy rendering when
21
+ * their window is behind another (app-nap behavior).
22
+ */
23
+ const WindowActiveContext = createContext(true);
24
+
25
+ export const WindowActiveProvider = WindowActiveContext.Provider;
26
+
27
+ export function useWindowActive(): boolean {
28
+ return useContext(WindowActiveContext);
29
+ }
@@ -0,0 +1,21 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ /**
6
+ * Returns true when the user prefers reduced motion (OS setting or app toggle).
7
+ */
8
+ export function useReducedMotion(): boolean {
9
+ const [reduced, setReduced] = useState(false);
10
+
11
+ useEffect(() => {
12
+ const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
13
+ setReduced(mq.matches || document.documentElement.dataset.reduceMotion === 'true');
14
+
15
+ const handler = (e: MediaQueryListEvent) => setReduced(e.matches);
16
+ mq.addEventListener('change', handler);
17
+ return () => mq.removeEventListener('change', handler);
18
+ }, []);
19
+
20
+ return reduced;
21
+ }
package/src/index.ts ADDED
@@ -0,0 +1,58 @@
1
+ // App component props type
2
+ export interface AppComponentProps {
3
+ windowId: string;
4
+ meta?: Record<string, unknown>;
5
+ }
6
+
7
+ // Auth — re-export from SDK
8
+ export { SUBJECTS, useKhalAuth, useKhalAuth as useOSAuth, useNats, useNatsSubscription } from '@khal-os/sdk/app';
9
+ // shadcn/ui components — local implementations
10
+ export * from './components/avatar';
11
+ export * from './components/badge';
12
+ export * from './components/button';
13
+ export * from './components/ContextMenu';
14
+ export * from './components/command';
15
+ // Design system components — local implementations
16
+ export * from './components/cost-counter';
17
+ // Design system components — LP section patterns
18
+ export * from './components/data-row';
19
+ export * from './components/dropdown-menu';
20
+ export * from './components/glass-card';
21
+ export * from './components/input';
22
+ export * from './components/khal-logo';
23
+ export * from './components/live-feed';
24
+ export * from './components/mesh-gradient';
25
+ export * from './components/metric-display';
26
+ export * from './components/note';
27
+ export * from './components/number-flow';
28
+ export * from './components/pill-badge';
29
+ export * from './components/progress-bar';
30
+ export * from './components/section-card';
31
+ export * from './components/separator';
32
+ export * from './components/spinner';
33
+ export * from './components/status-dot';
34
+ export * from './components/switch';
35
+ export * from './components/theme-provider';
36
+ export * from './components/theme-switcher';
37
+ export * from './components/ticker-bar';
38
+ export * from './components/tooltip';
39
+ export {
40
+ useWindowActive,
41
+ useWindowMinimized,
42
+ WindowActiveProvider,
43
+ WindowMinimizedProvider,
44
+ } from './components/window-minimized-context';
45
+ // Hooks
46
+ export { useReducedMotion } from './hooks/useReducedMotion';
47
+ // Animations
48
+ export { fadeIn, fadeUp, khalEasing, scaleUp, springConfig, staggerChild, staggerContainer } from './lib/animations';
49
+ // OS Primitives — local implementations
50
+ export * from './primitives';
51
+ export type { DesktopNotification, DesktopNotifMode, NotificationUrgency, TrayIcon } from './stores/notification-store';
52
+ // Stores (OS-level state)
53
+ export { useNotificationStore } from './stores/notification-store';
54
+ export { useThemeStore } from './stores/theme-store';
55
+ // LP Design Tokens
56
+ export * from './tokens/lp-tokens';
57
+ // Utilities
58
+ export { cn } from './utils';
@@ -0,0 +1,50 @@
1
+ /**
2
+ * KhalOS animation presets — motion.js configurations for OS-wide use.
3
+ * Import these in components for consistent, branded animations.
4
+ */
5
+
6
+ /** Custom easing curve — KhalOS primary */
7
+ export const khalEasing = [0.22, 1, 0.36, 1] as const;
8
+
9
+ /** Spring config for interactive elements */
10
+ export const springConfig = {
11
+ stiffness: 300,
12
+ damping: 22,
13
+ } as const;
14
+
15
+ /** Window open animation — fade up with blur */
16
+ export const fadeUp = {
17
+ initial: { opacity: 0, y: 12, filter: 'blur(4px)' },
18
+ animate: { opacity: 1, y: 0, filter: 'blur(0px)' },
19
+ transition: { duration: 0.7, ease: khalEasing },
20
+ } as const;
21
+
22
+ /** App launch animation — scale up with blur */
23
+ export const scaleUp = {
24
+ initial: { opacity: 0, scale: 0.96, filter: 'blur(6px)' },
25
+ animate: { opacity: 1, scale: 1, filter: 'blur(0px)' },
26
+ transition: { duration: 0.9, ease: khalEasing },
27
+ } as const;
28
+
29
+ /** Stagger container — children appear with 0.12s delay */
30
+ export const staggerContainer = {
31
+ animate: {
32
+ transition: {
33
+ staggerChildren: 0.12,
34
+ },
35
+ },
36
+ } as const;
37
+
38
+ /** Stagger child — each item fades up */
39
+ export const staggerChild = {
40
+ initial: { opacity: 0, y: 8 },
41
+ animate: { opacity: 1, y: 0 },
42
+ transition: { duration: 0.4, ease: khalEasing },
43
+ } as const;
44
+
45
+ /** Fade in — simple opacity animation */
46
+ export const fadeIn = {
47
+ initial: { opacity: 0 },
48
+ animate: { opacity: 1 },
49
+ transition: { duration: 0.5, ease: khalEasing },
50
+ } as const;
@@ -0,0 +1,226 @@
1
+ 'use client';
2
+
3
+ import { createContext, type ReactNode, useCallback, useContext, useRef, useState } from 'react';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // CollapsibleSidebar — resizable sidebar with collapse/expand toggle.
7
+ //
8
+ // Usage:
9
+ // <CollapsibleSidebar defaultSize={220} min={160} max={360}>
10
+ // <CollapsibleSidebar.Header>
11
+ // <span>Explorer</span>
12
+ // <CollapsibleSidebar.CollapseButton />
13
+ // </CollapsibleSidebar.Header>
14
+ // <CollapsibleSidebar.Content>
15
+ // <FileTree />
16
+ // </CollapsibleSidebar.Content>
17
+ // </CollapsibleSidebar>
18
+ // ---------------------------------------------------------------------------
19
+
20
+ interface SidebarContextValue {
21
+ collapsed: boolean;
22
+ toggle: () => void;
23
+ size: number;
24
+ }
25
+
26
+ const SidebarContext = createContext<SidebarContextValue>({
27
+ collapsed: false,
28
+ toggle: () => {},
29
+ size: 220,
30
+ });
31
+
32
+ export function useSidebar() {
33
+ return useContext(SidebarContext);
34
+ }
35
+
36
+ interface CollapsibleSidebarProps {
37
+ children: ReactNode;
38
+ defaultSize?: number;
39
+ min?: number;
40
+ max?: number;
41
+ defaultCollapsed?: boolean;
42
+ side?: 'left' | 'right';
43
+ className?: string;
44
+ }
45
+
46
+ function CollapsibleSidebarRoot({
47
+ children,
48
+ defaultSize = 220,
49
+ min = 140,
50
+ max = 400,
51
+ defaultCollapsed = false,
52
+ side = 'left',
53
+ className = '',
54
+ }: CollapsibleSidebarProps) {
55
+ const [collapsed, setCollapsed] = useState(defaultCollapsed);
56
+ const [size, setSize] = useState(defaultSize);
57
+ const dragging = useRef(false);
58
+ const startX = useRef(0);
59
+ const startSize = useRef(0);
60
+
61
+ const toggle = useCallback(() => setCollapsed((v) => !v), []);
62
+
63
+ const onPointerDown = useCallback(
64
+ (e: React.PointerEvent) => {
65
+ if (collapsed) return;
66
+ e.preventDefault();
67
+ dragging.current = true;
68
+ startX.current = e.clientX;
69
+ startSize.current = size;
70
+ (e.target as HTMLElement).setPointerCapture(e.pointerId);
71
+ },
72
+ [collapsed, size]
73
+ );
74
+
75
+ const onPointerMove = useCallback(
76
+ (e: React.PointerEvent) => {
77
+ if (!dragging.current) return;
78
+ const delta = side === 'left' ? e.clientX - startX.current : startX.current - e.clientX;
79
+ setSize(Math.min(max, Math.max(min, startSize.current + delta)));
80
+ },
81
+ [side, min, max]
82
+ );
83
+
84
+ const onPointerUp = useCallback(() => {
85
+ dragging.current = false;
86
+ }, []);
87
+
88
+ const resizeHandle = (
89
+ <div
90
+ className={`w-px shrink-0 cursor-col-resize bg-gray-alpha-200 transition-colors hover:w-0.5 hover:bg-blue-700/50 active:w-0.5 active:bg-blue-700 ${
91
+ collapsed ? 'pointer-events-none' : ''
92
+ }`}
93
+ onPointerDown={onPointerDown}
94
+ onPointerMove={onPointerMove}
95
+ onPointerUp={onPointerUp}
96
+ role="separator"
97
+ aria-orientation="vertical"
98
+ />
99
+ );
100
+
101
+ return (
102
+ <SidebarContext.Provider value={{ collapsed, toggle, size }}>
103
+ <div className={`flex shrink-0 ${className}`} style={{ width: collapsed ? 0 : size }}>
104
+ {side === 'right' && resizeHandle}
105
+ <div
106
+ className={`flex h-full flex-col overflow-hidden transition-[width] duration-150 ${
107
+ collapsed ? 'w-0' : 'w-full'
108
+ }`}
109
+ >
110
+ {children}
111
+ </div>
112
+ {side === 'left' && resizeHandle}
113
+ </div>
114
+ </SidebarContext.Provider>
115
+ );
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // CollapsibleSidebar.Header
120
+ // ---------------------------------------------------------------------------
121
+
122
+ function SidebarHeader({ children, className = '' }: { children: ReactNode; className?: string }) {
123
+ return (
124
+ <div className={`flex h-9 shrink-0 items-center justify-between border-b border-gray-alpha-200 px-3 ${className}`}>
125
+ {children}
126
+ </div>
127
+ );
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // CollapsibleSidebar.CollapseButton
132
+ // ---------------------------------------------------------------------------
133
+
134
+ function CollapseButton({ className = '' }: { className?: string }) {
135
+ const { collapsed, toggle } = useSidebar();
136
+ return (
137
+ <button
138
+ onClick={toggle}
139
+ className={`inline-flex h-5 w-5 items-center justify-center rounded text-gray-800 hover:bg-gray-alpha-200 hover:text-gray-1000 transition-colors ${className}`}
140
+ aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
141
+ >
142
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
143
+ <path
144
+ d={collapsed ? 'M4.5 2L8.5 6L4.5 10' : 'M8.5 2L4.5 6L8.5 10'}
145
+ stroke="currentColor"
146
+ strokeWidth="1.5"
147
+ strokeLinecap="round"
148
+ strokeLinejoin="round"
149
+ />
150
+ </svg>
151
+ </button>
152
+ );
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // CollapsibleSidebar.Content
157
+ // ---------------------------------------------------------------------------
158
+
159
+ function SidebarContent({ children, className = '' }: { children: ReactNode; className?: string }) {
160
+ return <div className={`flex-1 overflow-y-auto ${className}`}>{children}</div>;
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // CollapsibleSidebar.Section
165
+ // ---------------------------------------------------------------------------
166
+
167
+ function SidebarSection({
168
+ title,
169
+ children,
170
+ className = '',
171
+ }: {
172
+ title?: string;
173
+ children: ReactNode;
174
+ className?: string;
175
+ }) {
176
+ return (
177
+ <div className={`${className}`}>
178
+ {title && (
179
+ <div className="px-3 pt-2 pb-1">
180
+ <span className="text-label-13 font-medium text-gray-800">{title}</span>
181
+ </div>
182
+ )}
183
+ {children}
184
+ </div>
185
+ );
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // CollapsibleSidebar.Item
190
+ // ---------------------------------------------------------------------------
191
+
192
+ interface SidebarItemProps {
193
+ children: ReactNode;
194
+ icon?: ReactNode;
195
+ active?: boolean;
196
+ indent?: number;
197
+ onClick?: () => void;
198
+ className?: string;
199
+ }
200
+
201
+ function SidebarItem({ children, icon, active, indent = 0, onClick, className = '' }: SidebarItemProps) {
202
+ return (
203
+ <button
204
+ onClick={onClick}
205
+ className={`flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-label-13 transition-colors
206
+ ${active ? 'bg-gray-alpha-200 text-gray-1000' : 'text-gray-900 hover:bg-gray-alpha-100 hover:text-gray-1000'}
207
+ ${className}`}
208
+ style={{ paddingLeft: 8 + indent * 12 }}
209
+ >
210
+ {icon && <span className="shrink-0 text-gray-800 [&>svg]:h-3.5 [&>svg]:w-3.5">{icon}</span>}
211
+ <span className="min-w-0 truncate">{children}</span>
212
+ </button>
213
+ );
214
+ }
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // Export
218
+ // ---------------------------------------------------------------------------
219
+
220
+ export const CollapsibleSidebar = Object.assign(CollapsibleSidebarRoot, {
221
+ Header: SidebarHeader,
222
+ CollapseButton,
223
+ Content: SidebarContent,
224
+ Section: SidebarSection,
225
+ Item: SidebarItem,
226
+ });
@@ -0,0 +1,76 @@
1
+ 'use client';
2
+
3
+ import type { ButtonHTMLAttributes, ReactNode } from 'react';
4
+
5
+ /* ------------------------------------------------------------------ */
6
+ /* Minimal compound Dialog primitive for OS chrome. */
7
+ /* Enough to satisfy DeleteConfirmDialog; expand as needed. */
8
+ /* ------------------------------------------------------------------ */
9
+
10
+ interface DialogRootProps {
11
+ open: boolean;
12
+ onClose: () => void;
13
+ children: ReactNode;
14
+ }
15
+
16
+ function DialogRoot({ open, onClose, children }: DialogRootProps) {
17
+ if (!open) return null;
18
+ return (
19
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={onClose}>
20
+ <div
21
+ className="bg-popover text-popover-foreground rounded-lg border p-6 shadow-lg"
22
+ onClick={(e) => e.stopPropagation()}
23
+ >
24
+ {children}
25
+ </div>
26
+ </div>
27
+ );
28
+ }
29
+
30
+ function Body({ children }: { children: ReactNode }) {
31
+ return <div className="flex items-start gap-4">{children}</div>;
32
+ }
33
+
34
+ function Icon({ children, variant: _variant }: { children: ReactNode; variant?: string }) {
35
+ return <div className="shrink-0">{children}</div>;
36
+ }
37
+
38
+ function Title({ children }: { children: ReactNode }) {
39
+ return <h2 className="text-lg font-semibold">{children}</h2>;
40
+ }
41
+
42
+ function Description({ children }: { children: ReactNode }) {
43
+ return <p className="text-muted-foreground text-sm">{children}</p>;
44
+ }
45
+
46
+ function Actions({ children }: { children: ReactNode }) {
47
+ return <div className="mt-4 flex justify-end gap-2">{children}</div>;
48
+ }
49
+
50
+ type BtnProps = ButtonHTMLAttributes<HTMLButtonElement> & { variant?: string };
51
+
52
+ function Cancel({ children, ...props }: BtnProps) {
53
+ return (
54
+ <button type="button" className="rounded px-3 py-1.5 text-sm hover:bg-muted" {...props}>
55
+ {children}
56
+ </button>
57
+ );
58
+ }
59
+
60
+ function Confirm({ children, variant: _variant, ...props }: BtnProps) {
61
+ return (
62
+ <button type="button" className="rounded bg-destructive px-3 py-1.5 text-sm text-destructive-foreground" {...props}>
63
+ {children}
64
+ </button>
65
+ );
66
+ }
67
+
68
+ export const Dialog = Object.assign(DialogRoot, {
69
+ Body,
70
+ Icon,
71
+ Title,
72
+ Description,
73
+ Actions,
74
+ Cancel,
75
+ Confirm,
76
+ });
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+
3
+ import { type ReactNode } from 'react';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // EmptyState — placeholder for empty views (no files, no results, etc.).
7
+ //
8
+ // Usage:
9
+ // <EmptyState
10
+ // icon={<FolderOpen />}
11
+ // title="No files"
12
+ // description="This folder is empty."
13
+ // action={<Button onClick={upload}>Upload File</Button>}
14
+ // />
15
+ // ---------------------------------------------------------------------------
16
+
17
+ interface EmptyStateProps {
18
+ icon?: ReactNode;
19
+ title: string;
20
+ description?: string;
21
+ action?: ReactNode;
22
+ compact?: boolean;
23
+ className?: string;
24
+ }
25
+
26
+ export function EmptyState({ icon, title, description, action, compact, className = '' }: EmptyStateProps) {
27
+ return (
28
+ <div
29
+ className={`flex flex-col items-center justify-center text-center ${
30
+ compact ? 'gap-2 py-6' : 'gap-3 py-12'
31
+ } ${className}`}
32
+ >
33
+ {icon && (
34
+ <div className={`text-gray-700 ${compact ? '[&>svg]:h-5 [&>svg]:w-5' : '[&>svg]:h-8 [&>svg]:w-8'}`}>{icon}</div>
35
+ )}
36
+ <div className="space-y-0.5">
37
+ <p className="text-label-13 font-medium text-gray-1000">{title}</p>
38
+ {description && <p className={`text-gray-800 ${compact ? 'text-label-13' : 'text-label-13'}`}>{description}</p>}
39
+ </div>
40
+ {action && <div className="mt-2 shrink-0">{action}</div>}
41
+ </div>
42
+ );
43
+ }