@nocobase/app-template-default 0.0.0-stage → 1.0.0-beta.58

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 (249) hide show
  1. package/.cursor/mcp.json +8 -0
  2. package/.mcp.json +8 -0
  3. package/.prettierignore +20 -0
  4. package/.vscode/mcp.json +8 -0
  5. package/AGENTS.md +434 -0
  6. package/CLAUDE.md +9 -0
  7. package/Dockerfile +132 -0
  8. package/Dockerfile.dockerignore +45 -0
  9. package/README.MD +485 -0
  10. package/cli/plugins.ts +8 -0
  11. package/client/agent-annotations-host.ts +19 -0
  12. package/client/app.ts +38 -0
  13. package/client/components/back-button.tsx +49 -0
  14. package/client/components/breadcrumbs.tsx +167 -0
  15. package/client/components/inbox-badge.ts +49 -0
  16. package/client/components/inbox-button.tsx +132 -0
  17. package/client/components/inbox-header-button.tsx +25 -0
  18. package/client/components/loading.tsx +35 -0
  19. package/client/components/page-container.tsx +24 -0
  20. package/client/components/page-header.tsx +33 -0
  21. package/client/components/route-child-page.tsx +121 -0
  22. package/client/components/route-dialog.tsx +10 -0
  23. package/client/components/route-drawer.tsx +10 -0
  24. package/client/components/route-overlay.tsx +190 -0
  25. package/client/components/ui/alert.tsx +75 -0
  26. package/client/components/ui/badge.tsx +51 -0
  27. package/client/components/ui/breadcrumb.tsx +121 -0
  28. package/client/components/ui/button.tsx +58 -0
  29. package/client/components/ui/collapsible.tsx +19 -0
  30. package/client/components/ui/context-menu.tsx +271 -0
  31. package/client/components/ui/dialog.tsx +160 -0
  32. package/client/components/ui/dropdown-menu.tsx +270 -0
  33. package/client/components/ui/empty.tsx +103 -0
  34. package/client/components/ui/field.tsx +236 -0
  35. package/client/components/ui/input-group.tsx +158 -0
  36. package/client/components/ui/input.tsx +20 -0
  37. package/client/components/ui/kbd.tsx +26 -0
  38. package/client/components/ui/label.tsx +18 -0
  39. package/client/components/ui/popover.tsx +89 -0
  40. package/client/components/ui/select.tsx +200 -0
  41. package/client/components/ui/separator.tsx +22 -0
  42. package/client/components/ui/sheet.tsx +137 -0
  43. package/client/components/ui/sidebar.tsx +723 -0
  44. package/client/components/ui/skeleton.tsx +13 -0
  45. package/client/components/ui/spinner.tsx +20 -0
  46. package/client/components/ui/tabs.tsx +79 -0
  47. package/client/components/ui/textarea.tsx +17 -0
  48. package/client/components/ui/toast.tsx +231 -0
  49. package/client/components/ui/tooltip.tsx +63 -0
  50. package/client/components/use-route-overlay.ts +26 -0
  51. package/client/config/api.ts +13 -0
  52. package/client/config/app.ts +8 -0
  53. package/client/config/auth.ts +18 -0
  54. package/client/config/index.ts +17 -0
  55. package/client/extensions/nocobase-auth-forms/field-props.ts +13 -0
  56. package/client/extensions/nocobase-auth-forms/form-parts.tsx +101 -0
  57. package/client/extensions/nocobase-auth-forms/password-input.tsx +41 -0
  58. package/client/extensions/nocobase-auth-forms/password-login-form.tsx +123 -0
  59. package/client/extensions/nocobase-auth-forms/password-registration-form.tsx +193 -0
  60. package/client/extensions/nocobase-auth-forms/password-reset-form.tsx +132 -0
  61. package/client/extensions/nocobase-auth-forms/password-reset-request-form.tsx +99 -0
  62. package/client/extensions/nocobase-auth-methods/auth-methods.tsx +170 -0
  63. package/client/extensions/nocobase-auth-split-layout/auth-split-layout.tsx +126 -0
  64. package/client/extensions/nocobase-device-approval/device-approval.tsx +277 -0
  65. package/client/extensions/nocobase-device-approval/locales/en-US.ts +55 -0
  66. package/client/extensions/nocobase-device-approval/locales/zh-CN.ts +27 -0
  67. package/client/extensions/nocobase-device-approval/use-device-approval.ts +176 -0
  68. package/client/extensions/nocobase-file-component-ui/README.md +13 -0
  69. package/client/extensions/nocobase-file-component-ui/components/file-list.tsx +147 -0
  70. package/client/extensions/nocobase-file-component-ui/components/file-preview-dialog.tsx +263 -0
  71. package/client/extensions/nocobase-file-component-ui/components/file-preview-field.tsx +68 -0
  72. package/client/extensions/nocobase-file-component-ui/components/file-thumbnail.tsx +73 -0
  73. package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +386 -0
  74. package/client/extensions/nocobase-file-component-ui/components/previewers/file-preview-content.tsx +255 -0
  75. package/client/extensions/nocobase-file-component-ui/components/previewers/office-open-xml-preview.tsx +226 -0
  76. package/client/extensions/nocobase-file-component-ui/index.ts +8 -0
  77. package/client/extensions/nocobase-file-component-ui/lib/file-preview.ts +194 -0
  78. package/client/extensions/nocobase-file-component-ui/lib/file-url.ts +22 -0
  79. package/client/extensions/nocobase-file-component-ui/lib/message-key.ts +18 -0
  80. package/client/extensions/nocobase-file-component-ui/types.ts +69 -0
  81. package/client/extensions/nocobase-inbox/decision-actions-bar.tsx +140 -0
  82. package/client/extensions/nocobase-inbox/frame.ts +31 -0
  83. package/client/extensions/nocobase-inbox/inbox-actions.ts +21 -0
  84. package/client/extensions/nocobase-inbox/inbox-detail.tsx +386 -0
  85. package/client/extensions/nocobase-inbox/inbox-item.tsx +274 -0
  86. package/client/extensions/nocobase-inbox/inbox-list.tsx +433 -0
  87. package/client/extensions/nocobase-inbox/inbox-page.tsx +545 -0
  88. package/client/extensions/nocobase-inbox/locales/en-US.ts +111 -0
  89. package/client/extensions/nocobase-inbox/locales/zh-CN.ts +57 -0
  90. package/client/extensions/nocobase-inbox/model.ts +428 -0
  91. package/client/extensions/nocobase-inbox/registry-scope.tsx +49 -0
  92. package/client/extensions/nocobase-inbox/registry.ts +303 -0
  93. package/client/extensions/nocobase-inbox/source.ts +117 -0
  94. package/client/hooks/use-mobile.ts +21 -0
  95. package/client/index.tsx +60 -0
  96. package/client/layouts/app-layout.tsx +105 -0
  97. package/client/layouts/components/app-brand.tsx +49 -0
  98. package/client/layouts/components/app-sidebar.tsx +196 -0
  99. package/client/layouts/components/header-actions.tsx +50 -0
  100. package/client/layouts/components/language-switcher.tsx +95 -0
  101. package/client/layouts/components/layout-header.tsx +17 -0
  102. package/client/layouts/components/navigation-menu.tsx +411 -0
  103. package/client/layouts/components/sidebar-footer.tsx +107 -0
  104. package/client/layouts/components/surface-empty.tsx +35 -0
  105. package/client/layouts/components/user-menu.tsx +127 -0
  106. package/client/layouts/index.ts +2 -0
  107. package/client/layouts/settings-layout.tsx +172 -0
  108. package/client/layouts/use-sidebar-preference.ts +57 -0
  109. package/client/lib/constants.ts +5 -0
  110. package/client/lib/toaster.ts +56 -0
  111. package/client/lib/utils.ts +1 -0
  112. package/client/locales/en-US.ts +178 -0
  113. package/client/locales/index.ts +9 -0
  114. package/client/locales/zh-CN.ts +161 -0
  115. package/client/pages/auth/device.tsx +41 -0
  116. package/client/pages/auth/forgot-password.tsx +39 -0
  117. package/client/pages/auth/login.tsx +66 -0
  118. package/client/pages/auth/register.tsx +46 -0
  119. package/client/pages/auth/reset-password.tsx +38 -0
  120. package/client/pages/auth/shared.tsx +111 -0
  121. package/client/pages/home.tsx +16 -0
  122. package/client/pages/inbox.tsx +12 -0
  123. package/client/plugins.ts +37 -0
  124. package/client/react-providers.ts +27 -0
  125. package/client/route-overrides.ts +9 -0
  126. package/client/routes.ts +75 -0
  127. package/client/routing/app-router.tsx +127 -0
  128. package/client/routing/client-route.tsx +129 -0
  129. package/client/routing/route-context.tsx +85 -0
  130. package/client/routing/route-navigation.ts +153 -0
  131. package/client/routing/route-tree.tsx +42 -0
  132. package/client/routing/standalone-page-layout.tsx +15 -0
  133. package/client/runtime.ts +26 -0
  134. package/client/service-provider.ts +40 -0
  135. package/client/source-extensions.ts +19 -0
  136. package/client/startup.tsx +22 -0
  137. package/client/styles.css +122 -0
  138. package/client/theme/index.ts +4 -0
  139. package/client/theme/theme-context.ts +12 -0
  140. package/client/theme/theme-preferences.ts +84 -0
  141. package/client/theme/theme-presets.ts +7 -0
  142. package/client/theme/theme-provider.tsx +106 -0
  143. package/client/theme/theme-settings.tsx +176 -0
  144. package/client/theme/themes/compact.css +140 -0
  145. package/client/theme/themes/default.css +117 -0
  146. package/client/vite-env.d.ts +5 -0
  147. package/components.json +27 -0
  148. package/config.example.yml +156 -0
  149. package/database/main/migrations/.gitkeep +0 -0
  150. package/database/main/migrations/202610060001_create_device_code.ts +37 -0
  151. package/database/main/seeds/.gitkeep +0 -0
  152. package/database/tsconfig.json +4 -0
  153. package/ecosystem.config.js +35 -0
  154. package/eslint.config.js +24 -0
  155. package/index.html +100 -0
  156. package/package.json +198 -3
  157. package/playwright.config.ts +10 -0
  158. package/public/assets/favicon.ico +0 -0
  159. package/public/assets/logo-dark.png +0 -0
  160. package/public/assets/logo-mark-dark.png +0 -0
  161. package/public/assets/logo-mark.png +0 -0
  162. package/public/assets/logo.png +0 -0
  163. package/server/app.ts +50 -0
  164. package/server/config/api.ts +8 -0
  165. package/server/config/app.ts +40 -0
  166. package/server/config/auth.ts +36 -0
  167. package/server/config/authorization.ts +18 -0
  168. package/server/config/caching.ts +21 -0
  169. package/server/config/database.ts +22 -0
  170. package/server/config/drive.ts +30 -0
  171. package/server/config/i18n.ts +17 -0
  172. package/server/config/index.ts +67 -0
  173. package/server/config/jobs.ts +27 -0
  174. package/server/config/logging.ts +23 -0
  175. package/server/config/notification.ts +11 -0
  176. package/server/config/queue.ts +28 -0
  177. package/server/config/scheduler.ts +23 -0
  178. package/server/config/secrets.ts +10 -0
  179. package/server/config/server.ts +26 -0
  180. package/server/config/session.ts +60 -0
  181. package/server/config/snowflake.ts +21 -0
  182. package/server/config/spa.ts +24 -0
  183. package/server/config/users.ts +11 -0
  184. package/server/config/workflow.ts +24 -0
  185. package/server/config.ts +24 -0
  186. package/server/embedded.ts +23 -0
  187. package/server/locales/en-US.ts +10 -0
  188. package/server/locales/index.ts +10 -0
  189. package/server/locales/zh-CN.ts +5 -0
  190. package/server/plugins.ts +37 -0
  191. package/server/providers/index.ts +5 -0
  192. package/server/routes/index.ts +6 -0
  193. package/server/runtime.ts +24 -0
  194. package/server/standalone.ts +29 -0
  195. package/server/tsconfig.json +4 -0
  196. package/tailwind.config.mjs +59 -0
  197. package/tests/components/app-layout-routes.test.tsx +132 -0
  198. package/tests/components/app-title.test.tsx +57 -0
  199. package/tests/components/auth-i18n.test.tsx +87 -0
  200. package/tests/components/back-button.test.tsx +80 -0
  201. package/tests/components/file-preview.test.tsx +101 -0
  202. package/tests/components/header-hover.test.tsx +87 -0
  203. package/tests/components/inbox.test.tsx +139 -0
  204. package/tests/components/language-switcher.test.tsx +298 -0
  205. package/tests/components/layout-containers.test.tsx +15 -0
  206. package/tests/components/layout-i18n.test.tsx +187 -0
  207. package/tests/components/loading.test.tsx +25 -0
  208. package/tests/components/navigation-menu.test.tsx +355 -0
  209. package/tests/components/page-harness.test.tsx +318 -0
  210. package/tests/components/primitive-labels.test.tsx +80 -0
  211. package/tests/components/sidebar-permissions.test.tsx +110 -0
  212. package/tests/components/sidebar-preference.test.tsx +42 -0
  213. package/tests/fixtures/theme-tokens.html +12 -0
  214. package/tests/fixtures/theme-tokens.tsx +111 -0
  215. package/tests/logic/account-permissions.test.tsx +204 -0
  216. package/tests/logic/app-locale-coverage.test.ts +136 -0
  217. package/tests/logic/app-server.test.ts +1589 -0
  218. package/tests/logic/asset-url-build.test.ts +106 -0
  219. package/tests/logic/client-auth.test.tsx +122 -0
  220. package/tests/logic/client-plugin-registry.test.ts +63 -0
  221. package/tests/logic/client-route-recovery.test.tsx +80 -0
  222. package/tests/logic/client-routes.test.ts +126 -0
  223. package/tests/logic/client-runtime.test.ts +112 -0
  224. package/tests/logic/client-settings.test.tsx +841 -0
  225. package/tests/logic/client-shell.test.tsx +466 -0
  226. package/tests/logic/client-theme.test.tsx +441 -0
  227. package/tests/logic/config.test.ts +197 -0
  228. package/tests/logic/dev-client-watch.test.ts +108 -0
  229. package/tests/logic/dev-routes-production-build.test.ts +168 -0
  230. package/tests/logic/device-code-migration.test.ts +70 -0
  231. package/tests/logic/lifecycle.test.ts +221 -0
  232. package/tests/logic/plugin-commands.test.ts +69 -0
  233. package/tests/logic/plugin-page-authz.test.ts +53 -0
  234. package/tests/logic/route-navigation.test.ts +66 -0
  235. package/tests/logic/route-overlay.test.tsx +353 -0
  236. package/tests/logic/server-imports.test.ts +88 -0
  237. package/tests/logic/tailwind-sources.test.ts +151 -0
  238. package/tests/logic/template-package-name.test.ts +88 -0
  239. package/tests/logic/theme-preferences.test.ts +86 -0
  240. package/tests/logic/theme-tokens.test.ts +248 -0
  241. package/tests/logic/toaster.test.tsx +108 -0
  242. package/tests/logic/user-menu-sign-out.test.tsx +98 -0
  243. package/tests/setup/client-config.ts +27 -0
  244. package/tsconfig.json +11 -0
  245. package/tsconfig.node.json +12 -0
  246. package/tsconfig.server.json +19 -0
  247. package/vite.config.ts +92 -0
  248. package/vitest.config.ts +44 -0
  249. package/README.md +0 -3
@@ -0,0 +1,84 @@
1
+ import { readAppClientRuntimeConfig } from '@nocobase/app-client/runtime';
2
+
3
+ // Shared preference helpers for normal client startup and the React provider.
4
+ export function themeStorageKeys(base: string): {
5
+ mode: string;
6
+ preset: string;
7
+ } {
8
+ const path = base.replace(/^\/+|\/+$/g, '');
9
+ const scope = path ? encodeURIComponent(path) : '%2F';
10
+ return {
11
+ mode: `nocobase:${scope}:theme:color-scheme`,
12
+ preset: `nocobase:${scope}:theme:preset`,
13
+ };
14
+ }
15
+
16
+ export function readThemePreference(key: string): string | null {
17
+ try {
18
+ return localStorage.getItem(key);
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ export function initializeTheme(
25
+ base: string,
26
+ presets: readonly string[],
27
+ ): void {
28
+ const defaults = readThemeDefaults(presets);
29
+ const keys = themeStorageKeys(base);
30
+ const savedMode = readThemePreference(keys.mode);
31
+ const mode =
32
+ savedMode && ['light', 'dark', 'system'].includes(savedMode)
33
+ ? savedMode
34
+ : defaults.mode;
35
+ const savedPreset = readThemePreference(keys.preset);
36
+ const preset =
37
+ savedPreset && presets.includes(savedPreset)
38
+ ? savedPreset
39
+ : defaults.preset;
40
+ const resolved =
41
+ mode === 'system'
42
+ ? window.matchMedia('(prefers-color-scheme: dark)').matches
43
+ ? 'dark'
44
+ : 'light'
45
+ : mode;
46
+ document.documentElement.classList.remove('light', 'dark');
47
+ document.documentElement.classList.add(resolved);
48
+ document.documentElement.dataset.theme = preset;
49
+ document.documentElement.style.colorScheme = resolved;
50
+ // next-themes reads storage itself; normalize invalid modes before it mounts.
51
+ if (savedMode && !['light', 'dark', 'system'].includes(savedMode)) {
52
+ try {
53
+ localStorage.removeItem(keys.mode);
54
+ } catch {
55
+ /* Storage can be unavailable. */
56
+ }
57
+ }
58
+ }
59
+
60
+ // Read the same server-injected config at startup and Provider mount without
61
+ // coupling the shared client runtime to application-owned theme presets.
62
+ export function readThemeDefaults(presets: readonly string[]): {
63
+ mode: string;
64
+ preset: string;
65
+ } {
66
+ const config = readAppClientRuntimeConfig() as { app?: unknown };
67
+ const app = config.app;
68
+ const values =
69
+ typeof app === 'object' && app !== null
70
+ ? (app as Record<string, unknown>)
71
+ : {};
72
+ const mode = values.defaultColorScheme;
73
+ const preset = values.defaultTheme;
74
+ return {
75
+ mode:
76
+ mode === 'light' || mode === 'dark' || mode === 'system'
77
+ ? mode
78
+ : 'system',
79
+ preset:
80
+ typeof preset === 'string' && presets.includes(preset)
81
+ ? preset
82
+ : presets[0],
83
+ };
84
+ }
@@ -0,0 +1,7 @@
1
+ export const themePresets = [
2
+ { id: 'compact', labelKey: 'appearance.themes.compact' },
3
+ { id: 'default', labelKey: 'appearance.themes.default' },
4
+ ] as const;
5
+
6
+ export type ThemePresetId = (typeof themePresets)[number]['id'];
7
+ export const defaultThemePreset: ThemePresetId = 'compact';
@@ -0,0 +1,106 @@
1
+ import { PresetContext } from './theme-context';
2
+ import { resolveAppBase } from '@nocobase/app-client';
3
+ import {
4
+ ThemeProvider as NextThemesProvider,
5
+ type ThemeProviderProps,
6
+ } from 'next-themes';
7
+ import { useEffect, useState, type ReactElement } from 'react';
8
+ import {
9
+ readThemeDefaults,
10
+ readThemePreference,
11
+ themeStorageKeys,
12
+ } from './theme-preferences';
13
+ import { themePresets, type ThemePresetId } from './theme-presets';
14
+
15
+ const validPreset = (
16
+ value: string | null,
17
+ fallback: ThemePresetId,
18
+ ): ThemePresetId =>
19
+ themePresets.find((item) => item.id === value)?.id ?? fallback;
20
+
21
+ function ModeStorageSync({
22
+ storageKey,
23
+ defaultTheme,
24
+ }: {
25
+ storageKey: string;
26
+ defaultTheme: string;
27
+ }): null {
28
+ useEffect(() => {
29
+ const sync = (event: StorageEvent) => {
30
+ if (
31
+ event.key === null ||
32
+ (event.key === storageKey &&
33
+ event.newValue !== defaultTheme &&
34
+ !['light', 'dark', 'system'].includes(event.newValue ?? ''))
35
+ ) {
36
+ // Consume invalid mode events before next-themes can persist its fallback.
37
+ // Re-dispatch a valid value for its state-only synchronization path.
38
+ // Let clear events through so the preset listener also resets.
39
+ if (event.key === storageKey) event.stopImmediatePropagation();
40
+ queueMicrotask(() => {
41
+ window.dispatchEvent(
42
+ new StorageEvent('storage', {
43
+ key: storageKey,
44
+ newValue: defaultTheme,
45
+ }),
46
+ );
47
+ });
48
+ }
49
+ };
50
+ window.addEventListener('storage', sync, true);
51
+ return () => window.removeEventListener('storage', sync, true);
52
+ }, [defaultTheme, storageKey]);
53
+ return null;
54
+ }
55
+
56
+ export function AppThemeProvider({
57
+ children,
58
+ attribute = 'class',
59
+ defaultTheme,
60
+ enableSystem = true,
61
+ storageKey = themeStorageKeys(resolveAppBase()).mode,
62
+ ...props
63
+ }: ThemeProviderProps): ReactElement {
64
+ const [defaults] = useState(() =>
65
+ readThemeDefaults(themePresets.map(({ id }) => id)),
66
+ );
67
+ const mode = defaultTheme ?? defaults.mode;
68
+ const fallbackPreset = defaults.preset as ThemePresetId;
69
+ const presetKey = themeStorageKeys(resolveAppBase()).preset;
70
+ const [preset, setPreset] = useState(() =>
71
+ validPreset(readThemePreference(presetKey), fallbackPreset),
72
+ );
73
+ const selectPreset = (value: ThemePresetId) => {
74
+ setPreset(value);
75
+ try {
76
+ localStorage.setItem(presetKey, value);
77
+ } catch {
78
+ /* Keep the in-memory selection. */
79
+ }
80
+ };
81
+ useEffect(() => {
82
+ document.documentElement.dataset.theme = preset;
83
+ }, [preset]);
84
+ useEffect(() => {
85
+ const sync = (event: StorageEvent) => {
86
+ if (event.key === presetKey || event.key === null)
87
+ setPreset(validPreset(event.newValue, fallbackPreset));
88
+ };
89
+ window.addEventListener('storage', sync, true);
90
+ return () => window.removeEventListener('storage', sync, true);
91
+ }, [presetKey, fallbackPreset]);
92
+ return (
93
+ <NextThemesProvider
94
+ {...props}
95
+ attribute={attribute}
96
+ defaultTheme={mode}
97
+ enableSystem={enableSystem}
98
+ storageKey={storageKey}
99
+ >
100
+ <ModeStorageSync storageKey={storageKey} defaultTheme={mode} />
101
+ <PresetContext.Provider value={{ preset, setPreset: selectPreset }}>
102
+ {children}
103
+ </PresetContext.Provider>
104
+ </NextThemesProvider>
105
+ );
106
+ }
@@ -0,0 +1,176 @@
1
+ import { Check, Monitor, Moon, Palette, Sun } from 'lucide-react';
2
+ import { useTheme } from 'next-themes';
3
+ import { useTranslation } from '@nocobase/i18n/client';
4
+ import { useId, type ReactElement } from 'react';
5
+ import { Button } from '#components/ui/button';
6
+ import {
7
+ Popover,
8
+ PopoverTrigger,
9
+ PopoverContent,
10
+ PopoverTitle,
11
+ } from '#components/ui/popover';
12
+ import { cn } from 'cn';
13
+ import { useThemePreset } from './theme-context';
14
+ import { themePresets } from './theme-presets';
15
+
16
+ const COLOR_MODES = [
17
+ { id: 'light', icon: Sun },
18
+ { id: 'dark', icon: Moon },
19
+ { id: 'system', icon: Monitor },
20
+ ] as const;
21
+
22
+ export function ThemeSettings(): ReactElement {
23
+ const { theme, setTheme } = useTheme();
24
+ const { preset, setPreset } = useThemePreset();
25
+ const { t } = useTranslation();
26
+ const id = useId();
27
+ const title = t('appearance.title', { defaultValue: 'Appearance' });
28
+ return (
29
+ <Popover>
30
+ <PopoverTrigger
31
+ openOnHover
32
+ delay={0}
33
+ render={
34
+ <Button
35
+ aria-label={title}
36
+ size='icon'
37
+ variant='outline'
38
+ className='size-10 rounded-xl border-border/70 bg-background/60 hover:bg-accent/50'
39
+ />
40
+ }
41
+ >
42
+ <Palette aria-hidden='true' className='size-5' />
43
+ </PopoverTrigger>
44
+ {/* Keep the panel width independent of density; its content still uses theme tokens. */}
45
+ <PopoverContent
46
+ align='end'
47
+ className='w-xs max-w-[calc(100vw-2rem)] gap-4 p-4'
48
+ >
49
+ <div className='flex items-center justify-between'>
50
+ <PopoverTitle className='text-sm font-semibold text-foreground'>
51
+ {title}
52
+ </PopoverTitle>
53
+ </div>
54
+
55
+ <fieldset className='space-y-2'>
56
+ <legend className='text-xs font-medium text-muted-foreground'>
57
+ {t('appearance.mode', { defaultValue: 'Color mode' })}
58
+ </legend>
59
+ <div className='grid grid-cols-3 gap-1 rounded-lg bg-muted/70 p-1'>
60
+ {COLOR_MODES.map(({ id: mode, icon: ModeIcon }) => {
61
+ const isSelected = theme === mode;
62
+ return (
63
+ <label
64
+ key={mode}
65
+ className={cn(
66
+ 'relative flex min-w-0 cursor-pointer items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-medium transition-colors select-none has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-ring',
67
+ isSelected
68
+ ? 'bg-background text-foreground shadow-xs'
69
+ : 'text-muted-foreground hover:bg-background/40 hover:text-foreground',
70
+ )}
71
+ >
72
+ <input
73
+ type='radio'
74
+ name={id + '-mode'}
75
+ value={mode}
76
+ checked={isSelected}
77
+ onChange={() => setTheme(mode)}
78
+ className='sr-only'
79
+ />
80
+ <ModeIcon aria-hidden='true' className='size-3.5 shrink-0' />
81
+ <span className='whitespace-nowrap'>
82
+ {t('appearance.' + mode, {
83
+ defaultValue: mode[0].toUpperCase() + mode.slice(1),
84
+ })}
85
+ </span>
86
+ </label>
87
+ );
88
+ })}
89
+ </div>
90
+ </fieldset>
91
+
92
+ <fieldset className='space-y-2'>
93
+ <legend className='text-xs font-medium text-muted-foreground'>
94
+ {t('appearance.preset', { defaultValue: 'Theme' })}
95
+ </legend>
96
+ <div className='grid grid-cols-2 gap-2.5'>
97
+ {themePresets.map((item) => {
98
+ const isSelected = preset === item.id;
99
+ return (
100
+ <label
101
+ key={item.id}
102
+ className={cn(
103
+ 'group relative flex cursor-pointer flex-col rounded-xl border p-2 text-left transition-all select-none has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-ring',
104
+ isSelected
105
+ ? 'border-primary ring-2 ring-primary/20 bg-primary/5 shadow-xs'
106
+ : 'border-border/70 hover:border-border hover:bg-muted/30',
107
+ )}
108
+ >
109
+ <input
110
+ type='radio'
111
+ name={id + '-preset'}
112
+ value={item.id}
113
+ checked={isSelected}
114
+ onChange={() => setPreset(item.id)}
115
+ className='sr-only'
116
+ />
117
+ <div className='mb-2 h-16 w-full'>
118
+ <div
119
+ aria-hidden='true'
120
+ data-theme={item.id}
121
+ className='theme-preview relative flex h-full overflow-hidden rounded-lg border border-border/80 bg-background shadow-2xs'
122
+ >
123
+ {/* Mini Sidebar */}
124
+ <div className='flex w-6 flex-col gap-1 border-r border-sidebar-border/50 bg-sidebar p-1'>
125
+ <div className='size-2 rounded-xs bg-sidebar-primary-foreground' />
126
+ <div className='h-1 w-full rounded-xs bg-sidebar-foreground/30' />
127
+ <div className='h-1 w-full rounded-xs bg-sidebar-foreground/20' />
128
+ </div>
129
+ {/* Mini Main Content Area */}
130
+ <div className='flex flex-1 flex-col p-1.5'>
131
+ <div className='mb-1 flex items-center justify-between border-b border-border/40 pb-1'>
132
+ <div className='h-1 w-7 rounded-xs bg-foreground/70' />
133
+ <div className='size-1.5 rounded-full bg-primary' />
134
+ </div>
135
+ <div className='flex flex-1 flex-col justify-between'>
136
+ <div className='h-1 w-full rounded-xs bg-muted' />
137
+ <div className='flex items-center justify-between pt-1'>
138
+ <div className='h-1 w-6 rounded-xs bg-muted' />
139
+ <div className='h-3 w-5 rounded-xs bg-primary shadow-xs' />
140
+ </div>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ <div className='mt-auto flex min-h-5 items-center justify-between gap-2 px-0.5'>
146
+ <span
147
+ className={cn(
148
+ 'text-xs transition-colors',
149
+ isSelected
150
+ ? 'font-medium text-foreground'
151
+ : 'text-muted-foreground group-hover:text-foreground',
152
+ )}
153
+ >
154
+ {t(item.labelKey, {
155
+ defaultValue:
156
+ item.id.charAt(0).toUpperCase() + item.id.slice(1),
157
+ })}
158
+ </span>
159
+ {isSelected && (
160
+ <span
161
+ aria-hidden='true'
162
+ className='flex size-3.5 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground'
163
+ >
164
+ <Check className='size-2 stroke-[3]' />
165
+ </span>
166
+ )}
167
+ </div>
168
+ </label>
169
+ );
170
+ })}
171
+ </div>
172
+ </fieldset>
173
+ </PopoverContent>
174
+ </Popover>
175
+ );
176
+ }
@@ -0,0 +1,140 @@
1
+ :root[data-theme='compact'],
2
+ .theme-preview[data-theme='compact'] {
3
+ --font-sans:
4
+ ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
5
+ 'Microsoft YaHei', sans-serif;
6
+ --font-serif:
7
+ ui-serif, Georgia, Cambria, 'Times New Roman', 'Songti SC', SimSun, serif;
8
+ --font-mono:
9
+ ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
10
+ 'Courier New', monospace;
11
+ --font-heading: var(--font-sans);
12
+ --spacing: 0.2rem;
13
+ --text-xs: 0.75rem;
14
+ --text-xs--line-height: calc(1 / 0.75);
15
+ --text-sm: 0.875rem;
16
+ --text-sm--line-height: calc(1.2 / 0.875);
17
+ --text-base: 1rem;
18
+ --text-base--line-height: calc(1.4 / 1);
19
+ --text-lg: 1.125rem;
20
+ --text-lg--line-height: calc(1.6 / 1.125);
21
+ --text-xl: 1.25rem;
22
+ --text-xl--line-height: calc(1.6 / 1.25);
23
+ --text-2xl: 1.5rem;
24
+ --text-2xl--line-height: calc(1.8 / 1.5);
25
+ --text-3xl: 1.875rem;
26
+ --text-3xl--line-height: calc(2.1 / 1.875);
27
+ --text-4xl: 2.25rem;
28
+ --text-4xl--line-height: calc(2.3 / 2.25);
29
+ --text-5xl: 3rem;
30
+ --text-5xl--line-height: 1;
31
+ --text-6xl: 3.75rem;
32
+ --text-6xl--line-height: 1;
33
+ --text-7xl: 4.5rem;
34
+ --text-7xl--line-height: 1;
35
+ --text-8xl: 6rem;
36
+ --text-8xl--line-height: 1;
37
+ --text-9xl: 8rem;
38
+ --text-9xl--line-height: 1;
39
+ --shadow-2xs: 0 1px rgb(0 0 0 / 0.05);
40
+ --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05);
41
+ --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
42
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
43
+ --shadow-lg:
44
+ 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
45
+ --shadow-xl:
46
+ 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
47
+ --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
48
+ --radius: 0.375rem;
49
+ --background: oklch(0.985 0 0);
50
+ --foreground: oklch(0.16 0 0);
51
+ --card: oklch(1 0 0);
52
+ --card-foreground: oklch(0.16 0 0);
53
+ --popover: oklch(1 0 0);
54
+ --popover-foreground: oklch(0.16 0 0);
55
+ --primary: oklch(0.18 0 0);
56
+ --primary-foreground: oklch(0.985 0 0);
57
+ --secondary: oklch(0.96 0 0);
58
+ --secondary-foreground: oklch(0.22 0 0);
59
+ --muted: oklch(0.96 0 0);
60
+ --muted-foreground: oklch(0.52 0 0);
61
+ --accent: oklch(0.94 0 0);
62
+ --accent-foreground: oklch(0.22 0 0);
63
+ --destructive: oklch(0.577 0.245 27.325);
64
+ --border: oklch(0.9 0 0);
65
+ --input: oklch(0.9 0 0);
66
+ --ring: oklch(0.55 0 0);
67
+ --chart-1: oklch(0.646 0.222 41.116);
68
+ --chart-2: oklch(0.6 0.118 184.704);
69
+ --chart-3: oklch(0.398 0.07 227.392);
70
+ --chart-4: oklch(0.828 0.189 84.429);
71
+ --chart-5: oklch(0.769 0.188 70.08);
72
+ --sidebar: var(--card);
73
+ --sidebar-foreground: var(--card-foreground);
74
+ --sidebar-primary: color-mix(in oklch, var(--primary) 10%, var(--sidebar));
75
+ --sidebar-primary-foreground: var(--primary);
76
+ --sidebar-accent: var(--muted);
77
+ --sidebar-accent-foreground: var(--foreground);
78
+ --sidebar-border: var(--border);
79
+ --sidebar-ring: var(--ring);
80
+ }
81
+
82
+ :root.dark[data-theme='compact'],
83
+ :root.dark .theme-preview[data-theme='compact'] {
84
+ --background: oklch(0.13 0 0);
85
+ --foreground: oklch(0.96 0 0);
86
+ --card: oklch(0.18 0 0);
87
+ --card-foreground: oklch(0.985 0 0);
88
+ --popover: oklch(0.18 0 0);
89
+ --popover-foreground: oklch(0.985 0 0);
90
+ --primary: oklch(0.94 0 0);
91
+ --primary-foreground: oklch(0.16 0 0);
92
+ --secondary: oklch(0.24 0 0);
93
+ --secondary-foreground: oklch(0.985 0 0);
94
+ --muted: oklch(0.23 0 0);
95
+ --muted-foreground: oklch(0.7 0 0);
96
+ --accent: oklch(0.27 0 0);
97
+ --accent-foreground: oklch(0.985 0 0);
98
+ --destructive: oklch(0.704 0.191 22.216);
99
+ --border: oklch(1 0 0 / 10%);
100
+ --input: oklch(1 0 0 / 15%);
101
+ --ring: oklch(0.62 0 0);
102
+ --chart-1: oklch(0.488 0.243 264.376);
103
+ --chart-2: oklch(0.696 0.17 162.48);
104
+ --chart-3: oklch(0.769 0.188 70.08);
105
+ --chart-4: oklch(0.627 0.265 303.9);
106
+ --chart-5: oklch(0.645 0.246 16.439);
107
+ --sidebar: var(--card);
108
+ --sidebar-foreground: var(--card-foreground);
109
+ --sidebar-primary: color-mix(in oklch, var(--primary) 10%, var(--sidebar));
110
+ --sidebar-primary-foreground: var(--primary);
111
+ --sidebar-accent: var(--muted);
112
+ --sidebar-accent-foreground: var(--foreground);
113
+ --sidebar-border: var(--border);
114
+ --sidebar-ring: var(--ring);
115
+ }
116
+
117
+ /* The Switch track is fixed in pixels while its thumb follows `--spacing`, so this preset's
118
+ smaller spacing leaves the checked knob short of the end. The literal values equal 4.6/8
119
+ (sm 3.5/6) times the default spacing; deriving the track from the token restores the
120
+ geometry. A descendant selector, so the token comparison only sees the declarations above. */
121
+ :root[data-theme='compact'] [data-slot='switch'][data-size='default'],
122
+ .theme-preview[data-theme='compact'] [data-slot='switch'][data-size='default'] {
123
+ width: calc(var(--spacing) * 8);
124
+ height: calc(var(--spacing) * 4.6);
125
+ }
126
+
127
+ :root[data-theme='compact'] [data-slot='switch'][data-size='sm'],
128
+ .theme-preview[data-theme='compact'] [data-slot='switch'][data-size='sm'] {
129
+ width: calc(var(--spacing) * 6);
130
+ height: calc(var(--spacing) * 3.5);
131
+ }
132
+
133
+ /* Recharts draws a pie from fixed-pixel radii (`innerRadius={50}`) while the chart box height
134
+ follows `--spacing`, so this preset leaves the ring visibly thinner. Rendering the pie's
135
+ subtree at the spacious density restores the ring and the legend it sits beside; `:has`
136
+ limits the override to pie charts, leaving the h-72 trend chart scaled. */
137
+ :root[data-theme='compact'] [data-slot='chart']:has(.recharts-pie),
138
+ .theme-preview[data-theme='compact'] [data-slot='chart']:has(.recharts-pie) {
139
+ --spacing: 0.25rem;
140
+ }
@@ -0,0 +1,117 @@
1
+ :root,
2
+ :root[data-theme='default'],
3
+ .theme-preview[data-theme='default'] {
4
+ --font-sans:
5
+ ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
6
+ 'Microsoft YaHei', sans-serif;
7
+ --font-serif:
8
+ ui-serif, Georgia, Cambria, 'Times New Roman', 'Songti SC', SimSun, serif;
9
+ --font-mono:
10
+ ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
11
+ 'Courier New', monospace;
12
+ --font-heading: var(--font-sans);
13
+ --spacing: 0.25rem;
14
+ --text-xs: 0.75rem;
15
+ --text-xs--line-height: calc(1 / 0.75);
16
+ --text-sm: 0.875rem;
17
+ --text-sm--line-height: calc(1.25 / 0.875);
18
+ --text-base: 1rem;
19
+ --text-base--line-height: calc(1.5 / 1);
20
+ --text-lg: 1.125rem;
21
+ --text-lg--line-height: calc(1.75 / 1.125);
22
+ --text-xl: 1.25rem;
23
+ --text-xl--line-height: calc(1.75 / 1.25);
24
+ --text-2xl: 1.5rem;
25
+ --text-2xl--line-height: calc(2 / 1.5);
26
+ --text-3xl: 1.875rem;
27
+ --text-3xl--line-height: calc(2.25 / 1.875);
28
+ --text-4xl: 2.25rem;
29
+ --text-4xl--line-height: calc(2.5 / 2.25);
30
+ --text-5xl: 3rem;
31
+ --text-5xl--line-height: 1;
32
+ --text-6xl: 3.75rem;
33
+ --text-6xl--line-height: 1;
34
+ --text-7xl: 4.5rem;
35
+ --text-7xl--line-height: 1;
36
+ --text-8xl: 6rem;
37
+ --text-8xl--line-height: 1;
38
+ --text-9xl: 8rem;
39
+ --text-9xl--line-height: 1;
40
+ --shadow-2xs: 0 1px rgb(0 0 0 / 0.05);
41
+ --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05);
42
+ --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
43
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
44
+ --shadow-lg:
45
+ 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
46
+ --shadow-xl:
47
+ 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
48
+ --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
49
+ --radius: 0.5rem;
50
+ --background: oklch(0.985 0 0);
51
+ --foreground: oklch(0.16 0 0);
52
+ --card: oklch(1 0 0);
53
+ --card-foreground: oklch(0.16 0 0);
54
+ --popover: oklch(1 0 0);
55
+ --popover-foreground: oklch(0.16 0 0);
56
+ --primary: oklch(0.18 0 0);
57
+ --primary-foreground: oklch(0.985 0 0);
58
+ --secondary: oklch(0.96 0 0);
59
+ --secondary-foreground: oklch(0.22 0 0);
60
+ --muted: oklch(0.96 0 0);
61
+ --muted-foreground: oklch(0.52 0 0);
62
+ --accent: oklch(0.94 0 0);
63
+ --accent-foreground: oklch(0.22 0 0);
64
+ --destructive: oklch(0.577 0.245 27.325);
65
+ --border: oklch(0.9 0 0);
66
+ --input: oklch(0.9 0 0);
67
+ --ring: oklch(0.55 0 0);
68
+ --chart-1: oklch(0.646 0.222 41.116);
69
+ --chart-2: oklch(0.6 0.118 184.704);
70
+ --chart-3: oklch(0.398 0.07 227.392);
71
+ --chart-4: oklch(0.828 0.189 84.429);
72
+ --chart-5: oklch(0.769 0.188 70.08);
73
+ --sidebar: var(--card);
74
+ --sidebar-foreground: var(--card-foreground);
75
+ --sidebar-primary: color-mix(in oklch, var(--primary) 10%, var(--sidebar));
76
+ --sidebar-primary-foreground: var(--primary);
77
+ --sidebar-accent: var(--muted);
78
+ --sidebar-accent-foreground: var(--foreground);
79
+ --sidebar-border: var(--border);
80
+ --sidebar-ring: var(--ring);
81
+ }
82
+
83
+ :root.dark,
84
+ :root.dark[data-theme='default'],
85
+ :root.dark .theme-preview[data-theme='default'] {
86
+ --background: oklch(0.13 0 0);
87
+ --foreground: oklch(0.96 0 0);
88
+ --card: oklch(0.18 0 0);
89
+ --card-foreground: oklch(0.985 0 0);
90
+ --popover: oklch(0.18 0 0);
91
+ --popover-foreground: oklch(0.985 0 0);
92
+ --primary: oklch(0.94 0 0);
93
+ --primary-foreground: oklch(0.16 0 0);
94
+ --secondary: oklch(0.24 0 0);
95
+ --secondary-foreground: oklch(0.985 0 0);
96
+ --muted: oklch(0.23 0 0);
97
+ --muted-foreground: oklch(0.7 0 0);
98
+ --accent: oklch(0.27 0 0);
99
+ --accent-foreground: oklch(0.985 0 0);
100
+ --destructive: oklch(0.704 0.191 22.216);
101
+ --border: oklch(1 0 0 / 10%);
102
+ --input: oklch(1 0 0 / 15%);
103
+ --ring: oklch(0.62 0 0);
104
+ --chart-1: oklch(0.488 0.243 264.376);
105
+ --chart-2: oklch(0.696 0.17 162.48);
106
+ --chart-3: oklch(0.769 0.188 70.08);
107
+ --chart-4: oklch(0.627 0.265 303.9);
108
+ --chart-5: oklch(0.645 0.246 16.439);
109
+ --sidebar: var(--card);
110
+ --sidebar-foreground: var(--card-foreground);
111
+ --sidebar-primary: color-mix(in oklch, var(--primary) 10%, var(--sidebar));
112
+ --sidebar-primary-foreground: var(--primary);
113
+ --sidebar-accent: var(--muted);
114
+ --sidebar-accent-foreground: var(--foreground);
115
+ --sidebar-border: var(--border);
116
+ --sidebar-ring: var(--ring);
117
+ }
@@ -0,0 +1,5 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface Window {
4
+ [key: string]: unknown;
5
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "base-nova",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "client/styles.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "rtl": false,
15
+ "aliases": {
16
+ "components": "#components",
17
+ "utils": "#lib/utils",
18
+ "ui": "#components/ui",
19
+ "lib": "#lib",
20
+ "hooks": "#hooks"
21
+ },
22
+ "menuColor": "default",
23
+ "menuAccent": "subtle",
24
+ "registries": {
25
+ "@nocobase": "https://ui.nocobase.com/r/{name}.json"
26
+ }
27
+ }