@opensaas/stack-ui 0.1.6 → 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.
- package/.turbo/turbo-build.log +3 -2
- package/CHANGELOG.md +11 -0
- package/dist/components/AdminUI.d.ts +2 -1
- package/dist/components/AdminUI.d.ts.map +1 -1
- package/dist/components/AdminUI.js +2 -2
- package/dist/components/ItemFormClient.d.ts.map +1 -1
- package/dist/components/ItemFormClient.js +78 -60
- package/dist/components/Navigation.d.ts +2 -1
- package/dist/components/Navigation.d.ts.map +1 -1
- package/dist/components/Navigation.js +3 -2
- package/dist/components/UserMenu.d.ts +11 -0
- package/dist/components/UserMenu.d.ts.map +1 -0
- package/dist/components/UserMenu.js +18 -0
- package/dist/components/fields/TextField.d.ts +2 -1
- package/dist/components/fields/TextField.d.ts.map +1 -1
- package/dist/components/fields/TextField.js +4 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/primitives/button.d.ts +1 -1
- package/dist/styles/globals.css +25 -1
- package/package.json +14 -5
- package/src/components/AdminUI.tsx +3 -0
- package/src/components/ItemFormClient.tsx +84 -62
- package/src/components/Navigation.tsx +9 -20
- package/src/components/UserMenu.tsx +44 -0
- package/src/components/fields/TextField.tsx +7 -2
- package/src/index.ts +2 -0
- package/tests/browser/README.md +154 -0
- package/tests/browser/fields/CheckboxField.browser.test.tsx +245 -0
- package/tests/browser/fields/SelectField.browser.test.tsx +263 -0
- package/tests/browser/fields/TextField.browser.test.tsx +204 -0
- package/tests/browser/fields/__screenshots__/CheckboxField.browser.test.tsx/CheckboxField--Browser--edit-mode-should-not-be-clickable-when-disabled-1.png +0 -0
- package/tests/browser/fields/__screenshots__/CheckboxField.browser.test.tsx/CheckboxField--Browser--edit-mode-should-toggle-state-with-multiple-clicks-1.png +0 -0
- package/tests/browser/fields/__screenshots__/TextField.browser.test.tsx/TextField--Browser--edit-mode-should-handle-special-characters-1.png +0 -0
- package/tests/browser/fields/__screenshots__/TextField.browser.test.tsx/TextField--Browser--edit-mode-should-support-copy-and-paste-1.png +0 -0
- package/tests/browser/primitives/Button.browser.test.tsx +122 -0
- package/tests/browser/primitives/Dialog.browser.test.tsx +279 -0
- package/tests/browser/primitives/__screenshots__/Button.browser.test.tsx/Button--Browser--should-not-trigger-click-when-disabled-1.png +0 -0
- package/tests/components/CheckboxField.test.tsx +130 -0
- package/tests/components/DeleteButton.test.tsx +331 -0
- package/tests/components/IntegerField.test.tsx +147 -0
- package/tests/components/ListTable.test.tsx +457 -0
- package/tests/components/ListViewClient.test.tsx +415 -0
- package/tests/components/SearchBar.test.tsx +254 -0
- package/tests/components/SelectField.test.tsx +192 -0
- package/vitest.config.ts +20 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
|
|
2
|
-
> @opensaas/stack-ui@0.
|
|
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.
|
|
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
|
@@ -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;
|
|
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,
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (fieldAny
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
58
|
+
// Single relationship: use connect format
|
|
59
|
+
if (value) {
|
|
60
|
+
transformedData[fieldName] = {
|
|
61
|
+
connect: { id: value },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
71
64
|
}
|
|
72
65
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
93
|
+
// Handle error response
|
|
94
|
+
if (actionResult.fieldErrors) {
|
|
95
|
+
setErrors(actionResult.fieldErrors);
|
|
96
|
+
}
|
|
97
|
+
setGeneralError(actionResult.error);
|
|
92
98
|
}
|
|
93
99
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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(
|
|
130
|
+
setGeneralError(actionResult.error);
|
|
118
131
|
}
|
|
119
132
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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;
|
|
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(
|
|
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":"
|
|
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
|
-
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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?: "
|
|
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> {
|
package/dist/styles/globals.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! tailwindcss v4.1.
|
|
1
|
+
/*! tailwindcss v4.1.17 | MIT License | https://tailwindcss.com */
|
|
2
2
|
@layer properties;
|
|
3
3
|
@layer theme, base, components, utilities;
|
|
4
4
|
@layer theme {
|
|
@@ -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.
|
|
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.6"
|
|
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.
|
|
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.
|
|
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>
|