@opensaas/stack-ui 0.1.7 → 0.3.0

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 (47) hide show
  1. package/.turbo/turbo-build.log +3 -2
  2. package/CHANGELOG.md +4 -0
  3. package/dist/components/AdminUI.d.ts +2 -1
  4. package/dist/components/AdminUI.d.ts.map +1 -1
  5. package/dist/components/AdminUI.js +2 -2
  6. package/dist/components/ItemFormClient.d.ts.map +1 -1
  7. package/dist/components/ItemFormClient.js +78 -60
  8. package/dist/components/Navigation.d.ts +2 -1
  9. package/dist/components/Navigation.d.ts.map +1 -1
  10. package/dist/components/Navigation.js +3 -2
  11. package/dist/components/UserMenu.d.ts +11 -0
  12. package/dist/components/UserMenu.d.ts.map +1 -0
  13. package/dist/components/UserMenu.js +18 -0
  14. package/dist/components/fields/TextField.d.ts +2 -1
  15. package/dist/components/fields/TextField.d.ts.map +1 -1
  16. package/dist/components/fields/TextField.js +4 -2
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/primitives/button.d.ts +1 -1
  21. package/dist/styles/globals.css +24 -0
  22. package/package.json +14 -5
  23. package/src/components/AdminUI.tsx +3 -0
  24. package/src/components/ItemFormClient.tsx +84 -62
  25. package/src/components/Navigation.tsx +9 -20
  26. package/src/components/UserMenu.tsx +44 -0
  27. package/src/components/fields/TextField.tsx +7 -2
  28. package/src/index.ts +2 -0
  29. package/tests/browser/README.md +154 -0
  30. package/tests/browser/fields/CheckboxField.browser.test.tsx +245 -0
  31. package/tests/browser/fields/SelectField.browser.test.tsx +263 -0
  32. package/tests/browser/fields/TextField.browser.test.tsx +204 -0
  33. package/tests/browser/fields/__screenshots__/CheckboxField.browser.test.tsx/CheckboxField--Browser--edit-mode-should-not-be-clickable-when-disabled-1.png +0 -0
  34. package/tests/browser/fields/__screenshots__/CheckboxField.browser.test.tsx/CheckboxField--Browser--edit-mode-should-toggle-state-with-multiple-clicks-1.png +0 -0
  35. package/tests/browser/fields/__screenshots__/TextField.browser.test.tsx/TextField--Browser--edit-mode-should-handle-special-characters-1.png +0 -0
  36. package/tests/browser/fields/__screenshots__/TextField.browser.test.tsx/TextField--Browser--edit-mode-should-support-copy-and-paste-1.png +0 -0
  37. package/tests/browser/primitives/Button.browser.test.tsx +122 -0
  38. package/tests/browser/primitives/Dialog.browser.test.tsx +279 -0
  39. package/tests/browser/primitives/__screenshots__/Button.browser.test.tsx/Button--Browser--should-not-trigger-click-when-disabled-1.png +0 -0
  40. package/tests/components/CheckboxField.test.tsx +130 -0
  41. package/tests/components/DeleteButton.test.tsx +331 -0
  42. package/tests/components/IntegerField.test.tsx +147 -0
  43. package/tests/components/ListTable.test.tsx +457 -0
  44. package/tests/components/ListViewClient.test.tsx +415 -0
  45. package/tests/components/SearchBar.test.tsx +254 -0
  46. package/tests/components/SelectField.test.tsx +192 -0
  47. package/vitest.config.ts +20 -0
@@ -1,10 +1,11 @@
1
1
 
2
- > @opensaas/stack-ui@0.1.7 build /home/runner/work/stack/stack/packages/ui
2
+ > @opensaas/stack-ui@0.3.0 build /home/runner/work/stack/stack/packages/ui
3
3
  > tsc && npm run build:css
4
4
 
5
5
  npm warn Unknown env config "verify-deps-before-run". This will stop working in the next major version of npm.
6
+ npm warn Unknown env config "npm-globalconfig". This will stop working in the next major version of npm.
6
7
  npm warn Unknown env config "_jsr-registry". This will stop working in the next major version of npm.
7
8
 
8
- > @opensaas/stack-ui@0.1.7 build:css
9
+ > @opensaas/stack-ui@0.3.0 build:css
9
10
  > mkdir -p dist/styles && postcss ./src/styles/globals.css -o ./dist/styles/globals.css
10
11
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @opensaas/stack-ui
2
2
 
3
+ ## 0.3.0
4
+
5
+ ## 0.2.0
6
+
3
7
  ## 0.1.7
4
8
 
5
9
  ### Patch Changes
@@ -9,6 +9,7 @@ export interface AdminUIProps<TPrisma> {
9
9
  };
10
10
  basePath?: string;
11
11
  serverAction: (input: ServerActionInput) => Promise<unknown>;
12
+ onSignOut?: () => Promise<void>;
12
13
  }
13
14
  /**
14
15
  * Main AdminUI component - complete admin interface with routing
@@ -20,5 +21,5 @@ export interface AdminUIProps<TPrisma> {
20
21
  * - [list, 'create'] → ItemForm (create)
21
22
  * - [list, id] → ItemForm (edit)
22
23
  */
23
- export declare function AdminUI<TPrisma>({ context, config, params, searchParams, basePath, serverAction, }: AdminUIProps<TPrisma>): import("react/jsx-runtime").JSX.Element;
24
+ export declare function AdminUI<TPrisma>({ context, config, params, searchParams, basePath, serverAction, onSignOut, }: AdminUIProps<TPrisma>): import("react/jsx-runtime").JSX.Element;
24
25
  //# sourceMappingURL=AdminUI.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AdminUI.d.ts","sourceRoot":"","sources":["../../src/components/AdminUI.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,aAAa,EAAqB,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAGvF,MAAM,WAAW,YAAY,CAAC,OAAO;IACnC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,YAAY,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAA;KAAE,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAC7D;AAED;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,EAC/B,OAAO,EACP,MAAM,EACN,MAAW,EACX,YAAiB,EACjB,QAAmB,EACnB,YAAY,GACb,EAAE,YAAY,CAAC,OAAO,CAAC,2CA2EvB"}
1
+ {"version":3,"file":"AdminUI.d.ts","sourceRoot":"","sources":["../../src/components/AdminUI.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,aAAa,EAAqB,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAGvF,MAAM,WAAW,YAAY,CAAC,OAAO;IACnC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,YAAY,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAA;KAAE,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC5D,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAChC;AAED;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,EAC/B,OAAO,EACP,MAAM,EACN,MAAW,EACX,YAAiB,EACjB,QAAmB,EACnB,YAAY,EACZ,SAAS,GACV,EAAE,YAAY,CAAC,OAAO,CAAC,2CA4EvB"}
@@ -15,7 +15,7 @@ import { generateThemeCSS } from '../lib/theme.js';
15
15
  * - [list, 'create'] → ItemForm (create)
16
16
  * - [list, id] → ItemForm (edit)
17
17
  */
18
- export function AdminUI({ context, config, params = [], searchParams = {}, basePath = '/admin', serverAction, }) {
18
+ export function AdminUI({ context, config, params = [], searchParams = {}, basePath = '/admin', serverAction, onSignOut, }) {
19
19
  // Parse route from params
20
20
  const [urlSegment, action] = params;
21
21
  // Convert URL segment (kebab-case) to PascalCase listKey
@@ -44,5 +44,5 @@ export function AdminUI({ context, config, params = [], searchParams = {}, baseP
44
44
  }
45
45
  // Generate theme styles if custom theme is configured
46
46
  const themeStyles = config.ui?.theme ? generateThemeCSS(config.ui.theme) : null;
47
- return (_jsxs(_Fragment, { children: [themeStyles && _jsx("style", { dangerouslySetInnerHTML: { __html: themeStyles } }), _jsxs("div", { className: "flex min-h-screen bg-background", children: [_jsx(Navigation, { context: context, config: config, basePath: basePath, currentPath: currentPath }), _jsx("main", { className: "flex-1 overflow-y-auto", children: content })] })] }));
47
+ return (_jsxs(_Fragment, { children: [themeStyles && _jsx("style", { dangerouslySetInnerHTML: { __html: themeStyles } }), _jsxs("div", { className: "flex min-h-screen bg-background", children: [_jsx(Navigation, { context: context, config: config, basePath: basePath, currentPath: currentPath, onSignOut: onSignOut }), _jsx("main", { className: "flex-1 overflow-y-auto", children: content })] })] }));
48
48
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ItemFormClient.d.ts","sourceRoot":"","sources":["../../src/components/ItemFormClient.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAA;AAE7E,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAA;IACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAA;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAA;CACxE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,MAAM,EACN,IAAI,EACJ,MAAM,EACN,WAAgB,EAChB,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,gBAAqB,GACtB,EAAE,mBAAmB,2CAwMrB"}
1
+ {"version":3,"file":"ItemFormClient.d.ts","sourceRoot":"","sources":["../../src/components/ItemFormClient.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAA;AAE7E,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAA;IACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAA;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAA;CACxE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,MAAM,EACN,IAAI,EACJ,MAAM,EACN,WAAgB,EAChB,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,gBAAqB,GACtB,EAAE,mBAAmB,2CA8NrB"}
@@ -33,67 +33,78 @@ export function ItemFormClient({ listKey, urlKey, mode, fields, initialData = {}
33
33
  setErrors({});
34
34
  setGeneralError(null);
35
35
  startTransition(async () => {
36
- try {
37
- // Transform relationship fields to Prisma format
38
- // Filter out password fields with isSet objects (unchanged passwords)
39
- // File/Image fields: pass File objects through (Next.js will serialize them)
40
- const transformedData = {};
41
- for (const [fieldName, value] of Object.entries(formData)) {
42
- const fieldConfig = fields[fieldName];
43
- // Skip password fields that have { isSet: boolean } value (not being changed)
44
- if (typeof value === 'object' && value !== null && 'isSet' in value) {
45
- continue;
46
- }
47
- // Transform relationship fields - check discriminated union type
48
- const fieldAny = fieldConfig;
49
- if (fieldAny?.type === 'relationship') {
50
- if (fieldAny.many) {
51
- // Many relationship: use connect format
52
- if (Array.isArray(value) && value.length > 0) {
53
- transformedData[fieldName] = {
54
- connect: value.map((id) => ({ id })),
55
- };
56
- }
57
- }
58
- else {
59
- // Single relationship: use connect format
60
- if (value) {
61
- transformedData[fieldName] = {
62
- connect: { id: value },
63
- };
64
- }
36
+ // Transform relationship fields to Prisma format
37
+ // Filter out password fields with isSet objects (unchanged passwords)
38
+ // File/Image fields: pass File objects through (Next.js will serialize them)
39
+ const transformedData = {};
40
+ for (const [fieldName, value] of Object.entries(formData)) {
41
+ const fieldConfig = fields[fieldName];
42
+ // Skip password fields that have { isSet: boolean } value (not being changed)
43
+ if (typeof value === 'object' && value !== null && 'isSet' in value) {
44
+ continue;
45
+ }
46
+ // Transform relationship fields - check discriminated union type
47
+ const fieldAny = fieldConfig;
48
+ if (fieldAny?.type === 'relationship') {
49
+ if (fieldAny.many) {
50
+ // Many relationship: use connect format
51
+ if (Array.isArray(value) && value.length > 0) {
52
+ transformedData[fieldName] = {
53
+ connect: value.map((id) => ({ id })),
54
+ };
65
55
  }
66
56
  }
67
57
  else {
68
- // Non-relationship field: pass through (including File objects for file/image fields)
69
- // File objects will be serialized by Next.js server action
70
- transformedData[fieldName] = value;
58
+ // Single relationship: use connect format
59
+ if (value) {
60
+ transformedData[fieldName] = {
61
+ connect: { id: value },
62
+ };
63
+ }
71
64
  }
72
65
  }
73
- const result = mode === 'create'
74
- ? await serverAction({
75
- listKey,
76
- action: 'create',
77
- data: transformedData,
78
- })
79
- : await serverAction({
80
- listKey,
81
- action: 'update',
82
- id: itemId,
83
- data: transformedData,
84
- });
85
- if (result) {
66
+ else {
67
+ // Non-relationship field: pass through (including File objects for file/image fields)
68
+ // File objects will be serialized by Next.js server action
69
+ transformedData[fieldName] = value;
70
+ }
71
+ }
72
+ const result = mode === 'create'
73
+ ? await serverAction({
74
+ listKey,
75
+ action: 'create',
76
+ data: transformedData,
77
+ })
78
+ : await serverAction({
79
+ listKey,
80
+ action: 'update',
81
+ id: itemId,
82
+ data: transformedData,
83
+ });
84
+ // Check if result has the new format with success/error fields
85
+ if (result && typeof result === 'object' && 'success' in result) {
86
+ const actionResult = result;
87
+ if (actionResult.success) {
86
88
  // Navigate back to list view
87
89
  router.push(`${basePath}/${urlKey}`);
88
90
  router.refresh();
89
91
  }
90
92
  else {
91
- setGeneralError('Access denied or operation failed');
93
+ // Handle error response
94
+ if (actionResult.fieldErrors) {
95
+ setErrors(actionResult.fieldErrors);
96
+ }
97
+ setGeneralError(actionResult.error);
92
98
  }
93
99
  }
94
- catch (error) {
95
- const errorMessage = error instanceof Error ? error.message : 'Failed to save item';
96
- setGeneralError(errorMessage);
100
+ else if (result) {
101
+ // Legacy format: result is the data itself
102
+ router.push(`${basePath}/${urlKey}`);
103
+ router.refresh();
104
+ }
105
+ else {
106
+ // null result means access denied
107
+ setGeneralError('Access denied or operation failed');
97
108
  }
98
109
  });
99
110
  };
@@ -103,23 +114,30 @@ export function ItemFormClient({ listKey, urlKey, mode, fields, initialData = {}
103
114
  setGeneralError(null);
104
115
  setShowDeleteConfirm(false);
105
116
  startTransition(async () => {
106
- try {
107
- const result = await serverAction({
108
- listKey,
109
- action: 'delete',
110
- id: itemId,
111
- });
112
- if (result) {
117
+ const result = await serverAction({
118
+ listKey,
119
+ action: 'delete',
120
+ id: itemId,
121
+ });
122
+ // Check if result has the new format with success/error fields
123
+ if (result && typeof result === 'object' && 'success' in result) {
124
+ const actionResult = result;
125
+ if (actionResult.success) {
113
126
  router.push(`${basePath}/${urlKey}`);
114
127
  router.refresh();
115
128
  }
116
129
  else {
117
- setGeneralError('Access denied or failed to delete item');
130
+ setGeneralError(actionResult.error);
118
131
  }
119
132
  }
120
- catch (error) {
121
- const errorMessage = error instanceof Error ? error.message : 'Failed to delete item';
122
- setGeneralError(errorMessage);
133
+ else if (result) {
134
+ // Legacy format: result is the data itself
135
+ router.push(`${basePath}/${urlKey}`);
136
+ router.refresh();
137
+ }
138
+ else {
139
+ // null result means access denied
140
+ setGeneralError('Access denied or failed to delete item');
123
141
  }
124
142
  });
125
143
  };
@@ -4,10 +4,11 @@ export interface NavigationProps<TPrisma> {
4
4
  config: OpenSaasConfig;
5
5
  basePath?: string;
6
6
  currentPath?: string;
7
+ onSignOut?: () => Promise<void>;
7
8
  }
8
9
  /**
9
10
  * Navigation sidebar showing all lists
10
11
  * Server Component
11
12
  */
12
- export declare function Navigation<TPrisma>({ context, config, basePath, currentPath, }: NavigationProps<TPrisma>): import("react/jsx-runtime").JSX.Element;
13
+ export declare function Navigation<TPrisma>({ context, config, basePath, currentPath, onSignOut, }: NavigationProps<TPrisma>): import("react/jsx-runtime").JSX.Element;
13
14
  //# sourceMappingURL=Navigation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Navigation.d.ts","sourceRoot":"","sources":["../../src/components/Navigation.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAa,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAE/E,MAAM,WAAW,eAAe,CAAC,OAAO;IACtC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,EAClC,OAAO,EACP,MAAM,EACN,QAAmB,EACnB,WAAgB,GACjB,EAAE,eAAe,CAAC,OAAO,CAAC,2CAgG1B"}
1
+ {"version":3,"file":"Navigation.d.ts","sourceRoot":"","sources":["../../src/components/Navigation.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAa,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAG/E,MAAM,WAAW,eAAe,CAAC,OAAO;IACtC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAChC;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,EAClC,OAAO,EACP,MAAM,EACN,QAAmB,EACnB,WAAgB,EAChB,SAAS,GACV,EAAE,eAAe,CAAC,OAAO,CAAC,2CAkF1B"}
@@ -2,11 +2,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import Link from 'next/link.js';
3
3
  import { formatListName } from '../lib/utils.js';
4
4
  import { getUrlKey } from '@opensaas/stack-core';
5
+ import { UserMenu } from './UserMenu.js';
5
6
  /**
6
7
  * Navigation sidebar showing all lists
7
8
  * Server Component
8
9
  */
9
- export function Navigation({ context, config, basePath = '/admin', currentPath = '', }) {
10
+ export function Navigation({ context, config, basePath = '/admin', currentPath = '', onSignOut, }) {
10
11
  const lists = Object.keys(config.lists || {});
11
12
  return (_jsxs("nav", { className: "w-64 border-r border-border bg-card h-screen sticky top-0 flex flex-col", children: [_jsxs("div", { className: "p-6 border-b border-border relative overflow-hidden", children: [_jsx("div", { className: "absolute inset-0 bg-gradient-to-br from-primary/10 to-accent/10 opacity-50" }), _jsx(Link, { href: basePath, className: "block relative", children: _jsx("h1", { className: "text-xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent", children: "OpenSaas Admin" }) })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-4", children: _jsxs("div", { className: "space-y-1", children: [_jsxs(Link, { href: basePath, className: `block px-3 py-2.5 rounded-lg text-sm font-medium transition-all relative overflow-hidden group ${currentPath === ''
12
13
  ? 'bg-gradient-to-r from-primary to-accent text-primary-foreground shadow-lg shadow-primary/25'
@@ -16,5 +17,5 @@ export function Navigation({ context, config, basePath = '/admin', currentPath =
16
17
  return (_jsxs(Link, { href: `${basePath}/${urlKey}`, className: `block px-3 py-2.5 rounded-lg text-sm font-medium transition-all relative overflow-hidden group ${isActive
17
18
  ? 'bg-gradient-to-r from-primary to-accent text-primary-foreground shadow-lg shadow-primary/25'
18
19
  : 'text-foreground hover:bg-accent/50 hover:text-accent-foreground'}`, children: [isActive && (_jsx("div", { className: "absolute inset-0 bg-gradient-to-r from-primary/20 to-accent/20 animate-pulse" })), _jsxs("span", { className: "relative flex items-center gap-2", children: [_jsx("span", { className: "opacity-60 group-hover:opacity-100 transition-opacity", children: "\uD83D\uDCC1" }), formatListName(listKey)] })] }, listKey));
19
- })] }))] }) }), context.session && (_jsx("div", { className: "p-4 border-t border-border bg-gradient-to-br from-primary/5 to-accent/5", children: _jsxs("div", { className: "flex items-center space-x-3", children: [_jsx("div", { className: "h-9 w-9 rounded-full bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-lg shadow-primary/25", children: _jsx("span", { className: "text-sm font-bold text-primary-foreground", children: String(context.session.data?.name)?.[0]?.toUpperCase() || '?' }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "text-sm font-medium truncate", children: String(context.session.data?.name) || 'User' }), _jsx("p", { className: "text-xs text-muted-foreground truncate", children: String(context.session.data?.email) || '' })] })] }) }))] }));
20
+ })] }))] }) }), context.session && (_jsx(UserMenu, { userName: String(context.session.data?.name) || 'User', userEmail: String(context.session.data?.email) || '', onSignOut: onSignOut }))] }));
20
21
  }
@@ -0,0 +1,11 @@
1
+ export interface UserMenuProps {
2
+ userName?: string;
3
+ userEmail?: string;
4
+ onSignOut?: () => Promise<void>;
5
+ }
6
+ /**
7
+ * User menu component with sign-out button
8
+ * Client Component
9
+ */
10
+ export declare function UserMenu({ userName, userEmail, onSignOut }: UserMenuProps): import("react/jsx-runtime").JSX.Element;
11
+ //# sourceMappingURL=UserMenu.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserMenu.d.ts","sourceRoot":"","sources":["../../src/components/UserMenu.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAChC;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,aAAa,2CA4BzE"}
@@ -0,0 +1,18 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useRouter } from 'next/navigation.js';
4
+ import { Button } from '../primitives/button.js';
5
+ /**
6
+ * User menu component with sign-out button
7
+ * Client Component
8
+ */
9
+ export function UserMenu({ userName, userEmail, onSignOut }) {
10
+ const router = useRouter();
11
+ const handleSignOut = async () => {
12
+ if (onSignOut) {
13
+ await onSignOut();
14
+ }
15
+ router.push('/sign-in');
16
+ };
17
+ return (_jsxs("div", { className: "p-4 border-t border-border bg-gradient-to-br from-primary/5 to-accent/5", children: [_jsxs("div", { className: "flex items-center space-x-3 mb-3", children: [_jsx("div", { className: "h-9 w-9 rounded-full bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-lg shadow-primary/25", children: _jsx("span", { className: "text-sm font-bold text-primary-foreground", children: userName?.[0]?.toUpperCase() || '?' }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "text-sm font-medium truncate", children: userName || 'User' }), _jsx("p", { className: "text-xs text-muted-foreground truncate", children: userEmail || '' })] })] }), _jsx(Button, { onClick: handleSignOut, variant: "outline", size: "sm", className: "w-full text-sm", children: "Sign Out" })] }));
18
+ }
@@ -8,6 +8,7 @@ export interface TextFieldProps {
8
8
  disabled?: boolean;
9
9
  required?: boolean;
10
10
  mode?: 'read' | 'edit';
11
+ displayMode?: 'input' | 'textarea';
11
12
  }
12
- export declare function TextField({ name, value, onChange, label, placeholder, error, disabled, required, mode, }: TextFieldProps): import("react/jsx-runtime").JSX.Element;
13
+ export declare function TextField({ name, value, onChange, label, placeholder, error, disabled, required, mode, displayMode, }: TextFieldProps): import("react/jsx-runtime").JSX.Element;
13
14
  //# sourceMappingURL=TextField.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TextField.d.ts","sourceRoot":"","sources":["../../../src/components/fields/TextField.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CACvB;AAED,wBAAgB,SAAS,CAAC,EACxB,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,KAAK,EACL,WAAW,EACX,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,IAAa,GACd,EAAE,cAAc,2CA8BhB"}
1
+ {"version":3,"file":"TextField.d.ts","sourceRoot":"","sources":["../../../src/components/fields/TextField.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,WAAW,CAAC,EAAE,OAAO,GAAG,UAAU,CAAA;CACnC;AAED,wBAAgB,SAAS,CAAC,EACxB,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,KAAK,EACL,WAAW,EACX,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,IAAa,EACb,WAAqB,GACtB,EAAE,cAAc,2CAgChB"}
@@ -1,11 +1,13 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Input } from '../../primitives/input.js';
4
+ import { Textarea } from '../../primitives/textarea.js';
4
5
  import { Label } from '../../primitives/label.js';
5
6
  import { cn } from '../../lib/utils.js';
6
- export function TextField({ name, value, onChange, label, placeholder, error, disabled, required, mode = 'edit', }) {
7
+ export function TextField({ name, value, onChange, label, placeholder, error, disabled, required, mode = 'edit', displayMode = 'input', }) {
7
8
  if (mode === 'read') {
8
9
  return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-muted-foreground", children: label }), _jsx("p", { className: "text-sm", children: value || '-' })] }));
9
10
  }
10
- return (_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: name, children: [label, required && _jsx("span", { className: "text-destructive ml-1", children: "*" })] }), _jsx(Input, { id: name, name: name, type: "text", value: value || '', onChange: (e) => onChange(e.target.value), placeholder: placeholder, disabled: disabled, required: required, className: cn(error && 'border-destructive') }), error && _jsx("p", { className: "text-sm text-destructive", children: error })] }));
11
+ const InputComponent = displayMode === 'textarea' ? Textarea : Input;
12
+ return (_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: name, children: [label, required && _jsx("span", { className: "text-destructive ml-1", children: "*" })] }), _jsx(InputComponent, { id: name, name: name, type: displayMode === 'input' ? 'text' : undefined, value: value || '', onChange: (e) => onChange(e.target.value), placeholder: placeholder, disabled: disabled, required: required, className: cn(error && 'border-destructive') }), error && _jsx("p", { className: "text-sm text-destructive", children: error })] }));
11
13
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { AdminUI } from './components/AdminUI.js';
2
2
  export { Dashboard } from './components/Dashboard.js';
3
3
  export { Navigation } from './components/Navigation.js';
4
+ export { UserMenu } from './components/UserMenu.js';
4
5
  export { ListView } from './components/ListView.js';
5
6
  export { ListViewClient } from './components/ListViewClient.js';
6
7
  export { ItemForm } from './components/ItemForm.js';
@@ -12,6 +13,7 @@ export { TextField, IntegerField, CheckboxField, SelectField, TimestampField, Pa
12
13
  export type { AdminUIProps } from './components/AdminUI.js';
13
14
  export type { DashboardProps } from './components/Dashboard.js';
14
15
  export type { NavigationProps } from './components/Navigation.js';
16
+ export type { UserMenuProps } from './components/UserMenu.js';
15
17
  export type { ListViewProps } from './components/ListView.js';
16
18
  export type { ListViewClientProps } from './components/ListViewClient.js';
17
19
  export type { ItemFormProps } from './components/ItemForm.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAA;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAA;AAC/D,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAA;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAA;AAC/D,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAA;AAG5F,OAAO,EACL,SAAS,EACT,YAAY,EACZ,aAAa,EACb,WAAW,EACX,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,sBAAsB,EACtB,sBAAsB,EACtB,iBAAiB,GAClB,MAAM,8BAA8B,CAAA;AAGrC,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAC3D,YAAY,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAC/D,YAAY,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AACjE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAA;AACzE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAA;AACzE,YAAY,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAA;AACvE,YAAY,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAA;AACzE,YAAY,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAA;AAEzE,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,EACnB,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,cAAc,EACd,mBAAmB,GACpB,MAAM,8BAA8B,CAAA;AAGrC,OAAO,EACL,cAAc,EACd,YAAY,EACZ,SAAS,EACT,SAAS,EACT,YAAY,GACb,MAAM,kCAAkC,CAAA;AAEzC,YAAY,EACV,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,iBAAiB,GAClB,MAAM,kCAAkC,CAAA;AAGzC,OAAO,EAAE,EAAE,EAAE,cAAc,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAG1F,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAA;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAA;AAC/D,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAA;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAA;AAC/D,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAA;AAG5F,OAAO,EACL,SAAS,EACT,YAAY,EACZ,aAAa,EACb,WAAW,EACX,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,sBAAsB,EACtB,sBAAsB,EACtB,iBAAiB,GAClB,MAAM,8BAA8B,CAAA;AAGrC,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAC3D,YAAY,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAC/D,YAAY,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AACjE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC7D,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAA;AACzE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAA;AACzE,YAAY,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAA;AACvE,YAAY,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAA;AACzE,YAAY,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAA;AAEzE,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,EACnB,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,cAAc,EACd,mBAAmB,GACpB,MAAM,8BAA8B,CAAA;AAGrC,OAAO,EACL,cAAc,EACd,YAAY,EACZ,SAAS,EACT,SAAS,EACT,YAAY,GACb,MAAM,kCAAkC,CAAA;AAEzC,YAAY,EACV,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,iBAAiB,GAClB,MAAM,kCAAkC,CAAA;AAGzC,OAAO,EAAE,EAAE,EAAE,cAAc,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAG1F,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA"}
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  export { AdminUI } from './components/AdminUI.js';
3
3
  export { Dashboard } from './components/Dashboard.js';
4
4
  export { Navigation } from './components/Navigation.js';
5
+ export { UserMenu } from './components/UserMenu.js';
5
6
  export { ListView } from './components/ListView.js';
6
7
  export { ListViewClient } from './components/ListViewClient.js';
7
8
  export { ItemForm } from './components/ItemForm.js';
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { type VariantProps } from 'class-variance-authority';
3
3
  declare const buttonVariants: (props?: ({
4
- variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
4
+ variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | null | undefined;
5
5
  size?: "default" | "sm" | "lg" | "icon" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
@@ -244,6 +244,9 @@
244
244
  }
245
245
  }
246
246
  @layer utilities {
247
+ .pointer-events-none {
248
+ pointer-events: none;
249
+ }
247
250
  .sr-only {
248
251
  position: absolute;
249
252
  width: 1px;
@@ -300,6 +303,24 @@
300
303
  .z-50 {
301
304
  z-index: 50;
302
305
  }
306
+ .container {
307
+ width: 100%;
308
+ @media (width >= 40rem) {
309
+ max-width: 40rem;
310
+ }
311
+ @media (width >= 48rem) {
312
+ max-width: 48rem;
313
+ }
314
+ @media (width >= 64rem) {
315
+ max-width: 64rem;
316
+ }
317
+ @media (width >= 80rem) {
318
+ max-width: 80rem;
319
+ }
320
+ @media (width >= 96rem) {
321
+ max-width: 96rem;
322
+ }
323
+ }
303
324
  .-mx-1 {
304
325
  margin-inline: calc(var(--spacing) * -1);
305
326
  }
@@ -333,6 +354,9 @@
333
354
  .mb-2 {
334
355
  margin-bottom: calc(var(--spacing) * 2);
335
356
  }
357
+ .mb-3 {
358
+ margin-bottom: calc(var(--spacing) * 3);
359
+ }
336
360
  .mb-4 {
337
361
  margin-bottom: calc(var(--spacing) * 4);
338
362
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opensaas/stack-ui",
3
- "version": "0.1.7",
3
+ "version": "0.3.0",
4
4
  "description": "Composable React UI components for OpenSaas Stack",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -51,10 +51,10 @@
51
51
  "url": "https://github.com/OpenSaasAU/stack/issues"
52
52
  },
53
53
  "peerDependencies": {
54
+ "@opensaas/stack-core": "^0",
54
55
  "next": "^15.0.0 || ^16.0.0",
55
56
  "react": "^19.0.0",
56
- "react-dom": "^19.0.0",
57
- "@opensaas/stack-core": "0.1.7"
57
+ "react-dom": "^19.0.0"
58
58
  },
59
59
  "dependencies": {
60
60
  "@radix-ui/react-checkbox": "^1.3.3",
@@ -68,7 +68,7 @@
68
68
  "class-variance-authority": "^0.7.1",
69
69
  "clsx": "^2.1.1",
70
70
  "date-fns": "^4.1.0",
71
- "lucide-react": "^0.553.0",
71
+ "lucide-react": "^0.554.0",
72
72
  "react-hook-form": "^7.54.2",
73
73
  "tailwind-merge": "^3.3.1"
74
74
  },
@@ -81,14 +81,20 @@
81
81
  "@types/react": "^19.2.2",
82
82
  "@types/react-dom": "^19.2.2",
83
83
  "@vitejs/plugin-react": "^5.0.0",
84
+ "@vitest/browser": "^4.0.9",
85
+ "@vitest/browser-playwright": "^4.0.9",
84
86
  "@vitest/coverage-v8": "^4.0.4",
85
87
  "happy-dom": "^20.0.0",
88
+ "next": "^16.0.1",
89
+ "playwright": "^1.56.1",
86
90
  "postcss": "^8.4.49",
87
91
  "postcss-cli": "^11.0.0",
92
+ "react": "^19.2.0",
93
+ "react-dom": "^19.2.0",
88
94
  "tailwindcss": "^4.0.0",
89
95
  "typescript": "^5.9.3",
90
96
  "vitest": "^4.0.0",
91
- "@opensaas/stack-core": "0.1.7"
97
+ "@opensaas/stack-core": "0.3.0"
92
98
  },
93
99
  "scripts": {
94
100
  "build": "tsc && npm run build:css",
@@ -97,6 +103,9 @@
97
103
  "test": "vitest",
98
104
  "test:ui": "vitest --ui",
99
105
  "test:coverage": "vitest --coverage",
106
+ "test:browser": "BROWSER_TEST=true vitest --run",
107
+ "test:browser:ui": "BROWSER_TEST=true vitest --ui",
108
+ "test:browser:coverage": "BROWSER_TEST=true vitest --coverage --run",
100
109
  "clean": "rm -rf .turbo dist tsconfig.tsbuildinfo"
101
110
  }
102
111
  }
@@ -15,6 +15,7 @@ export interface AdminUIProps<TPrisma> {
15
15
  basePath?: string
16
16
  // Server action can return any shape depending on the list item type
17
17
  serverAction: (input: ServerActionInput) => Promise<unknown>
18
+ onSignOut?: () => Promise<void>
18
19
  }
19
20
 
20
21
  /**
@@ -34,6 +35,7 @@ export function AdminUI<TPrisma>({
34
35
  searchParams = {},
35
36
  basePath = '/admin',
36
37
  serverAction,
38
+ onSignOut,
37
39
  }: AdminUIProps<TPrisma>) {
38
40
  // Parse route from params
39
41
  const [urlSegment, action] = params
@@ -104,6 +106,7 @@ export function AdminUI<TPrisma>({
104
106
  config={config}
105
107
  basePath={basePath}
106
108
  currentPath={currentPath}
109
+ onSignOut={onSignOut}
107
110
  />
108
111
  <main className="flex-1 overflow-y-auto">{content}</main>
109
112
  </div>