@polderlabs/bizar 2.6.0 â 3.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/cli/bin.mjs +158 -130
- package/cli/copy.mjs +39 -34
- package/cli/plan.test.mjs +2331 -0
- package/cli/service.mjs +309 -0
- package/package.json +19 -27
- package/cli/dashboard/api.mjs +0 -473
- package/cli/dashboard/browser.mjs +0 -40
- package/cli/dashboard/server.mjs +0 -366
- package/cli/dashboard/state.mjs +0 -438
- package/cli/dashboard/tasks-store.mjs +0 -203
- package/cli/dashboard/watcher.mjs +0 -81
- package/cli/dashboard.mjs +0 -97
- package/dist/assets/index-BVvY22Gt.css +0 -1
- package/dist/assets/index-CO3c8O32.js +0 -285
- package/dist/assets/index-CO3c8O32.js.map +0 -1
- package/dist/index.html +0 -18
- package/src/App.tsx +0 -233
- package/src/components/Button.tsx +0 -55
- package/src/components/Card.tsx +0 -40
- package/src/components/EmptyState.tsx +0 -30
- package/src/components/Modal.tsx +0 -137
- package/src/components/Spinner.tsx +0 -19
- package/src/components/StatusBadge.tsx +0 -25
- package/src/components/Tag.tsx +0 -28
- package/src/components/Toast.tsx +0 -142
- package/src/components/Topbar.tsx +0 -88
- package/src/index.html +0 -17
- package/src/lib/api.ts +0 -71
- package/src/lib/markdown.tsx +0 -59
- package/src/lib/types.ts +0 -200
- package/src/lib/utils.ts +0 -79
- package/src/lib/ws.ts +0 -132
- package/src/main.tsx +0 -12
- package/src/styles/main.css +0 -2324
- package/src/views/Agents.tsx +0 -199
- package/src/views/Chat.tsx +0 -255
- package/src/views/Config.tsx +0 -250
- package/src/views/Overview.tsx +0 -267
- package/src/views/Plans.tsx +0 -667
- package/src/views/Projects.tsx +0 -155
- package/src/views/Settings.tsx +0 -253
- package/src/views/Tasks.tsx +0 -567
- package/tsconfig.json +0 -23
- package/vite.config.ts +0 -24
package/dist/index.html
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>Bizar Dashboard</title>
|
|
7
|
-
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>đĒŠ</text></svg>" />
|
|
8
|
-
<link rel="preconnect" href="https://rsms.me/" />
|
|
9
|
-
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
10
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
11
|
-
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
|
12
|
-
<script type="module" crossorigin src="./assets/index-CO3c8O32.js"></script>
|
|
13
|
-
<link rel="stylesheet" crossorigin href="./assets/index-BVvY22Gt.css">
|
|
14
|
-
</head>
|
|
15
|
-
<body>
|
|
16
|
-
<div id="root"></div>
|
|
17
|
-
</body>
|
|
18
|
-
</html>
|
package/src/App.tsx
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
// src/App.tsx â root shell. Wires data + contexts + tab routing.
|
|
2
|
-
|
|
3
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
-
import { Topbar, TABS } from './components/Topbar';
|
|
5
|
-
import { ModalProvider } from './components/Modal';
|
|
6
|
-
import { ToastProvider, useToast } from './components/Toast';
|
|
7
|
-
import { api } from './lib/api';
|
|
8
|
-
import { Ws } from './lib/ws';
|
|
9
|
-
import {
|
|
10
|
-
applyTheme,
|
|
11
|
-
type Settings,
|
|
12
|
-
type SettingsResponse,
|
|
13
|
-
type Snapshot,
|
|
14
|
-
type WsMessage,
|
|
15
|
-
type WsStatus,
|
|
16
|
-
} from './lib/types';
|
|
17
|
-
import { Overview } from './views/Overview';
|
|
18
|
-
import { Chat } from './views/Chat';
|
|
19
|
-
import { Agents } from './views/Agents';
|
|
20
|
-
import { Plans } from './views/Plans';
|
|
21
|
-
import { Projects } from './views/Projects';
|
|
22
|
-
import { Tasks } from './views/Tasks';
|
|
23
|
-
import { Config } from './views/Config';
|
|
24
|
-
import { SettingsView } from './views/Settings';
|
|
25
|
-
import { Spinner } from './components/Spinner';
|
|
26
|
-
import './styles/main.css';
|
|
27
|
-
|
|
28
|
-
type ViewProps = {
|
|
29
|
-
snapshot: Snapshot;
|
|
30
|
-
settings: Settings;
|
|
31
|
-
activeTab: string;
|
|
32
|
-
setActiveTab: (id: string) => void;
|
|
33
|
-
refreshSnapshot: () => Promise<void>;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const VIEW_MAP: Record<string, (p: ViewProps) => React.ReactNode> = {
|
|
37
|
-
overview: Overview,
|
|
38
|
-
chat: Chat,
|
|
39
|
-
agents: Agents,
|
|
40
|
-
plans: Plans,
|
|
41
|
-
projects: Projects,
|
|
42
|
-
tasks: Tasks,
|
|
43
|
-
config: Config,
|
|
44
|
-
settings: SettingsView,
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const VERSION = 'v2.6.0';
|
|
48
|
-
|
|
49
|
-
export function App() {
|
|
50
|
-
return (
|
|
51
|
-
<ToastProvider>
|
|
52
|
-
<ModalProvider>
|
|
53
|
-
<Shell />
|
|
54
|
-
</ModalProvider>
|
|
55
|
-
</ToastProvider>
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function Shell() {
|
|
60
|
-
const [activeTab, setActiveTab] = useState<string>('overview');
|
|
61
|
-
const [snapshot, setSnapshot] = useState<Snapshot | null>(null);
|
|
62
|
-
const [settings, setSettings] = useState<Settings | null>(null);
|
|
63
|
-
const [wsStatus, setWsStatus] = useState<WsStatus>('connecting');
|
|
64
|
-
const [bootError, setBootError] = useState<string | null>(null);
|
|
65
|
-
const wsRef = useRef<Ws | null>(null);
|
|
66
|
-
const toast = useToast();
|
|
67
|
-
|
|
68
|
-
// Apply theme whenever settings are loaded or theme changes
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
if (settings?.theme) applyTheme(settings.theme);
|
|
71
|
-
}, [settings?.theme]);
|
|
72
|
-
|
|
73
|
-
// React to system preference when in 'system' mode
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
if (settings?.theme !== 'system') return;
|
|
76
|
-
const mql = window.matchMedia('(prefers-color-scheme: light)');
|
|
77
|
-
const handler = () => applyTheme('system');
|
|
78
|
-
mql.addEventListener('change', handler);
|
|
79
|
-
return () => mql.removeEventListener('change', handler);
|
|
80
|
-
}, [settings?.theme]);
|
|
81
|
-
|
|
82
|
-
// Initial fetch
|
|
83
|
-
useEffect(() => {
|
|
84
|
-
let cancelled = false;
|
|
85
|
-
Promise.all([
|
|
86
|
-
api.get<Snapshot>('/snapshot').catch(() => null),
|
|
87
|
-
api.get<SettingsResponse>('/settings').catch(() => null),
|
|
88
|
-
])
|
|
89
|
-
.then(([snap, set]) => {
|
|
90
|
-
if (cancelled) return;
|
|
91
|
-
if (snap) setSnapshot(snap);
|
|
92
|
-
if (set?.data) setSettings(set.data);
|
|
93
|
-
if (!snap && !set) {
|
|
94
|
-
setBootError('Dashboard server unreachable.');
|
|
95
|
-
}
|
|
96
|
-
})
|
|
97
|
-
.catch((err) => {
|
|
98
|
-
if (cancelled) return;
|
|
99
|
-
const msg = (err as Error)?.message ?? 'unknown error';
|
|
100
|
-
setBootError(msg);
|
|
101
|
-
toast.error(`Failed to load: ${msg}`);
|
|
102
|
-
});
|
|
103
|
-
return () => {
|
|
104
|
-
cancelled = true;
|
|
105
|
-
};
|
|
106
|
-
}, [toast]);
|
|
107
|
-
|
|
108
|
-
// WebSocket lifecycle
|
|
109
|
-
useEffect(() => {
|
|
110
|
-
const ws = new Ws();
|
|
111
|
-
wsRef.current = ws;
|
|
112
|
-
const offStatus = ws.onStatus((s) => setWsStatus(s));
|
|
113
|
-
const offMsg = ws.on((msg: WsMessage) => {
|
|
114
|
-
if (msg.type === 'snapshot' && 'data' in msg && msg.data) {
|
|
115
|
-
setSnapshot((cur) => ({ ...(cur ?? ({} as Snapshot)), ...msg.data }));
|
|
116
|
-
} else if (msg.type === 'change') {
|
|
117
|
-
const m = msg;
|
|
118
|
-
const file = m.path?.split('/').pop() || m.path || '';
|
|
119
|
-
toast.info(`File changed: ${file}`, 2500);
|
|
120
|
-
api
|
|
121
|
-
.get<Snapshot>('/snapshot')
|
|
122
|
-
.then((s) =>
|
|
123
|
-
setSnapshot((cur) => ({ ...(cur ?? ({} as Snapshot)), ...s })),
|
|
124
|
-
)
|
|
125
|
-
.catch(() => undefined);
|
|
126
|
-
} else if (msg.type === 'tasks:change') {
|
|
127
|
-
const m = msg;
|
|
128
|
-
setSnapshot((cur) => {
|
|
129
|
-
if (!cur) return cur;
|
|
130
|
-
const tasks = (cur.tasks || []).map((t) =>
|
|
131
|
-
t.id === m.task.id ? m.task : t,
|
|
132
|
-
);
|
|
133
|
-
const exists = tasks.some((t) => t.id === m.task.id);
|
|
134
|
-
return { ...cur, tasks: exists ? tasks : [m.task, ...tasks] };
|
|
135
|
-
});
|
|
136
|
-
} else if (msg.type === 'tasks:delete') {
|
|
137
|
-
const m = msg;
|
|
138
|
-
setSnapshot((cur) =>
|
|
139
|
-
cur
|
|
140
|
-
? { ...cur, tasks: (cur.tasks || []).filter((t) => t.id !== m.id) }
|
|
141
|
-
: cur,
|
|
142
|
-
);
|
|
143
|
-
} else if (msg.type === 'settings:change') {
|
|
144
|
-
const m = msg;
|
|
145
|
-
if (m.settings) setSettings(m.settings);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
return () => {
|
|
149
|
-
offStatus();
|
|
150
|
-
offMsg();
|
|
151
|
-
ws.close();
|
|
152
|
-
};
|
|
153
|
-
}, [toast]);
|
|
154
|
-
|
|
155
|
-
// Keyboard shortcuts â 1..8 â switch tabs
|
|
156
|
-
useEffect(() => {
|
|
157
|
-
const map: Record<string, string> = {};
|
|
158
|
-
TABS.forEach((t, i) => {
|
|
159
|
-
map[String(i + 1)] = t.id;
|
|
160
|
-
});
|
|
161
|
-
const handler = (e: KeyboardEvent) => {
|
|
162
|
-
const t = (e.target as HTMLElement | null)?.tagName?.toLowerCase();
|
|
163
|
-
if (
|
|
164
|
-
t === 'input' ||
|
|
165
|
-
t === 'textarea' ||
|
|
166
|
-
(e.target as HTMLElement)?.isContentEditable ||
|
|
167
|
-
e.metaKey ||
|
|
168
|
-
e.ctrlKey ||
|
|
169
|
-
e.altKey
|
|
170
|
-
)
|
|
171
|
-
return;
|
|
172
|
-
const id = map[e.key];
|
|
173
|
-
if (id) {
|
|
174
|
-
e.preventDefault();
|
|
175
|
-
setActiveTab(id);
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
|
-
document.addEventListener('keydown', handler);
|
|
179
|
-
return () => document.removeEventListener('keydown', handler);
|
|
180
|
-
}, []);
|
|
181
|
-
|
|
182
|
-
const View = VIEW_MAP[activeTab];
|
|
183
|
-
|
|
184
|
-
const refreshSnapshot = useMemo(
|
|
185
|
-
() => async () => {
|
|
186
|
-
try {
|
|
187
|
-
const s = await api.get<Snapshot>('/snapshot');
|
|
188
|
-
setSnapshot((cur) => ({ ...(cur ?? ({} as Snapshot)), ...s }));
|
|
189
|
-
} catch (err) {
|
|
190
|
-
toast.error(`Refresh failed: ${(err as Error).message}`);
|
|
191
|
-
}
|
|
192
|
-
},
|
|
193
|
-
[toast],
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
return (
|
|
197
|
-
<div className="app">
|
|
198
|
-
<Topbar
|
|
199
|
-
activeTab={activeTab}
|
|
200
|
-
onTabChange={setActiveTab}
|
|
201
|
-
wsStatus={wsStatus}
|
|
202
|
-
version={VERSION}
|
|
203
|
-
/>
|
|
204
|
-
<main className="content">
|
|
205
|
-
{bootError && (
|
|
206
|
-
<div className="boot-error">
|
|
207
|
-
<h2>Dashboard unavailable</h2>
|
|
208
|
-
<p>{bootError}</p>
|
|
209
|
-
<p className="boot-error-hint">
|
|
210
|
-
Make sure the Bizar dashboard server is running. Try{' '}
|
|
211
|
-
<code>bizar dashboard start</code> in your terminal.
|
|
212
|
-
</p>
|
|
213
|
-
</div>
|
|
214
|
-
)}
|
|
215
|
-
{!bootError && (!snapshot || !settings) && (
|
|
216
|
-
<div className="loading">
|
|
217
|
-
<Spinner size="lg" />
|
|
218
|
-
<p>Loading BizarâĻ</p>
|
|
219
|
-
</div>
|
|
220
|
-
)}
|
|
221
|
-
{snapshot && settings && View && (
|
|
222
|
-
<View
|
|
223
|
-
snapshot={snapshot}
|
|
224
|
-
settings={settings}
|
|
225
|
-
activeTab={activeTab}
|
|
226
|
-
setActiveTab={setActiveTab}
|
|
227
|
-
refreshSnapshot={refreshSnapshot}
|
|
228
|
-
/>
|
|
229
|
-
)}
|
|
230
|
-
</main>
|
|
231
|
-
</div>
|
|
232
|
-
);
|
|
233
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
// src/components/Button.tsx â typed button with variants + sizes.
|
|
2
|
-
|
|
3
|
-
import { type ButtonHTMLAttributes, forwardRef } from 'react';
|
|
4
|
-
import { cn } from '../lib/utils';
|
|
5
|
-
|
|
6
|
-
export type ButtonVariant =
|
|
7
|
-
| 'primary'
|
|
8
|
-
| 'secondary'
|
|
9
|
-
| 'ghost'
|
|
10
|
-
| 'danger'
|
|
11
|
-
| 'accent';
|
|
12
|
-
export type ButtonSize = 'sm' | 'md' | 'lg';
|
|
13
|
-
|
|
14
|
-
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
15
|
-
variant?: ButtonVariant;
|
|
16
|
-
size?: ButtonSize;
|
|
17
|
-
iconOnly?: boolean;
|
|
18
|
-
loading?: boolean;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
22
|
-
function Button(
|
|
23
|
-
{
|
|
24
|
-
variant = 'secondary',
|
|
25
|
-
size = 'md',
|
|
26
|
-
iconOnly = false,
|
|
27
|
-
loading = false,
|
|
28
|
-
disabled,
|
|
29
|
-
className,
|
|
30
|
-
children,
|
|
31
|
-
...rest
|
|
32
|
-
},
|
|
33
|
-
ref,
|
|
34
|
-
) {
|
|
35
|
-
return (
|
|
36
|
-
<button
|
|
37
|
-
ref={ref}
|
|
38
|
-
type={rest.type ?? 'button'}
|
|
39
|
-
disabled={disabled || loading}
|
|
40
|
-
className={cn(
|
|
41
|
-
'btn',
|
|
42
|
-
`btn-${variant}`,
|
|
43
|
-
`btn-size-${size}`,
|
|
44
|
-
iconOnly && 'btn-icon',
|
|
45
|
-
loading && 'btn-loading',
|
|
46
|
-
className,
|
|
47
|
-
)}
|
|
48
|
-
{...rest}
|
|
49
|
-
>
|
|
50
|
-
{loading ? <span className="btn-spinner" aria-hidden /> : null}
|
|
51
|
-
{children}
|
|
52
|
-
</button>
|
|
53
|
-
);
|
|
54
|
-
},
|
|
55
|
-
);
|
package/src/components/Card.tsx
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
// src/components/Card.tsx â wrapper card.
|
|
2
|
-
|
|
3
|
-
import type { HTMLAttributes, ReactNode } from 'react';
|
|
4
|
-
import { cn } from '../lib/utils';
|
|
5
|
-
|
|
6
|
-
export type CardProps = HTMLAttributes<HTMLDivElement> & {
|
|
7
|
-
variant?: 'elevated' | 'outlined' | 'filled';
|
|
8
|
-
interactive?: boolean;
|
|
9
|
-
children?: ReactNode;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export function Card({
|
|
13
|
-
variant = 'elevated',
|
|
14
|
-
interactive = false,
|
|
15
|
-
className,
|
|
16
|
-
children,
|
|
17
|
-
...rest
|
|
18
|
-
}: CardProps) {
|
|
19
|
-
return (
|
|
20
|
-
<div
|
|
21
|
-
className={cn(
|
|
22
|
-
'card',
|
|
23
|
-
`card-${variant}`,
|
|
24
|
-
interactive && 'card-interactive',
|
|
25
|
-
className,
|
|
26
|
-
)}
|
|
27
|
-
{...rest}
|
|
28
|
-
>
|
|
29
|
-
{children}
|
|
30
|
-
</div>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function CardTitle({ children, className }: { children: ReactNode; className?: string }) {
|
|
35
|
-
return <h3 className={cn('card-title', className)}>{children}</h3>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function CardMeta({ children, className }: { children: ReactNode; className?: string }) {
|
|
39
|
-
return <div className={cn('card-meta', className)}>{children}</div>;
|
|
40
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
// src/components/EmptyState.tsx â friendly empty / error placeholder.
|
|
2
|
-
|
|
3
|
-
import type { ReactNode } from 'react';
|
|
4
|
-
import { Inbox } from 'lucide-react';
|
|
5
|
-
import { cn } from '../lib/utils';
|
|
6
|
-
|
|
7
|
-
export type EmptyStateProps = {
|
|
8
|
-
icon?: ReactNode;
|
|
9
|
-
title: string;
|
|
10
|
-
message?: ReactNode;
|
|
11
|
-
action?: ReactNode;
|
|
12
|
-
className?: string;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export function EmptyState({
|
|
16
|
-
icon,
|
|
17
|
-
title,
|
|
18
|
-
message,
|
|
19
|
-
action,
|
|
20
|
-
className,
|
|
21
|
-
}: EmptyStateProps) {
|
|
22
|
-
return (
|
|
23
|
-
<div className={cn('empty-state', className)}>
|
|
24
|
-
<div className="empty-icon">{icon ?? <Inbox size={32} />}</div>
|
|
25
|
-
<div className="empty-title">{title}</div>
|
|
26
|
-
{message && <div className="empty-message">{message}</div>}
|
|
27
|
-
{action && <div className="empty-action">{action}</div>}
|
|
28
|
-
</div>
|
|
29
|
-
);
|
|
30
|
-
}
|
package/src/components/Modal.tsx
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
// src/components/Modal.tsx â modal context + provider + portal.
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
createContext,
|
|
5
|
-
useCallback,
|
|
6
|
-
useContext,
|
|
7
|
-
useEffect,
|
|
8
|
-
useState,
|
|
9
|
-
type ReactNode,
|
|
10
|
-
} from 'react';
|
|
11
|
-
import { createPortal } from 'react-dom';
|
|
12
|
-
import { X } from 'lucide-react';
|
|
13
|
-
|
|
14
|
-
export type ModalProps = {
|
|
15
|
-
title?: string;
|
|
16
|
-
children: ReactNode;
|
|
17
|
-
footer?: ReactNode;
|
|
18
|
-
onClose?: () => void;
|
|
19
|
-
width?: number;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export type ModalApi = {
|
|
23
|
-
open: (props: ModalProps) => string;
|
|
24
|
-
close: (id?: string) => void;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const ModalContext = createContext<ModalApi | null>(null);
|
|
28
|
-
|
|
29
|
-
export function useModal(): ModalApi {
|
|
30
|
-
const ctx = useContext(ModalContext);
|
|
31
|
-
if (!ctx) {
|
|
32
|
-
return { open: () => '', close: () => undefined };
|
|
33
|
-
}
|
|
34
|
-
return ctx;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
type OpenModal = ModalProps & { id: string };
|
|
38
|
-
|
|
39
|
-
export function ModalProvider({ children }: { children: ReactNode }) {
|
|
40
|
-
const [stack, setStack] = useState<OpenModal[]>([]);
|
|
41
|
-
|
|
42
|
-
const close = useCallback((id?: string) => {
|
|
43
|
-
setStack((cur) => {
|
|
44
|
-
if (!id) return [];
|
|
45
|
-
const idx = cur.findIndex((m) => m.id === id);
|
|
46
|
-
if (idx === -1) return cur;
|
|
47
|
-
// close only the top-most matching
|
|
48
|
-
if (idx !== cur.length - 1) return cur;
|
|
49
|
-
return cur.slice(0, -1);
|
|
50
|
-
});
|
|
51
|
-
}, []);
|
|
52
|
-
|
|
53
|
-
const open = useCallback((props: ModalProps) => {
|
|
54
|
-
const id = `m${Math.random().toString(36).slice(2, 9)}`;
|
|
55
|
-
setStack((cur) => [...cur, { ...props, id }]);
|
|
56
|
-
return id;
|
|
57
|
-
}, []);
|
|
58
|
-
|
|
59
|
-
// Escape key â close the topmost
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
if (stack.length === 0) return;
|
|
62
|
-
const handler = (e: KeyboardEvent) => {
|
|
63
|
-
if (e.key === 'Escape') {
|
|
64
|
-
e.stopPropagation();
|
|
65
|
-
close();
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
document.addEventListener('keydown', handler);
|
|
69
|
-
return () => document.removeEventListener('keydown', handler);
|
|
70
|
-
}, [stack.length, close]);
|
|
71
|
-
|
|
72
|
-
return (
|
|
73
|
-
<ModalContext.Provider value={{ open, close }}>
|
|
74
|
-
{children}
|
|
75
|
-
{typeof document !== 'undefined' &&
|
|
76
|
-
createPortal(
|
|
77
|
-
<div className="modal-stack" aria-hidden={stack.length === 0}>
|
|
78
|
-
{stack.map((m, idx) => (
|
|
79
|
-
<ModalShell
|
|
80
|
-
key={m.id}
|
|
81
|
-
modal={m}
|
|
82
|
-
onClose={() => {
|
|
83
|
-
m.onClose?.();
|
|
84
|
-
close(m.id);
|
|
85
|
-
}}
|
|
86
|
-
depth={idx}
|
|
87
|
-
/>
|
|
88
|
-
))}
|
|
89
|
-
</div>,
|
|
90
|
-
document.body,
|
|
91
|
-
)}
|
|
92
|
-
</ModalContext.Provider>
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function ModalShell({
|
|
97
|
-
modal,
|
|
98
|
-
onClose,
|
|
99
|
-
depth,
|
|
100
|
-
}: {
|
|
101
|
-
modal: OpenModal;
|
|
102
|
-
onClose: () => void;
|
|
103
|
-
depth: number;
|
|
104
|
-
}) {
|
|
105
|
-
return (
|
|
106
|
-
<div
|
|
107
|
-
className="modal-backdrop"
|
|
108
|
-
onClick={(e) => {
|
|
109
|
-
if (e.target === e.currentTarget) onClose();
|
|
110
|
-
}}
|
|
111
|
-
style={depth > 0 ? { background: 'rgba(0,0,0,0.4)' } : undefined}
|
|
112
|
-
>
|
|
113
|
-
<div
|
|
114
|
-
className="modal"
|
|
115
|
-
role="dialog"
|
|
116
|
-
aria-modal="true"
|
|
117
|
-
style={modal.width ? { maxWidth: modal.width } : undefined}
|
|
118
|
-
>
|
|
119
|
-
{modal.title && (
|
|
120
|
-
<header className="modal-header">
|
|
121
|
-
<h2 className="modal-title">{modal.title}</h2>
|
|
122
|
-
<button
|
|
123
|
-
type="button"
|
|
124
|
-
className="icon-btn"
|
|
125
|
-
aria-label="Close"
|
|
126
|
-
onClick={onClose}
|
|
127
|
-
>
|
|
128
|
-
<X size={16} />
|
|
129
|
-
</button>
|
|
130
|
-
</header>
|
|
131
|
-
)}
|
|
132
|
-
<div className="modal-body">{modal.children}</div>
|
|
133
|
-
{modal.footer && <footer className="modal-footer">{modal.footer}</footer>}
|
|
134
|
-
</div>
|
|
135
|
-
</div>
|
|
136
|
-
);
|
|
137
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// src/components/Spinner.tsx â loading indicator.
|
|
2
|
-
|
|
3
|
-
import { cn } from '../lib/utils';
|
|
4
|
-
|
|
5
|
-
export type SpinnerProps = {
|
|
6
|
-
size?: 'sm' | 'md' | 'lg';
|
|
7
|
-
className?: string;
|
|
8
|
-
label?: string;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export function Spinner({ size = 'md', className, label }: SpinnerProps) {
|
|
12
|
-
return (
|
|
13
|
-
<span
|
|
14
|
-
className={cn('spinner', `spinner-${size}`, className)}
|
|
15
|
-
role="status"
|
|
16
|
-
aria-label={label || 'Loading'}
|
|
17
|
-
/>
|
|
18
|
-
);
|
|
19
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// src/components/StatusBadge.tsx â small colored pill.
|
|
2
|
-
|
|
3
|
-
import type { ReactNode } from 'react';
|
|
4
|
-
import { cn } from '../lib/utils';
|
|
5
|
-
|
|
6
|
-
export type StatusBadgeProps = {
|
|
7
|
-
kind?: 'success' | 'warning' | 'error' | 'info' | 'accent' | 'neutral';
|
|
8
|
-
children: ReactNode;
|
|
9
|
-
className?: string;
|
|
10
|
-
dot?: boolean;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export function StatusBadge({
|
|
14
|
-
kind = 'neutral',
|
|
15
|
-
children,
|
|
16
|
-
className,
|
|
17
|
-
dot = false,
|
|
18
|
-
}: StatusBadgeProps) {
|
|
19
|
-
return (
|
|
20
|
-
<span className={cn('badge', `badge-${kind}`, className)}>
|
|
21
|
-
{dot && <span className="badge-dot" />}
|
|
22
|
-
{children}
|
|
23
|
-
</span>
|
|
24
|
-
);
|
|
25
|
-
}
|
package/src/components/Tag.tsx
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// src/components/Tag.tsx â pill for tags.
|
|
2
|
-
|
|
3
|
-
import type { ReactNode } from 'react';
|
|
4
|
-
import { cn } from '../lib/utils';
|
|
5
|
-
|
|
6
|
-
export type TagProps = {
|
|
7
|
-
children: ReactNode;
|
|
8
|
-
className?: string;
|
|
9
|
-
onRemove?: () => void;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export function Tag({ children, className, onRemove }: TagProps) {
|
|
13
|
-
return (
|
|
14
|
-
<span className={cn('tag', className)}>
|
|
15
|
-
{children}
|
|
16
|
-
{onRemove && (
|
|
17
|
-
<button
|
|
18
|
-
type="button"
|
|
19
|
-
className="tag-remove"
|
|
20
|
-
aria-label={`Remove ${typeof children === 'string' ? children : 'tag'}`}
|
|
21
|
-
onClick={onRemove}
|
|
22
|
-
>
|
|
23
|
-
Ã
|
|
24
|
-
</button>
|
|
25
|
-
)}
|
|
26
|
-
</span>
|
|
27
|
-
);
|
|
28
|
-
}
|