@ploykit/ui 0.0.0-stage → 0.1.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 +41 -3
- package/dist/components/ConfirmDialog.d.ts +13 -0
- package/dist/components/ConfirmDialog.js +29 -0
- package/dist/components/CronInput.d.ts +15 -0
- package/dist/components/CronInput.js +112 -0
- package/dist/components/DataTable.d.ts +22 -0
- package/dist/components/DataTable.js +13 -0
- package/dist/components/ErrorBoundary.d.ts +16 -0
- package/dist/components/ErrorBoundary.js +24 -0
- package/dist/components/FormField.d.ts +10 -0
- package/dist/components/FormField.js +22 -0
- package/dist/components/ImpersonationBanner.d.ts +3 -0
- package/dist/components/ImpersonationBanner.js +10 -0
- package/dist/components/NotificationBell.d.ts +3 -0
- package/dist/components/NotificationBell.js +47 -0
- package/dist/components/OnboardingChecklist.d.ts +12 -0
- package/dist/components/OnboardingChecklist.js +43 -0
- package/dist/components/Page.d.ts +17 -0
- package/dist/components/Page.js +16 -0
- package/dist/components/SecretModal.d.ts +16 -0
- package/dist/components/SecretModal.js +45 -0
- package/dist/components/SettingsCard.d.ts +10 -0
- package/dist/components/SettingsCard.js +6 -0
- package/dist/components/SiteBanner.d.ts +6 -0
- package/dist/components/SiteBanner.js +11 -0
- package/dist/components/SiteBannerLayer.d.ts +4 -0
- package/dist/components/SiteBannerLayer.js +9 -0
- package/dist/components/UserMenu.d.ts +13 -0
- package/dist/components/UserMenu.js +30 -0
- package/dist/components/WorkspaceSwitcher.d.ts +1 -0
- package/dist/components/WorkspaceSwitcher.js +53 -0
- package/dist/components/toast.d.ts +7 -0
- package/dist/components/toast.js +11 -0
- package/dist/components/ui/Badge.d.ts +13 -0
- package/dist/components/ui/Badge.js +21 -0
- package/dist/components/ui/Button.d.ts +10 -0
- package/dist/components/ui/Button.js +33 -0
- package/dist/components/ui/Input.d.ts +3 -0
- package/dist/components/ui/Input.js +10 -0
- package/dist/components/ui/alert-dialog.d.ts +18 -0
- package/dist/components/ui/alert-dialog.js +41 -0
- package/dist/components/ui/avatar.d.ts +11 -0
- package/dist/components/ui/avatar.js +22 -0
- package/dist/components/ui/breadcrumb.d.ts +10 -0
- package/dist/components/ui/breadcrumb.js +36 -0
- package/dist/components/ui/card.d.ts +13 -0
- package/dist/components/ui/card.js +24 -0
- package/dist/components/ui/checkbox.d.ts +3 -0
- package/dist/components/ui/checkbox.js +8 -0
- package/dist/components/ui/dialog.d.ts +15 -0
- package/dist/components/ui/dialog.js +26 -0
- package/dist/components/ui/empty.d.ts +13 -0
- package/dist/components/ui/empty.js +36 -0
- package/dist/components/ui/field.d.ts +24 -0
- package/dist/components/ui/field.js +67 -0
- package/dist/components/ui/label.d.ts +3 -0
- package/dist/components/ui/label.js +6 -0
- package/dist/components/ui/menu.d.ts +29 -0
- package/dist/components/ui/menu.js +50 -0
- package/dist/components/ui/popover.d.ts +9 -0
- package/dist/components/ui/popover.js +22 -0
- package/dist/components/ui/progress.d.ts +7 -0
- package/dist/components/ui/progress.js +19 -0
- package/dist/components/ui/select.d.ts +15 -0
- package/dist/components/ui/select.js +33 -0
- package/dist/components/ui/separator.d.ts +3 -0
- package/dist/components/ui/separator.js +7 -0
- package/dist/components/ui/sheet.d.ts +14 -0
- package/dist/components/ui/sheet.js +36 -0
- package/dist/components/ui/sidebar.d.ts +63 -0
- package/dist/components/ui/sidebar.js +265 -0
- package/dist/components/ui/skeleton.d.ts +3 -0
- package/dist/components/ui/skeleton.js +6 -0
- package/dist/components/ui/switch.d.ts +5 -0
- package/dist/components/ui/switch.js +7 -0
- package/dist/components/ui/tabs.d.ts +10 -0
- package/dist/components/ui/tabs.js +28 -0
- package/dist/components/ui/tooltip.d.ts +6 -0
- package/dist/components/ui/tooltip.js +16 -0
- package/dist/hooks/use-mobile.d.ts +1 -0
- package/dist/hooks/use-mobile.js +15 -0
- package/dist/hooks/useApi.d.ts +10 -0
- package/dist/hooks/useApi.js +11 -0
- package/dist/hooks/useAudit.d.ts +39 -0
- package/dist/hooks/useAudit.js +55 -0
- package/dist/hooks/useAuth.d.ts +1 -0
- package/dist/hooks/useAuth.js +4 -0
- package/dist/hooks/useBilling.d.ts +44 -0
- package/dist/hooks/useBilling.js +64 -0
- package/dist/hooks/useLogin.d.ts +10 -0
- package/dist/hooks/useLogin.js +39 -0
- package/dist/hooks/useMembers.d.ts +125 -0
- package/dist/hooks/useMembers.js +127 -0
- package/dist/hooks/useNotificationPrefs.d.ts +20 -0
- package/dist/hooks/useNotificationPrefs.js +42 -0
- package/dist/hooks/usePasswordReset.d.ts +5 -0
- package/dist/hooks/usePasswordReset.js +12 -0
- package/dist/hooks/useSEO.d.ts +6 -0
- package/dist/hooks/useSEO.js +14 -0
- package/dist/hooks/useSessions.d.ts +20 -0
- package/dist/hooks/useSessions.js +38 -0
- package/dist/hooks/useSiteConfig.d.ts +12 -0
- package/dist/hooks/useSiteConfig.js +11 -0
- package/dist/hooks/useTokens.d.ts +36 -0
- package/dist/hooks/useTokens.js +35 -0
- package/dist/hooks/useUsage.d.ts +20 -0
- package/dist/hooks/useUsage.js +21 -0
- package/dist/hooks/useWorkspace.d.ts +26 -0
- package/dist/hooks/useWorkspace.js +32 -0
- package/dist/hooks/useZodForm.d.ts +6 -0
- package/dist/hooks/useZodForm.js +6 -0
- package/dist/hooks/ws.d.ts +4 -0
- package/dist/hooks/ws.js +21 -0
- package/dist/index.d.ts +124 -0
- package/dist/index.js +94 -0
- package/dist/layouts/AppShell.d.ts +20 -0
- package/dist/layouts/AppShell.js +21 -0
- package/dist/layouts/AuthShell.d.ts +7 -0
- package/dist/layouts/AuthShell.js +4 -0
- package/dist/layouts/SettingsShell.d.ts +19 -0
- package/dist/layouts/SettingsShell.js +21 -0
- package/dist/lib/api-error.d.ts +1 -0
- package/dist/lib/api-error.js +74 -0
- package/dist/lib/perm.d.ts +2 -0
- package/dist/lib/perm.js +7 -0
- package/dist/lib/utils.d.ts +12 -0
- package/dist/lib/utils.js +84 -0
- package/dist/lib/ws-query-bridge.d.ts +7 -0
- package/dist/lib/ws-query-bridge.js +35 -0
- package/dist/pages/LandingPage.d.ts +22 -0
- package/dist/pages/LandingPage.js +5 -0
- package/dist/pages/LoginPage.d.ts +15 -0
- package/dist/pages/LoginPage.js +87 -0
- package/dist/pages/account/NotificationPreferencesPage.d.ts +13 -0
- package/dist/pages/account/NotificationPreferencesPage.js +41 -0
- package/dist/pages/account/ProfileSettingsPage.d.ts +4 -0
- package/dist/pages/account/ProfileSettingsPage.js +96 -0
- package/dist/pages/account/SecuritySettingsPage.d.ts +1 -0
- package/dist/pages/account/SecuritySettingsPage.js +134 -0
- package/dist/pages/account/TokensPage.d.ts +1 -0
- package/dist/pages/account/TokensPage.js +117 -0
- package/dist/pages/admin/AdminSettingsPage.d.ts +1 -0
- package/dist/pages/admin/AdminSettingsPage.js +210 -0
- package/dist/pages/admin/AdminShell.d.ts +7 -0
- package/dist/pages/admin/AdminShell.js +79 -0
- package/dist/pages/admin/AuditPage.d.ts +1 -0
- package/dist/pages/admin/AuditPage.js +164 -0
- package/dist/pages/admin/OrdersPage.d.ts +1 -0
- package/dist/pages/admin/OrdersPage.js +236 -0
- package/dist/pages/admin/OverviewPage.d.ts +1 -0
- package/dist/pages/admin/OverviewPage.js +200 -0
- package/dist/pages/admin/SSOPage.d.ts +1 -0
- package/dist/pages/admin/SSOPage.js +53 -0
- package/dist/pages/admin/UserDetailPage.d.ts +1 -0
- package/dist/pages/admin/UserDetailPage.js +266 -0
- package/dist/pages/admin/UsersPage.d.ts +4 -0
- package/dist/pages/admin/UsersPage.js +201 -0
- package/dist/pages/admin/WebhookDeliveriesPage.d.ts +1 -0
- package/dist/pages/admin/WebhookDeliveriesPage.js +128 -0
- package/dist/pages/admin/WorkspaceDetailPage.d.ts +1 -0
- package/dist/pages/admin/WorkspaceDetailPage.js +202 -0
- package/dist/pages/admin/WorkspacesPage.d.ts +1 -0
- package/dist/pages/admin/WorkspacesPage.js +133 -0
- package/dist/pages/admin/shared.d.ts +8 -0
- package/dist/pages/admin/shared.js +23 -0
- package/dist/pages/auth/ForgotPasswordPage.d.ts +6 -0
- package/dist/pages/auth/ForgotPasswordPage.js +32 -0
- package/dist/pages/auth/InviteAcceptPage.d.ts +5 -0
- package/dist/pages/auth/InviteAcceptPage.js +40 -0
- package/dist/pages/auth/RegisterPage.d.ts +15 -0
- package/dist/pages/auth/RegisterPage.js +80 -0
- package/dist/pages/auth/ResetPasswordPage.d.ts +4 -0
- package/dist/pages/auth/ResetPasswordPage.js +71 -0
- package/dist/pages/auth/VerifyEmailPage.d.ts +1 -0
- package/dist/pages/auth/VerifyEmailPage.js +45 -0
- package/dist/pages/workspace/BillingPage.d.ts +4 -0
- package/dist/pages/workspace/BillingPage.js +183 -0
- package/dist/pages/workspace/BillingSuccessPage.d.ts +4 -0
- package/dist/pages/workspace/BillingSuccessPage.js +72 -0
- package/dist/pages/workspace/MembersPage.d.ts +3 -0
- package/dist/pages/workspace/MembersPage.js +262 -0
- package/dist/pages/workspace/SchedulesPage.d.ts +4 -0
- package/dist/pages/workspace/SchedulesPage.js +177 -0
- package/dist/pages/workspace/UsagePage.d.ts +15 -0
- package/dist/pages/workspace/UsagePage.js +74 -0
- package/dist/pages/workspace/WebhooksPage.d.ts +3 -0
- package/dist/pages/workspace/WebhooksPage.js +242 -0
- package/dist/pages/workspace/WorkspaceAuditPage.d.ts +3 -0
- package/dist/pages/workspace/WorkspaceAuditPage.js +109 -0
- package/dist/pages/workspace/WorkspaceGeneralPage.d.ts +4 -0
- package/dist/pages/workspace/WorkspaceGeneralPage.js +101 -0
- package/dist/provider/PloykitProvider.d.ts +50 -0
- package/dist/provider/PloykitProvider.js +240 -0
- package/dist/realtime.d.ts +3 -0
- package/dist/realtime.js +11 -0
- package/package.json +63 -6
package/README.md
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# @ploykit/ui
|
|
2
|
+
|
|
3
|
+
React 前端壳:登录/工作区/计费的 Headless Hook + 默认页面。
|
|
4
|
+
|
|
5
|
+
## 快速使用
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { PloykitProvider, useAuth, useLogin, LoginPage } from '@ploykit/ui'
|
|
9
|
+
|
|
10
|
+
// App 根部
|
|
11
|
+
<PloykitProvider>
|
|
12
|
+
<App />
|
|
13
|
+
</PloykitProvider>
|
|
14
|
+
|
|
15
|
+
// 任意组件中
|
|
16
|
+
const { user, workspaces, loading } = useAuth()
|
|
17
|
+
const { sendCode, submit } = useLogin()
|
|
18
|
+
|
|
19
|
+
// 或直接用默认登录页
|
|
20
|
+
<LoginPage />
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 核心 API
|
|
24
|
+
|
|
25
|
+
| 导出 | 说明 |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `PloykitProvider` | 根 Provider(App 根部包裹一次) |
|
|
28
|
+
| `useAuth()` | `{ user, workspaces, loading, refresh, logout }` |
|
|
29
|
+
| `useLogin()` | `{ sendCode, submit, loading, error }` |
|
|
30
|
+
| `useWorkspace()` | `{ current, switchTo, clear }` |
|
|
31
|
+
| `useWorkspaceSwitch()` | `{ current, list, switchTo, create, creating }` |
|
|
32
|
+
| `useBilling()` | `{ subscription, plans, checkout, loading }` |
|
|
33
|
+
| `PlanGate` | 付费门 `<PlanGate plan="pro" denied={<Upgrade/>}>` |
|
|
34
|
+
| `useApi()` | `{ get, post, put, delete }` 带认证 + 工作区上下文 |
|
|
35
|
+
| `LoginPage` | 默认登录页(可拷贝自定义,Hook 不变) |
|
|
36
|
+
|
|
37
|
+
## 自定义三个层级
|
|
38
|
+
|
|
39
|
+
1. **换皮**:CSS/Tailwind 覆盖样式
|
|
40
|
+
2. **换壳**:拷贝 `LoginPage.tsx` 到产品 `src/pages/`,改 JSX,Hook 调用不变
|
|
41
|
+
3. **纯逻辑**:只 `import { useLogin } from '@ploykit/ui'`,完全自己画 UI
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
interface ConfirmOptions {
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
confirmText?: string;
|
|
6
|
+
cancelText?: string;
|
|
7
|
+
danger?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function ConfirmProvider({ children }: {
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
12
|
+
export declare function useConfirm(): (opts: ConfirmOptions) => Promise<boolean>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useCallback, useContext, useState } from 'react';
|
|
3
|
+
import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, } from './ui/alert-dialog';
|
|
4
|
+
const ConfirmContext = createContext(async () => false);
|
|
5
|
+
export function ConfirmProvider({ children }) {
|
|
6
|
+
const [state, setState] = useState(null);
|
|
7
|
+
const confirm = useCallback((opts) => {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
let settled = false;
|
|
10
|
+
setState({
|
|
11
|
+
...opts,
|
|
12
|
+
resolve: (v) => {
|
|
13
|
+
if (settled)
|
|
14
|
+
return;
|
|
15
|
+
settled = true;
|
|
16
|
+
resolve(v);
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}, []);
|
|
21
|
+
const handleClose = (result) => {
|
|
22
|
+
state?.resolve(result);
|
|
23
|
+
setState(null);
|
|
24
|
+
};
|
|
25
|
+
return (_jsxs(ConfirmContext.Provider, { value: confirm, children: [children, _jsx(AlertDialog, { open: !!state, onOpenChange: (open) => !open && handleClose(false), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: state?.title }), state?.description && _jsx(AlertDialogDescription, { children: state.description })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { onClick: () => handleClose(false), children: state?.cancelText ?? '取消' }), _jsx(AlertDialogAction, { variant: state?.danger ? 'destructive' : 'default', onClick: () => handleClose(true), children: state?.confirmText ?? '确认' })] })] }) })] }));
|
|
26
|
+
}
|
|
27
|
+
export function useConfirm() {
|
|
28
|
+
return useContext(ConfirmContext);
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface CronValue {
|
|
2
|
+
cron: string;
|
|
3
|
+
timezone: string;
|
|
4
|
+
}
|
|
5
|
+
export interface CronInputProps {
|
|
6
|
+
value: CronValue;
|
|
7
|
+
onChange: (v: CronValue) => void;
|
|
8
|
+
idPrefix?: string;
|
|
9
|
+
previewCount?: number;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare const FALLBACK_TIMEZONES: string[];
|
|
13
|
+
export declare function timezoneOptions(): string[];
|
|
14
|
+
export declare function describeCron(expr: string): string;
|
|
15
|
+
export declare function CronInput({ value, onChange, idPrefix, previewCount, disabled, }: CronInputProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { api } from '@ploykit/client';
|
|
4
|
+
import { apiErrorMessage } from '../lib/api-error';
|
|
5
|
+
import { Button } from './ui/Button';
|
|
6
|
+
import { Input } from './ui/Input';
|
|
7
|
+
export const FALLBACK_TIMEZONES = [
|
|
8
|
+
'UTC',
|
|
9
|
+
'Asia/Shanghai',
|
|
10
|
+
'Asia/Hong_Kong',
|
|
11
|
+
'Asia/Singapore',
|
|
12
|
+
'Asia/Tokyo',
|
|
13
|
+
'Europe/London',
|
|
14
|
+
'Europe/Berlin',
|
|
15
|
+
'Europe/Paris',
|
|
16
|
+
'America/New_York',
|
|
17
|
+
'America/Chicago',
|
|
18
|
+
'America/Los_Angeles',
|
|
19
|
+
'Australia/Sydney',
|
|
20
|
+
];
|
|
21
|
+
export function timezoneOptions() {
|
|
22
|
+
let zones = [];
|
|
23
|
+
try {
|
|
24
|
+
const i = Intl;
|
|
25
|
+
if (typeof i.supportedValuesOf === 'function') {
|
|
26
|
+
zones = i.supportedValuesOf('timeZone');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// fall through 到内置列表
|
|
31
|
+
}
|
|
32
|
+
if (zones.length === 0)
|
|
33
|
+
return FALLBACK_TIMEZONES;
|
|
34
|
+
return Array.from(new Set(['UTC', ...zones]));
|
|
35
|
+
}
|
|
36
|
+
const DOW_LABEL = {
|
|
37
|
+
'0': '周日', '1': '周一', '2': '周二', '3': '周三', '4': '周四', '5': '周五', '6': '周六', '7': '周日',
|
|
38
|
+
};
|
|
39
|
+
export function describeCron(expr) {
|
|
40
|
+
const fields = expr.trim().split(/\s+/);
|
|
41
|
+
if (fields.length !== 5)
|
|
42
|
+
return expr;
|
|
43
|
+
const [min, hour, dom, mon, dow] = fields;
|
|
44
|
+
if (mon !== '*')
|
|
45
|
+
return expr;
|
|
46
|
+
const intIn = (s, lo, hi) => /^\d+$/.test(s) && Number(s) >= lo && Number(s) <= hi;
|
|
47
|
+
if (min !== '*' && !intIn(min, 0, 59))
|
|
48
|
+
return expr;
|
|
49
|
+
if (hour !== '*' && !intIn(hour, 0, 23))
|
|
50
|
+
return expr;
|
|
51
|
+
if (dow !== '*' && !intIn(dow, 0, 7))
|
|
52
|
+
return expr;
|
|
53
|
+
const hhmm = `${hour.padStart(2, '0')}:${min.padStart(2, '0')}`;
|
|
54
|
+
if (min === '*' && hour === '*' && dom === '*' && dow === '*')
|
|
55
|
+
return '每分钟';
|
|
56
|
+
if (hour === '*' && dom === '*' && dow === '*')
|
|
57
|
+
return `每小时(第 ${min.padStart(2, '0')} 分)`;
|
|
58
|
+
if (dom === '*' && dow === '*')
|
|
59
|
+
return `每天 ${hhmm}`;
|
|
60
|
+
if (dom === '*' && hour !== '*' && min !== '*')
|
|
61
|
+
return `每${DOW_LABEL[dow] ?? dow} ${hhmm}`;
|
|
62
|
+
return expr;
|
|
63
|
+
}
|
|
64
|
+
function formatPreviewTime(iso, tz) {
|
|
65
|
+
const d = new Date(iso);
|
|
66
|
+
if (Number.isNaN(d.getTime()))
|
|
67
|
+
return iso;
|
|
68
|
+
try {
|
|
69
|
+
return new Intl.DateTimeFormat('zh-CN', {
|
|
70
|
+
timeZone: tz, dateStyle: 'medium', timeStyle: 'short',
|
|
71
|
+
}).format(d);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return d.toISOString();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function CronInput({ value, onChange, idPrefix = 'cron', previewCount = 3, disabled, }) {
|
|
78
|
+
const [preview, setPreview] = useState(null);
|
|
79
|
+
const [error, setError] = useState(null);
|
|
80
|
+
const [loading, setLoading] = useState(false);
|
|
81
|
+
const valueRef = useRef(value);
|
|
82
|
+
valueRef.current = value;
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
setPreview(null);
|
|
85
|
+
}, [value.cron, value.timezone]);
|
|
86
|
+
const runPreview = async () => {
|
|
87
|
+
const reqCron = value.cron.trim();
|
|
88
|
+
const reqTz = value.timezone;
|
|
89
|
+
setLoading(true);
|
|
90
|
+
setError(null);
|
|
91
|
+
setPreview(null);
|
|
92
|
+
const stillCurrent = () => valueRef.current.cron.trim() === reqCron && valueRef.current.timezone === reqTz;
|
|
93
|
+
try {
|
|
94
|
+
const d = await api.post('/api/schedules/preview', {
|
|
95
|
+
cron_expr: reqCron,
|
|
96
|
+
timezone: reqTz,
|
|
97
|
+
count: previewCount,
|
|
98
|
+
});
|
|
99
|
+
if (stillCurrent())
|
|
100
|
+
setPreview(d?.items ?? []);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
if (stillCurrent())
|
|
104
|
+
setError(apiErrorMessage(e, '预览失败,请检查表达式后重试'));
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
setLoading(false);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const zones = timezoneOptions();
|
|
111
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex gap-2", children: [_jsxs("div", { className: "min-w-0 flex-1 space-y-1", children: [_jsx("label", { htmlFor: `${idPrefix}-expr`, className: "text-sm font-medium", children: "Cron \u8868\u8FBE\u5F0F" }), _jsx(Input, { id: `${idPrefix}-expr`, placeholder: "0 9 * * *", className: "font-mono", value: value.cron, disabled: disabled, onChange: (e) => onChange({ ...value, cron: e.target.value }) }), _jsx("p", { className: "text-xs text-muted-foreground", "data-testid": "cron-hint", children: value.cron.trim() ? describeCron(value.cron) : '五字段:分 时 日 月 周' })] }), _jsxs("div", { className: "w-48 shrink-0 space-y-1", children: [_jsx("label", { htmlFor: `${idPrefix}-tz`, className: "text-sm font-medium", children: "\u65F6\u533A" }), _jsx("select", { id: `${idPrefix}-tz`, className: "h-9 w-full rounded-md border border-input bg-transparent px-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", value: value.timezone, disabled: disabled, onChange: (e) => onChange({ ...value, timezone: e.target.value }), children: zones.map((tz) => (_jsx("option", { value: tz, children: tz }, tz))) })] })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Button, { type: "button", size: "sm", variant: "outline", disabled: disabled || loading || !value.cron.trim(), onClick: () => void runPreview(), children: loading ? '预览中…' : `预览下次 ${previewCount} 次` }), error && (_jsx("p", { role: "alert", className: "text-xs text-destructive", children: error })), preview && preview.length > 0 && (_jsx("ul", { "data-testid": "cron-preview", className: "list-inside list-disc text-xs text-muted-foreground", children: preview.map((t, i) => (_jsx("li", { children: formatPreviewTime(t, value.timezone) }, i))) }))] })] }));
|
|
112
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export interface Column<T> {
|
|
3
|
+
key: string;
|
|
4
|
+
header: ReactNode;
|
|
5
|
+
render?: (row: T) => ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface DataTablePagination {
|
|
9
|
+
page: number;
|
|
10
|
+
pageSize: number;
|
|
11
|
+
total: number;
|
|
12
|
+
onPageChange: (page: number) => void;
|
|
13
|
+
}
|
|
14
|
+
export interface DataTableProps<T> {
|
|
15
|
+
columns: Column<T>[];
|
|
16
|
+
rows: T[];
|
|
17
|
+
rowKey: (row: T) => string;
|
|
18
|
+
loading?: boolean;
|
|
19
|
+
empty?: ReactNode;
|
|
20
|
+
pagination?: DataTablePagination;
|
|
21
|
+
}
|
|
22
|
+
export declare function DataTable<T>({ columns, rows, rowKey, loading, empty, pagination }: DataTableProps<T>): import("react").JSX.Element;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from '../lib/utils';
|
|
3
|
+
import { PageEmpty } from './Page';
|
|
4
|
+
import { Skeleton } from './ui/skeleton';
|
|
5
|
+
import { Button } from './ui/Button';
|
|
6
|
+
export function DataTable({ columns, rows, rowKey, loading = false, empty, pagination }) {
|
|
7
|
+
const stalePage = !!pagination && !loading && rows.length === 0 && (pagination.total > 0 || pagination.page > 1);
|
|
8
|
+
if (!loading && rows.length === 0 && !stalePage) {
|
|
9
|
+
return _jsx(_Fragment, { children: empty ?? _jsx(PageEmpty, {}) });
|
|
10
|
+
}
|
|
11
|
+
const lastPage = pagination ? Math.max(1, Math.ceil(pagination.total / Math.max(1, pagination.pageSize))) : 1;
|
|
12
|
+
return (_jsxs("div", { className: "bg-card rounded-lg border overflow-hidden", children: [_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { children: _jsx("tr", { className: "border-b bg-muted/50 text-left text-muted-foreground", children: columns.map((col) => (_jsx("th", { className: cn('px-4 py-3 font-medium whitespace-nowrap', col.className), children: col.header }, col.key))) }) }), _jsx("tbody", { children: loading ? (_jsx("tr", { className: "border-b last:border-0", children: columns.map((col) => (_jsx("td", { className: cn('px-4 py-3', col.className), children: _jsx(Skeleton, { className: "h-4 w-full" }) }, col.key))) })) : stalePage ? (_jsx("tr", { children: _jsx("td", { colSpan: columns.length, className: "px-4 py-8 text-center text-sm text-muted-foreground", children: "\u672C\u9875\u6CA1\u6709\u6570\u636E\uFF0C\u53EF\u8FD4\u56DE\u4E0A\u4E00\u9875" }) })) : (rows.map((row) => (_jsx("tr", { className: "border-b last:border-0 hover:bg-muted/30", children: columns.map((col) => (_jsx("td", { className: cn('px-4 py-3', col.className), children: col.render ? col.render(row) : String(row[col.key] ?? '') }, col.key))) }, rowKey(row))))) })] }) }), pagination && (_jsxs("div", { "data-slot": "data-table-pagination", "aria-busy": loading || undefined, className: "flex items-center justify-between border-t px-4 py-3 text-sm text-muted-foreground", children: [loading ? (_jsx("span", { children: "\u52A0\u8F7D\u4E2D\u2026" })) : (_jsxs("span", { children: ["\u5171 ", _jsx("span", { className: "font-medium text-foreground", children: pagination.total }), " \u6761 \u00B7 \u7B2C", ' ', _jsxs("span", { className: "font-medium text-foreground", children: [pagination.page, "/", lastPage] }), ' ', "\u9875"] })), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { size: "sm", variant: "outline", disabled: loading || pagination.page <= 1, onClick: () => pagination.onPageChange(pagination.page - 1), children: "\u4E0A\u4E00\u9875" }), _jsx(Button, { size: "sm", variant: "outline", disabled: loading || pagination.page >= lastPage, onClick: () => pagination.onPageChange(pagination.page + 1), children: "\u4E0B\u4E00\u9875" })] })] }))] }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Component, type ReactNode } from 'react';
|
|
2
|
+
export interface ErrorBoundaryProps {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
|
|
5
|
+
}
|
|
6
|
+
interface ErrorBoundaryState {
|
|
7
|
+
error: Error | null;
|
|
8
|
+
}
|
|
9
|
+
export declare class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
10
|
+
state: ErrorBoundaryState;
|
|
11
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState;
|
|
12
|
+
componentDidCatch(error: Error): void;
|
|
13
|
+
reset: () => void;
|
|
14
|
+
render(): ReactNode;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Component } from 'react';
|
|
3
|
+
export class ErrorBoundary extends Component {
|
|
4
|
+
state = { error: null };
|
|
5
|
+
static getDerivedStateFromError(error) {
|
|
6
|
+
return { error };
|
|
7
|
+
}
|
|
8
|
+
componentDidCatch(error) {
|
|
9
|
+
console.error('[ErrorBoundary] 子树渲染出错:', error);
|
|
10
|
+
}
|
|
11
|
+
reset = () => {
|
|
12
|
+
this.setState({ error: null });
|
|
13
|
+
};
|
|
14
|
+
render() {
|
|
15
|
+
const { error } = this.state;
|
|
16
|
+
if (error === null)
|
|
17
|
+
return this.props.children;
|
|
18
|
+
const { fallback } = this.props;
|
|
19
|
+
if (fallback !== undefined) {
|
|
20
|
+
return typeof fallback === 'function' ? fallback(error, this.reset) : fallback;
|
|
21
|
+
}
|
|
22
|
+
return (_jsxs("div", { role: "alert", className: "flex flex-col items-center justify-center py-12 text-center", children: [_jsx("div", { className: "text-4xl mb-3", children: "\u26A0\uFE0F" }), _jsx("p", { className: "text-sm text-red-600", children: "\u51FA\u9519\u4E86" }), error.message && _jsx("p", { className: "mt-1 text-xs text-gray-400", children: error.message }), _jsx("button", { onClick: this.reset, className: "mt-4 px-4 py-2 text-sm border rounded-md hover:bg-gray-50", children: "\u91CD\u8BD5" })] }));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface FormFieldProps {
|
|
3
|
+
label: string;
|
|
4
|
+
error?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
required?: boolean;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
htmlFor?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function FormField({ label, error, description, required, children, htmlFor }: FormFieldProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cloneElement, isValidElement } from 'react';
|
|
3
|
+
import { Field, FieldLabel, FieldError, FieldDescription } from './ui/field';
|
|
4
|
+
function mergeDescribedBy(props, extra) {
|
|
5
|
+
const existing = typeof props['aria-describedby'] === 'string' ? props['aria-describedby'] : '';
|
|
6
|
+
return { 'aria-describedby': [existing, extra].filter(Boolean).join(' ') };
|
|
7
|
+
}
|
|
8
|
+
export function FormField({ label, error, description, required, children, htmlFor }) {
|
|
9
|
+
const errorId = htmlFor && error ? `${htmlFor}-error` : undefined;
|
|
10
|
+
const descId = htmlFor && description ? `${htmlFor}-desc` : undefined;
|
|
11
|
+
const describedBy = [descId, errorId].filter(Boolean).join(' ') || undefined;
|
|
12
|
+
const control = describedBy || error || required
|
|
13
|
+
? isValidElement(children)
|
|
14
|
+
? cloneElement(children, {
|
|
15
|
+
...(describedBy ? mergeDescribedBy(children.props, describedBy) : {}),
|
|
16
|
+
...(error ? { 'aria-invalid': true } : {}),
|
|
17
|
+
...(required ? { 'aria-required': true } : {}),
|
|
18
|
+
})
|
|
19
|
+
: children
|
|
20
|
+
: children;
|
|
21
|
+
return (_jsxs(Field, { "data-invalid": !!error, children: [_jsxs(FieldLabel, { htmlFor: htmlFor, children: [label, required && (_jsx("span", { "aria-hidden": "true", className: "ml-0.5 text-destructive", children: "*" }))] }), control, description && _jsx(FieldDescription, { id: descId, children: description }), _jsx(FieldError, { id: errorId, children: error })] }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { TriangleAlert } from 'lucide-react';
|
|
3
|
+
import { useAuth } from '../hooks/useAuth';
|
|
4
|
+
import { cn } from '../lib/utils';
|
|
5
|
+
export function ImpersonationBanner({ className }) {
|
|
6
|
+
const { user } = useAuth();
|
|
7
|
+
if (!user?.impersonated_by)
|
|
8
|
+
return null;
|
|
9
|
+
return (_jsxs("div", { role: "status", "data-slot": "impersonation-banner", className: cn('flex w-full items-center justify-center gap-2 border-b px-4 py-2 text-sm font-medium', 'border-destructive/30 bg-destructive/10 text-destructive', className), children: [_jsx(TriangleAlert, { className: "size-4 shrink-0", "aria-hidden": "true" }), _jsx("span", { children: "\u60A8\u6B63\u5904\u4E8E\u7BA1\u7406\u5458\u6A21\u62DF\u4F1A\u8BDD" })] }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
4
|
+
import { api } from '@ploykit/client';
|
|
5
|
+
import { queryKeys } from '../provider/PloykitProvider';
|
|
6
|
+
import { cn, formatDateTime } from '../lib/utils';
|
|
7
|
+
export function NotificationBell({ onNavigate }) {
|
|
8
|
+
const [open, setOpen] = useState(false);
|
|
9
|
+
const ref = useRef(null);
|
|
10
|
+
const qc = useQueryClient();
|
|
11
|
+
const badgeQ = useQuery({
|
|
12
|
+
queryKey: [queryKeys.notifications, 'badge'],
|
|
13
|
+
queryFn: () => api.get('/api/notifications/badge'),
|
|
14
|
+
refetchInterval: 60_000,
|
|
15
|
+
});
|
|
16
|
+
const unread = badgeQ.data?.count ?? 0;
|
|
17
|
+
const listQ = useQuery({
|
|
18
|
+
queryKey: [queryKeys.notifications, 'list'],
|
|
19
|
+
queryFn: () => api.get('/api/notifications?limit=8'),
|
|
20
|
+
staleTime: 30_000,
|
|
21
|
+
enabled: open,
|
|
22
|
+
});
|
|
23
|
+
const notifications = Array.isArray(listQ.data) ? listQ.data : [];
|
|
24
|
+
const invalidate = () => {
|
|
25
|
+
void qc.invalidateQueries({ queryKey: [queryKeys.notifications] });
|
|
26
|
+
};
|
|
27
|
+
const markAllReadM = useMutation({
|
|
28
|
+
mutationFn: () => api.post('/api/notifications/read-all'),
|
|
29
|
+
onSuccess: invalidate,
|
|
30
|
+
});
|
|
31
|
+
const markReadM = useMutation({
|
|
32
|
+
mutationFn: (id) => api.post(`/api/notifications/${id}/read`),
|
|
33
|
+
onSuccess: invalidate,
|
|
34
|
+
});
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const onClickOutside = (e) => {
|
|
37
|
+
if (ref.current && !ref.current.contains(e.target))
|
|
38
|
+
setOpen(false);
|
|
39
|
+
};
|
|
40
|
+
document.addEventListener('mousedown', onClickOutside);
|
|
41
|
+
return () => document.removeEventListener('mousedown', onClickOutside);
|
|
42
|
+
}, []);
|
|
43
|
+
return (_jsxs("div", { ref: ref, className: "relative", children: [_jsxs("button", { onClick: () => setOpen(!open), className: "relative p-2 text-muted-foreground hover:text-foreground hover:bg-accent rounded-md\r\n focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", "aria-label": "\u901A\u77E5", "aria-haspopup": "true", "aria-expanded": open, children: [_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" }) }), unread > 0 && (_jsx("span", { className: "absolute -top-0.5 -right-0.5 flex items-center justify-center min-w-[18px] h-[18px]\r\n px-1 text-xs font-bold text-white bg-red-500 rounded-full", children: unread > 99 ? '99+' : unread }))] }), open && (_jsxs("div", { className: "absolute right-0 mt-2 w-80 bg-white rounded-lg shadow-lg border z-50", role: "dialog", "aria-label": "\u901A\u77E5\u5217\u8868", onKeyDown: (e) => { if (e.key === 'Escape')
|
|
44
|
+
setOpen(false); }, children: [_jsxs("div", { className: "flex items-center justify-between px-4 py-3 border-b", children: [_jsx("span", { className: "text-sm font-semibold", children: "\u901A\u77E5" }), unread > 0 && (_jsx("button", { onClick: () => markAllReadM.mutate(), disabled: markAllReadM.isPending, className: "text-xs text-blue-600 hover:underline disabled:opacity-50\r\n focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded", children: "\u5168\u90E8\u5DF2\u8BFB" }))] }), _jsx("ul", { className: "max-h-80 overflow-y-auto list-none p-0 m-0", "data-slot": "notification-list", children: listQ.isPending ? (_jsx("li", { className: "py-8 text-center text-sm text-gray-400", "aria-live": "polite", children: "\u52A0\u8F7D\u4E2D\u2026" })) : notifications.length === 0 ? (_jsx("li", { className: "py-8 text-center text-sm text-gray-400", children: "\u6682\u65E0\u901A\u77E5" })) : notifications.map((n) => (_jsx("li", { className: "border-b last:border-0", children: _jsxs("button", { type: "button", onClick: () => { if (n.link)
|
|
45
|
+
onNavigate?.(n.link); markReadM.mutate(n.id); setOpen(false); }, className: cn('w-full text-left px-4 py-3 cursor-pointer hover:bg-gray-50 ' +
|
|
46
|
+
'focus-visible:outline-none focus-visible:bg-gray-50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring', !n.read_at && 'bg-blue-50/50'), children: [_jsxs("span", { className: "flex items-start justify-between gap-2", children: [_jsx("span", { className: "text-sm font-medium text-gray-900", children: n.title }), n.count > 1 && (_jsxs("span", { className: "text-xs bg-gray-100 text-gray-600 px-1.5 py-0.5 rounded-full shrink-0", children: ["\u00D7", n.count] }))] }), n.body && _jsx("span", { className: "block mt-0.5 text-xs text-gray-500 line-clamp-2", children: n.body }), _jsx("span", { className: "block mt-1 text-[10px] text-gray-400", children: formatDateTime(n.created_at) })] }) }, n.id))) })] }))] }));
|
|
47
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface OnboardingItem {
|
|
2
|
+
key: string;
|
|
3
|
+
label: string;
|
|
4
|
+
done: boolean;
|
|
5
|
+
onClick?: () => void;
|
|
6
|
+
}
|
|
7
|
+
export interface OnboardingChecklistProps {
|
|
8
|
+
items?: OnboardingItem[];
|
|
9
|
+
storageKey?: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function OnboardingChecklist({ items, storageKey, title, }?: OnboardingChecklistProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { X } from 'lucide-react';
|
|
4
|
+
import { cn } from '../lib/utils';
|
|
5
|
+
import { Progress, ProgressLabel, ProgressValue } from './ui/progress';
|
|
6
|
+
import { Button } from './ui/Button';
|
|
7
|
+
import { useAuth } from '../hooks/useAuth';
|
|
8
|
+
const DISMISS_KEY = 'pk_onboarding_dismissed';
|
|
9
|
+
function defaultItems(hasWorkspaces) {
|
|
10
|
+
return [
|
|
11
|
+
{ key: 'create-workspace', label: '创建第一个工作区', done: hasWorkspaces },
|
|
12
|
+
];
|
|
13
|
+
}
|
|
14
|
+
export function OnboardingChecklist({ items, storageKey = DISMISS_KEY, title = '开始使用', } = {}) {
|
|
15
|
+
const { user, workspaces } = useAuth();
|
|
16
|
+
const [dismissed, setDismissed] = useState(false);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
try {
|
|
19
|
+
setDismissed(localStorage.getItem(storageKey) === '1');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
/* localStorage 不可用(隐私模式等)按未关闭处理——effect 期兜底,非渲染期吞异常 */
|
|
23
|
+
}
|
|
24
|
+
}, [storageKey]);
|
|
25
|
+
const list = items ?? (user ? defaultItems(workspaces.length > 0) : []);
|
|
26
|
+
if (!user || dismissed || list.length === 0)
|
|
27
|
+
return null;
|
|
28
|
+
const doneCount = list.filter((i) => i.done).length;
|
|
29
|
+
const pct = Math.round((doneCount / list.length) * 100);
|
|
30
|
+
const allDone = doneCount === list.length;
|
|
31
|
+
const dismiss = () => {
|
|
32
|
+
try {
|
|
33
|
+
localStorage.setItem(storageKey, '1');
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* 隐私模式等场景忽略持久化失败 */
|
|
37
|
+
}
|
|
38
|
+
setDismissed(true);
|
|
39
|
+
};
|
|
40
|
+
return (_jsxs("section", { className: "w-full rounded-xl border bg-card p-4 shadow-xs", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold", children: title }), _jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: allDone ? '全部完成,开始你的旅程吧!' : `已完成 ${doneCount}/${list.length} 项` })] }), _jsx(Button, { variant: "ghost", size: "iconSm", "aria-label": "\u5173\u95ED\u5F15\u5BFC\u6E05\u5355", onClick: dismiss, children: _jsx(X, {}) })] }), _jsxs(Progress, { value: pct, className: "mt-3", children: [_jsx(ProgressLabel, { children: allDone ? '完成' : '进度' }), _jsx(ProgressValue, {})] }), _jsx("ul", { className: "mt-4 space-y-2", children: list.map((item) => (_jsx("li", { children: _jsxs("button", { type: "button", onClick: item.done ? undefined : item.onClick, disabled: item.done, className: cn('flex w-full items-center gap-3 rounded-lg border px-3 py-2 text-left text-sm transition-colors', item.done
|
|
41
|
+
? 'border-transparent bg-muted/50 text-muted-foreground'
|
|
42
|
+
: 'border-border hover:bg-accent hover:text-accent-foreground', !item.done && !item.onClick && 'cursor-default'), children: [_jsx("span", { "aria-hidden": "true", className: cn('flex size-5 shrink-0 items-center justify-center rounded-full border text-xs', item.done ? 'border-primary bg-primary text-primary-foreground' : 'border-input'), children: item.done ? '✓' : '' }), _jsx("span", { className: cn(item.done && 'line-through'), children: item.label })] }) }, item.key))) })] }));
|
|
43
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export declare function PageHeader({ title, description, actions }: {
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
actions?: ReactNode;
|
|
6
|
+
}): import("react").JSX.Element;
|
|
7
|
+
export declare function PageLoading({ label }: {
|
|
8
|
+
label?: string;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
10
|
+
export declare function PageEmpty({ label, hint }: {
|
|
11
|
+
label?: string;
|
|
12
|
+
hint?: string;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
14
|
+
export declare function PageError({ message, onRetry }: {
|
|
15
|
+
message: string;
|
|
16
|
+
onRetry?: () => void;
|
|
17
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Inbox, TriangleAlert } from 'lucide-react';
|
|
3
|
+
import { Button } from './ui/Button';
|
|
4
|
+
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent, } from './ui/empty';
|
|
5
|
+
export function PageHeader({ title, description, actions }) {
|
|
6
|
+
return (_jsxs("div", { className: "mb-6 flex flex-wrap items-start justify-between gap-4", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "text-2xl font-bold text-foreground", children: title }), description && _jsx("p", { className: "mt-1 max-w-2xl text-sm text-muted-foreground", children: description })] }), actions && _jsx("div", { className: "flex shrink-0 gap-2", children: actions })] }));
|
|
7
|
+
}
|
|
8
|
+
export function PageLoading({ label = '加载中…' }) {
|
|
9
|
+
return (_jsxs("div", { className: "flex items-center justify-center py-12", role: "status", "aria-live": "polite", children: [_jsx("div", { className: "h-6 w-6 animate-spin rounded-full border-2 border-muted-foreground/20 border-t-primary" }), _jsx("span", { className: "ml-3 text-sm text-muted-foreground", children: label })] }));
|
|
10
|
+
}
|
|
11
|
+
export function PageEmpty({ label = '暂无数据', hint }) {
|
|
12
|
+
return (_jsx(Empty, { className: "py-8", children: _jsxs(EmptyHeader, { children: [_jsx(EmptyMedia, { variant: "icon", children: _jsx(Inbox, { "aria-hidden": "true" }) }), _jsx(EmptyTitle, { children: label }), hint && _jsx(EmptyDescription, { children: hint })] }) }));
|
|
13
|
+
}
|
|
14
|
+
export function PageError({ message, onRetry }) {
|
|
15
|
+
return (_jsx(Empty, { className: "py-8", role: "alert", children: _jsxs(EmptyHeader, { children: [_jsx(EmptyMedia, { variant: "icon", className: "bg-destructive/10 text-destructive", children: _jsx(TriangleAlert, { "aria-hidden": "true" }) }), _jsx(EmptyTitle, { className: "text-destructive", children: message }), onRetry && (_jsx(EmptyContent, { children: _jsx(Button, { variant: "outline", onClick: onRetry, children: "\u91CD\u8BD5" }) }))] }) }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare function SecretModal({ open, onOpenChange, secretName, secretValue, title }: {
|
|
2
|
+
open: boolean;
|
|
3
|
+
onOpenChange: (open: boolean) => void;
|
|
4
|
+
secretName: string;
|
|
5
|
+
secretValue: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
8
|
+
export declare function ImpactConfirmation({ open, onConfirm, onOpenChange, title, impacts, confirmText, danger }: {
|
|
9
|
+
open: boolean;
|
|
10
|
+
onConfirm: () => void;
|
|
11
|
+
onOpenChange: (open: boolean) => void;
|
|
12
|
+
title: string;
|
|
13
|
+
impacts: string[];
|
|
14
|
+
confirmText?: string;
|
|
15
|
+
danger?: boolean;
|
|
16
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from './ui/dialog';
|
|
4
|
+
import { Button } from './ui/Button';
|
|
5
|
+
export function SecretModal({ open, onOpenChange, secretName, secretValue, title }) {
|
|
6
|
+
const [copied, setCopied] = useState(false);
|
|
7
|
+
const [copyFailed, setCopyFailed] = useState(false);
|
|
8
|
+
const [confirmed, setConfirmed] = useState(false);
|
|
9
|
+
const copyTimer = useRef(null);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
setCopied(false);
|
|
12
|
+
setCopyFailed(false);
|
|
13
|
+
setConfirmed(false);
|
|
14
|
+
if (copyTimer.current) {
|
|
15
|
+
clearTimeout(copyTimer.current);
|
|
16
|
+
copyTimer.current = null;
|
|
17
|
+
}
|
|
18
|
+
}, [open, secretValue]);
|
|
19
|
+
useEffect(() => () => {
|
|
20
|
+
if (copyTimer.current)
|
|
21
|
+
clearTimeout(copyTimer.current);
|
|
22
|
+
}, []);
|
|
23
|
+
const copy = async () => {
|
|
24
|
+
try {
|
|
25
|
+
await navigator.clipboard.writeText(secretValue);
|
|
26
|
+
setCopied(true);
|
|
27
|
+
setCopyFailed(false);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
setCopied(false);
|
|
31
|
+
setCopyFailed(true);
|
|
32
|
+
}
|
|
33
|
+
if (copyTimer.current)
|
|
34
|
+
clearTimeout(copyTimer.current);
|
|
35
|
+
copyTimer.current = setTimeout(() => {
|
|
36
|
+
setCopied(false);
|
|
37
|
+
setCopyFailed(false);
|
|
38
|
+
}, 2000);
|
|
39
|
+
};
|
|
40
|
+
return (_jsx(Dialog, { open: open, onOpenChange: (o) => confirmed && onOpenChange(o), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: title ?? `${secretName} 创建成功` }), _jsx(DialogDescription, { children: "\u6B64\u5BC6\u94A5\u53EA\u663E\u793A\u4E00\u6B21\uFF0C\u5173\u95ED\u540E\u65E0\u6CD5\u518D\u6B21\u67E5\u770B\u3002\u8BF7\u7ACB\u5373\u4FDD\u5B58\u5230\u5B89\u5168\u7684\u5730\u65B9\u3002" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("code", { className: "flex-1 px-3 py-2 bg-foreground/5 rounded font-mono text-sm break-all", children: secretValue }), _jsx(Button, { variant: "outline", size: "sm", onClick: copy, children: copied ? '已复制' : copyFailed ? '复制失败,请手动复制' : '复制' })] }), _jsxs("label", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx("input", { type: "checkbox", checked: confirmed, onChange: (e) => setConfirmed(e.target.checked), className: "rounded border-input" }), "\u6211\u5DF2\u5C06\u5BC6\u94A5\u4FDD\u5B58\u5230\u5B89\u5168\u7684\u5730\u65B9"] }), _jsx(DialogFooter, { children: _jsx(Button, { onClick: () => onOpenChange(false), disabled: !confirmed, children: confirmed ? '我已保存' : '请先确认已保存' }) })] }) }));
|
|
41
|
+
}
|
|
42
|
+
export function ImpactConfirmation({ open, onConfirm, onOpenChange, title, impacts, confirmText, danger }) {
|
|
43
|
+
const [checked, setChecked] = useState(false);
|
|
44
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: "\u6B64\u64CD\u4F5C\u5C06\u4EA7\u751F\u4EE5\u4E0B\u5F71\u54CD\uFF1A" })] }), _jsx("ul", { className: "space-y-1.5", children: impacts.map((impact) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-foreground", children: [_jsx("span", { className: "text-destructive mt-0.5", children: "\u2022" }), " ", impact] }, impact))) }), _jsxs("label", { className: "flex items-center gap-2 text-sm text-muted-foreground pt-2 border-t", children: [_jsx("input", { type: "checkbox", checked: checked, onChange: (e) => setChecked(e.target.checked), className: "rounded border-input" }), "\u6211\u7406\u89E3\u5E76\u63A5\u53D7\u4EE5\u4E0A\u5F71\u54CD"] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), children: "\u53D6\u6D88" }), _jsx(Button, { variant: danger ? 'destructive' : 'default', disabled: !checked, onClick: () => { onConfirm(); onOpenChange(false); }, children: confirmText ?? '确认执行' })] })] }) }));
|
|
45
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export interface SettingsCardProps {
|
|
3
|
+
title: ReactNode;
|
|
4
|
+
description?: ReactNode;
|
|
5
|
+
cta?: ReactNode;
|
|
6
|
+
danger?: boolean;
|
|
7
|
+
bodyClassName?: string;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
export declare function SettingsCard({ title, description, cta, danger, bodyClassName, children, }: SettingsCardProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from '../lib/utils';
|
|
3
|
+
import { Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, } from './ui/card';
|
|
4
|
+
export function SettingsCard({ title, description, cta, danger, bodyClassName, children, }) {
|
|
5
|
+
return (_jsxs(Card, { className: "my-4 w-full max-w-4xl text-left", size: "sm", children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { as: "h2", className: cn(danger && 'text-destructive'), children: title }), description && (_jsx(CardDescription, { className: cn(danger && 'text-destructive/80'), children: description })), cta && _jsx(CardAction, { children: cta })] }), _jsx(CardContent, { className: cn('text-left', bodyClassName), children: children })] }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { TriangleAlert } from 'lucide-react';
|
|
3
|
+
import { cn } from '../lib/utils';
|
|
4
|
+
export function SiteBanner({ text, kind = 'info', className }) {
|
|
5
|
+
if (!text || !text.trim())
|
|
6
|
+
return null;
|
|
7
|
+
const warn = kind === 'warn';
|
|
8
|
+
return (_jsxs("div", { role: "status", "data-slot": "site-banner", className: cn('flex w-full items-center justify-center gap-2 border-b px-4 py-2 text-sm', warn
|
|
9
|
+
? 'border-destructive/30 bg-destructive/10 text-destructive'
|
|
10
|
+
: 'border-primary/25 bg-primary/10 text-primary', className), children: [warn && _jsx(TriangleAlert, { className: "size-4 shrink-0", "aria-hidden": "true" }), _jsx("span", { className: "min-w-0 whitespace-pre-wrap text-balance", children: text })] }));
|
|
11
|
+
}
|