@allbluecn/admin-app 0.3.3 → 0.3.5

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 (109) hide show
  1. package/package.json +4 -3
  2. package/src/components/admin/admin-user-menu.tsx +4 -4
  3. package/src/components/admin/developer-log.tsx +7 -7
  4. package/src/components/ai-memory/memory-facts-panel.tsx +9 -9
  5. package/src/components/ai-prompts/preview-panel.tsx +1 -1
  6. package/src/components/ai-prompts/prompt-editor.tsx +8 -8
  7. package/src/components/ai-prompts/prompt-table.tsx +6 -6
  8. package/src/components/ai-prompts/section-editor.tsx +3 -3
  9. package/src/components/ai-prompts/variable-panel.tsx +2 -2
  10. package/src/components/ai-prompts/version-history.tsx +5 -5
  11. package/src/components/ai-provider/model-list-editor.tsx +2 -2
  12. package/src/components/ai-provider/provider-form.tsx +7 -7
  13. package/src/components/ai-provider/provider-table.tsx +6 -6
  14. package/src/components/auth/admin-login-form.tsx +6 -6
  15. package/src/components/auth/icon-input.tsx +2 -2
  16. package/src/components/auth/password-input.tsx +2 -2
  17. package/src/components/component/component-form-dialog.tsx +8 -8
  18. package/src/components/component/component-import-dialog.tsx +6 -6
  19. package/src/components/component/component-table.tsx +3 -3
  20. package/src/components/confirm-dialog.tsx +94 -0
  21. package/src/components/data-table/data-table-pagination.tsx +121 -0
  22. package/src/components/data-table/data-table.tsx +218 -0
  23. package/src/components/data-table/table-detail-layout.tsx +97 -0
  24. package/src/components/dependency/changelog-dialog.tsx +2 -2
  25. package/src/components/dependency/columns.tsx +3 -3
  26. package/src/components/dependency/dependency-table.tsx +7 -7
  27. package/src/components/dependency/edit-dialog.tsx +8 -8
  28. package/src/components/invitations/invitations-page.tsx +13 -13
  29. package/src/components/knowledge-public/bookmark-form-dialog.tsx +6 -6
  30. package/src/components/knowledge-public/compliance-form-dialog.tsx +6 -6
  31. package/src/components/knowledge-public/legal-form-dialog.tsx +7 -7
  32. package/src/components/layout/sidebar-02/app-sidebar.tsx +1 -1
  33. package/src/components/layout/sidebar-02/index.tsx +1 -1
  34. package/src/components/layout/sidebar-02/nav-main.tsx +2 -2
  35. package/src/components/layout/sidebar-02/nav-notifications.tsx +3 -3
  36. package/src/components/market/columns.tsx +4 -4
  37. package/src/components/market/market-app-form.tsx +6 -6
  38. package/src/components/market/market-app-list.tsx +4 -4
  39. package/src/components/monitoring/error-list.tsx +1 -1
  40. package/src/components/monitoring/metrics-stats.tsx +1 -1
  41. package/src/components/monitoring/request-log-table.tsx +1 -1
  42. package/src/components/notion-avatar.tsx +126 -0
  43. package/src/components/oauth-providers/provider-page.tsx +9 -9
  44. package/src/components/permissions/assign-dialog.tsx +5 -5
  45. package/src/components/permissions/create-role-dialog.tsx +6 -6
  46. package/src/components/ui/alert-dialog.tsx +185 -0
  47. package/src/components/ui/alert.tsx +75 -0
  48. package/src/components/ui/avatar.tsx +106 -0
  49. package/src/components/ui/badge.tsx +51 -0
  50. package/src/components/ui/button.tsx +57 -0
  51. package/src/components/ui/card.tsx +102 -0
  52. package/src/components/ui/checkbox.tsx +27 -0
  53. package/src/components/ui/collapsible.tsx +19 -0
  54. package/src/components/ui/dialog.tsx +155 -0
  55. package/src/components/ui/dropdown-menu.tsx +265 -0
  56. package/src/components/ui/field.tsx +236 -0
  57. package/src/components/ui/input-group.tsx +157 -0
  58. package/src/components/ui/input.tsx +19 -0
  59. package/src/components/ui/label.tsx +17 -0
  60. package/src/components/ui/pagination.tsx +130 -0
  61. package/src/components/ui/select.tsx +232 -0
  62. package/src/components/ui/separator.tsx +23 -0
  63. package/src/components/ui/sheet.tsx +134 -0
  64. package/src/components/ui/sidebar.tsx +721 -0
  65. package/src/components/ui/skeleton.tsx +13 -0
  66. package/src/components/ui/switch.tsx +30 -0
  67. package/src/components/ui/table.tsx +113 -0
  68. package/src/components/ui/textarea.tsx +17 -0
  69. package/src/components/ui/toast.tsx +308 -0
  70. package/src/components/ui/tooltip.tsx +64 -0
  71. package/src/components/user/columns.tsx +4 -4
  72. package/src/components/user/settings-activity-log.tsx +7 -7
  73. package/src/components/user/settings-profile/avatar-uploader.tsx +2 -2
  74. package/src/components/user/settings-profile/social-links.tsx +2 -2
  75. package/src/components/user/settings-profile.tsx +6 -6
  76. package/src/components/user/user-create-dialog.tsx +6 -6
  77. package/src/components/user/user-detail/error-boundary.tsx +1 -1
  78. package/src/components/user/user-detail/user-detail-dialogs.tsx +6 -6
  79. package/src/components/user/user-filters.tsx +3 -3
  80. package/src/components/user-avatar.tsx +82 -0
  81. package/src/hooks/use-mobile.ts +19 -0
  82. package/src/lib/datetime.ts +226 -0
  83. package/src/lib/sanitize.ts +18 -0
  84. package/src/providers/index.tsx +1 -1
  85. package/src/routes/__root.tsx +1 -1
  86. package/src/routes/_nav/audit/route.lazy.tsx +1 -1
  87. package/src/routes/_nav/compliance/route.lazy.tsx +3 -3
  88. package/src/routes/_nav/components/route.lazy.tsx +4 -4
  89. package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +6 -6
  90. package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +6 -6
  91. package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +6 -6
  92. package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +3 -3
  93. package/src/routes/_nav/licenses/new/route.lazy.tsx +5 -5
  94. package/src/routes/_nav/licenses/route.lazy.tsx +1 -1
  95. package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +2 -2
  96. package/src/routes/_nav/monitoring/errors/route.lazy.tsx +1 -1
  97. package/src/routes/_nav/monitoring/health/route.lazy.tsx +3 -3
  98. package/src/routes/_nav/monitoring/logs/route.lazy.tsx +3 -3
  99. package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +3 -3
  100. package/src/routes/_nav/monitoring/route.lazy.tsx +2 -2
  101. package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +1 -1
  102. package/src/routes/_nav/permissions/route.lazy.tsx +4 -4
  103. package/src/routes/_nav/sso/route.lazy.tsx +2 -2
  104. package/src/routes/_nav/users/$userId/route.lazy.tsx +8 -8
  105. package/src/routes/_nav/users/-components/users-table.tsx +1 -1
  106. package/src/routes/_nav/users/route.lazy.tsx +1 -1
  107. package/src/routes/setup/route.lazy.tsx +2 -2
  108. package/src/server/dependency/helpers.ts +0 -1
  109. package/src/styles/globals.css +3 -3
@@ -23,19 +23,19 @@ import {
23
23
  DialogHeader,
24
24
  DialogTitle,
25
25
  DialogFooter,
26
- } from "@allbluecn/ui/dialog";
27
- import { Button } from "@allbluecn/ui/button";
28
- import { Input } from "@allbluecn/ui/input";
29
- import { Label } from "@allbluecn/ui/label";
26
+ } from "@/components/ui/dialog";
27
+ import { Button } from "@/components/ui/button";
28
+ import { Input } from "@/components/ui/input";
29
+ import { Label } from "@/components/ui/label";
30
30
  import {
31
31
  Select,
32
32
  SelectContent,
33
33
  SelectItem,
34
34
  SelectTrigger,
35
35
  SelectValue,
36
- } from "@allbluecn/ui/select";
36
+ } from "@/components/ui/select";
37
37
  import { createUserFn } from "@/server/serverFns/user/create";
38
- import { toast } from '@allbluecn/ui/toast';
38
+ import { toast } from '@/components/ui/toast';
39
39
 
40
40
  interface UserCreateDialogProps {
41
41
  open: boolean;
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { m } from '@/paraglide/messages.js'
4
4
  import { AlertCircle } from 'lucide-react'
5
- import { Button } from '@allbluecn/ui/button'
5
+ import { Button } from '@/components/ui/button'
6
6
 
7
7
  export function AdminUserErrorBoundary({ error, reset }: { error: unknown; reset: () => void }) {
8
8
  return <AdminUserErrorBoundaryInner error={error instanceof Error ? error : new Error(String(error))} reset={reset} />
@@ -7,7 +7,7 @@ import {
7
7
  DialogFooter,
8
8
  DialogHeader,
9
9
  DialogTitle,
10
- } from '@allbluecn/ui/dialog'
10
+ } from '@/components/ui/dialog'
11
11
  import {
12
12
  AlertDialog,
13
13
  AlertDialogAction,
@@ -17,17 +17,17 @@ import {
17
17
  AlertDialogFooter,
18
18
  AlertDialogHeader,
19
19
  AlertDialogTitle,
20
- } from '@allbluecn/ui/alert-dialog'
20
+ } from '@/components/ui/alert-dialog'
21
21
  import {
22
22
  Select,
23
23
  SelectContent,
24
24
  SelectItem,
25
25
  SelectTrigger,
26
26
  SelectValue,
27
- } from '@allbluecn/ui/select'
28
- import { Input } from '@allbluecn/ui/input'
29
- import { Label } from '@allbluecn/ui/label'
30
- import { Button } from '@allbluecn/ui/button'
27
+ } from '@/components/ui/select'
28
+ import { Input } from '@/components/ui/input'
29
+ import { Label } from '@/components/ui/label'
30
+ import { Button } from '@/components/ui/button'
31
31
 
32
32
  export type UserDialogState = { open: boolean; value: string }
33
33
  export type UserDeleteDialogState = { open: boolean; confirmName: string }
@@ -18,15 +18,15 @@
18
18
  import { useNavigate } from "@tanstack/react-router";
19
19
  import { m } from '@/paraglide/messages.js'
20
20
  import { useCallback } from "react";
21
- import { Input } from "@allbluecn/ui/input";
22
- import { Button } from "@allbluecn/ui/button";
21
+ import { Input } from "@/components/ui/input";
22
+ import { Button } from "@/components/ui/button";
23
23
  import {
24
24
  Select,
25
25
  SelectContent,
26
26
  SelectItem,
27
27
  SelectTrigger,
28
28
  SelectValue,
29
- } from "@allbluecn/ui/select";
29
+ } from "@/components/ui/select";
30
30
  import { SearchIcon, XIcon } from "lucide-react";
31
31
 
32
32
  interface UserFiltersProps {
@@ -0,0 +1,82 @@
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
+
19
+ import { NotionAvatar } from "./notion-avatar"
20
+ import type { AvatarConfig, ShapeType } from "@allbluecn/kernel"
21
+
22
+ type UserAvatarProps = {
23
+ avatarConfig?: AvatarConfig | null
24
+ bgShape?: ShapeType | null
25
+ bgColor?: string | null
26
+ name?: string | null
27
+ imageUrl?: string | null
28
+ className?: string
29
+ size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl"
30
+ }
31
+
32
+ const sizeClasses: Record<string, string> = {
33
+ xs: "w-5 h-5",
34
+ sm: "w-6 h-6",
35
+ md: "w-8 h-8",
36
+ lg: "w-10 h-10",
37
+ xl: "w-12 h-12",
38
+ "2xl": "w-16 h-16",
39
+ "3xl": "w-20 h-20",
40
+ "4xl": "w-24 h-24",
41
+ }
42
+
43
+ export function UserAvatar({
44
+ avatarConfig,
45
+ bgShape,
46
+ bgColor,
47
+ name,
48
+ imageUrl,
49
+ className,
50
+ size = "md",
51
+ }: UserAvatarProps) {
52
+ const initial = (name || "U").charAt(0).toUpperCase()
53
+ const shapeClass =
54
+ bgShape === "circle" ? "rounded-full" : bgShape === "square" ? "rounded-none" : "rounded-lg"
55
+
56
+ if (avatarConfig) {
57
+ return (
58
+ <NotionAvatar
59
+ config={avatarConfig}
60
+ shape={bgShape ?? "square"}
61
+ bgColor={bgColor ?? "#fff"}
62
+ className={`${sizeClasses[size]} shrink-0 ${className ?? ""}`}
63
+ />
64
+ )
65
+ }
66
+
67
+ if (imageUrl) {
68
+ return (
69
+ <div className={`relative shrink-0 overflow-hidden ${shapeClass} ${sizeClasses[size]} ${className ?? ""}`}>
70
+ <img src={imageUrl} alt={name || ""} className="h-full w-full object-cover" />
71
+ </div>
72
+ )
73
+ }
74
+
75
+ return (
76
+ <div
77
+ className={`flex shrink-0 items-center justify-center ${shapeClass} bg-muted text-xs font-medium ${sizeClasses[size]} ${className ?? ""}`}
78
+ >
79
+ {initial}
80
+ </div>
81
+ )
82
+ }
@@ -0,0 +1,19 @@
1
+ import * as React from "react"
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12
+ }
13
+ mql.addEventListener("change", onChange)
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ return () => mql.removeEventListener("change", onChange)
16
+ }, [])
17
+
18
+ return !!isMobile
19
+ }
@@ -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
+ }
@@ -18,7 +18,7 @@
18
18
 
19
19
  import { ThemeProvider } from "next-themes";
20
20
  import { type ReactNode } from "react";
21
- import { TooltipProvider } from "@allbluecn/ui/tooltip";
21
+ import { TooltipProvider } from "@/components/ui/tooltip";
22
22
 
23
23
  export function Providers({
24
24
  children,
@@ -21,7 +21,7 @@ import globalsCss from '../styles/globals.css?url'
21
21
  import type { RouterContext } from '@/lib/router-context'
22
22
  import type { AppLocale } from '@/lib/i18n/config'
23
23
  import { Providers } from '@/providers'
24
- import { Toaster } from '@allbluecn/ui/toast'
24
+ import { Toaster } from '@/components/ui/toast'
25
25
  import { getSessionFn } from '@/server/serverFns/auth/session'
26
26
  import { getLocaleFn } from '@/server/serverFns/locale'
27
27
  import { reportClientError } from '@/server/serverFns/monitoring/report-error'
@@ -18,7 +18,7 @@
18
18
  import { createLazyFileRoute } from '@tanstack/react-router'
19
19
  import { m } from '@/paraglide/messages.js'
20
20
  import type React from 'react'
21
- import { Badge } from '@allbluecn/ui/badge'
21
+ import { Badge } from '@/components/ui/badge'
22
22
 
23
23
  export const Route = createLazyFileRoute('/_nav/audit')({
24
24
  pendingComponent: () => (
@@ -19,11 +19,11 @@ import { createLazyFileRoute } from '@tanstack/react-router'
19
19
  import { m } from '@/paraglide/messages.js'
20
20
  import { useQuery, useQueryClient } from '@tanstack/react-query'
21
21
  import { useServerFn } from '@tanstack/react-start'
22
- import { toast } from '@allbluecn/ui/toast'
22
+ import { toast } from '@/components/ui/toast'
23
23
  import type React from 'react'
24
24
  import { listComplianceExportsFn, executeComplianceJobFn } from '@/server/serverFns/compliance'
25
- import { Badge } from '@allbluecn/ui/badge'
26
- import { Button } from '@allbluecn/ui/button'
25
+ import { Badge } from '@/components/ui/badge'
26
+ import { Button } from '@/components/ui/button'
27
27
  import { Loader2 } from 'lucide-react'
28
28
 
29
29
  export const Route = createLazyFileRoute('/_nav/compliance')({
@@ -18,15 +18,15 @@
18
18
  import { useState } from 'react'
19
19
  import { createLazyFileRoute, useNavigate, useRouter } from '@tanstack/react-router'
20
20
  import { Plus, Search, Download } from 'lucide-react'
21
- import { Button } from '@allbluecn/ui/button'
22
- import { Input } from '@allbluecn/ui/input'
23
- import { ConfirmDialog } from '@allbluecn/ui/components/confirm-dialog'
21
+ import { Button } from '@/components/ui/button'
22
+ import { Input } from '@/components/ui/input'
23
+ import { ConfirmDialog } from '@/components/confirm-dialog'
24
24
  import { cn } from '@/lib/utils'
25
25
  import { ComponentTable, type ComponentItem } from '@/components/component/component-table'
26
26
  import { ComponentFormDialog } from '@/components/component/component-form-dialog'
27
27
  import { ComponentImportDialog } from '@/components/component/component-import-dialog'
28
28
  import { deleteComponentFn } from '@/server/serverFns/components/mutations'
29
- import { toast } from '@allbluecn/ui/toast'
29
+ import { toast } from '@/components/ui/toast'
30
30
 
31
31
  const CATEGORY_TABS = [
32
32
  { key: 'all', label: '全部' },
@@ -37,12 +37,12 @@ import {
37
37
  const categoryIconMap: Record<string, LucideIcon> = {
38
38
  Folder, Globe, Code, Book, BookOpen,
39
39
  }
40
- import { DataTable, type AdminColumnDef } from '@allbluecn/ui/data-table'
41
- import { Button } from '@allbluecn/ui/button'
42
- import { Input } from '@allbluecn/ui/input'
43
- import { Badge } from '@allbluecn/ui/badge'
44
- import { ConfirmDialog } from '@allbluecn/ui/components/confirm-dialog'
45
- import { toast } from '@allbluecn/ui/toast'
40
+ import { DataTable, type AdminColumnDef } from '@/components/data-table/data-table'
41
+ import { Button } from '@/components/ui/button'
42
+ import { Input } from '@/components/ui/input'
43
+ import { Badge } from '@/components/ui/badge'
44
+ import { ConfirmDialog } from '@/components/confirm-dialog'
45
+ import { toast } from '@/components/ui/toast'
46
46
  import {
47
47
  BookmarkFormDialog,
48
48
  type BookmarkRow,
@@ -19,12 +19,12 @@ import { createLazyFileRoute, useNavigate, useRouter } from '@tanstack/react-rou
19
19
  import { m } from '@/paraglide/messages.js'
20
20
  import { useState, useTransition } from 'react'
21
21
  import { PlusIcon, PencilIcon, Trash2Icon, SearchIcon } from 'lucide-react'
22
- import { DataTable, type AdminColumnDef } from '@allbluecn/ui/data-table'
23
- import { Button } from '@allbluecn/ui/button'
24
- import { Input } from '@allbluecn/ui/input'
25
- import { Badge } from '@allbluecn/ui/badge'
26
- import { ConfirmDialog } from '@allbluecn/ui/components/confirm-dialog'
27
- import { toast } from '@allbluecn/ui/toast'
22
+ import { DataTable, type AdminColumnDef } from '@/components/data-table/data-table'
23
+ import { Button } from '@/components/ui/button'
24
+ import { Input } from '@/components/ui/input'
25
+ import { Badge } from '@/components/ui/badge'
26
+ import { ConfirmDialog } from '@/components/confirm-dialog'
27
+ import { toast } from '@/components/ui/toast'
28
28
  import {
29
29
  ComplianceFormDialog,
30
30
  type ComplianceRow,
@@ -19,10 +19,10 @@ import { createLazyFileRoute, useNavigate, useRouter } from '@tanstack/react-rou
19
19
  import { m } from '@/paraglide/messages.js'
20
20
  import { useState, useTransition } from 'react'
21
21
  import { PlusIcon, PencilIcon, Trash2Icon, SearchIcon } from 'lucide-react'
22
- import { DataTable, type AdminColumnDef } from '@allbluecn/ui/data-table'
23
- import { Button } from '@allbluecn/ui/button'
24
- import { Input } from '@allbluecn/ui/input'
25
- import { Badge } from '@allbluecn/ui/badge'
22
+ import { DataTable, type AdminColumnDef } from '@/components/data-table/data-table'
23
+ import { Button } from '@/components/ui/button'
24
+ import { Input } from '@/components/ui/input'
25
+ import { Badge } from '@/components/ui/badge'
26
26
  import {
27
27
  AlertDialog,
28
28
  AlertDialogAction,
@@ -32,8 +32,8 @@ import {
32
32
  AlertDialogFooter,
33
33
  AlertDialogHeader,
34
34
  AlertDialogTitle,
35
- } from '@allbluecn/ui/alert-dialog'
36
- import { toast } from '@allbluecn/ui/toast'
35
+ } from '@/components/ui/alert-dialog'
36
+ import { toast } from '@/components/ui/toast'
37
37
  import { LegalFormDialog, type LegalRow } from '@/components/knowledge-public/legal-form-dialog'
38
38
  import { adminDeleteLegalFn } from '@/server/serverFns/knowledge-public/legal/delete'
39
39
 
@@ -18,10 +18,10 @@
18
18
  import { createLazyFileRoute, Link, useNavigate } from '@tanstack/react-router'
19
19
  import { m } from '@/paraglide/messages.js'
20
20
  import { useServerFn } from '@tanstack/react-start'
21
- import { toast } from '@allbluecn/ui/toast'
21
+ import { toast } from '@/components/ui/toast'
22
22
  import type React from 'react'
23
- import { Button } from '@allbluecn/ui/button'
24
- import { Card, CardContent, CardHeader, CardTitle } from '@allbluecn/ui/card'
23
+ import { Button } from '@/components/ui/button'
24
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
25
25
  import { revokeLicenseFn } from '@/server/serverFns/licenses'
26
26
 
27
27
  export const Route = createLazyFileRoute('/_nav/licenses/$licenseId')({
@@ -22,11 +22,11 @@ import { useState } from 'react'
22
22
  import { useForm } from '@tanstack/react-form'
23
23
  import { useServerFn } from '@tanstack/react-start'
24
24
  import { Check, Copy } from 'lucide-react'
25
- import { Button } from '@allbluecn/ui/button'
26
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@allbluecn/ui/card'
27
- import { Input } from '@allbluecn/ui/input'
28
- import { Label } from '@allbluecn/ui/label'
29
- import { Alert, AlertDescription, AlertTitle } from '@allbluecn/ui/alert'
25
+ import { Button } from '@/components/ui/button'
26
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
27
+ import { Input } from '@/components/ui/input'
28
+ import { Label } from '@/components/ui/label'
29
+ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
30
30
  import { issueLicenseFn } from '@/server/serverFns/licenses'
31
31
  import { FeatureRegistry } from '@allbluecn/kernel'
32
32
 
@@ -18,7 +18,7 @@
18
18
  import { createLazyFileRoute, Link } from '@tanstack/react-router'
19
19
  import { m } from '@/paraglide/messages.js'
20
20
  import type React from 'react'
21
- import { Button } from '@allbluecn/ui/button'
21
+ import { Button } from '@/components/ui/button'
22
22
 
23
23
  export const Route = createLazyFileRoute('/_nav/licenses')({
24
24
  pendingComponent: () => (
@@ -8,8 +8,8 @@ import { tooltip } from '@tanstack/charts/tooltip'
8
8
  import { Chart } from '@tanstack/charts/react'
9
9
  import { BarChart3, AlertCircle, AlertTriangle, ChevronLeft, ChevronRight, RefreshCw } from 'lucide-react'
10
10
  import { PageHeader } from '@/components/common/page-header'
11
- import { Card, CardContent, CardHeader, CardTitle } from '@allbluecn/ui/card'
12
- import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@allbluecn/ui/table'
11
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
12
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
13
13
  import { getAiUsageOverviewFn } from '@/server/serverFns/monitoring/get-ai-usage-overview'
14
14
  import { checkAiAlertsFn } from '@/server/serverFns/monitoring/check-ai-alerts'
15
15
  import type { AiUsageOverview } from '@/server/admin-ai-usage'
@@ -25,7 +25,7 @@ import { ErrorItem } from '@/components/monitoring/error-list'
25
25
  import { getErrors } from '@/server/serverFns/monitoring/get-errors'
26
26
  import { resetMonitoring } from '@/server/serverFns/monitoring/reset'
27
27
  import { jsonify } from '@/lib/loader-serial'
28
- import { toast } from '@allbluecn/ui/toast'
28
+ import { toast } from '@/components/ui/toast'
29
29
 
30
30
  export const Route = createLazyFileRoute('/_nav/monitoring/errors')({
31
31
  component: MonitoringErrors,