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

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 (250) 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 +432 -0
  6. package/CLAUDE.md +9 -0
  7. package/Dockerfile +132 -0
  8. package/Dockerfile.dockerignore +45 -0
  9. package/README.MD +481 -0
  10. package/cli/plugins.ts +14 -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 +41 -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 +172 -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 +200 -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/ai.ts +27 -0
  165. package/server/config/api.ts +8 -0
  166. package/server/config/app.ts +40 -0
  167. package/server/config/auth.ts +36 -0
  168. package/server/config/authorization.ts +18 -0
  169. package/server/config/caching.ts +21 -0
  170. package/server/config/database.ts +22 -0
  171. package/server/config/drive.ts +30 -0
  172. package/server/config/i18n.ts +17 -0
  173. package/server/config/index.ts +73 -0
  174. package/server/config/jobs.ts +27 -0
  175. package/server/config/logging.ts +23 -0
  176. package/server/config/notification.ts +11 -0
  177. package/server/config/queue.ts +28 -0
  178. package/server/config/scheduler.ts +23 -0
  179. package/server/config/secrets.ts +10 -0
  180. package/server/config/server.ts +26 -0
  181. package/server/config/session.ts +60 -0
  182. package/server/config/snowflake.ts +21 -0
  183. package/server/config/spa.ts +24 -0
  184. package/server/config/users.ts +11 -0
  185. package/server/config/workflow.ts +24 -0
  186. package/server/config.ts +24 -0
  187. package/server/embedded.ts +23 -0
  188. package/server/locales/en-US.ts +10 -0
  189. package/server/locales/index.ts +10 -0
  190. package/server/locales/zh-CN.ts +5 -0
  191. package/server/plugins.ts +41 -0
  192. package/server/providers/index.ts +5 -0
  193. package/server/routes/index.ts +6 -0
  194. package/server/runtime.ts +24 -0
  195. package/server/standalone.ts +29 -0
  196. package/server/tsconfig.json +4 -0
  197. package/tailwind.config.mjs +59 -0
  198. package/tests/components/app-layout-routes.test.tsx +132 -0
  199. package/tests/components/app-title.test.tsx +57 -0
  200. package/tests/components/auth-i18n.test.tsx +87 -0
  201. package/tests/components/back-button.test.tsx +80 -0
  202. package/tests/components/file-preview.test.tsx +101 -0
  203. package/tests/components/header-hover.test.tsx +87 -0
  204. package/tests/components/inbox.test.tsx +139 -0
  205. package/tests/components/language-switcher.test.tsx +298 -0
  206. package/tests/components/layout-containers.test.tsx +15 -0
  207. package/tests/components/layout-i18n.test.tsx +187 -0
  208. package/tests/components/loading.test.tsx +25 -0
  209. package/tests/components/navigation-menu.test.tsx +355 -0
  210. package/tests/components/page-harness.test.tsx +318 -0
  211. package/tests/components/primitive-labels.test.tsx +80 -0
  212. package/tests/components/sidebar-permissions.test.tsx +110 -0
  213. package/tests/components/sidebar-preference.test.tsx +42 -0
  214. package/tests/fixtures/theme-tokens.html +12 -0
  215. package/tests/fixtures/theme-tokens.tsx +111 -0
  216. package/tests/logic/account-permissions.test.tsx +204 -0
  217. package/tests/logic/app-locale-coverage.test.ts +136 -0
  218. package/tests/logic/app-server.test.ts +1589 -0
  219. package/tests/logic/asset-url-build.test.ts +106 -0
  220. package/tests/logic/client-auth.test.tsx +122 -0
  221. package/tests/logic/client-plugin-registry.test.ts +63 -0
  222. package/tests/logic/client-route-recovery.test.tsx +80 -0
  223. package/tests/logic/client-routes.test.ts +126 -0
  224. package/tests/logic/client-runtime.test.ts +112 -0
  225. package/tests/logic/client-settings.test.tsx +841 -0
  226. package/tests/logic/client-shell.test.tsx +466 -0
  227. package/tests/logic/client-theme.test.tsx +441 -0
  228. package/tests/logic/config.test.ts +197 -0
  229. package/tests/logic/dev-client-watch.test.ts +108 -0
  230. package/tests/logic/dev-routes-production-build.test.ts +168 -0
  231. package/tests/logic/device-code-migration.test.ts +70 -0
  232. package/tests/logic/lifecycle.test.ts +221 -0
  233. package/tests/logic/plugin-commands.test.ts +69 -0
  234. package/tests/logic/plugin-page-authz.test.ts +76 -0
  235. package/tests/logic/route-navigation.test.ts +66 -0
  236. package/tests/logic/route-overlay.test.tsx +353 -0
  237. package/tests/logic/server-imports.test.ts +88 -0
  238. package/tests/logic/tailwind-sources.test.ts +151 -0
  239. package/tests/logic/template-package-name.test.ts +88 -0
  240. package/tests/logic/theme-preferences.test.ts +86 -0
  241. package/tests/logic/theme-tokens.test.ts +248 -0
  242. package/tests/logic/toaster.test.tsx +108 -0
  243. package/tests/logic/user-menu-sign-out.test.tsx +98 -0
  244. package/tests/setup/client-config.ts +27 -0
  245. package/tsconfig.json +11 -0
  246. package/tsconfig.node.json +12 -0
  247. package/tsconfig.server.json +19 -0
  248. package/vite.config.ts +92 -0
  249. package/vitest.config.ts +37 -0
  250. package/README.md +0 -3
@@ -0,0 +1,190 @@
1
+ import { Dialog as DialogPrimitive } from '@base-ui/react/dialog';
2
+ import { useTranslation } from '@nocobase/i18n/client';
3
+ import { XIcon } from 'lucide-react';
4
+ import {
5
+ createContext,
6
+ useCallback,
7
+ useContext,
8
+ useLayoutEffect,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ type ReactElement,
13
+ type ReactNode,
14
+ type RefObject,
15
+ } from 'react';
16
+ import { useLocation, useNavigate, type To } from 'react-router';
17
+
18
+ import { Button } from '#components/ui/button';
19
+ import {
20
+ Dialog,
21
+ DialogClose,
22
+ DialogDescription,
23
+ DialogOverlay,
24
+ DialogPortal,
25
+ DialogTitle,
26
+ } from '#components/ui/dialog';
27
+ import { cn } from 'cn';
28
+
29
+ import { RouteOverlayContext } from './use-route-overlay.js';
30
+
31
+ export interface RouteOverlayProps {
32
+ readonly title: ReactNode;
33
+ readonly description?: ReactNode;
34
+ readonly children?: ReactNode;
35
+ readonly footer?: ReactNode;
36
+ /** Where closing navigates. Defaults to the parent route, keeping the current search string. */
37
+ readonly closeTo?: To;
38
+ /** Runs before every close; resolving `false` keeps the overlay open, for example to guard unsaved changes. */
39
+ readonly beforeClose?: () => boolean | Promise<boolean>;
40
+ readonly className?: string;
41
+ }
42
+
43
+ // Lets an overlay rendered through another overlay's outlet return focus into
44
+ // the enclosing panel. Two overlays reached by one URL mount in the same
45
+ // commit, so the nested one never observes the parent panel taking focus and
46
+ // would otherwise fall back to `document.body`.
47
+ const ParentPopupContext =
48
+ createContext<RefObject<HTMLDivElement | null> | null>(null);
49
+
50
+ /**
51
+ * The shared implementation of `RouteDialog` and `RouteDrawer`: a modal that is open for as long as its route matches,
52
+ * and closes by navigating away from it. Route registration stays with the application.
53
+ */
54
+ export function RouteOverlay({
55
+ title,
56
+ description,
57
+ children,
58
+ footer,
59
+ closeTo,
60
+ beforeClose,
61
+ className,
62
+ drawer = false,
63
+ }: RouteOverlayProps & { readonly drawer?: boolean }): ReactElement {
64
+ const { t } = useTranslation();
65
+ const parentPopup = useContext(ParentPopupContext);
66
+ const popupRef = useRef<HTMLDivElement>(null);
67
+ const previousFocusRef = useRef<HTMLElement | null>(null);
68
+ useLayoutEffect(() => {
69
+ previousFocusRef.current =
70
+ document.activeElement instanceof HTMLElement
71
+ ? document.activeElement
72
+ : null;
73
+ }, []);
74
+ const navigate = useNavigate();
75
+ const location = useLocation();
76
+ const [closingLocation, setClosingLocation] = useState<
77
+ typeof location | null
78
+ >(null);
79
+ const isClosing = closingLocation === location;
80
+ const pendingRef = useRef<Promise<void> | null>(null);
81
+ const generationRef = useRef(0);
82
+
83
+ // A still-mounted parent can change location while its confirmation is pending.
84
+ // Invalidate that request as well as requests from an unmounted route.
85
+ useLayoutEffect(() => {
86
+ generationRef.current += 1;
87
+ pendingRef.current = null;
88
+ return () => {
89
+ generationRef.current += 1;
90
+ };
91
+ }, [location]);
92
+
93
+ const close = useCallback((): Promise<void> => {
94
+ if (pendingRef.current) return pendingRef.current;
95
+ const requestGeneration = generationRef.current;
96
+ setClosingLocation(location);
97
+ const request = Promise.resolve()
98
+ .then(async () => {
99
+ const allowed = beforeClose ? await beforeClose() : true;
100
+ if (allowed && generationRef.current === requestGeneration) {
101
+ await navigate(
102
+ closeTo ?? { pathname: '..', search: location.search, hash: '' },
103
+ { relative: 'route', replace: true },
104
+ );
105
+ }
106
+ })
107
+ .finally(() => {
108
+ if (generationRef.current === requestGeneration) {
109
+ pendingRef.current = null;
110
+ setClosingLocation(null);
111
+ }
112
+ });
113
+ pendingRef.current = request;
114
+ return request;
115
+ }, [beforeClose, closeTo, location, navigate]);
116
+ const value = useMemo(() => ({ close, isClosing }), [close, isClosing]);
117
+
118
+ return (
119
+ <RouteOverlayContext.Provider value={value}>
120
+ {/* Covers the panel body too, so an overlay placed at this page's outlet
121
+ finds the enclosing panel without knowing where the outlet lives. */}
122
+ <ParentPopupContext.Provider value={popupRef}>
123
+ <Dialog
124
+ open
125
+ onOpenChange={(open) => {
126
+ if (!open)
127
+ void close().catch((error: unknown) => {
128
+ console.error('Failed to close route overlay', error);
129
+ });
130
+ }}
131
+ >
132
+ <DialogPortal>
133
+ {/* Each nested panel needs its own backdrop above its parent panel. */}
134
+ <DialogOverlay forceRender />
135
+ <DialogPrimitive.Popup
136
+ ref={popupRef}
137
+ finalFocus={() => {
138
+ const previous = previousFocusRef.current;
139
+ if (parentPopup?.current) {
140
+ return previous?.isConnected &&
141
+ parentPopup.current.contains(previous)
142
+ ? previous
143
+ : parentPopup.current;
144
+ }
145
+ return true;
146
+ }}
147
+ className={cn(
148
+ 'fixed top-1/2 left-1/2 z-50 w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 rounded-xl bg-popover text-popover-foreground shadow-lg outline-none duration-150 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95',
149
+ 'flex max-h-[calc(100dvh-2rem)] flex-col gap-0 overflow-hidden p-0 sm:max-w-2xl',
150
+ // The viewport constraints are deliberate; all ordinary styling uses theme tokens.
151
+ drawer &&
152
+ 'top-0 right-0 left-auto h-dvh max-h-dvh w-full max-w-full translate-x-0 translate-y-0 rounded-none sm:max-w-xl data-open:slide-in-from-right data-open:zoom-in-100',
153
+ className,
154
+ )}
155
+ >
156
+ <header className='shrink-0 space-y-2 border-b p-4 pr-12'>
157
+ <DialogTitle>{title}</DialogTitle>
158
+ {description != null && (
159
+ <DialogDescription>{description}</DialogDescription>
160
+ )}
161
+ </header>
162
+ <div className='min-h-0 flex-1 overflow-y-auto p-4'>
163
+ {children}
164
+ </div>
165
+ {footer != null && (
166
+ <footer className='flex shrink-0 flex-wrap justify-end gap-2 border-t p-4'>
167
+ {footer}
168
+ </footer>
169
+ )}
170
+ <DialogClose
171
+ render={
172
+ <Button
173
+ variant='ghost'
174
+ size='icon-sm'
175
+ className='absolute top-2 right-2'
176
+ />
177
+ }
178
+ >
179
+ <XIcon />
180
+ <span className='sr-only'>
181
+ {t('routeOverlay.close', { defaultValue: 'Close' })}
182
+ </span>
183
+ </DialogClose>
184
+ </DialogPrimitive.Popup>
185
+ </DialogPortal>
186
+ </Dialog>
187
+ </ParentPopupContext.Provider>
188
+ </RouteOverlayContext.Provider>
189
+ );
190
+ }
@@ -0,0 +1,75 @@
1
+ import * as React from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { cn } from 'cn';
4
+
5
+ const alertVariants = cva(
6
+ "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: 'bg-card text-card-foreground',
11
+ destructive:
12
+ 'bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current',
13
+ },
14
+ },
15
+ defaultVariants: {
16
+ variant: 'default',
17
+ },
18
+ },
19
+ );
20
+
21
+ function Alert({
22
+ className,
23
+ variant,
24
+ ...props
25
+ }: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
26
+ return (
27
+ <div
28
+ data-slot='alert'
29
+ role='alert'
30
+ className={cn(alertVariants({ variant }), className)}
31
+ {...props}
32
+ />
33
+ );
34
+ }
35
+
36
+ function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
37
+ return (
38
+ <div
39
+ data-slot='alert-title'
40
+ className={cn(
41
+ 'font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground',
42
+ className,
43
+ )}
44
+ {...props}
45
+ />
46
+ );
47
+ }
48
+
49
+ function AlertDescription({
50
+ className,
51
+ ...props
52
+ }: React.ComponentProps<'div'>) {
53
+ return (
54
+ <div
55
+ data-slot='alert-description'
56
+ className={cn(
57
+ 'text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4',
58
+ className,
59
+ )}
60
+ {...props}
61
+ />
62
+ );
63
+ }
64
+
65
+ function AlertAction({ className, ...props }: React.ComponentProps<'div'>) {
66
+ return (
67
+ <div
68
+ data-slot='alert-action'
69
+ className={cn('absolute top-2 right-2', className)}
70
+ {...props}
71
+ />
72
+ );
73
+ }
74
+
75
+ export { Alert, AlertTitle, AlertDescription, AlertAction };
@@ -0,0 +1,51 @@
1
+ import { mergeProps } from '@base-ui/react/merge-props';
2
+ import { useRender } from '@base-ui/react/use-render';
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
+ import { cn } from 'cn';
5
+
6
+ const badgeVariants = cva(
7
+ 'group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!',
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
12
+ secondary:
13
+ 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
14
+ destructive:
15
+ 'bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20',
16
+ outline:
17
+ 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
18
+ ghost:
19
+ 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
20
+ link: 'text-primary underline-offset-4 hover:underline',
21
+ },
22
+ },
23
+ defaultVariants: {
24
+ variant: 'default',
25
+ },
26
+ },
27
+ );
28
+
29
+ function Badge({
30
+ className,
31
+ variant = 'default',
32
+ render,
33
+ ...props
34
+ }: useRender.ComponentProps<'span'> & VariantProps<typeof badgeVariants>) {
35
+ return useRender({
36
+ defaultTagName: 'span',
37
+ props: mergeProps<'span'>(
38
+ {
39
+ className: cn(badgeVariants({ variant }), className),
40
+ },
41
+ props,
42
+ ),
43
+ render,
44
+ state: {
45
+ slot: 'badge',
46
+ variant,
47
+ },
48
+ });
49
+ }
50
+
51
+ export { Badge, badgeVariants };
@@ -0,0 +1,121 @@
1
+ import * as React from 'react';
2
+ import { mergeProps } from '@base-ui/react/merge-props';
3
+ import { useRender } from '@base-ui/react/use-render';
4
+ import { cn } from 'cn';
5
+ import { ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react';
6
+
7
+ function Breadcrumb({ className, ...props }: React.ComponentProps<'nav'>) {
8
+ return (
9
+ <nav
10
+ aria-label='breadcrumb'
11
+ data-slot='breadcrumb'
12
+ className={cn(className)}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
19
+ return (
20
+ <ol
21
+ data-slot='breadcrumb-list'
22
+ className={cn(
23
+ 'flex flex-wrap items-center gap-1.5 text-sm wrap-break-word text-muted-foreground',
24
+ className,
25
+ )}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
32
+ return (
33
+ <li
34
+ data-slot='breadcrumb-item'
35
+ className={cn('inline-flex items-center gap-1', className)}
36
+ {...props}
37
+ />
38
+ );
39
+ }
40
+
41
+ function BreadcrumbLink({
42
+ className,
43
+ render,
44
+ ...props
45
+ }: useRender.ComponentProps<'a'>) {
46
+ return useRender({
47
+ defaultTagName: 'a',
48
+ props: mergeProps<'a'>(
49
+ {
50
+ className: cn('transition-colors hover:text-foreground', className),
51
+ },
52
+ props,
53
+ ),
54
+ render,
55
+ state: {
56
+ slot: 'breadcrumb-link',
57
+ },
58
+ });
59
+ }
60
+
61
+ function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
62
+ return (
63
+ <span
64
+ data-slot='breadcrumb-page'
65
+ role='link'
66
+ aria-disabled='true'
67
+ aria-current='page'
68
+ className={cn('font-normal text-foreground', className)}
69
+ {...props}
70
+ />
71
+ );
72
+ }
73
+
74
+ function BreadcrumbSeparator({
75
+ children,
76
+ className,
77
+ ...props
78
+ }: React.ComponentProps<'li'>) {
79
+ return (
80
+ <li
81
+ data-slot='breadcrumb-separator'
82
+ role='presentation'
83
+ aria-hidden='true'
84
+ className={cn('[&>svg]:size-3.5', className)}
85
+ {...props}
86
+ >
87
+ {children ?? <ChevronRightIcon />}
88
+ </li>
89
+ );
90
+ }
91
+
92
+ function BreadcrumbEllipsis({
93
+ className,
94
+ ...props
95
+ }: React.ComponentProps<'span'>) {
96
+ return (
97
+ <span
98
+ data-slot='breadcrumb-ellipsis'
99
+ role='presentation'
100
+ aria-hidden='true'
101
+ className={cn(
102
+ 'flex size-5 items-center justify-center [&>svg]:size-4',
103
+ className,
104
+ )}
105
+ {...props}
106
+ >
107
+ <MoreHorizontalIcon />
108
+ <span className='sr-only'>More</span>
109
+ </span>
110
+ );
111
+ }
112
+
113
+ export {
114
+ Breadcrumb,
115
+ BreadcrumbList,
116
+ BreadcrumbItem,
117
+ BreadcrumbLink,
118
+ BreadcrumbPage,
119
+ BreadcrumbSeparator,
120
+ BreadcrumbEllipsis,
121
+ };
@@ -0,0 +1,58 @@
1
+ import { Button as ButtonPrimitive } from '@base-ui/react/button';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+
4
+ import { cn } from 'cn';
5
+
6
+ const buttonVariants = cva(
7
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: 'bg-primary text-primary-foreground hover:bg-primary/80',
12
+ outline:
13
+ 'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
14
+ secondary:
15
+ 'bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
16
+ ghost:
17
+ 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
18
+ destructive:
19
+ 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
20
+ link: 'text-primary underline-offset-4 hover:underline',
21
+ },
22
+ size: {
23
+ default:
24
+ 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
25
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
26
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-sm in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
27
+ lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
28
+ icon: 'size-8',
29
+ 'icon-xs':
30
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
31
+ 'icon-sm':
32
+ 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
33
+ 'icon-lg': 'size-9',
34
+ },
35
+ },
36
+ defaultVariants: {
37
+ variant: 'default',
38
+ size: 'default',
39
+ },
40
+ },
41
+ );
42
+
43
+ function Button({
44
+ className,
45
+ variant = 'default',
46
+ size = 'default',
47
+ ...props
48
+ }: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
49
+ return (
50
+ <ButtonPrimitive
51
+ data-slot='button'
52
+ className={cn(buttonVariants({ variant, size, className }))}
53
+ {...props}
54
+ />
55
+ );
56
+ }
57
+
58
+ export { Button, buttonVariants };
@@ -0,0 +1,19 @@
1
+ import { Collapsible as CollapsiblePrimitive } from '@base-ui/react/collapsible';
2
+
3
+ function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
4
+ return <CollapsiblePrimitive.Root data-slot='collapsible' {...props} />;
5
+ }
6
+
7
+ function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) {
8
+ return (
9
+ <CollapsiblePrimitive.Trigger data-slot='collapsible-trigger' {...props} />
10
+ );
11
+ }
12
+
13
+ function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) {
14
+ return (
15
+ <CollapsiblePrimitive.Panel data-slot='collapsible-content' {...props} />
16
+ );
17
+ }
18
+
19
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent };