@allbluecn/admin-app 0.3.2 → 0.3.4

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 (209) hide show
  1. package/allblue-ui/package.json +6 -0
  2. package/allblue-ui/src/components/confirm-dialog.tsx +94 -0
  3. package/allblue-ui/src/components/infinite-scroll-sentinel.tsx +37 -0
  4. package/allblue-ui/src/components/notion-avatar.tsx +126 -0
  5. package/allblue-ui/src/components/user-avatar.tsx +82 -0
  6. package/{src/components → allblue-ui/src}/data-table/data-table-pagination.tsx +14 -5
  7. package/{src/components → allblue-ui/src}/data-table/data-table.tsx +1 -1
  8. package/{src/components → allblue-ui/src}/data-table/table-detail-layout.tsx +5 -6
  9. package/allblue-ui/src/hooks/use-load-more.ts +57 -0
  10. package/allblue-ui/src/index.ts +31 -0
  11. package/allblue-ui/src/lib/datetime.ts +226 -0
  12. package/allblue-ui/src/lib/sanitize.ts +18 -0
  13. package/allblue-ui/src/lib/utils.ts +1 -0
  14. package/{src/components → allblue-ui/src}/ui/alert-dialog.tsx +1 -3
  15. package/{src/components → allblue-ui/src}/ui/checkbox.tsx +0 -1
  16. package/{src/components → allblue-ui/src}/ui/dialog.tsx +1 -1
  17. package/{src/components → allblue-ui/src}/ui/dropdown-menu.tsx +0 -2
  18. package/{src/components → allblue-ui/src}/ui/field.tsx +2 -2
  19. package/{src/components → allblue-ui/src}/ui/input-group.tsx +3 -4
  20. package/allblue-ui/src/ui/input.tsx +19 -0
  21. package/{src/components → allblue-ui/src}/ui/pagination.tsx +1 -1
  22. package/{src/components → allblue-ui/src}/ui/select.tsx +0 -2
  23. package/{src/components → allblue-ui/src}/ui/separator.tsx +1 -0
  24. package/{src/components → allblue-ui/src}/ui/sheet.tsx +1 -2
  25. package/{src/components → allblue-ui/src}/ui/sidebar.tsx +7 -7
  26. package/{src/components → allblue-ui/src}/ui/switch.tsx +1 -0
  27. package/{src/components → allblue-ui/src}/ui/table.tsx +0 -2
  28. package/{src/components → allblue-ui/src}/ui/toast.tsx +44 -18
  29. package/{src/components → allblue-ui/src}/ui/tooltip.tsx +1 -0
  30. package/package.json +21 -18
  31. package/src/components/admin/admin-user-menu.tsx +4 -4
  32. package/src/components/admin/developer-log.tsx +16 -32
  33. package/src/components/ai-memory/memory-facts-panel.tsx +30 -70
  34. package/src/components/ai-prompts/preview-panel.tsx +1 -1
  35. package/src/components/ai-prompts/prompt-editor.tsx +8 -9
  36. package/src/components/ai-prompts/prompt-table.tsx +6 -6
  37. package/src/components/ai-prompts/section-editor.tsx +3 -3
  38. package/src/components/ai-prompts/variable-panel.tsx +2 -2
  39. package/src/components/ai-prompts/version-history.tsx +21 -41
  40. package/src/components/ai-provider/model-list-editor.tsx +2 -3
  41. package/src/components/ai-provider/provider-form.tsx +7 -7
  42. package/src/components/ai-provider/provider-table.tsx +17 -40
  43. package/src/components/auth/admin-login-form.tsx +7 -7
  44. package/src/components/auth/icon-input.tsx +2 -2
  45. package/src/components/auth/password-input.tsx +2 -2
  46. package/src/components/component/component-form-dialog.tsx +10 -18
  47. package/src/components/component/component-import-dialog.tsx +6 -6
  48. package/src/components/component/component-preview.tsx +311 -0
  49. package/src/components/component/component-table.tsx +3 -3
  50. package/src/components/dependency/changelog-dialog.tsx +2 -2
  51. package/src/components/dependency/columns.tsx +3 -3
  52. package/src/components/dependency/dependency-table.tsx +7 -7
  53. package/src/components/dependency/edit-dialog.tsx +8 -8
  54. package/src/components/invitations/invitations-page.tsx +26 -49
  55. package/src/components/knowledge-public/bookmark-form-dialog.tsx +6 -6
  56. package/src/components/knowledge-public/compliance-form-dialog.tsx +6 -6
  57. package/src/components/knowledge-public/legal-form-dialog.tsx +7 -7
  58. package/src/components/layout/sidebar-02/app-sidebar.tsx +1 -2
  59. package/src/components/layout/sidebar-02/index.tsx +1 -1
  60. package/src/components/layout/sidebar-02/nav-main.tsx +2 -3
  61. package/src/components/layout/sidebar-02/nav-notifications.tsx +3 -3
  62. package/src/components/market/columns.tsx +4 -4
  63. package/src/components/market/market-app-form.tsx +6 -7
  64. package/src/components/market/market-app-list.tsx +14 -29
  65. package/src/components/monitoring/error-list.tsx +2 -2
  66. package/src/components/monitoring/metrics-stats.tsx +1 -1
  67. package/src/components/monitoring/request-log-table.tsx +2 -2
  68. package/src/components/oauth-providers/provider-page.tsx +20 -38
  69. package/src/components/permissions/assign-dialog.tsx +135 -0
  70. package/src/components/permissions/create-role-dialog.tsx +135 -0
  71. package/src/components/permissions/types.ts +49 -0
  72. package/src/components/user/columns.tsx +6 -6
  73. package/src/components/user/settings-activity-log.tsx +7 -7
  74. package/src/components/user/settings-profile/avatar-uploader.tsx +118 -0
  75. package/src/components/user/settings-profile/social-links.tsx +63 -0
  76. package/src/components/user/settings-profile.tsx +26 -125
  77. package/src/components/user/user-create-dialog.tsx +6 -6
  78. package/src/components/user/user-detail/error-boundary.tsx +19 -0
  79. package/src/components/user/user-detail/user-detail-dialogs.tsx +203 -0
  80. package/src/components/user/user-filters.tsx +4 -4
  81. package/src/lib/auth/page-gate.ts +28 -0
  82. package/src/lib/auth.config.ts +25 -9
  83. package/src/lib/i18n/detect.ts +1 -1
  84. package/src/lib/loader-serial.ts +14 -0
  85. package/src/providers/index.tsx +1 -1
  86. package/src/routeTree.gen.ts +67 -78
  87. package/src/routes/__root.tsx +4 -4
  88. package/src/routes/_nav/audit/route.lazy.tsx +1 -2
  89. package/src/routes/_nav/compliance/route.lazy.tsx +5 -3
  90. package/src/routes/_nav/components/route.lazy.tsx +14 -34
  91. package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +18 -38
  92. package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +18 -38
  93. package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +6 -6
  94. package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +3 -3
  95. package/src/routes/_nav/licenses/new/route.lazy.tsx +5 -5
  96. package/src/routes/_nav/licenses/route.lazy.tsx +1 -1
  97. package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +2 -2
  98. package/src/routes/_nav/monitoring/errors/route.lazy.tsx +10 -10
  99. package/src/routes/_nav/monitoring/errors/route.tsx +7 -0
  100. package/src/routes/_nav/monitoring/health/route.lazy.tsx +13 -15
  101. package/src/routes/_nav/monitoring/health/route.tsx +14 -0
  102. package/src/routes/_nav/monitoring/logs/route.lazy.tsx +24 -9
  103. package/src/routes/_nav/monitoring/logs/route.tsx +13 -0
  104. package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +12 -14
  105. package/src/routes/_nav/monitoring/metrics/route.tsx +7 -0
  106. package/src/routes/_nav/monitoring/overview/route.lazy.tsx +14 -13
  107. package/src/routes/_nav/monitoring/overview/route.tsx +17 -0
  108. package/src/routes/_nav/monitoring/route.lazy.tsx +6 -6
  109. package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +2 -2
  110. package/src/routes/_nav/permissions/route.lazy.tsx +21 -311
  111. package/src/routes/_nav/permissions/route.tsx +1 -1
  112. package/src/routes/_nav/route.tsx +5 -13
  113. package/src/routes/_nav/sso/route.lazy.tsx +2 -3
  114. package/src/routes/_nav/users/$userId/route.lazy.tsx +91 -156
  115. package/src/routes/_nav/users/-components/users-table.tsx +1 -1
  116. package/src/routes/_nav/users/route.lazy.tsx +1 -1
  117. package/src/routes/login/route.lazy.tsx +1 -1
  118. package/src/routes/setup/route.lazy.tsx +2 -2
  119. package/src/server/__tests__/admin-billing.test.ts +191 -16
  120. package/src/server/__tests__/compliance.test.ts +44 -7
  121. package/src/server/__tests__/permissions.test.ts +10 -5
  122. package/src/server/__tests__/prompt-defaults.test.ts +3 -3
  123. package/src/server/admin-ai-alerts.ts +2 -1
  124. package/src/server/admin-ai-usage.ts +2 -1
  125. package/src/server/ai-memory/store.ts +6 -2
  126. package/src/server/{serverFns/ai-prompts → ai-prompts}/prompt-defaults.ts +5 -3
  127. package/src/server/{serverFns/dependency → dependency}/helpers.ts +4 -3
  128. package/src/server/knowledge-public/github-sync.ts +2 -1
  129. package/src/server/license-issue.ts +2 -1
  130. package/src/server/middlewares/auth-rate-limit.ts +117 -0
  131. package/src/server/observability/health-check.ts +4 -1
  132. package/src/server/observability/metrics.ts +17 -15
  133. package/src/server/observability/request-middleware.ts +2 -1
  134. package/src/server/observability/timing.ts +14 -16
  135. package/src/server/serverFns/admin-billing.ts +107 -12
  136. package/src/server/serverFns/ai-memory/list-users.ts +2 -1
  137. package/src/server/serverFns/ai-prompts/activate.ts +2 -1
  138. package/src/server/serverFns/ai-prompts/get.ts +3 -2
  139. package/src/server/serverFns/ai-prompts/list.ts +3 -2
  140. package/src/server/serverFns/ai-prompts/rollback.ts +3 -1
  141. package/src/server/serverFns/ai-prompts/save.ts +5 -3
  142. package/src/server/serverFns/ai-provider/create.ts +4 -1
  143. package/src/server/serverFns/ai-provider/delete.ts +4 -1
  144. package/src/server/serverFns/ai-provider/get.ts +4 -1
  145. package/src/server/serverFns/ai-provider/list.ts +28 -1
  146. package/src/server/serverFns/ai-provider/seed-from-seed.ts +2 -1
  147. package/src/server/serverFns/ai-provider/update.ts +4 -1
  148. package/src/server/serverFns/anyhub/market.ts +8 -1
  149. package/src/server/serverFns/audit.ts +14 -1
  150. package/src/server/serverFns/auth/setup.ts +3 -1
  151. package/src/server/serverFns/compliance.ts +32 -3
  152. package/src/server/serverFns/components/import.ts +69 -14
  153. package/src/server/serverFns/components/list.ts +4 -1
  154. package/src/server/serverFns/components/mutations.ts +9 -1
  155. package/src/server/serverFns/dependency/list.ts +28 -5
  156. package/src/server/serverFns/dependency/sync.ts +7 -2
  157. package/src/server/serverFns/dependency/update.ts +4 -1
  158. package/src/server/serverFns/developer-log-auto-generate.ts +63 -32
  159. package/src/server/serverFns/developer-logs.ts +5 -1
  160. package/src/server/serverFns/invitations.ts +7 -1
  161. package/src/server/serverFns/knowledge-public/bookmarks/create.ts +2 -1
  162. package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +2 -1
  163. package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +2 -1
  164. package/src/server/serverFns/knowledge-public/bookmarks/list.ts +2 -1
  165. package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +2 -1
  166. package/src/server/serverFns/knowledge-public/bookmarks/update.ts +2 -1
  167. package/src/server/serverFns/knowledge-public/compliance/create.ts +2 -1
  168. package/src/server/serverFns/knowledge-public/compliance/delete.ts +2 -1
  169. package/src/server/serverFns/knowledge-public/compliance/detail.ts +2 -1
  170. package/src/server/serverFns/knowledge-public/compliance/list.ts +2 -1
  171. package/src/server/serverFns/knowledge-public/compliance/update.ts +2 -1
  172. package/src/server/serverFns/knowledge-public/legal/create.ts +2 -1
  173. package/src/server/serverFns/knowledge-public/legal/delete.ts +2 -1
  174. package/src/server/serverFns/knowledge-public/legal/detail.ts +2 -1
  175. package/src/server/serverFns/knowledge-public/legal/list.ts +2 -1
  176. package/src/server/serverFns/knowledge-public/legal/update.ts +2 -1
  177. package/src/server/serverFns/licenses.ts +8 -2
  178. package/src/server/serverFns/monitoring/get-errors.ts +2 -1
  179. package/src/server/serverFns/monitoring/get-health.ts +2 -1
  180. package/src/server/serverFns/monitoring/get-metrics.ts +2 -1
  181. package/src/server/serverFns/monitoring/get-request-logs.ts +2 -1
  182. package/src/server/serverFns/monitoring/report-error.ts +2 -1
  183. package/src/server/serverFns/monitoring/reset.ts +2 -1
  184. package/src/server/serverFns/oauth-providers.ts +4 -1
  185. package/src/server/serverFns/permissions.ts +18 -11
  186. package/src/server/serverFns/sentry/config.ts +11 -8
  187. package/src/server/serverFns/sso-config.ts +17 -2
  188. package/src/server/serverFns/user/create.ts +6 -1
  189. package/src/server/serverFns/user/delete.ts +6 -1
  190. package/src/server/serverFns/user/detail.ts +5 -2
  191. package/src/server/serverFns/user/list.ts +19 -4
  192. package/src/server/serverFns/user/reset-password.ts +10 -2
  193. package/src/server/serverFns/user/update-role.ts +10 -2
  194. package/src/server/serverFns/user/update-status.ts +10 -2
  195. package/src/styles/globals.css +4 -0
  196. package/src/components/ui/input.tsx +0 -19
  197. /package/{src → allblue-ui/src}/hooks/use-mobile.ts +0 -0
  198. /package/{src → allblue-ui/src}/lib/avatar.ts +0 -0
  199. /package/{src/components → allblue-ui/src}/ui/alert.tsx +0 -0
  200. /package/{src/components → allblue-ui/src}/ui/avatar.tsx +0 -0
  201. /package/{src/components → allblue-ui/src}/ui/badge.tsx +0 -0
  202. /package/{src/components → allblue-ui/src}/ui/button.tsx +0 -0
  203. /package/{src/components → allblue-ui/src}/ui/card.tsx +0 -0
  204. /package/{src/components → allblue-ui/src}/ui/collapsible.tsx +0 -0
  205. /package/{src/components → allblue-ui/src}/ui/label.tsx +0 -0
  206. /package/{src/components → allblue-ui/src}/ui/skeleton.tsx +0 -0
  207. /package/{src/components → allblue-ui/src}/ui/textarea.tsx +0 -0
  208. /package/src/routes/{api.auth.$.ts → api/auth/$.ts} +0 -0
  209. /package/src/routes/{api.sentry-test.ts → api/sentry-test.ts} +0 -0
@@ -0,0 +1,226 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { formatInTimeZone, toZonedTime, fromZonedTime } from 'date-fns-tz'
19
+ import { startOfDay, subDays } from 'date-fns'
20
+
21
+ const FALLBACK_TIMEZONE = 'Asia/Shanghai'
22
+
23
+ /**
24
+ * 解析生效时区:项目时区 > 用户时区 > 浏览器时区 > Asia/Shanghai
25
+ */
26
+ export function resolveTimezone(
27
+ projectTz?: string | null,
28
+ userTz?: string | null,
29
+ ): string {
30
+ const browserTz =
31
+ typeof Intl !== 'undefined'
32
+ ? Intl.DateTimeFormat().resolvedOptions().timeZone
33
+ : FALLBACK_TIMEZONE
34
+ return projectTz || userTz || browserTz || FALLBACK_TIMEZONE
35
+ }
36
+
37
+ /**
38
+ * 时区感知的时间格式化。
39
+ * @param date UTC Date 对象(来自 DB)
40
+ * @param timezone IANA 时区,如 "Asia/Shanghai"
41
+ * @param fmt date-fns 格式,默认 "yyyy-MM-dd HH:mm"
42
+ */
43
+ export function formatDate(
44
+ date: Date,
45
+ timezone: string,
46
+ fmt = 'yyyy-MM-dd HH:mm',
47
+ ): string {
48
+ try {
49
+ return formatInTimeZone(date, timezone, fmt)
50
+ } catch {
51
+ return formatInTimeZone(date, FALLBACK_TIMEZONE, fmt)
52
+ }
53
+ }
54
+
55
+ /**
56
+ * 确定性时间戳格式化:固定时区 + date-fns 数字格式,不依赖运行环境 locale/时区。
57
+ * SSR 与客户端输出字节级一致,专治 toLocaleString() 引发的 hydration mismatch。
58
+ * @param date Date 对象或 ISO 字符串
59
+ * @param fmt date-fns 格式,默认 "yyyy-MM-dd HH:mm"
60
+ */
61
+ export function formatTimestamp(
62
+ date: Date | string,
63
+ fmt = 'yyyy-MM-dd HH:mm',
64
+ ): string {
65
+ const d = typeof date === 'string' ? new Date(date) : date
66
+ if (Number.isNaN(d.getTime())) return '-'
67
+ return formatDate(d, FALLBACK_TIMEZONE, fmt)
68
+ }
69
+
70
+ /**
71
+ * 相对时间(今天/昨天/具体日期),基于时区计算「今天的 0 点」。
72
+ * @param now 当前时间(测试注入;生产用默认 new Date())
73
+ */
74
+ export function formatRelative(
75
+ date: Date,
76
+ timezone: string,
77
+ now: Date = new Date(),
78
+ ): string {
79
+ try {
80
+ const todayStart = startOfDayInTimeZone(now, timezone)
81
+ const yesterdayStart = new Date(todayStart)
82
+ yesterdayStart.setDate(yesterdayStart.getDate() - 1)
83
+
84
+ const dateStart = startOfDayInTimeZone(date, timezone)
85
+
86
+ if (dateStart.getTime() === todayStart.getTime()) return '今天'
87
+ if (dateStart.getTime() === yesterdayStart.getTime()) return '昨天'
88
+ return formatInTimeZone(date, timezone, 'yyyy-MM-dd')
89
+ } catch {
90
+ return formatInTimeZone(date, FALLBACK_TIMEZONE, 'yyyy-MM-dd')
91
+ }
92
+ }
93
+
94
+ /**
95
+ * 获取指定时区下某天的 0 点(UTC Date 对象)。
96
+ */
97
+ function startOfDayInTimeZone(date: Date, timezone: string): Date {
98
+ const zoned = toZonedTime(date, timezone)
99
+ const dayStart = startOfDay(zoned)
100
+ return fromZonedTime(dayStart, timezone)
101
+ }
102
+
103
+ /**
104
+ * 对话历史日期分组标签(今天/昨天/本周/具体日期)。
105
+ * @param date UTC Date 对象
106
+ * @param timezone IANA 时区
107
+ */
108
+ export function formatChatDate(date: Date, timezone: string): string {
109
+ try {
110
+ const now = new Date()
111
+ const todayStart = startOfDayInTimeZone(now, timezone)
112
+ const yesterdayStart = subDays(todayStart, 1)
113
+ const weekStart = subDays(todayStart, 7)
114
+
115
+ const dateStart = startOfDayInTimeZone(date, timezone)
116
+
117
+ if (dateStart.getTime() === todayStart.getTime()) return '今天'
118
+ if (dateStart.getTime() === yesterdayStart.getTime()) return '昨天'
119
+ if (dateStart.getTime() >= weekStart.getTime()) return '本周'
120
+
121
+ const currentYear = Number(formatInTimeZone(now, timezone, 'yyyy'))
122
+ const dateYear = Number(formatInTimeZone(date, timezone, 'yyyy'))
123
+ if (dateYear === currentYear) {
124
+ return formatInTimeZone(date, timezone, 'M月d日')
125
+ }
126
+ return formatInTimeZone(date, timezone, 'yyyy年M月d日')
127
+ } catch {
128
+ return formatInTimeZone(date, FALLBACK_TIMEZONE, 'yyyy-MM-dd')
129
+ }
130
+ }
131
+
132
+
133
+ // ---- 本地日历工具(自 lib/date-utils.ts 并入:按本地时区做日历网格/日键计算,无时区转换) ----
134
+
135
+ /** 解析 "yyyy-MM-dd" 前缀为本地 Date(避免 UTC 偏移) */
136
+ export function parseDate(s: string): Date {
137
+ const datePart = s.slice(0, 10)
138
+ const [y, m, d] = datePart.split('-').map(Number)
139
+ return new Date(y, m - 1, d)
140
+ }
141
+
142
+ /** 本地时区 yyyy-MM-dd 日键(与 timezone 版 formatDate 区分) */
143
+ export function formatDateLocal(d: Date): string {
144
+ const y = d.getFullYear()
145
+ const m = String(d.getMonth() + 1).padStart(2, '0')
146
+ const day = String(d.getDate()).padStart(2, '0')
147
+ return `${y}-${m}-${day}`
148
+ }
149
+
150
+ export function isSameDay(a: Date, b: Date): boolean {
151
+ return (
152
+ a.getFullYear() === b.getFullYear() &&
153
+ a.getMonth() === b.getMonth() &&
154
+ a.getDate() === b.getDate()
155
+ )
156
+ }
157
+
158
+ export function getMonthGrid(
159
+ year: number,
160
+ month: number,
161
+ ): Array<{ date: Date; inMonth: boolean }> {
162
+ const firstDay = new Date(year, month, 1)
163
+ const startOffset = firstDay.getDay() // 周日=0
164
+ const gridStart = new Date(year, month, 1 - startOffset)
165
+ const result: Array<{ date: Date; inMonth: boolean }> = []
166
+ for (let i = 0; i < 42; i++) {
167
+ const date = new Date(gridStart)
168
+ date.setDate(gridStart.getDate() + i)
169
+ result.push({ date, inMonth: date.getMonth() === month })
170
+ }
171
+ return result
172
+ }
173
+
174
+ export function getWeekGrid(
175
+ year: number,
176
+ month: number,
177
+ day: number,
178
+ ): Array<{ date: Date; inMonth: boolean }> {
179
+ const anchor = new Date(year, month, day)
180
+ const startOffset = anchor.getDay()
181
+ const gridStart = new Date(year, month, day - startOffset)
182
+ const result: Array<{ date: Date; inMonth: boolean }> = []
183
+ for (let i = 0; i < 7; i++) {
184
+ const date = new Date(gridStart)
185
+ date.setDate(gridStart.getDate() + i)
186
+ result.push({ date, inMonth: date.getMonth() === month })
187
+ }
188
+ return result
189
+ }
190
+
191
+ export function buildTimelineRange(
192
+ start: Date,
193
+ end: Date,
194
+ mode: 'month' | 'week',
195
+ ): Array<{ start: Date; label: string }> {
196
+ const ticks: Array<{ start: Date; label: string }> = []
197
+ const cursor = new Date(start.getFullYear(), start.getMonth(), 1)
198
+ const endDate = new Date(end.getFullYear(), end.getMonth(), 1)
199
+ while (cursor <= endDate) {
200
+ const label =
201
+ mode === 'month'
202
+ ? `${cursor.getFullYear()}-${String(cursor.getMonth() + 1).padStart(2, '0')}`
203
+ : `${cursor.getFullYear()}-${String(cursor.getMonth() + 1).padStart(2, '0')}-${String(cursor.getDate()).padStart(2, '0')}`
204
+ ticks.push({ start: new Date(cursor), label })
205
+ if (mode === 'month') cursor.setMonth(cursor.getMonth() + 1)
206
+ else cursor.setDate(cursor.getDate() + 7)
207
+ }
208
+ return ticks
209
+ }
210
+
211
+ export function getRangeFromDates(dates: Date[]): { start: Date; end: Date } {
212
+ if (dates.length === 0) {
213
+ const now = new Date()
214
+ return {
215
+ start: new Date(now.getFullYear(), now.getMonth(), 1),
216
+ end: new Date(now.getFullYear(), now.getMonth() + 2, 0),
217
+ }
218
+ }
219
+ const times = dates.map((d) => d.getTime())
220
+ const min = new Date(Math.min(...times))
221
+ const max = new Date(Math.max(...times))
222
+ return {
223
+ start: new Date(min.getFullYear(), min.getMonth() - 1, 1),
224
+ end: new Date(max.getFullYear(), max.getMonth() + 1, 0),
225
+ }
226
+ }
@@ -0,0 +1,18 @@
1
+ import DOMPurify from 'isomorphic-dompurify'
2
+
3
+ // HTML 清理:默认配置(html + svg + mathMl profile 均启用),
4
+ // 覆盖富文本 diff、附件预览等 7 处 HTML 渲染场景。
5
+ export function sanitize(html: string): string {
6
+ return DOMPurify.sanitize(html)
7
+ }
8
+
9
+ // SVG 清理:头像渲染(NotionAvatar)专用。
10
+ // 显式复用默认配置——DOMPurify 默认 profile 已包含 SVG 滤镜元素
11
+ // (<defs>/<filter>/<feMorphology>/<feFlood>/<feComposite>/<feMerge>/<feMergeNode>),
12
+ // 同时清除 <script>/on*/javascript: 等 XSS 载荷。
13
+ // 禁止改成 USE_PROFILES: { svg: true }——严格 SVG profile 会移除全部 fe* 滤镜元素,
14
+ // 头像轮廓辉光(feMorphology+feFlood+feComposite+feMerge)会失效渲染白屏。
15
+ // 结构完整性由 __tests__/security/xss-sanitize.test.ts 的双向断言锁定。
16
+ export function sanitizeSvg(svg: string): string {
17
+ return DOMPurify.sanitize(svg)
18
+ }
@@ -0,0 +1 @@
1
+ export { cn } from "cn"
@@ -1,10 +1,8 @@
1
- "use client"
2
-
3
1
  import * as React from "react"
4
2
  import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"
5
3
  import { cn } from "cn"
6
4
 
7
- import { Button } from "@/components/ui/button"
5
+ import { Button } from "./button"
8
6
 
9
7
  function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
10
8
  return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
@@ -1,4 +1,3 @@
1
- "use client"
2
1
 
3
2
  import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
4
3
  import { cn } from "cn"
@@ -2,7 +2,7 @@ import * as React from "react"
2
2
  import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
3
3
  import { cn } from "cn"
4
4
 
5
- import { Button } from "@/components/ui/button"
5
+ import { Button } from "./button"
6
6
  import { XIcon } from "lucide-react"
7
7
 
8
8
  function Dialog({ ...props }: DialogPrimitive.Root.Props) {
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import * as React from "react"
4
2
  import { Menu as MenuPrimitive } from "@base-ui/react/menu"
5
3
  import { cn } from "cn"
@@ -2,8 +2,8 @@ import { useMemo } from "react"
2
2
  import { cva, type VariantProps } from "class-variance-authority"
3
3
  import { cn } from "cn"
4
4
 
5
- import { Label } from "@/components/ui/label"
6
- import { Separator } from "@/components/ui/separator"
5
+ import { Label } from "./label"
6
+ import { Separator } from "./separator"
7
7
 
8
8
  function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
9
9
  return (
@@ -1,12 +1,11 @@
1
- "use client"
2
1
 
3
2
  import * as React from "react"
4
3
  import { cva, type VariantProps } from "class-variance-authority"
5
4
  import { cn } from "cn"
6
5
 
7
- import { Button } from "@/components/ui/button"
8
- import { Input } from "@/components/ui/input"
9
- import { Textarea } from "@/components/ui/textarea"
6
+ import { Button } from "./button"
7
+ import { Input } from "./input"
8
+ import { Textarea } from "./textarea"
10
9
 
11
10
  function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
12
11
  return (
@@ -0,0 +1,19 @@
1
+ import * as React from "react"
2
+ import { Input as InputPrimitive } from "@base-ui/react/input"
3
+ import { cn } from "cn"
4
+
5
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
+ return (
7
+ <InputPrimitive
8
+ type={type}
9
+ data-slot="input"
10
+ className={cn(
11
+ "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-2.5 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
12
+ className
13
+ )}
14
+ {...props}
15
+ />
16
+ )
17
+ }
18
+
19
+ export { Input }
@@ -1,7 +1,7 @@
1
1
  import * as React from "react"
2
2
  import { cn } from "cn"
3
3
 
4
- import { Button } from "@/components/ui/button"
4
+ import { Button } from "./button"
5
5
  import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
6
6
 
7
7
  function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import * as React from "react"
4
2
  import { Select as SelectPrimitive } from "@base-ui/react/select"
5
3
  import { cn } from "cn"
@@ -1,3 +1,4 @@
1
+
1
2
  import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
2
3
  import { cn } from "cn"
3
4
 
@@ -1,10 +1,9 @@
1
- "use client"
2
1
 
3
2
  import * as React from "react"
4
3
  import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
5
4
  import { cn } from "cn"
6
5
 
7
- import { Button } from "@/components/ui/button"
6
+ import { Button } from "./button"
8
7
  import { XIcon } from "lucide-react"
9
8
 
10
9
  function Sheet({ ...props }: SheetPrimitive.Root.Props) {
@@ -4,23 +4,23 @@ import { useRender } from "@base-ui/react/use-render"
4
4
  import { cva, type VariantProps } from "class-variance-authority"
5
5
  import { cn } from "cn"
6
6
 
7
- import { useIsMobile } from "@/hooks/use-mobile"
8
- import { Button } from "@/components/ui/button"
9
- import { Input } from "@/components/ui/input"
10
- import { Separator } from "@/components/ui/separator"
7
+ import { useIsMobile } from "../hooks/use-mobile"
8
+ import { Button } from "./button"
9
+ import { Input } from "./input"
10
+ import { Separator } from "./separator"
11
11
  import {
12
12
  Sheet,
13
13
  SheetContent,
14
14
  SheetDescription,
15
15
  SheetHeader,
16
16
  SheetTitle,
17
- } from "@/components/ui/sheet"
18
- import { Skeleton } from "@/components/ui/skeleton"
17
+ } from "./sheet"
18
+ import { Skeleton } from "./skeleton"
19
19
  import {
20
20
  Tooltip,
21
21
  TooltipContent,
22
22
  TooltipTrigger,
23
- } from "@/components/ui/tooltip"
23
+ } from "./tooltip"
24
24
  import { PanelLeftIcon } from "lucide-react"
25
25
 
26
26
  const SIDEBAR_COOKIE_NAME = "sidebar_state"
@@ -1,3 +1,4 @@
1
+
1
2
  import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
2
3
  import { cn } from "cn"
3
4
 
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import * as React from "react"
4
2
  import { cn } from "cn"
5
3
 
@@ -2,11 +2,17 @@ import * as React from "react"
2
2
  import { Toast as ToastPrimitive } from "@base-ui/react/toast"
3
3
  import { cn } from "cn"
4
4
 
5
- import { Button } from "@/components/ui/button"
5
+ import { Button } from "./button"
6
6
  import { XIcon, CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
7
7
 
8
8
  const toastManager = ToastPrimitive.createToastManager()
9
9
 
10
+ // 双端历史分叉:web 顶部居中堆叠(top-center),admin 右下角堆叠(bottom-right)。
11
+ // 差异为纯 class 组 + swipeDirection,通过 position 参数承载,行为逐字节等价于原实现。
12
+ type ToasterPosition = "top-center" | "bottom-right"
13
+
14
+ const ToastPositionContext = React.createContext<ToasterPosition>("top-center")
15
+
10
16
  interface SonnerToastOptions {
11
17
  description?: React.ReactNode
12
18
  duration?: number
@@ -67,11 +73,14 @@ function ToastPortal({ ...props }: ToastPrimitive.Portal.Props) {
67
73
  }
68
74
 
69
75
  function ToastViewport({ className, ...props }: ToastPrimitive.Viewport.Props) {
76
+ const top = React.useContext(ToastPositionContext) === "top-center"
70
77
  return (
71
78
  <ToastPrimitive.Viewport
72
79
  data-slot="toast-viewport"
73
80
  className={cn(
74
- "pointer-events-none fixed bottom-4 right-4 z-50 w-full max-w-sm outline-none sm:bottom-6 sm:right-6",
81
+ top
82
+ ? "pointer-events-none fixed top-4 left-0 right-0 z-50 mx-auto w-full max-w-sm outline-none sm:top-6"
83
+ : "pointer-events-none fixed bottom-4 right-4 z-50 w-full max-w-sm outline-none sm:bottom-6 sm:right-6",
75
84
  className
76
85
  )}
77
86
  {...props}
@@ -80,17 +89,30 @@ function ToastViewport({ className, ...props }: ToastPrimitive.Viewport.Props) {
80
89
  }
81
90
 
82
91
  function Toast({ className, ...props }: ToastPrimitive.Root.Props) {
92
+ const top = React.useContext(ToastPositionContext) === "top-center"
83
93
  return (
84
94
  <ToastPrimitive.Root
85
95
  data-slot="toast"
86
96
  className={cn(
87
- "group/toast pointer-events-auto absolute right-0 bottom-0 z-[calc(1000-var(--toast-index))] w-full origin-bottom rounded-2xl border bg-popover text-popover-foreground shadow-lg will-change-transform outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
88
- "[--gap:0.75rem] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))]",
89
- "h-(--height) [transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] [transition:transform_500ms_cubic-bezier(0.22,1,0.36,1),opacity_500ms,height_150ms]",
90
- "after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-['']",
97
+ top
98
+ ? "group/toast pointer-events-auto absolute top-0 right-0 left-0 z-[calc(1000-var(--toast-index))] mx-auto w-full origin-top rounded-2xl border bg-popover text-popover-foreground shadow-lg will-change-transform outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
99
+ : "group/toast pointer-events-auto absolute right-0 bottom-0 z-[calc(1000-var(--toast-index))] w-full origin-bottom rounded-2xl border bg-popover text-popover-foreground shadow-lg will-change-transform outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
100
+ top
101
+ ? "[--gap:0.75rem] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)+calc(var(--toast-index)*var(--gap))+var(--toast-swipe-movement-y))] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))]"
102
+ : "[--gap:0.75rem] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))]",
103
+ top
104
+ ? "h-(--height) [transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--peek))+(var(--shrink)*var(--height))))_scale(var(--scale))] [transition:transform_500ms_cubic-bezier(0.22,1,0.36,1),opacity_500ms,height_150ms]"
105
+ : "h-(--height) [transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] [transition:transform_500ms_cubic-bezier(0.22,1,0.36,1),opacity_500ms,height_150ms]",
106
+ top
107
+ ? "after:absolute after:bottom-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-['']"
108
+ : "after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-['']",
91
109
  "data-expanded:h-(--toast-height) data-expanded:[transform:translateX(var(--toast-swipe-movement-x))_translateY(var(--offset-y))]",
92
- "data-limited:opacity-0 data-starting-style:[transform:translateY(150%)]",
93
- "[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(150%)]",
110
+ top
111
+ ? "data-limited:opacity-0 data-starting-style:[transform:translateY(-150%)]"
112
+ : "data-limited:opacity-0 data-starting-style:[transform:translateY(150%)]",
113
+ top
114
+ ? "[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(-150%)]"
115
+ : "[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(150%)]",
94
116
  "data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]",
95
117
  "data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]",
96
118
  "data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]",
@@ -230,9 +252,10 @@ function ToastIcon({ type }: { type: string | undefined }) {
230
252
 
231
253
  function ToastList() {
232
254
  const { toasts } = ToastPrimitive.useToastManager()
255
+ const top = React.useContext(ToastPositionContext) === "top-center"
233
256
 
234
257
  return toasts.map((toastItem) => (
235
- <Toast key={toastItem.id} toast={toastItem}>
258
+ <Toast key={toastItem.id} toast={toastItem} swipeDirection={top ? "up" : undefined}>
236
259
  <ToastContent>
237
260
  <ToastIcon type={toastItem.type} />
238
261
  <div className="flex min-w-0 flex-1 flex-col gap-1">
@@ -248,17 +271,20 @@ function ToastList() {
248
271
 
249
272
  function Toaster({
250
273
  children,
274
+ position = "top-center",
251
275
  ...props
252
- }: ToastPrimitive.Provider.Props) {
276
+ }: ToastPrimitive.Provider.Props & { position?: ToasterPosition }) {
253
277
  return (
254
- <ToastProvider toastManager={toastManager} {...props}>
255
- {children}
256
- <ToastPortal>
257
- <ToastViewport>
258
- <ToastList />
259
- </ToastViewport>
260
- </ToastPortal>
261
- </ToastProvider>
278
+ <ToastPositionContext.Provider value={position}>
279
+ <ToastProvider toastManager={toastManager} {...props}>
280
+ {children}
281
+ <ToastPortal>
282
+ <ToastViewport>
283
+ <ToastList />
284
+ </ToastViewport>
285
+ </ToastPortal>
286
+ </ToastProvider>
287
+ </ToastPositionContext.Provider>
262
288
  )
263
289
  }
264
290
 
@@ -1,3 +1,4 @@
1
+
1
2
  import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
2
3
  import { cn } from "cn"
3
4
 
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@allbluecn/admin-app",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "private": false,
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
7
7
  "dependencies": {
8
- "@allbluecn/database": "0.9.1",
9
- "@allbluecn/kernel": "0.9.0",
10
- "@allbluecn/web-app": "0.17.2",
8
+ "@allbluecn/database": "0.9.2",
9
+ "@allbluecn/kernel": "0.9.1",
11
10
  "@auth/core": "^0.41.2",
12
11
  "@base-ui/react": "^1.8.0",
13
12
  "@fontsource-variable/inter": "^5.3.0",
@@ -24,14 +23,14 @@
24
23
  "@tabler/icons-react": "^3.44.0",
25
24
  "@tanstack/charts": "^0.16.0",
26
25
  "@tanstack/react-form": "2.0.0-alpha.2",
27
- "@tanstack/react-pacer": "^0.23.0",
28
- "@tanstack/react-query": "^5.102.8",
29
- "@tanstack/react-router": "1.170.32",
30
- "@tanstack/react-router-ssr-query": "^1.167.2",
31
- "@tanstack/react-start": "^1.168.49",
32
- "@tanstack/react-table": "^9.2.3",
33
- "@tanstack/react-virtual": "^3.14.10",
34
- "@tanstack/store": "^0.11.1",
26
+ "@tanstack/react-pacer": "^0.24.1",
27
+ "@tanstack/react-query": "^5.104.1",
28
+ "@tanstack/react-router": "1.170.41",
29
+ "@tanstack/react-router-ssr-query": "^1.167.3",
30
+ "@tanstack/react-start": "1.168.60",
31
+ "@tanstack/react-table": "^9.2.6",
32
+ "@tanstack/react-virtual": "^3.14.13",
33
+ "@tanstack/store": "^0.11.2",
35
34
  "bcryptjs": "^3.0.3",
36
35
  "class-variance-authority": "^0.7.1",
37
36
  "clsx": "^2.1.1",
@@ -56,8 +55,8 @@
56
55
  "@inlang/paraglide-js": "^2.25.0",
57
56
  "@inlang/plugin-message-format": "^4.4.4",
58
57
  "@tailwindcss/vite": "^4.3.3",
59
- "@tanstack/react-query-devtools": "5.102.8",
60
- "@tanstack/react-router-devtools": "1.167.1",
58
+ "@tanstack/react-query-devtools": "5.104.1",
59
+ "@tanstack/react-router-devtools": "1.167.2",
61
60
  "@testing-library/jest-dom": "7.0.1",
62
61
  "@testing-library/react": "^16.3.2",
63
62
  "@types/bcryptjs": "^2.4.6",
@@ -67,18 +66,22 @@
67
66
  "@vitejs/plugin-react": "^6.0.1",
68
67
  "@vitest/coverage-v8": "5.0.0",
69
68
  "eslint": "^9",
69
+ "eslint-plugin-unused-imports": "^4.4.1",
70
70
  "jsdom": "^29.1.1",
71
71
  "nitro": "3.0.260903-beta",
72
72
  "shadcn": "^4.21.0",
73
73
  "tailwindcss": "^4.3.3",
74
- "typescript": "^7",
75
- "vite": "^8.3.0",
74
+ "typescript": "~6.0.3",
75
+ "typescript-eslint": "^8.70.1",
76
+ "vite": "^8.3.3",
76
77
  "vitest": "5.0.0",
77
- "wrangler": "^4.99.0"
78
+ "wrangler": "^4.99.0",
79
+ "@allbluecn/ui": "0.1.0"
78
80
  },
79
81
  "files": [
80
82
  "src",
81
- "public"
83
+ "public",
84
+ "allblue-ui"
82
85
  ],
83
86
  "publishConfig": {
84
87
  "access": "public"
@@ -17,17 +17,17 @@
17
17
 
18
18
  import { LogOut } from "lucide-react";
19
19
  import { m } from '@/paraglide/messages.js'
20
- import { Badge } from "@/components/ui/badge";
20
+ import { Badge } from "@allbluecn/ui/badge";
21
21
  import {
22
22
  SidebarMenu,
23
23
  SidebarMenuAction,
24
24
  SidebarMenuButton,
25
25
  SidebarMenuItem,
26
- } from "@/components/ui/sidebar";
27
- import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
26
+ } from "@allbluecn/ui/sidebar";
27
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@allbluecn/ui/tooltip";
28
28
  import { useRouter, useMatches } from "@tanstack/react-router";
29
29
  import { signOut, useSession } from "@/lib/auth-client";
30
- import { UserAvatar } from "@allbluecn/web-app/user-avatar";
30
+ import { UserAvatar } from "@allbluecn/ui/components/user-avatar";
31
31
  import type { AvatarConfig, ShapeType } from "@allbluecn/kernel";
32
32
 
33
33
  const ROLE_LABELS: Record<string, () => string> = {