@flow97/react-toolkit 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,383 @@
1
+ # @flow97/react-toolkit
2
+
3
+ 一套面向企业级应用的前端工具集,基于 React、Ant Design 与 TanStack Query。开箱即用的应用壳、权限/菜单、通用列表(分页/无限滚动)、抽屉/弹框表单与国际化支持,助你快速搭建一致、可维护的中后台应用。
4
+
5
+ ## 亮点特性
6
+
7
+ - 应用壳与导航:`Layout`、侧边菜单、面包屑、登录页、404
8
+ - 列表范式:`QueryList`(分页)、`InfiniteList`(无限滚动)
9
+ - 表单弹层:`useFormModal`、`useFormDrawer`
10
+ - 权限体系:`RequireAuth`、`AuthButton`,以及权限/菜单路由片段
11
+ - 数据获取:服务层 hooks(如 `useAuth`、`useMenuList`、`useGames`)
12
+ - 子路径导出按域组织,Tree Shaking 友好
13
+
14
+ ## 安装与要求
15
+
16
+ ```bash
17
+ pnpm add @flow97/react-toolkit
18
+ ```
19
+
20
+ 对等依赖(需由你的应用提供):
21
+
22
+ - react、react-dom:^19
23
+ - antd:^6
24
+ - react-router:^7
25
+ - @tanstack/react-query:^5
26
+
27
+ 样式:
28
+
29
+ ```ts
30
+ import '@flow97/react-toolkit/style.css'
31
+ ```
32
+
33
+ ## 快速上手
34
+
35
+ ```tsx
36
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
37
+ import 'react-toolkits/style.css'
38
+ import { createRoot } from 'react-dom/client'
39
+ import { RouterProvider } from 'react-router'
40
+ import { ToolkitsProvider } from 'react-toolkits/components'
41
+ import { AuthMode } from 'react-toolkits/constants'
42
+
43
+ import router from './router'
44
+
45
+ // 创建 QueryClient 实例
46
+ const queryClient = new QueryClient({
47
+ defaultOptions: {
48
+ queries: {
49
+ refetchOnWindowFocus: false,
50
+ retry: false,
51
+ staleTime: 5 * 60 * 1000,
52
+ gcTime: 10 * 60 * 1000,
53
+ },
54
+ mutations: {
55
+ retry: false,
56
+ },
57
+ },
58
+ })
59
+
60
+ const root = createRoot(document.getElementById('root') as HTMLElement)
61
+
62
+ root.render(
63
+ <QueryClientProvider client={queryClient}>
64
+ <ToolkitsProvider collapsible gameApiV2 auth={{ mode: AuthMode.GROUP_BASED }} loginPath="/sign_in" mainPagePath="/">
65
+ <RouterProvider router={router} />
66
+ </ToolkitsProvider>
67
+ </QueryClientProvider>,
68
+ )
69
+ ```
70
+
71
+ **重要提示**:`QueryClientProvider` 必须包裹 `ToolkitsProvider`,因为 `react-toolkits` 内部使用了 `@tanstack/react-query` 的 hooks(如 `useQueryClient`、`useQuery`、`useMutation`)。
72
+
73
+ ### 配置项(`ToolkitsProvider`)
74
+
75
+ - **loginPath**: 登录页路径(未鉴权时会跳转)
76
+ - **mainPagePath**: 登录后主页路径(鉴权通过时进入)
77
+ - **auth.mode**: 认证模式,建议使用 `AuthMode.GROUP_BASED`
78
+ - **collapsible**: 侧边栏是否可折叠
79
+ - **gameApiV2**: 是否启用游戏相关 V2 接口适配(如不涉及可忽略)
80
+ - 其余高级配置见类型定义 `react-toolkits/components` 内导出的 Provider Props
81
+
82
+ ## 子路径导出(推荐)
83
+
84
+ - `react-toolkits/components`:组件与 Hooks(如 `Layout`、`ToolkitsProvider`、`InfiniteList`)
85
+ - `react-toolkits/hooks`:表单弹层 Hooks(如 `useFormDrawer`、`useFormModal`)
86
+ - `react-toolkits/pages`:内置页面与路由片段(如 `permissionRoutes`、`menuRoutes`、`SignIn`)
87
+ - `react-toolkits/services`:请求相关 Hooks(如 `useAuth`、`useMenuList`、`useGames`)
88
+ - `react-toolkits/constants`:常量与枚举(如 `APP_ID_HEADER`、`AuthMode`)
89
+ - `react-toolkits/types`:公共类型
90
+ - `react-toolkits/utils`:工具函数
91
+ - `react-toolkits/locale` 与 `react-toolkits/locale/*`:国际化资源与工具
92
+
93
+ 示例:
94
+
95
+ ```ts
96
+ import { Layout } from 'react-toolkits/components'
97
+ import { useFormDrawer } from 'react-toolkits/hooks'
98
+ import { permissionRoutes } from 'react-toolkits/pages'
99
+ import { useAuth } from 'react-toolkits/services'
100
+ import { APP_ID_HEADER } from 'react-toolkits/constants'
101
+ import type { NavMenuItem } from 'react-toolkits/types'
102
+ ```
103
+
104
+ ### 权限与菜单集成示例
105
+
106
+ ```tsx
107
+ import { createBrowserRouter } from 'react-router'
108
+ import { Layout } from 'react-toolkits/components'
109
+ import { permissionRoutes, menuRoutes, SignIn } from 'react-toolkits/pages'
110
+
111
+ const router = createBrowserRouter([
112
+ {
113
+ path: '/',
114
+ element: <Layout />, // 自动接入面包屑、侧边栏与鉴权
115
+ children: [
116
+ // 权限/菜单内置片段(可按需选择)
117
+ ...permissionRoutes,
118
+ ...menuRoutes,
119
+ ],
120
+ },
121
+ { path: '/sign_in', element: <SignIn /> },
122
+ ])
123
+
124
+ export default router
125
+ ```
126
+
127
+ ## 国际化(Locale)
128
+
129
+ 内置基础语言包与上下文工具,也可与第三方 i18n 方案并存:
130
+
131
+ ```ts
132
+ import { useTranslation } from 'react-toolkits/locale'
133
+ import zhCN from 'react-toolkits/locale/zh_CN'
134
+ import enGB from 'react-toolkits/locale/en_GB'
135
+
136
+ const { t } = useTranslation()
137
+ // t('FilterFormWrapper.confirmText') → "查询"
138
+ ```
139
+
140
+ 如果你已使用 `react-i18next` 等,可以仅复用本包的页面/菜单能力。
141
+
142
+ ### 自定义菜单文案
143
+
144
+ - 使用 `react-toolkits/locale` 的 `useTranslation` 配合内置 key,或在你的 i18n 方案中映射同名 key
145
+ - 可按需引入 `react-toolkits/locale/zh_CN`、`en_GB` 等作为基础词条
146
+
147
+ ## 目录结构(概览)
148
+
149
+ ```
150
+ src/
151
+ components/ // 组件与 Hooks(以 react-toolkits/components 导出)
152
+ hooks/ // 表单弹层 Hooks(以 react-toolkits/hooks 导出)
153
+ features/ // 特性模块(权限/菜单等:hooks/组件/服务聚合)
154
+ pages/ // 内置页面与路由片段
155
+ services/ // 数据服务 hooks(依赖 libs/ky 与全局上下文)
156
+ constants/ // 常量与枚举
157
+ utils/ // 工具函数
158
+ locale/ // 国际化上下文与语言包
159
+ ```
160
+
161
+ ## 常用用法示例
162
+
163
+ ### 列表(分页 `QueryList`)
164
+
165
+ ```tsx
166
+ import { QueryList } from 'react-toolkits/components'
167
+
168
+ export default function UserTable() {
169
+ return (
170
+ <QueryList
171
+ queryKey={['users']}
172
+ queryFn={async ({ page, pageSize }) => {
173
+ // 返回 { list: T[]; total: number }
174
+ const res = await fetch(`/api/users?page=${page}&pageSize=${pageSize}`)
175
+ return res.json()
176
+ }}
177
+ columns={[
178
+ { title: 'ID', dataIndex: 'id' },
179
+ { title: 'Name', dataIndex: 'name' },
180
+ ]}
181
+ />
182
+ )
183
+ }
184
+ ```
185
+
186
+ ### 列表(无限滚动 `InfiniteList`)
187
+
188
+ ```tsx
189
+ import { InfiniteList } from 'react-toolkits/components'
190
+
191
+ export default function LogList() {
192
+ return (
193
+ <InfiniteList
194
+ queryKey={['logs']}
195
+ queryFn={async ({ pageParam = 1 }) => {
196
+ // 返回 { list: T[]; nextPage?: number }
197
+ const res = await fetch(`/api/logs?page=${pageParam}`)
198
+ return res.json()
199
+ }}
200
+ itemRender={item => <div>{item.message}</div>}
201
+ />
202
+ )
203
+ }
204
+ ```
205
+
206
+ ### 表单弹层(`useFormDrawer`)
207
+
208
+ ```tsx
209
+ import { Button } from 'antd'
210
+ import { useFormDrawer } from 'react-toolkits/hooks'
211
+
212
+ export default function CreateUser() {
213
+ const { show, drawer } = useFormDrawer({
214
+ title: '新建用户',
215
+ onConfirm: async values => {
216
+ await fetch('/api/users', { method: 'POST', body: JSON.stringify(values) })
217
+ },
218
+ content: (
219
+ <>
220
+ <Form.Item name="name" label="姓名" rules={[{ required: true }]}>
221
+ <Input />
222
+ </Form.Item>
223
+ <Form.Item name="role" label="角色" rules={[{ required: true }]}>
224
+ <Select
225
+ options={[
226
+ { value: 'admin', label: '管理员' },
227
+ { value: 'user', label: '用户' },
228
+ ]}
229
+ />
230
+ </Form.Item>
231
+ </>
232
+ ),
233
+ })
234
+
235
+ return (
236
+ <>
237
+ <Button onClick={show}>新建</Button>
238
+ {drawer}
239
+ </>
240
+ )
241
+ }
242
+ ```
243
+
244
+ ### 表单弹层(`useFormModal`)
245
+
246
+ ```tsx
247
+ import { Button, Form, Input, Select } from 'antd'
248
+ import { useFormModal } from 'react-toolkits/hooks'
249
+
250
+ export default function EditUser() {
251
+ const { show, modal } = useFormModal({
252
+ title: '编辑用户',
253
+ onConfirm: async values => {
254
+ await fetch('/api/users', { method: 'PUT', body: JSON.stringify(values) })
255
+ },
256
+ content: (
257
+ <>
258
+ <Form.Item name="name" label="姓名" rules={[{ required: true }]}>
259
+ <Input />
260
+ </Form.Item>
261
+ <Form.Item name="role" label="角色" rules={[{ required: true }]}>
262
+ <Select
263
+ options={[
264
+ { value: 'admin', label: '管理员' },
265
+ { value: 'user', label: '用户' },
266
+ ]}
267
+ />
268
+ </Form.Item>
269
+ </>
270
+ ),
271
+ })
272
+
273
+ return (
274
+ <>
275
+ <Button onClick={() => show({ initialValues: { name: 'John', role: 'user' } })}>编辑</Button>
276
+ {modal}
277
+ </>
278
+ )
279
+ }
280
+ ```
281
+
282
+ ### 服务层 Hooks(`react-toolkits/services`)
283
+
284
+ ```tsx
285
+ import { useAuth, useMenuList } from 'react-toolkits/services'
286
+
287
+ export function UseDataExample() {
288
+ const { data: permissions } = useAuth()
289
+ const { data: menus } = useMenuList()
290
+ // 根据你的业务渲染
291
+ return null
292
+ }
293
+ ```
294
+
295
+ ### Ky 与请求拦截(`useKy`)
296
+
297
+ - 内置 `useKy` 封装了鉴权与 `APP_ID_HEADER` 注入逻辑,结合 Provider 的上下文使用
298
+ - 你也可以直接使用 `fetch`,但推荐统一通过 `services` 或 `useKy`
299
+
300
+ ```tsx
301
+ import { useEffect } from 'react'
302
+ import { useKy } from 'react-toolkits/components'
303
+ import { APP_ID_HEADER } from 'react-toolkits/constants'
304
+
305
+ export default function FetchWithKy() {
306
+ const ky = useKy({
307
+ headers: {
308
+ [APP_ID_HEADER]: 'my-app-id',
309
+ },
310
+ })
311
+
312
+ useEffect(() => {
313
+ ky.get('/api/ping').json()
314
+ }, [ky])
315
+
316
+ return null
317
+ }
318
+ ```
319
+
320
+ 提示:若你的应用已有全局请求层,可仅复用本包页面/组件,将头与鉴权逻辑放到你的层中保持一致性。
321
+
322
+ ## 迁移指南(从根导入 → 子路径导出)
323
+
324
+ 过去:
325
+
326
+ ```ts
327
+ import { ToolkitsProvider, Layout, useFormDrawer } from '@flow97/react-toolkit'
328
+ ```
329
+
330
+ 现在(推荐):
331
+
332
+ ```ts
333
+ import { ToolkitsProvider, Layout } from 'react-toolkits/components'
334
+ import { useFormDrawer, useFormModal } from 'react-toolkits/hooks'
335
+ import { permissionRoutes } from 'react-toolkits/pages'
336
+ import { useAuth } from 'react-toolkits/services'
337
+ ```
338
+
339
+ 收益:边界清晰、按需打包更友好(Tree Shaking)。
340
+
341
+ ### 权限版本兼容
342
+
343
+ - 历史上存在不同权限数据结构,`AuthMode` 用于适配后端变体
344
+ - 新项目建议使用 `V3`;旧项目如为 `V2`,只需在 `ToolkitsProvider` 中切换版本
345
+
346
+ ## 常见问题(FAQ)
347
+
348
+ - 如何引入样式?
349
+ - 在应用入口一次性引入:`import 'react-toolkits/style.css'`
350
+ - Provider 放哪?
351
+ - 在应用根部使用 `ToolkitsProvider`,传入登录路径、主页路径、权限版本等配置。
352
+ - 如何自定义菜单?
353
+ - 通过 `Layout` 的 `items` 或参考示例应用的 `menu-items.tsx`。
354
+ - 如何统一请求与拦截?
355
+ - 使用 `useKy` 或 `services` 中的 hooks(自动注入鉴权与 App-ID 头)。
356
+ - 若需要自定义拦截,传入 `useKy` 的配置或在应用层包裹一层。
357
+ - 列表的数据结构需要什么格式?
358
+ - 分页:`{ list: T[]; total: number }`;无限滚动:`{ list: T[]; nextPage?: number }`
359
+ - 可以只用页面和路由,不用 Provider 吗?
360
+ - 不建议。多数功能依赖上下文(鉴权、国际化、请求)。若必须,请自行在应用层提供等价上下文。
361
+
362
+ ## 本地开发与构建
363
+
364
+ ```bash
365
+ # 构建
366
+ pnpm -C packages/react-toolkits build
367
+
368
+ # 开发调试(监听)
369
+ pnpm -C packages/react-toolkits dev
370
+ ```
371
+
372
+ 构建将产出多入口:`lib/index.js`、`lib/components.js`、`lib/hooks.js`、`lib/pages.js`、`lib/services.js`、`lib/types.js`、`lib/utils.js`,以及 `locale/*`。
373
+
374
+ 提示:发布前请确保示例应用验证通过,并更新 `CHANGELOG.md` 描述变更与迁移说明。
375
+
376
+ ## 变更与发布
377
+
378
+ - 更新日志见 `CHANGELOG.md`
379
+ - 遵循 semver;涉及子路径导出/目录变更会在次版本提供迁移说明
380
+
381
+ ## 许可证
382
+
383
+ MIT
package/lib/index.css ADDED
@@ -0,0 +1 @@
1
+ @layer properties{@supports ((-webkit-hyphens:none) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,::backdrop,:after,:before{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:host,:root{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-blue-50:oklch(97% .014 254.604);--color-indigo-50:oklch(96.2% .018 272.314);--color-purple-50:oklch(97.7% .014 308.299);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-white:#fff;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:1.42857;--text-base:1rem;--text-base--line-height:1.5;--text-xl:1.25rem;--text-xl--line-height:1.4;--text-2xl:1.5rem;--text-2xl--line-height:1.33333;--text-3xl:1.875rem;--text-3xl--line-height:1.2;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--leading-tight:1.25;--radius-lg:.5rem;--radius-2xl:1rem;--ease-out:cubic-bezier(0,0,.2,1);--blur-sm:8px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,::backdrop,:after,:before{border:0 solid;box-sizing:border-box;margin:0;padding:0}::file-selector-button{border:0 solid;box-sizing:border-box;margin:0;padding:0}:host,html{-webkit-text-size-adjust:100%;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-size:1em;font-variation-settings:var(--default-mono-font-variation-settings,normal)}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}menu,ol,ul{list-style:none}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}button,input,optgroup,select,textarea{background-color:#0000;border-radius:0;color:inherit;font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;opacity:1}::file-selector-button{background-color:#0000;border-radius:0;color:inherit;font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;opacity:1}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::-moz-placeholder{opacity:1}::placeholder{opacity:1}@supports (not (-webkit-appearance:-apple-pay-button)) or (contain-intrinsic-size:1px){::-moz-placeholder{color:currentColor}::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::-moz-placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit,::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer antd,components;@layer utilities{.collapse{visibility:collapse}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.top-0{top:calc(var(--spacing)*0)}.top-1\/2{top:50%}.top-6{top:calc(var(--spacing)*6)}.right-6{right:calc(var(--spacing)*6)}.right-\[100px\]{right:100px}.right-\[550px\]{right:550px}.bottom-0{bottom:calc(var(--spacing)*0)}.left-0{left:calc(var(--spacing)*0)}.left-6{left:calc(var(--spacing)*6)}.z-10{z-index:10}.z-20{z-index:20}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-4{margin-inline:calc(var(--spacing)*4)}.my-6{margin-block:calc(var(--spacing)*6)}.my-12{margin-block:calc(var(--spacing)*12)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-4{margin-top:calc(var(--spacing)*4)}.mr-6{margin-right:calc(var(--spacing)*6)}.mb-0{margin-bottom:calc(var(--spacing)*0)}.mb-0\!{margin-bottom:calc(var(--spacing)*0)!important}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.mb-12{margin-bottom:calc(var(--spacing)*12)}.ml-4{margin-left:calc(var(--spacing)*4)}.block{display:block}.contents{display:contents}.flex{display:flex}.hidden{display:none}.inline{display:inline}.h-12{height:calc(var(--spacing)*12)}.h-\[200px\]{height:200px}.h-\[300px\]{height:300px}.h-\[400px\]{height:400px}.h-\[calc\(100vh-64px\)\]{height:calc(100vh - 64px)}.h-auto{height:auto}.h-full{height:100%}.h-screen{height:100vh}.w-5\/6{width:83.3333%}.w-8{width:calc(var(--spacing)*8)}.w-40{width:calc(var(--spacing)*40)}.w-\[78px\]{width:78px}.w-\[100px\]{width:100px}.w-\[250px\]{width:250px}.w-\[450px\]{width:450px}.w-full{width:100%}.w-screen{width:100vw}.max-w-full{max-width:100%}.max-w-none{max-width:none}.min-w-0{min-width:calc(var(--spacing)*0)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow-0{flex-grow:0}.-translate-y-3\/4{--tw-translate-y:-75%;translate:var(--tw-translate-x)var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-4{gap:calc(var(--spacing)*4)}.gap-8{gap:calc(var(--spacing)*8)}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-lg{border-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-r-0{border-right-style:var(--tw-border-style);border-right-width:0}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-gray-200{border-color:var(--color-gray-200)}.bg-linear-to-br{--tw-gradient-position:to bottom right}@supports (background-image:linear-gradient(in lab,red,red)){.bg-linear-to-br{--tw-gradient-position:to bottom right in oklab}}.bg-linear-to-br{background-image:linear-gradient(var(--tw-gradient-stops))}.bg-linear-to-r{--tw-gradient-position:to right}@supports (background-image:linear-gradient(in lab,red,red)){.bg-linear-to-r{--tw-gradient-position:to right in oklab}}.bg-linear-to-r{background-image:linear-gradient(var(--tw-gradient-stops))}.from-blue-50{--tw-gradient-from:var(--color-blue-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-800{--tw-gradient-from:var(--color-gray-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-white{--tw-gradient-from:var(--color-white);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.via-gray-700{--tw-gradient-via:var(--color-gray-700);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-indigo-50\/50{--tw-gradient-via:#eef2ff80}@supports (color:color-mix(in lab,red,red)){.via-indigo-50\/50{--tw-gradient-via:color-mix(in oklab,var(--color-indigo-50)50%,transparent)}}.via-indigo-50\/50{--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-gray-50\/30{--tw-gradient-to:#f9fafb4d}@supports (color:color-mix(in lab,red,red)){.to-gray-50\/30{--tw-gradient-to:color-mix(in oklab,var(--color-gray-50)30%,transparent)}}.to-gray-50\/30{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-600{--tw-gradient-to:var(--color-gray-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-50{--tw-gradient-to:var(--color-purple-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.object-contain{-o-object-fit:contain;object-fit:contain}.p-1{padding:calc(var(--spacing)*1)}.p-6{padding:calc(var(--spacing)*6)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.py-1{padding-block:calc(var(--spacing)*1)}.py-2{padding-block:calc(var(--spacing)*2)}.py-6{padding-block:calc(var(--spacing)*6)}.text-center{text-align:center}.text-end{text-align:end}.text-start{text-align:start}.align-top{vertical-align:top}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl\!{font-size:var(--text-3xl)!important;line-height:var(--tw-leading,var(--text-3xl--line-height))!important}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.text-gray-500{color:var(--color-gray-500)}.text-transparent{color:#0000}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040)}.shadow-2xl,.shadow-md{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.transition-all{transition-duration:var(--tw-duration,var(--default-transition-duration));transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}@media (hover:hover){.hover\:scale-\[1\.02\]:hover{scale:1.02}.hover\:shadow-\[0_20px_60px_-15px_rgba\(0\,0\,0\,0\.3\)\]:hover{--tw-shadow:0 20px 60px -15px var(--tw-shadow-color,#0000004d)}.hover\:shadow-\[0_20px_60px_-15px_rgba\(0\,0\,0\,0\.3\)\]:hover,.hover\:shadow-lg:hover{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a)}}@media (min-width:48rem){.md\:top-10{top:calc(var(--spacing)*10)}.md\:top-16{top:calc(var(--spacing)*16)}.md\:right-16{right:calc(var(--spacing)*16)}.md\:left-14{left:calc(var(--spacing)*14)}}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}