@ploykit/ui 0.1.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +183 -23
  2. package/dist/AppProviders.d.ts +15 -0
  3. package/dist/AppProviders.js +15 -0
  4. package/dist/components/ConfirmDialog.js +8 -2
  5. package/dist/components/CronInput.js +2 -2
  6. package/dist/components/DataTable.d.ts +12 -3
  7. package/dist/components/DataTable.js +7 -3
  8. package/dist/components/ErrorBoundary.js +1 -1
  9. package/dist/components/ImpersonationBanner.js +1 -1
  10. package/dist/components/NotificationBell.js +5 -5
  11. package/dist/components/OnboardingChecklist.js +3 -3
  12. package/dist/components/Page.js +1 -1
  13. package/dist/components/ReauthDialog.d.ts +15 -0
  14. package/dist/components/ReauthDialog.js +42 -0
  15. package/dist/components/SecretModal.d.ts +8 -1
  16. package/dist/components/SecretModal.js +6 -3
  17. package/dist/components/SettingsCard.js +2 -2
  18. package/dist/components/SiteBannerLayer.js +1 -1
  19. package/dist/components/StatusBadge.d.ts +4 -0
  20. package/dist/components/StatusBadge.js +7 -0
  21. package/dist/components/UserMenu.js +2 -2
  22. package/dist/components/WorkspaceSwitcher.js +4 -4
  23. package/dist/components/ui/alert-dialog.d.ts +2 -2
  24. package/dist/components/ui/alert-dialog.js +10 -9
  25. package/dist/components/ui/avatar.js +3 -2
  26. package/dist/components/ui/badge.d.ts +7 -0
  27. package/dist/components/ui/badge.js +33 -0
  28. package/dist/components/ui/breadcrumb.js +7 -7
  29. package/dist/components/ui/button.d.ts +8 -0
  30. package/dist/components/ui/button.js +34 -0
  31. package/dist/components/ui/card.d.ts +2 -4
  32. package/dist/components/ui/card.js +7 -7
  33. package/dist/components/ui/checkbox.js +3 -2
  34. package/dist/components/ui/dialog.d.ts +22 -14
  35. package/dist/components/ui/dialog.js +40 -25
  36. package/dist/components/ui/dropdown-menu.js +50 -0
  37. package/dist/components/ui/empty.d.ts +7 -9
  38. package/dist/components/ui/empty.js +6 -9
  39. package/dist/components/ui/field.js +10 -9
  40. package/dist/components/ui/input.d.ts +3 -0
  41. package/dist/components/ui/input.js +7 -0
  42. package/dist/components/ui/popover.js +3 -2
  43. package/dist/components/ui/progress.js +2 -1
  44. package/dist/components/ui/select.d.ts +20 -1
  45. package/dist/components/ui/select.js +28 -7
  46. package/dist/components/ui/separator.js +1 -0
  47. package/dist/components/ui/sheet.d.ts +9 -1
  48. package/dist/components/ui/sheet.js +11 -4
  49. package/dist/components/ui/sidebar.d.ts +2 -2
  50. package/dist/components/ui/sidebar.js +25 -34
  51. package/dist/components/ui/skeleton.d.ts +1 -2
  52. package/dist/components/ui/switch.js +1 -1
  53. package/dist/components/ui/tabs.js +3 -3
  54. package/dist/components/ui/textarea.d.ts +9 -0
  55. package/dist/components/ui/textarea.js +12 -0
  56. package/dist/components/ui/tooltip.js +1 -1
  57. package/dist/index.d.ts +10 -40
  58. package/dist/index.js +10 -26
  59. package/dist/layouts/AppShell.d.ts +1 -1
  60. package/dist/layouts/AppShell.js +23 -1
  61. package/dist/layouts/SettingsShell.d.ts +1 -1
  62. package/dist/layouts/SettingsShell.js +3 -3
  63. package/dist/pages/LandingPage.js +1 -1
  64. package/dist/pages/LoginPage.js +7 -7
  65. package/dist/pages/account/NotificationPreferencesPage.js +2 -2
  66. package/dist/pages/account/ProfileSettingsPage.js +7 -7
  67. package/dist/pages/account/SecuritySettingsPage.js +8 -8
  68. package/dist/pages/account/TokensPage.js +4 -4
  69. package/dist/pages/admin/AdminSettingsPage.js +5 -5
  70. package/dist/pages/admin/AdminShell.js +2 -2
  71. package/dist/pages/admin/AuditPage.js +3 -3
  72. package/dist/pages/admin/OrdersPage.js +4 -3
  73. package/dist/pages/admin/OverviewPage.js +4 -4
  74. package/dist/pages/admin/SSOPage.js +3 -3
  75. package/dist/pages/admin/UserDetailPage.js +6 -5
  76. package/dist/pages/admin/UsersPage.js +7 -6
  77. package/dist/pages/admin/WebhookDeliveriesPage.js +4 -3
  78. package/dist/pages/admin/WorkspaceDetailPage.js +5 -5
  79. package/dist/pages/admin/WorkspacesPage.js +4 -4
  80. package/dist/pages/admin/shared.js +1 -1
  81. package/dist/pages/auth/ForgotPasswordPage.js +3 -3
  82. package/dist/pages/auth/InviteAcceptPage.js +4 -4
  83. package/dist/pages/auth/RegisterPage.js +6 -6
  84. package/dist/pages/auth/ResetPasswordPage.js +4 -4
  85. package/dist/pages/auth/VerifyEmailPage.js +4 -4
  86. package/dist/pages/workspace/BillingPage.js +8 -7
  87. package/dist/pages/workspace/BillingSuccessPage.js +3 -3
  88. package/dist/pages/workspace/MembersPage.js +7 -6
  89. package/dist/pages/workspace/RolePermissionsPage.d.ts +3 -0
  90. package/dist/pages/workspace/RolePermissionsPage.js +118 -0
  91. package/dist/pages/workspace/SchedulesPage.js +5 -5
  92. package/dist/pages/workspace/UsagePage.d.ts +1 -1
  93. package/dist/pages/workspace/UsagePage.js +2 -2
  94. package/dist/pages/workspace/WebhooksPage.js +9 -9
  95. package/dist/pages/workspace/WorkspaceAuditPage.js +3 -3
  96. package/dist/pages/workspace/WorkspaceGeneralPage.js +7 -7
  97. package/package.json +9 -5
  98. package/dist/components/ui/Badge.d.ts +0 -13
  99. package/dist/components/ui/Badge.js +0 -21
  100. package/dist/components/ui/Button.d.ts +0 -10
  101. package/dist/components/ui/Button.js +0 -33
  102. package/dist/components/ui/Input.d.ts +0 -3
  103. package/dist/components/ui/Input.js +0 -10
  104. package/dist/components/ui/menu.js +0 -50
  105. package/dist/hooks/useApi.d.ts +0 -10
  106. package/dist/hooks/useApi.js +0 -11
  107. package/dist/hooks/useAudit.d.ts +0 -39
  108. package/dist/hooks/useAudit.js +0 -55
  109. package/dist/hooks/useAuth.d.ts +0 -1
  110. package/dist/hooks/useAuth.js +0 -4
  111. package/dist/hooks/useBilling.d.ts +0 -44
  112. package/dist/hooks/useBilling.js +0 -64
  113. package/dist/hooks/useLogin.d.ts +0 -10
  114. package/dist/hooks/useLogin.js +0 -39
  115. package/dist/hooks/useMembers.d.ts +0 -125
  116. package/dist/hooks/useMembers.js +0 -127
  117. package/dist/hooks/useNotificationPrefs.d.ts +0 -20
  118. package/dist/hooks/useNotificationPrefs.js +0 -42
  119. package/dist/hooks/usePasswordReset.d.ts +0 -5
  120. package/dist/hooks/usePasswordReset.js +0 -12
  121. package/dist/hooks/useSEO.d.ts +0 -6
  122. package/dist/hooks/useSEO.js +0 -14
  123. package/dist/hooks/useSessions.d.ts +0 -20
  124. package/dist/hooks/useSessions.js +0 -38
  125. package/dist/hooks/useSiteConfig.d.ts +0 -12
  126. package/dist/hooks/useSiteConfig.js +0 -11
  127. package/dist/hooks/useTokens.d.ts +0 -36
  128. package/dist/hooks/useTokens.js +0 -35
  129. package/dist/hooks/useUsage.d.ts +0 -20
  130. package/dist/hooks/useUsage.js +0 -21
  131. package/dist/hooks/useWorkspace.d.ts +0 -26
  132. package/dist/hooks/useWorkspace.js +0 -32
  133. package/dist/hooks/useZodForm.d.ts +0 -6
  134. package/dist/hooks/useZodForm.js +0 -6
  135. package/dist/hooks/ws.d.ts +0 -4
  136. package/dist/hooks/ws.js +0 -21
  137. package/dist/lib/perm.d.ts +0 -2
  138. package/dist/lib/perm.js +0 -7
  139. package/dist/lib/ws-query-bridge.d.ts +0 -7
  140. package/dist/lib/ws-query-bridge.js +0 -35
  141. package/dist/provider/PloykitProvider.d.ts +0 -50
  142. package/dist/provider/PloykitProvider.js +0 -240
  143. package/dist/realtime.d.ts +0 -3
  144. package/dist/realtime.js +0 -11
  145. /package/dist/components/ui/{menu.d.ts → dropdown-menu.d.ts} +0 -0
package/README.md CHANGED
@@ -1,41 +1,201 @@
1
1
  # @ploykit/ui
2
2
 
3
- React 前端壳:登录/工作区/计费的 Headless Hook + 默认页面。
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 等打包器解析)。
4
12
 
5
13
  ## 快速使用
6
14
 
7
15
  ```tsx
8
- import { PloykitProvider, useAuth, useLogin, LoginPage } from '@ploykit/ui'
16
+ import { AppProviders } from '@ploykit/ui'
17
+ import { useAuth, useLogin, useApi } from '@ploykit/hooks'
18
+ import { LoginPage } from '@ploykit/ui'
9
19
 
10
- // App 根部
11
- <PloykitProvider>
20
+ // 应用根部:一层包完(PloykitProvider > ConfirmProvider > Toaster 三层组合)
21
+ <AppProviders>
12
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>
13
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 }
14
93
 
15
- // 任意组件中
16
- const { user, workspaces, loading } = useAuth()
17
- const { sendCode, submit } = useLogin()
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
+ ]
18
103
 
19
- // 或直接用默认登录页
20
- <LoginPage />
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
+ />
21
113
  ```
22
114
 
23
- ## 核心 API
115
+ - `Column<T>` = `{ key, header: ReactNode, render?(row), className? }`;不传 `render` 时取 `row[key]` 字符串化。
116
+ - `rowKey: (row, index) => string`(第二参可省)。
117
+ - 分页条内置"上一页/下一页";翻页后数据为空时显示"本页没有数据,可返回上一页"而非空态。
118
+ - `loading` 时表体渲染骨架行;`empty` 可自定义空态(默认 `<PageEmpty />`)。
24
119
 
25
- | 导出 | 说明 |
120
+ ### Dialog / Sheet 尺寸档位
121
+
122
+ `DialogContent` 新增 `size` 档位(默认 `sm`,即原有 384px 宽度,现有代码不受影响):
123
+
124
+ | size | max-width |
26
125
  |---|---|
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 不变) |
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`)、`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)。
36
196
 
37
197
  ## 自定义三个层级
38
198
 
39
- 1. **换皮**:CSS/Tailwind 覆盖样式
40
- 2. **换壳**:拷贝 `LoginPage.tsx` 到产品 `src/pages/`,改 JSX,Hook 调用不变
41
- 3. **纯逻辑**:只 `import { useLogin } from '@ploykit/ui'`,完全自己画 UI
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
- const ConfirmContext = createContext(async () => false);
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
- return useContext(ConfirmContext);
31
+ const confirm = useContext(ConfirmContext);
32
+ if (!confirm)
33
+ throw new Error('useConfirm must be used within ConfirmProvider');
34
+ return confirm;
29
35
  }
@@ -2,8 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
3
  import { api } from '@ploykit/client';
4
4
  import { apiErrorMessage } from '../lib/api-error';
5
- import { Button } from './ui/Button';
6
- import { Input } from './ui/Input';
5
+ import { Button } from './ui/button';
6
+ import { Input } from './ui/input';
7
7
  export const FALLBACK_TIMEZONES = [
8
8
  'UTC',
9
9
  'Asia/Shanghai',
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from 'react';
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
- rowKey: (row: T) => string;
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
- import { Button } from './ui/Button';
6
- export function DataTable({ columns, rows, rowKey, loading = false, empty, pagination }) {
6
+ import { Button } from './ui/button';
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) => (_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
+ 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-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" })] }));
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
  }
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { TriangleAlert } from 'lucide-react';
3
- import { useAuth } from '../hooks/useAuth';
3
+ import { useAuth } from '@ploykit/hooks';
4
4
  import { cn } from '../lib/utils';
5
5
  export function ImpersonationBanner({ className }) {
6
6
  const { user } = useAuth();
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
3
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
4
  import { api } from '@ploykit/client';
5
- import { queryKeys } from '../provider/PloykitProvider';
5
+ import { queryKeys } from '@ploykit/hooks';
6
6
  import { cn, formatDateTime } from '../lib/utils';
7
7
  export function NotificationBell({ onNavigate }) {
8
8
  const [open, setOpen] = useState(false);
@@ -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-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\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))) })] }))] }));
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
  }
@@ -3,8 +3,8 @@ import { useEffect, useState } from 'react';
3
3
  import { X } from 'lucide-react';
4
4
  import { cn } from '../lib/utils';
5
5
  import { Progress, ProgressLabel, ProgressValue } from './ui/progress';
6
- import { Button } from './ui/Button';
7
- import { useAuth } from '../hooks/useAuth';
6
+ import { Button } from './ui/button';
7
+ import { useAuth } from '@ploykit/hooks';
8
8
  const DISMISS_KEY = 'pk_onboarding_dismissed';
9
9
  function defaultItems(hasWorkspaces) {
10
10
  return [
@@ -37,7 +37,7 @@ export function OnboardingChecklist({ items, storageKey = DISMISS_KEY, title = '
37
37
  }
38
38
  setDismissed(true);
39
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
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: "icon-sm", "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
41
  ? 'border-transparent bg-muted/50 text-muted-foreground'
42
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
43
  }
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Inbox, TriangleAlert } from 'lucide-react';
3
- import { Button } from './ui/Button';
3
+ import { Button } from './ui/button';
4
4
  import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent, } from './ui/empty';
5
5
  export function PageHeader({ title, description, actions }) {
6
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 })] }));
@@ -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
- impacts: string[];
19
+ /** 影响项列表。支持任意 ReactNode(可含链接/加粗等富文本);key 按下标生成,注意项内容需自行保证可读。 */
20
+ impacts: ReactNode[];
14
21
  confirmText?: string;
15
22
  danger?: boolean;
16
23
  }): import("react").JSX.Element;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
3
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from './ui/dialog';
4
- import { Button } from './ui/Button';
4
+ import { Button } from './ui/button';
5
5
  export function SecretModal({ open, onOpenChange, secretName, secretValue, title }) {
6
6
  const [copied, setCopied] = useState(false);
7
7
  const [copyFailed, setCopyFailed] = useState(false);
@@ -37,9 +37,12 @@ export function SecretModal({ open, onOpenChange, secretName, secretValue, title
37
37
  setCopyFailed(false);
38
38
  }, 2000);
39
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 ? '我已保存' : '请先确认已保存' }) })] }) }));
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] }, 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 ?? '确认执行' })] })] }) }));
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,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cn } from '../lib/utils';
3
- import { Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, } from './ui/card';
3
+ import { Card, CardHeader, CardDescription, CardAction, CardContent, } from './ui/card';
4
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 })] }));
5
+ return (_jsxs(Card, { className: "my-4 w-full max-w-4xl text-left", size: "sm", children: [_jsxs(CardHeader, { children: [_jsx("h2", { "data-slot": "card-title", className: cn('flex items-center gap-2 font-semibold leading-none tracking-tight', 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
6
  }
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { SiteBanner } from './SiteBanner';
3
- import { useSiteConfig } from '../hooks/useSiteConfig';
3
+ import { useSiteConfig } from '@ploykit/hooks';
4
4
  export function SiteBannerLayer({ maintenanceText, } = {}) {
5
5
  const { data } = useSiteConfig();
6
6
  if (!data)
@@ -0,0 +1,4 @@
1
+ export declare function StatusBadge({ status, className }: {
2
+ status: string;
3
+ className?: string;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cn, statusLabel, statusTone, STATUS_TONE_CLASS } from '../lib/utils';
3
+ // 业务语义件(shadcn 无对应物,按"有则用 stock、无则留业务层"规则留守):
4
+ // 按订阅/订单/投递等业务状态映射色调与文案的徽章,从 stock 镜像 badge.tsx 迁出。
5
+ export function StatusBadge({ status, className }) {
6
+ return (_jsx("span", { className: cn('inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium whitespace-nowrap', STATUS_TONE_CLASS[statusTone(status)], className), children: statusLabel(status) }));
7
+ }
@@ -2,8 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useNavigate } from 'react-router';
3
3
  import { LogOut, Settings, ShieldCheck } from 'lucide-react';
4
4
  import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
5
- import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from './ui/menu';
6
- import { useAuth } from '../hooks/useAuth';
5
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from './ui/dropdown-menu';
6
+ import { useAuth } from '@ploykit/hooks';
7
7
  import { toast } from './toast';
8
8
  function initialsOf(name, email) {
9
9
  const src = name.trim() || email.trim();