@ploykit/ui 0.2.0 → 0.4.1
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 +201 -41
- package/dist/AppProviders.d.ts +15 -0
- package/dist/AppProviders.js +15 -0
- package/dist/components/ConfirmDialog.js +8 -2
- package/dist/components/DataTable.d.ts +12 -3
- package/dist/components/DataTable.js +6 -2
- package/dist/components/ErrorBoundary.js +1 -1
- package/dist/components/NotificationBell.js +4 -4
- package/dist/components/ReauthDialog.d.ts +15 -0
- package/dist/components/ReauthDialog.js +42 -0
- package/dist/components/SecretModal.d.ts +8 -1
- package/dist/components/SecretModal.js +5 -2
- package/dist/components/ui/{Badge.d.ts → badge.d.ts} +1 -1
- package/dist/components/ui/{Button.d.ts → button.d.ts} +1 -1
- package/dist/components/ui/dialog.d.ts +7 -1
- package/dist/components/ui/dialog.js +7 -2
- package/dist/components/ui/select.d.ts +20 -1
- package/dist/components/ui/select.js +22 -1
- package/dist/components/ui/sheet.d.ts +9 -1
- package/dist/components/ui/sheet.js +9 -2
- package/dist/components/ui/textarea.d.ts +9 -0
- package/dist/components/ui/textarea.js +12 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/pages/LandingPage.js +1 -1
- package/dist/pages/LoginPage.js +2 -2
- package/dist/pages/account/ProfileSettingsPage.js +1 -1
- package/dist/pages/account/SecuritySettingsPage.js +1 -1
- package/dist/pages/auth/InviteAcceptPage.js +1 -1
- package/dist/pages/auth/ResetPasswordPage.js +1 -1
- package/dist/pages/auth/VerifyEmailPage.js +1 -1
- package/dist/pages/workspace/RolePermissionsPage.d.ts +3 -0
- package/dist/pages/workspace/RolePermissionsPage.js +118 -0
- package/package.json +3 -3
- package/dist/components/ui/menu.d.ts +0 -29
- package/dist/components/ui/menu.js +0 -50
- package/dist/hooks/useApi.d.ts +0 -10
- package/dist/hooks/useApi.js +0 -11
- package/dist/hooks/useAudit.d.ts +0 -39
- package/dist/hooks/useAudit.js +0 -55
- package/dist/hooks/useAuth.d.ts +0 -1
- package/dist/hooks/useAuth.js +0 -4
- package/dist/hooks/useBilling.d.ts +0 -44
- package/dist/hooks/useBilling.js +0 -64
- package/dist/hooks/useLogin.d.ts +0 -10
- package/dist/hooks/useLogin.js +0 -39
- package/dist/hooks/useMembers.d.ts +0 -125
- package/dist/hooks/useMembers.js +0 -127
- package/dist/hooks/useNotificationPrefs.d.ts +0 -20
- package/dist/hooks/useNotificationPrefs.js +0 -42
- package/dist/hooks/usePasswordReset.d.ts +0 -5
- package/dist/hooks/usePasswordReset.js +0 -12
- package/dist/hooks/useSEO.d.ts +0 -6
- package/dist/hooks/useSEO.js +0 -14
- package/dist/hooks/useSessions.d.ts +0 -20
- package/dist/hooks/useSessions.js +0 -38
- package/dist/hooks/useSiteConfig.d.ts +0 -12
- package/dist/hooks/useSiteConfig.js +0 -11
- package/dist/hooks/useTokens.d.ts +0 -36
- package/dist/hooks/useTokens.js +0 -35
- package/dist/hooks/useUsage.d.ts +0 -20
- package/dist/hooks/useUsage.js +0 -21
- package/dist/hooks/useWorkspace.d.ts +0 -26
- package/dist/hooks/useWorkspace.js +0 -32
- package/dist/hooks/useZodForm.d.ts +0 -6
- package/dist/hooks/useZodForm.js +0 -6
- package/dist/hooks/ws.d.ts +0 -4
- package/dist/hooks/ws.js +0 -21
- package/dist/lib/perm.d.ts +0 -2
- package/dist/lib/perm.js +0 -7
- package/dist/lib/ws-query-bridge.d.ts +0 -7
- package/dist/lib/ws-query-bridge.js +0 -35
- package/dist/provider/PloykitProvider.d.ts +0 -50
- package/dist/provider/PloykitProvider.js +0 -240
- package/dist/realtime.d.ts +0 -3
- package/dist/realtime.js +0 -11
- /package/dist/components/ui/{Badge.js → badge.js} +0 -0
- /package/dist/components/ui/{Button.js → button.js} +0 -0
- /package/dist/components/ui/{Input.d.ts → input.d.ts} +0 -0
- /package/dist/components/ui/{Input.js → input.js} +0 -0
package/README.md
CHANGED
|
@@ -1,41 +1,201 @@
|
|
|
1
|
-
# @ploykit/ui
|
|
2
|
-
|
|
3
|
-
React
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
1
|
+
# @ploykit/ui
|
|
2
|
+
|
|
3
|
+
React 前端壳:默认页面 + 组件。逻辑与状态在 [hooks](../hooks) 与 [client](../client) 里,本包是它们的渲染层。
|
|
4
|
+
|
|
5
|
+
| 包 | 内容 |
|
|
6
|
+
|---|---|
|
|
7
|
+
| `@ploykit/ui` | 组件壳、默认页面、全局组合件(本包) |
|
|
8
|
+
| `@ploykit/hooks` | `PloykitProvider` + headless hooks(认证/工作区/计费) |
|
|
9
|
+
| `@ploykit/client` | typed API client + CSRF(hooks 的底层) |
|
|
10
|
+
|
|
11
|
+
依赖方向:ui → hooks → client。Peer deps:`react` 19 / `react-dom` 19 / `@tanstack/react-query` 5 / `react-router` 7 / `tailwindcss` 4。产物为 ESM(相对导入无扩展名,需经 vite/esbuild 等打包器解析)。
|
|
12
|
+
|
|
13
|
+
## 快速使用
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { AppProviders } from '@ploykit/ui'
|
|
17
|
+
import { useAuth, useLogin, useApi } from '@ploykit/hooks'
|
|
18
|
+
import { LoginPage } from '@ploykit/ui'
|
|
19
|
+
|
|
20
|
+
// 应用根部:一层包完(PloykitProvider > ConfirmProvider > Toaster 三层组合)
|
|
21
|
+
<AppProviders>
|
|
22
|
+
<App />
|
|
23
|
+
</AppProviders>
|
|
24
|
+
|
|
25
|
+
// 已有自己的 QueryClient 时透传(不传则内部创建默认 QueryClient)
|
|
26
|
+
<AppProviders queryClient={queryClient}>...</AppProviders>
|
|
27
|
+
|
|
28
|
+
// 数据 hooks 一律来自 @ploykit/hooks
|
|
29
|
+
const { user, loading } = useAuth()
|
|
30
|
+
const api = useApi()
|
|
31
|
+
|
|
32
|
+
// 默认页面来自 @ploykit/ui
|
|
33
|
+
<Route path="/login" element={<LoginPage />} />
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
等价的手动三层挂法(需要插入自定义 Provider 层时用):
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { PloykitProvider } from '@ploykit/hooks'
|
|
40
|
+
import { ConfirmProvider, Toaster } from '@ploykit/ui'
|
|
41
|
+
|
|
42
|
+
<PloykitProvider queryClient={queryClient}>
|
|
43
|
+
<ConfirmProvider>
|
|
44
|
+
<App />
|
|
45
|
+
<Toaster />
|
|
46
|
+
</ConfirmProvider>
|
|
47
|
+
</PloykitProvider>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
要复用全局 QueryClient 实例时,自行 `new QueryClient()` 后经 `queryClient` 传给 `PloykitProvider`。默认 QueryClient 的行为:`staleTime` 30s、4xx 不重试、窗口聚焦不重取。
|
|
51
|
+
|
|
52
|
+
## 全局 Provider 清单
|
|
53
|
+
|
|
54
|
+
| Provider | 导入自 | 职责 |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `PloykitProvider` | `@ploykit/hooks` | 认证会话(`/auth/me`)+ 当前工作区上下文 + react-query `QueryClientProvider` |
|
|
57
|
+
| `ConfirmProvider` | `@ploykit/ui` | 提供 `useConfirm()` 的 Promise 式弹窗确认 |
|
|
58
|
+
| `Toaster` | `@ploykit/ui` | sonner 通知渲染(默认右上角、richColors;`toast.error` 时长 8s) |
|
|
59
|
+
|
|
60
|
+
漏挂症状:
|
|
61
|
+
|
|
62
|
+
- 缺 `PloykitProvider`:`useAuth` / `useWorkspace` / `useApi` 等直接 throw `must be used within PloykitProvider`。
|
|
63
|
+
- 缺 `ConfirmProvider`:`useConfirm()` 直接 throw `useConfirm must be used within ConfirmProvider`(fail-fast;旧版曾静默返回 `false`,导致"点了没反应且无报错",已废弃)。
|
|
64
|
+
- 缺 `Toaster`:不报错,但所有 `toast()` 调用无任何可见提示。
|
|
65
|
+
|
|
66
|
+
三个 Provider 都不相互依赖,`AppProviders` 的嵌套顺序只是惯例;推荐统一挂最外层,保证登录前页面也能用 `useConfirm` / `toast`。
|
|
67
|
+
|
|
68
|
+
## Headless hooks(`@ploykit/hooks`)
|
|
69
|
+
|
|
70
|
+
| 导出 | 契约 |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `useAuth()` | `{ user, workspaces, loading, error, refresh, logout }` |
|
|
73
|
+
| `useLogin()` | `{ sendCode(email), submit(email, code), loading, error }`;`submit` 返回 `{ ok, error? }` |
|
|
74
|
+
| `useWorkspace()` | `{ current, switchTo(id), clear }`;当前工作区 ID 同步进 URL `?ws=` 与 localStorage |
|
|
75
|
+
| `useWorkspaceSwitch()` | `{ current, list, switchTo, create(name, slug), creating, error }`;`create` 失败返回 `null` |
|
|
76
|
+
| `useBilling()` | `{ subscription, plans, checkout(planCode, interval, channel), checkoutError, loading, refresh }` |
|
|
77
|
+
| `useUsagePreview()` | `{ preview, loading }` |
|
|
78
|
+
| `PlanGate` | `<PlanGate plan="pro" denied={...}>{children}</PlanGate>`;按 `plans.sort_no` 做档位判定,`busy` prop 自定义加载态 |
|
|
79
|
+
| `useApi()` | `{ get, post, put, patch, delete }`;自动携带凭据与当前工作区 ID |
|
|
80
|
+
| `ApiError` | 带 `status` 的 API 错误类(可 `instanceof` 判 4xx) |
|
|
81
|
+
| `queryKeys` | react-query query key 常量表(`queryKeys.me`、`queryKeys.billingSubscription(wsId)` 等) |
|
|
82
|
+
|
|
83
|
+
其余 hooks(`useSessions` / `useTokens` / `useMembers` / `useAudit` / `useSEO` / `useZodForm` / `useWsStatus` 等)见 `packages/hooks/src/index.ts`。
|
|
84
|
+
|
|
85
|
+
## 组件
|
|
86
|
+
|
|
87
|
+
### DataTable
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
import { DataTable, type Column } from '@ploykit/ui'
|
|
91
|
+
|
|
92
|
+
interface User { id: string; name: string; code: string }
|
|
93
|
+
|
|
94
|
+
const columns: Column<User>[] = [
|
|
95
|
+
{ key: 'name', header: '名称', render: (u) => <b>{u.name}</b> },
|
|
96
|
+
{
|
|
97
|
+
key: 'actions',
|
|
98
|
+
header: <span className="sr-only">操作</span>, // 别传 ''——空串 th 会塌陷
|
|
99
|
+
className: 'whitespace-nowrap', // 操作列防换行惯例
|
|
100
|
+
render: (u) => <Button size="xs" variant="outline">编辑</Button>,
|
|
101
|
+
},
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
<DataTable
|
|
105
|
+
columns={columns}
|
|
106
|
+
rows={rows}
|
|
107
|
+
rowKey={(row) => row.id} // 同业务键多行(如同 code 多版本)时用下标拼唯一 key:(row, i) => `${row.code}:${i}`
|
|
108
|
+
onRowClick={(row) => openDetail(row)} // 整行点击;传入后行 hover 显示 pointer
|
|
109
|
+
renderExpanded={(row) => row.open ? <Detail row={row} /> : null} // 返回非 null 即在该行后插入 colSpan 全宽展开行;展开状态由行数据自行表达
|
|
110
|
+
loading={isLoading}
|
|
111
|
+
pagination={{ page, pageSize, total, onPageChange: setPage }}
|
|
112
|
+
/>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- `Column<T>` = `{ key, header: ReactNode, render?(row), className? }`;不传 `render` 时取 `row[key]` 字符串化。
|
|
116
|
+
- `rowKey: (row, index) => string`(第二参可省)。
|
|
117
|
+
- 分页条内置"上一页/下一页";翻页后数据为空时显示"本页没有数据,可返回上一页"而非空态。
|
|
118
|
+
- `loading` 时表体渲染骨架行;`empty` 可自定义空态(默认 `<PageEmpty />`)。
|
|
119
|
+
|
|
120
|
+
### Dialog / Sheet 尺寸档位
|
|
121
|
+
|
|
122
|
+
`DialogContent` 新增 `size` 档位(默认 `sm`,即原有 384px 宽度,现有代码不受影响):
|
|
123
|
+
|
|
124
|
+
| size | max-width |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `"sm"`(默认) | `sm:max-w-sm` |
|
|
127
|
+
| `"md"` | `sm:max-w-lg` |
|
|
128
|
+
| `"lg"` | `sm:max-w-2xl` |
|
|
129
|
+
| `"xl"` | `sm:max-w-4xl` |
|
|
130
|
+
|
|
131
|
+
`SheetContent` 新增 `size?: "sm" | "md" | "lg"`(默认 `sm` = 原有 `sm:max-w-sm`);`side` 仍为 `"top" | "right" | "bottom" | "left"`(默认右侧),右侧/底部预设自带 `overflow-y-auto`。两者另有 `showCloseButton?: boolean`(默认 `true`)。Sheet 全套组合件从 barrel 导出:`Sheet`(受控 Root)、`SheetTrigger`、`SheetClose`、`SheetHeader`、`SheetFooter`、`SheetTitle`、`SheetDescription`。
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
135
|
+
<DialogContent size="lg">...</DialogContent>
|
|
136
|
+
</Dialog>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Select 使用注意
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@ploykit/ui'
|
|
143
|
+
|
|
144
|
+
<Select value={v} onValueChange={(value) => setV(value)}> {/* value: string,不再是 string | null */}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- `onValueChange` 契约为 `(value: string) => void`:清空/取消选择时收到空串 `''`,不会出现 `string | null`。
|
|
148
|
+
- `SelectTrigger` 自有 `size?: "sm" | "default"`(高度档位:`sm` = h-7,`default` = h-8)。注意与 Button 的 `size` 枚举(`default` / `xs` / `sm` / `lg` / `icon`…)不同名不同义。
|
|
149
|
+
- 其余件(`SelectContent` / `SelectItem` / `SelectGroup` / `SelectLabel` / `SelectSeparator` 等)为 `@base-ui/react` Select 直出。
|
|
150
|
+
|
|
151
|
+
### SecretModal(仅显示一次的密钥弹窗)
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
<SecretModal
|
|
155
|
+
open={open}
|
|
156
|
+
onOpenChange={setOpen}
|
|
157
|
+
secretName="My Token"
|
|
158
|
+
secretValue={token}
|
|
159
|
+
title="新密钥已生成" // 可选;不传时默认 `${secretName} 创建成功`
|
|
160
|
+
/>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
安全契约:用户勾选"我已将密钥保存到安全的地方"之前,`onOpenChange` **不会收到 `false`**——ESC、遮罩点击、关闭按钮均被拦截,调用方无需自行兜底。内置复制按钮(2s 成功/失败反馈),每次打开重置勾选与复制状态。`title` prop 用于"重置 Key"等非创建成功场景覆盖默认标题。
|
|
164
|
+
|
|
165
|
+
### ImpactConfirmation(破坏性操作确认)
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
<ImpactConfirmation
|
|
169
|
+
open={open}
|
|
170
|
+
onOpenChange={setOpen}
|
|
171
|
+
onConfirm={doDelete}
|
|
172
|
+
title="删除调度"
|
|
173
|
+
impacts={[
|
|
174
|
+
'将删除 3 条调度',
|
|
175
|
+
<>关联的 <b>12</b> 条执行历史将被保留</>, // impacts: ReactNode[],可含富文本
|
|
176
|
+
]}
|
|
177
|
+
confirmText="确认删除"
|
|
178
|
+
danger
|
|
179
|
+
/>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- 勾选"我理解并接受以上影响"前确认按钮禁用;确认后先回调 `onConfirm` 再关闭。
|
|
183
|
+
- 影响列表区自带 `max-h-48 overflow-y-auto`,项多时内部滚动,弹窗不被撑出视口。
|
|
184
|
+
|
|
185
|
+
### Textarea
|
|
186
|
+
|
|
187
|
+
与 `Input` 同 token 的多行文本域(`rounded-lg border-input` 等一致),从 barrel 直接导入。
|
|
188
|
+
|
|
189
|
+
### 其他常用导出
|
|
190
|
+
|
|
191
|
+
- **布局**:`AppShell` / `SettingsShell` / `AuthShell` / `AdminShell` + 全套 Sidebar。
|
|
192
|
+
- **页面**:`LoginPage`(props:`resetPath` / `registerPath` / `oauthProviders` / `enableFedLogin` / `onSuccess` / `title` / `description`;`resetPath` / `registerPath` **传空串即隐藏对应链接**——产品未挂载 /forgot-password、/register 路由时防止死链 404)、`RegisterPage`、`ForgotPasswordPage`、`ResetPasswordPage`、`VerifyEmailPage`、`InviteAcceptPage`,以及 workspace/account/admin 各默认页面。
|
|
193
|
+
- **反馈与状态**:`StatusBadge`、`PageHeader` / `PageLoading` / `PageEmpty` / `PageError`、`ErrorBoundary`、`ReauthDialog`(step-up sudo 模式弹窗)。
|
|
194
|
+
- **工具**:`cn`、`formatDate` / `formatDateTime` / `shortID`、`statusTone` / `statusLabel`、`apiErrorMessage`、`FormField`、`CronInput`。
|
|
195
|
+
- 基础 UI 件(Button / Input / Card / Tabs / AlertDialog / DropdownMenu / Popover / Switch / Checkbox / Progress / Avatar / Breadcrumb / Empty / Field / Label / Tooltip / Skeleton / Separator…)全部从 barrel 导出;也可走子路径 `import { Button } from '@ploykit/ui/components/ui/button'`(推荐统一走 barrel)。
|
|
196
|
+
|
|
197
|
+
## 自定义三个层级
|
|
198
|
+
|
|
199
|
+
1. **换皮**:CSS/Tailwind 覆盖样式(组件全部走 design token:`bg-popover`、`text-muted-foreground` 等)。
|
|
200
|
+
2. **换壳**:拷贝 `LoginPage.tsx` 等默认页面到产品 `src/pages/`,改 JSX,hook 调用不变(hooks 从 `@ploykit/hooks` 导入)。
|
|
201
|
+
3. **纯逻辑**:只 `import { useLogin } from '@ploykit/hooks'`,完全自己画 UI。
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { QueryClient } from '@tanstack/react-query';
|
|
3
|
+
/**
|
|
4
|
+
* 框架级全局 Provider 一步到位组合件:PloykitProvider > ConfirmProvider > Toaster。
|
|
5
|
+
*
|
|
6
|
+
* 产品侧用它替换"逐个手挂三层 Provider"的样板(example/web/src/routes.tsx 的
|
|
7
|
+
* AppProviders 即此组合的 historically hand-written 版本)。嵌套顺序即依赖顺序:
|
|
8
|
+
* confirm 与 toast 均不依赖 auth/workspace 上下文,但统一挂在最外层可保证任何
|
|
9
|
+
* 页面(含登录前页面)都能调用 useConfirm / toast。
|
|
10
|
+
*/
|
|
11
|
+
export declare function AppProviders({ children, queryClient }: {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
/** 透传给 PloykitProvider;不传时由其内部创建默认 QueryClient。 */
|
|
14
|
+
queryClient?: QueryClient;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PloykitProvider } from '@ploykit/hooks';
|
|
3
|
+
import { ConfirmProvider } from './components/ConfirmDialog';
|
|
4
|
+
import { Toaster } from './components/toast';
|
|
5
|
+
/**
|
|
6
|
+
* 框架级全局 Provider 一步到位组合件:PloykitProvider > ConfirmProvider > Toaster。
|
|
7
|
+
*
|
|
8
|
+
* 产品侧用它替换"逐个手挂三层 Provider"的样板(example/web/src/routes.tsx 的
|
|
9
|
+
* AppProviders 即此组合的 historically hand-written 版本)。嵌套顺序即依赖顺序:
|
|
10
|
+
* confirm 与 toast 均不依赖 auth/workspace 上下文,但统一挂在最外层可保证任何
|
|
11
|
+
* 页面(含登录前页面)都能调用 useConfirm / toast。
|
|
12
|
+
*/
|
|
13
|
+
export function AppProviders({ children, queryClient }) {
|
|
14
|
+
return (_jsx(PloykitProvider, { queryClient: queryClient, children: _jsxs(ConfirmProvider, { children: [children, _jsx(Toaster, {})] }) }));
|
|
15
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useCallback, useContext, useState } from 'react';
|
|
3
3
|
import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, } from './ui/alert-dialog';
|
|
4
|
-
|
|
4
|
+
// 缺省值必须是 null + useConfirm fail-fast(对齐 hooks 包 useAuthCtx 的行为):
|
|
5
|
+
// 曾经的缺省值是 `async () => false`,产品忘挂 ConfirmProvider 时 confirm 静默返回
|
|
6
|
+
// false,"重置 Key"之类的按钮点了没反应且无任何报错,排查损耗极大。
|
|
7
|
+
const ConfirmContext = createContext(null);
|
|
5
8
|
export function ConfirmProvider({ children }) {
|
|
6
9
|
const [state, setState] = useState(null);
|
|
7
10
|
const confirm = useCallback((opts) => {
|
|
@@ -25,5 +28,8 @@ export function ConfirmProvider({ children }) {
|
|
|
25
28
|
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
29
|
}
|
|
27
30
|
export function useConfirm() {
|
|
28
|
-
|
|
31
|
+
const confirm = useContext(ConfirmContext);
|
|
32
|
+
if (!confirm)
|
|
33
|
+
throw new Error('useConfirm must be used within ConfirmProvider');
|
|
34
|
+
return confirm;
|
|
29
35
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
2
|
export interface Column<T> {
|
|
3
3
|
key: string;
|
|
4
4
|
header: ReactNode;
|
|
@@ -14,9 +14,18 @@ export interface DataTablePagination {
|
|
|
14
14
|
export interface DataTableProps<T> {
|
|
15
15
|
columns: Column<T>[];
|
|
16
16
|
rows: T[];
|
|
17
|
-
|
|
17
|
+
/** 行 key 生成器。第二参数是该行在 rows 中的下标,用于同业务键多行(如同 code 多版本)时拼出唯一 key。少参回调仍然兼容。 */
|
|
18
|
+
rowKey: (row: T, index: number) => string;
|
|
18
19
|
loading?: boolean;
|
|
19
20
|
empty?: ReactNode;
|
|
21
|
+
/** 整行点击回调。传入后行 hover 显示 pointer 光标。 */
|
|
22
|
+
onRowClick?: (row: T) => void;
|
|
23
|
+
/**
|
|
24
|
+
* 展开行扩展点:对每行调用,返回非 null 时在该行下方插入一个展开行
|
|
25
|
+
* (单个 td,colSpan 覆盖全部列)。展开/收起状态由调用方用行数据自行表达
|
|
26
|
+
* (收起的行返回 null 即不渲染)——本组件不持有内部展开状态。
|
|
27
|
+
*/
|
|
28
|
+
renderExpanded?: (row: T, index: number) => ReactNode;
|
|
20
29
|
pagination?: DataTablePagination;
|
|
21
30
|
}
|
|
22
|
-
export declare function DataTable<T>({ columns, rows, rowKey, loading, empty, pagination }: DataTableProps<T>): import("react").JSX.Element;
|
|
31
|
+
export declare function DataTable<T>({ columns, rows, rowKey, loading, empty, onRowClick, renderExpanded, pagination }: DataTableProps<T>): import("react").JSX.Element;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment } from 'react';
|
|
2
3
|
import { cn } from '../lib/utils';
|
|
3
4
|
import { PageEmpty } from './Page';
|
|
4
5
|
import { Skeleton } from './ui/skeleton';
|
|
5
6
|
import { Button } from './ui/button';
|
|
6
|
-
export function DataTable({ columns, rows, rowKey, loading = false, empty, pagination }) {
|
|
7
|
+
export function DataTable({ columns, rows, rowKey, loading = false, empty, onRowClick, renderExpanded, pagination }) {
|
|
7
8
|
const stalePage = !!pagination && !loading && rows.length === 0 && (pagination.total > 0 || pagination.page > 1);
|
|
8
9
|
if (!loading && rows.length === 0 && !stalePage) {
|
|
9
10
|
return _jsx(_Fragment, { children: empty ?? _jsx(PageEmpty, {}) });
|
|
10
11
|
}
|
|
11
12
|
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
|
|
13
|
+
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, index) => {
|
|
14
|
+
const expanded = renderExpanded ? renderExpanded(row, index) : null;
|
|
15
|
+
return (_jsxs(Fragment, { children: [_jsx("tr", { className: cn('border-b last:border-0 hover:bg-muted/30', onRowClick && 'cursor-pointer'), onClick: onRowClick ? () => onRowClick(row) : undefined, 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))) }), expanded !== null && (_jsx("tr", { className: "border-b last:border-0", "data-slot": "data-table-expanded-row", children: _jsx("td", { colSpan: columns.length, className: "px-4 py-3", children: expanded }) }))] }, rowKey(row, index)));
|
|
16
|
+
})) })] }) }), 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
17
|
}
|
|
@@ -19,6 +19,6 @@ export class ErrorBoundary extends Component {
|
|
|
19
19
|
if (fallback !== undefined) {
|
|
20
20
|
return typeof fallback === 'function' ? fallback(error, this.reset) : fallback;
|
|
21
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-
|
|
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-destructive", children: "\u51FA\u9519\u4E86" }), error.message && _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: error.message }), _jsx("button", { onClick: this.reset, className: "mt-4 px-4 py-2 text-sm border rounded-md hover:bg-muted", children: "\u91CD\u8BD5" })] }));
|
|
23
23
|
}
|
|
24
24
|
}
|
|
@@ -40,8 +40,8 @@ export function NotificationBell({ onNavigate }) {
|
|
|
40
40
|
document.addEventListener('mousedown', onClickOutside);
|
|
41
41
|
return () => document.removeEventListener('mousedown', onClickOutside);
|
|
42
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\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]\n px-1 text-xs font-bold text-white bg-
|
|
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-
|
|
45
|
-
onNavigate?.(n.link); markReadM.mutate(n.id); setOpen(false); }, className: cn('w-full text-left px-4 py-3 cursor-pointer hover:bg-
|
|
46
|
-
'focus-visible:outline-none focus-visible:bg-
|
|
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\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]\n px-1 text-xs font-bold text-white bg-destructive 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-primary hover:underline disabled:opacity-50\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-muted-foreground", "aria-live": "polite", children: "\u52A0\u8F7D\u4E2D\u2026" })) : notifications.length === 0 ? (_jsx("li", { className: "py-8 text-center text-sm text-muted-foreground", 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-muted ' +
|
|
46
|
+
'focus-visible:outline-none focus-visible:bg-muted focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring', !n.read_at && 'bg-accent/50'), children: [_jsxs("span", { className: "flex items-start justify-between gap-2", children: [_jsx("span", { className: "text-sm font-medium text-foreground", children: n.title }), n.count > 1 && (_jsxs("span", { className: "text-xs bg-muted text-muted-foreground 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-muted-foreground line-clamp-2", children: n.body }), _jsx("span", { className: "block mt-1 text-[10px] text-muted-foreground", children: formatDateTime(n.created_at) })] }) }, n.id))) })] }))] }));
|
|
47
47
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step-up(sudo mode)两段式协议的第二段(ADR 0011):敏感操作被
|
|
3
|
+
* 403 E_REAUTH_REQUIRED 拒绝后,弹本对话框验证密码(POST
|
|
4
|
+
* /auth/confirm-password),成功后自动重试原请求。
|
|
5
|
+
*
|
|
6
|
+
* 用法:页面捕获 isApiError(err, 'E_REAUTH_REQUIRED') 时,把"重试动作"
|
|
7
|
+
* (通常是 mutation.mutateAsync 绑定原参数)交给 retry,打开对话框即可。
|
|
8
|
+
*/
|
|
9
|
+
export interface ReauthDialogProps {
|
|
10
|
+
open: boolean;
|
|
11
|
+
/** 被挑战的原动作;确认成功后重新执行一次。为 null 时对话框只确认不重试。 */
|
|
12
|
+
retry: (() => Promise<unknown>) | null;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function ReauthDialog({ open, retry, onClose }: ReauthDialogProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { ShieldCheck } from 'lucide-react';
|
|
4
|
+
import { useApi } from '@ploykit/hooks';
|
|
5
|
+
import { isApiError } from '@ploykit/client';
|
|
6
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from './ui/dialog';
|
|
7
|
+
import { Button } from './ui/button';
|
|
8
|
+
import { Input } from './ui/input';
|
|
9
|
+
import { toast } from './toast';
|
|
10
|
+
export function ReauthDialog({ open, retry, onClose }) {
|
|
11
|
+
const api = useApi();
|
|
12
|
+
const [password, setPassword] = useState('');
|
|
13
|
+
const [error, setError] = useState('');
|
|
14
|
+
const [busy, setBusy] = useState(false);
|
|
15
|
+
const confirm = async () => {
|
|
16
|
+
if (!password || busy)
|
|
17
|
+
return;
|
|
18
|
+
setBusy(true);
|
|
19
|
+
setError('');
|
|
20
|
+
try {
|
|
21
|
+
await api.post('/auth/confirm-password', { password });
|
|
22
|
+
if (retry)
|
|
23
|
+
await retry();
|
|
24
|
+
toast.success('已验证,操作已重试');
|
|
25
|
+
setPassword('');
|
|
26
|
+
onClose();
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
if (isApiError(err, 'E_UNAUTHENTICATED')) {
|
|
30
|
+
setError('密码不正确,请重试');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
toast.error('操作失败,请稍后重试');
|
|
34
|
+
onClose();
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
setBusy(false);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
return (_jsx(Dialog, { open: open, onOpenChange: (o) => { if (!o && !busy)
|
|
41
|
+
onClose(); }, children: _jsxs(DialogContent, { className: "max-w-sm", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(ShieldCheck, { className: "size-4" }), "\u9700\u8981\u9A8C\u8BC1\u5BC6\u7801"] }), _jsx(DialogDescription, { children: "\u8FD9\u662F\u654F\u611F\u64CD\u4F5C\uFF0C\u9700\u8981\u786E\u8BA4\u5BC6\u7801\u540E\u81EA\u52A8\u91CD\u8BD5\u3002\u5BC6\u7801\u786E\u8BA4\u5728\u4E00\u6BB5\u65F6\u95F4\u5185\u6709\u6548\u3002" })] }), _jsxs("form", { className: "space-y-1", onSubmit: (e) => { e.preventDefault(); void confirm(); }, children: [_jsx(Input, { type: "password", autoFocus: true, autoComplete: "current-password", "aria-label": "\u5F53\u524D\u5BC6\u7801", placeholder: "\u5F53\u524D\u5BC6\u7801", value: password, onChange: (e) => setPassword(e.target.value), disabled: busy }), error && _jsx("p", { className: "text-sm text-destructive", children: error }), _jsxs(DialogFooter, { className: "mt-4", children: [_jsx(Button, { type: "button", variant: "outline", onClick: onClose, disabled: busy, children: "\u53D6\u6D88" }), _jsx(Button, { type: "submit", disabled: !password || busy, children: busy ? '验证中…' : '验证并继续' })] })] })] }) }));
|
|
42
|
+
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
1
2
|
export declare function SecretModal({ open, onOpenChange, secretName, secretValue, title }: {
|
|
2
3
|
open: boolean;
|
|
4
|
+
/** 关闭回调。契约:用户勾选"我已保存"之前,本组件不会把 false 传进来——ESC、遮罩点击、关闭按钮均被拦截,调用方无需自行兜底。 */
|
|
3
5
|
onOpenChange: (open: boolean) => void;
|
|
6
|
+
/** 密钥名称,用于默认标题「{secretName} 创建成功」。 */
|
|
4
7
|
secretName: string;
|
|
8
|
+
/** 仅显示一次的密钥明文。 */
|
|
5
9
|
secretValue: string;
|
|
10
|
+
/** 可选标题,覆盖默认的「{secretName} 创建成功」——用于"重置/查看"等非创建成功场景。 */
|
|
6
11
|
title?: string;
|
|
7
12
|
}): import("react").JSX.Element;
|
|
8
13
|
export declare function ImpactConfirmation({ open, onConfirm, onOpenChange, title, impacts, confirmText, danger }: {
|
|
9
14
|
open: boolean;
|
|
15
|
+
/** 点击确认按钮后回调;回调返回后组件会立即请求关闭。 */
|
|
10
16
|
onConfirm: () => void;
|
|
11
17
|
onOpenChange: (open: boolean) => void;
|
|
12
18
|
title: string;
|
|
13
|
-
|
|
19
|
+
/** 影响项列表。支持任意 ReactNode(可含链接/加粗等富文本);key 按下标生成,注意项内容需自行保证可读。 */
|
|
20
|
+
impacts: ReactNode[];
|
|
14
21
|
confirmText?: string;
|
|
15
22
|
danger?: boolean;
|
|
16
23
|
}): import("react").JSX.Element;
|
|
@@ -37,9 +37,12 @@ export function SecretModal({ open, onOpenChange, secretName, secretValue, title
|
|
|
37
37
|
setCopyFailed(false);
|
|
38
38
|
}, 2000);
|
|
39
39
|
};
|
|
40
|
-
return (
|
|
40
|
+
return (
|
|
41
|
+
// 契约(见 props JSDoc):确认勾选前 onOpenChange 不会收到 false——
|
|
42
|
+
// ESC/遮罩/关闭按钮在此被统一拦截,防止密钥未保存即被关掉。
|
|
43
|
+
_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
44
|
}
|
|
42
45
|
export function ImpactConfirmation({ open, onConfirm, onOpenChange, title, impacts, confirmText, danger }) {
|
|
43
46
|
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] },
|
|
47
|
+
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: "max-h-48 space-y-1.5 overflow-y-auto", children: impacts.map((impact, index) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-foreground", children: [_jsx("span", { className: "text-destructive mt-0.5", children: "\u2022" }), " ", impact] }, index))) }), _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
48
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useRender } from "@base-ui/react/use-render";
|
|
2
2
|
import { type VariantProps } from "class-variance-authority";
|
|
3
3
|
declare const badgeVariants: (props?: ({
|
|
4
|
-
variant?: "default" | "
|
|
4
|
+
variant?: "default" | "link" | "outline" | "secondary" | "ghost" | "destructive" | null | undefined;
|
|
5
5
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
6
|
declare function Badge({ className, variant, render, ...props }: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
7
7
|
export { Badge, badgeVariants };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
|
2
2
|
import { type VariantProps } from "class-variance-authority";
|
|
3
3
|
declare const buttonVariants: (props?: ({
|
|
4
|
-
variant?: "default" | "
|
|
4
|
+
variant?: "default" | "link" | "outline" | "secondary" | "ghost" | "destructive" | null | undefined;
|
|
5
5
|
size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
|
|
6
6
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
7
|
declare function Button({ className, variant, size, ...props }: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>): import("react").JSX.Element;
|
|
@@ -5,7 +5,13 @@ declare function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props): Rea
|
|
|
5
5
|
declare function DialogPortal({ ...props }: DialogPrimitive.Portal.Props): React.JSX.Element;
|
|
6
6
|
declare function DialogClose({ ...props }: DialogPrimitive.Close.Props): React.JSX.Element;
|
|
7
7
|
declare function DialogOverlay({ className, ...props }: DialogPrimitive.Backdrop.Props): React.JSX.Element;
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* DialogContent 内容面板。size 控制最大宽度档位(经 data-size 落到
|
|
10
|
+
* Tailwind data-[size=*] 变体):sm=sm:max-w-sm(默认,历史视觉)/
|
|
11
|
+
* md=sm:max-w-lg / lg=sm:max-w-2xl / xl=sm:max-w-4xl。
|
|
12
|
+
*/
|
|
13
|
+
declare function DialogContent({ className, children, size, showCloseButton, ...props }: DialogPrimitive.Popup.Props & {
|
|
14
|
+
size?: "sm" | "md" | "lg" | "xl";
|
|
9
15
|
showCloseButton?: boolean;
|
|
10
16
|
}): React.JSX.Element;
|
|
11
17
|
declare function DialogHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
@@ -18,8 +18,13 @@ function DialogClose({ ...props }) {
|
|
|
18
18
|
function DialogOverlay({ className, ...props }) {
|
|
19
19
|
return (_jsx(DialogPrimitive.Backdrop, { "data-slot": "dialog-overlay", className: cn("fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0", className), ...props }));
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* DialogContent 内容面板。size 控制最大宽度档位(经 data-size 落到
|
|
23
|
+
* Tailwind data-[size=*] 变体):sm=sm:max-w-sm(默认,历史视觉)/
|
|
24
|
+
* md=sm:max-w-lg / lg=sm:max-w-2xl / xl=sm:max-w-4xl。
|
|
25
|
+
*/
|
|
26
|
+
function DialogContent({ className, children, size = "sm", showCloseButton = true, ...props }) {
|
|
27
|
+
return (_jsxs(DialogPortal, { children: [_jsx(DialogOverlay, {}), _jsxs(DialogPrimitive.Popup, { "data-slot": "dialog-content", "data-size": size, className: cn("fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[size=md]:sm:max-w-lg data-[size=lg]:sm:max-w-2xl data-[size=xl]:sm:max-w-4xl", className), ...props, children: [children, showCloseButton && (_jsxs(DialogPrimitive.Close, { "data-slot": "dialog-close", render: _jsx(Button, { variant: "ghost", className: "absolute top-2 right-2", size: "icon-sm" }), children: [_jsx(XIcon, {}), _jsx("span", { className: "sr-only", children: "Close" })] }))] })] }));
|
|
23
28
|
}
|
|
24
29
|
function DialogHeader({ className, ...props }) {
|
|
25
30
|
return (_jsx("div", { "data-slot": "dialog-header", className: cn("flex flex-col gap-2", className), ...props }));
|
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { Select as SelectPrimitive } from "@base-ui/react/select";
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Select 根组件:base-ui Select Root 的 string 契约包装。
|
|
5
|
+
*
|
|
6
|
+
* 契约:onValueChange 永远收到 string——base-ui 原生回调的 value 可为 null
|
|
7
|
+
* (清空/取消选择时),包装层统一归一为空串(''),产品侧无需再写
|
|
8
|
+
* `v ?? ''` 卫语句。受控 value 仍允许 null、类型不收窄(与 base-ui 一致);
|
|
9
|
+
* 其余 props(items/defaultValue/children 等)原样透传。
|
|
10
|
+
* 泛型保留(默认 string):个别历史调用点用数值 item(如 days),此时
|
|
11
|
+
* 回调运行时收到的是原始数值——签名仍标为 string,属已知偏差。
|
|
12
|
+
* 多选(multiple)不在本契约内;如需多选请直接使用
|
|
13
|
+
* `@base-ui/react/select` 的 Root 原语。
|
|
14
|
+
*/
|
|
15
|
+
declare function Select<Value = string>({ onValueChange, ...props }: Omit<SelectPrimitive.Root.Props<Value, false>, "onValueChange" | "multiple"> & {
|
|
16
|
+
onValueChange?: (value: string) => void;
|
|
17
|
+
}): React.JSX.Element;
|
|
4
18
|
declare function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props): React.JSX.Element;
|
|
5
19
|
declare function SelectValue({ className, ...props }: SelectPrimitive.Value.Props): React.JSX.Element;
|
|
20
|
+
/**
|
|
21
|
+
* SelectTrigger 触发按钮。size 控制高度档位(经 data-size 落到 Tailwind
|
|
22
|
+
* data-[size=*] 变体):'default' = h-8(表单默认高度);'sm' = h-7 +
|
|
23
|
+
* 更小圆角(工具栏/紧凑筛选条)。默认 'default'。
|
|
24
|
+
*/
|
|
6
25
|
declare function SelectTrigger({ className, size, children, ...props }: SelectPrimitive.Trigger.Props & {
|
|
7
26
|
size?: "sm" | "default";
|
|
8
27
|
}): React.JSX.Element;
|