@opensaas/stack-ui 0.1.7 → 0.4.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 (61) hide show
  1. package/.turbo/turbo-build.log +3 -2
  2. package/CHANGELOG.md +140 -0
  3. package/dist/components/AdminUI.d.ts +5 -4
  4. package/dist/components/AdminUI.d.ts.map +1 -1
  5. package/dist/components/AdminUI.js +2 -2
  6. package/dist/components/Dashboard.d.ts +4 -4
  7. package/dist/components/Dashboard.d.ts.map +1 -1
  8. package/dist/components/Dashboard.js +4 -3
  9. package/dist/components/ItemForm.d.ts +4 -4
  10. package/dist/components/ItemForm.d.ts.map +1 -1
  11. package/dist/components/ItemForm.js +9 -5
  12. package/dist/components/ItemFormClient.d.ts.map +1 -1
  13. package/dist/components/ItemFormClient.js +78 -60
  14. package/dist/components/ListView.d.ts +4 -4
  15. package/dist/components/ListView.d.ts.map +1 -1
  16. package/dist/components/ListView.js +18 -11
  17. package/dist/components/Navigation.d.ts +5 -4
  18. package/dist/components/Navigation.d.ts.map +1 -1
  19. package/dist/components/Navigation.js +3 -2
  20. package/dist/components/UserMenu.d.ts +11 -0
  21. package/dist/components/UserMenu.d.ts.map +1 -0
  22. package/dist/components/UserMenu.js +18 -0
  23. package/dist/components/fields/TextField.d.ts +2 -1
  24. package/dist/components/fields/TextField.d.ts.map +1 -1
  25. package/dist/components/fields/TextField.js +4 -2
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -0
  29. package/dist/primitives/button.d.ts +1 -1
  30. package/dist/primitives/button.d.ts.map +1 -1
  31. package/dist/styles/globals.css +24 -0
  32. package/package.json +32 -23
  33. package/src/components/AdminUI.tsx +8 -5
  34. package/src/components/Dashboard.tsx +7 -10
  35. package/src/components/ItemForm.tsx +14 -10
  36. package/src/components/ItemFormClient.tsx +84 -62
  37. package/src/components/ListView.tsx +23 -21
  38. package/src/components/Navigation.tsx +14 -25
  39. package/src/components/UserMenu.tsx +44 -0
  40. package/src/components/fields/TextField.tsx +7 -2
  41. package/src/index.ts +2 -0
  42. package/src/primitives/button.tsx +1 -2
  43. package/tests/browser/README.md +154 -0
  44. package/tests/browser/fields/CheckboxField.browser.test.tsx +245 -0
  45. package/tests/browser/fields/SelectField.browser.test.tsx +263 -0
  46. package/tests/browser/fields/TextField.browser.test.tsx +204 -0
  47. package/tests/browser/fields/__screenshots__/CheckboxField.browser.test.tsx/CheckboxField--Browser--edit-mode-should-not-be-clickable-when-disabled-1.png +0 -0
  48. package/tests/browser/fields/__screenshots__/CheckboxField.browser.test.tsx/CheckboxField--Browser--edit-mode-should-toggle-state-with-multiple-clicks-1.png +0 -0
  49. package/tests/browser/fields/__screenshots__/TextField.browser.test.tsx/TextField--Browser--edit-mode-should-handle-special-characters-1.png +0 -0
  50. package/tests/browser/fields/__screenshots__/TextField.browser.test.tsx/TextField--Browser--edit-mode-should-support-copy-and-paste-1.png +0 -0
  51. package/tests/browser/primitives/Button.browser.test.tsx +122 -0
  52. package/tests/browser/primitives/Dialog.browser.test.tsx +279 -0
  53. package/tests/browser/primitives/__screenshots__/Button.browser.test.tsx/Button--Browser--should-not-trigger-click-when-disabled-1.png +0 -0
  54. package/tests/components/CheckboxField.test.tsx +130 -0
  55. package/tests/components/DeleteButton.test.tsx +331 -0
  56. package/tests/components/IntegerField.test.tsx +147 -0
  57. package/tests/components/ListTable.test.tsx +457 -0
  58. package/tests/components/ListViewClient.test.tsx +415 -0
  59. package/tests/components/SearchBar.test.tsx +254 -0
  60. package/tests/components/SelectField.test.tsx +192 -0
  61. 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.4.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.4.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,145 @@
1
1
  # @opensaas/stack-ui
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#170](https://github.com/OpenSaasAU/stack/pull/170) [`3c4db9d`](https://github.com/OpenSaasAU/stack/commit/3c4db9d8318fc73d291991d8bdfa4f607c3a50ea) Thanks [@list({](https://github.com/list({)! - Add support for virtual fields with proper TypeScript type generation
8
+
9
+ Virtual fields are computed fields that don't exist in the database but are added to query results at runtime. This feature enables derived or computed values to be included in your API responses with full type safety.
10
+
11
+ **New Features:**
12
+ - Added `virtual()` field type for defining computed fields in your schema
13
+ - Virtual fields are automatically excluded from database schema and input types
14
+ - Virtual fields appear in output types with full TypeScript autocomplete
15
+ - Virtual fields support `resolveOutput` hooks for custom computation logic
16
+
17
+ **Type System Improvements:**
18
+ - Generated Context type now properly extends AccessContext from core
19
+ - Separate Input and Output types (e.g., `UserOutput` includes virtual fields, `UserCreateInput` does not)
20
+ - UI components now accept `AccessContext<any>` for better compatibility with custom context types
21
+ - Type aliases provide convenience (e.g., `User = UserOutput`)
22
+
23
+ **Example Usage:**
24
+
25
+ ```typescript
26
+ import { list, text, virtual } from '@opensaas/stack-core'
27
+
28
+ export default config({
29
+ lists: {
30
+
31
+ fields: {
32
+ name: text(),
33
+ email: text(),
34
+ displayName: virtual({
35
+ type: 'string',
36
+ hooks: {
37
+ resolveOutput: async ({ item }) => {
38
+ return `${item.name} (${item.email})`
39
+ },
40
+ },
41
+ }),
42
+ },
43
+ }),
44
+ },
45
+ })
46
+ ```
47
+
48
+ The `displayName` field will automatically appear in query results with full TypeScript support, but won't be part of create/update operations or the database schema.
49
+
50
+ ### Patch Changes
51
+
52
+ - [#172](https://github.com/OpenSaasAU/stack/pull/172) [`929a2a9`](https://github.com/OpenSaasAU/stack/commit/929a2a9a2dfa80b1d973d259dd87828d644ea58d) Thanks [@list<Lists.User.TypeInfo>({](https://github.com/list<Lists.User.TypeInfo>({), [@list<Lists.User.TypeInfo>({](https://github.com/list<Lists.User.TypeInfo>({)! - Improve TypeScript type inference for field configs and list-level hooks by automatically passing TypeInfo from list level down
53
+
54
+ This change eliminates the need to manually specify type parameters on field builders when using features like virtual fields, and fixes a critical bug where list-level hooks weren't receiving properly typed parameters.
55
+
56
+ ## Field Type Inference Improvements
57
+
58
+ Previously, users had to write `virtual<Lists.User.TypeInfo>({...})` to get proper type inference. Now TypeScript automatically infers the correct types from the list-level type parameter.
59
+
60
+ **Example:**
61
+
62
+ ```typescript
63
+ // Before
64
+
65
+ fields: {
66
+ displayName: virtual<Lists.User.TypeInfo>({
67
+ type: 'string',
68
+ hooks: {
69
+ resolveOutput: ({ item }) => `${item.name} (${item.email})`,
70
+ },
71
+ }),
72
+ },
73
+ })
74
+
75
+ // After
76
+
77
+ fields: {
78
+ displayName: virtual({
79
+ type: 'string',
80
+ hooks: {
81
+ resolveOutput: ({ item }) => `${item.name} (${item.email})`,
82
+ },
83
+ }),
84
+ },
85
+ })
86
+ ```
87
+
88
+ ## List-Level Hooks Type Inference Fix
89
+
90
+ Fixed a critical type parameter mismatch where `Hooks<TTypeInfo>` was passing the entire TypeInfo object as the first parameter instead of properly destructuring it into three required parameters:
91
+ 1. `TOutput` - The item type (what's stored in DB)
92
+ 2. `TCreateInput` - Prisma create input type
93
+ 3. `TUpdateInput` - Prisma update input type
94
+
95
+ **Impact:**
96
+ - `resolveInput` now receives proper Prisma input types (e.g., `PostCreateInput`, `PostUpdateInput`)
97
+ - `validateInput` has access to properly typed input data
98
+ - `beforeOperation` and `afterOperation` have correct item types
99
+ - All list-level hook callbacks now get full IntelliSense and type checking
100
+
101
+ **Example:**
102
+
103
+ ```typescript
104
+ Post: list<Lists.Post.TypeInfo>({
105
+ fields: { title: text(), content: text() },
106
+ hooks: {
107
+ resolveInput: async ({ operation, resolvedData }) => {
108
+ // ✅ resolvedData is now properly typed as PostCreateInput or PostUpdateInput
109
+ // ✅ Full autocomplete for title, content, etc.
110
+ if (operation === 'create') {
111
+ console.log(resolvedData.title) // TypeScript knows this is string | undefined
112
+ }
113
+ return resolvedData
114
+ },
115
+ beforeOperation: async ({ operation, item }) => {
116
+ // ✅ item is now properly typed as Post with all fields
117
+ if (operation === 'update' && item) {
118
+ console.log(item.title) // TypeScript knows this is string
119
+ console.log(item.createdAt) // TypeScript knows this is Date
120
+ }
121
+ },
122
+ },
123
+ })
124
+ ```
125
+
126
+ ## Breaking Changes
127
+ - Field types now accept full `TTypeInfo extends TypeInfo` instead of just `TItem`
128
+ - `FieldsWithItemType` utility replaced with `FieldsWithTypeInfo`
129
+ - All field builders updated to use new type signature
130
+ - List-level hooks now receive properly typed parameters (may reveal existing type errors)
131
+
132
+ ## Benefits
133
+ - ✨ Cleaner code without manual type parameter repetition
134
+ - 🎯 Better type inference in both field-level and list-level hooks
135
+ - 🔄 Consistent type flow from list configuration down to individual fields
136
+ - 🛡️ Maintained full type safety with improved DX
137
+ - 💡 Full IntelliSense support in all hook callbacks
138
+
139
+ ## 0.3.0
140
+
141
+ ## 0.2.0
142
+
3
143
  ## 0.1.7
4
144
 
5
145
  ### Patch Changes
@@ -1,7 +1,7 @@
1
1
  import type { ServerActionInput } from '../server/types.js';
2
- import { AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
3
- export interface AdminUIProps<TPrisma> {
4
- context: AccessContext<TPrisma>;
2
+ import { type AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
3
+ export interface AdminUIProps {
4
+ context: AccessContext<unknown>;
5
5
  config: OpenSaasConfig;
6
6
  params?: string[];
7
7
  searchParams?: {
@@ -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({ context, config, params, searchParams, basePath, serverAction, onSignOut, }: AdminUIProps): 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,KAAK,aAAa,EAAqB,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAG5F,MAAM,WAAW,YAAY;IAC3B,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,EACtB,OAAO,EACP,MAAM,EACN,MAAW,EACX,YAAiB,EACjB,QAAmB,EACnB,YAAY,EACZ,SAAS,GACV,EAAE,YAAY,2CA4Ed"}
@@ -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,6 +1,6 @@
1
- import { AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
2
- export interface DashboardProps<TPrisma> {
3
- context: AccessContext<TPrisma>;
1
+ import { type AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
2
+ export interface DashboardProps {
3
+ context: AccessContext<unknown>;
4
4
  config: OpenSaasConfig;
5
5
  basePath?: string;
6
6
  }
@@ -8,5 +8,5 @@ export interface DashboardProps<TPrisma> {
8
8
  * Dashboard landing page showing all available lists
9
9
  * Server Component
10
10
  */
11
- export declare function Dashboard<TPrisma>({ context, config, basePath, }: DashboardProps<TPrisma>): Promise<import("react/jsx-runtime").JSX.Element>;
11
+ export declare function Dashboard({ context, config, basePath }: DashboardProps): Promise<import("react/jsx-runtime").JSX.Element>;
12
12
  //# sourceMappingURL=Dashboard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Dashboard.d.ts","sourceRoot":"","sources":["../../src/components/Dashboard.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAuB,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAGzF,MAAM,WAAW,cAAc,CAAC,OAAO;IACrC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,EACvC,OAAO,EACP,MAAM,EACN,QAAmB,GACpB,EAAE,cAAc,CAAC,OAAO,CAAC,oDAkHzB"}
1
+ {"version":3,"file":"Dashboard.d.ts","sourceRoot":"","sources":["../../src/components/Dashboard.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAAuB,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAG9F,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,QAAmB,EAAE,EAAE,cAAc,oDAmHvF"}
@@ -7,13 +7,14 @@ import { Card, CardContent, CardHeader, CardTitle } from '../primitives/card.js'
7
7
  * Dashboard landing page showing all available lists
8
8
  * Server Component
9
9
  */
10
- export async function Dashboard({ context, config, basePath = '/admin', }) {
10
+ export async function Dashboard({ context, config, basePath = '/admin' }) {
11
11
  const lists = Object.keys(config.lists || {});
12
12
  // Get counts for each list
13
13
  const listCounts = await Promise.all(lists.map(async (listKey) => {
14
14
  try {
15
- const count = await context.db[getDbKey(listKey)]?.count();
16
- return { listKey, count: count || 0 };
15
+ const delegate = context.db[getDbKey(listKey)];
16
+ const count = delegate?.count ? await delegate.count() : 0;
17
+ return { listKey, count };
17
18
  }
18
19
  catch (error) {
19
20
  console.error(`Failed to get count for ${listKey}:`, error);
@@ -1,7 +1,7 @@
1
1
  import type { ServerActionInput } from '../server/types.js';
2
- import { AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
3
- export interface ItemFormProps<TPrisma> {
4
- context: AccessContext<TPrisma>;
2
+ import { type AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
3
+ export interface ItemFormProps {
4
+ context: AccessContext<unknown>;
5
5
  config: OpenSaasConfig;
6
6
  listKey: string;
7
7
  mode: 'create' | 'edit';
@@ -13,5 +13,5 @@ export interface ItemFormProps<TPrisma> {
13
13
  * Item form component - create or edit an item
14
14
  * Server Component that fetches data and sets up actions
15
15
  */
16
- export declare function ItemForm<TPrisma>({ context, config, listKey, mode, itemId, basePath, serverAction, }: ItemFormProps<TPrisma>): Promise<import("react/jsx-runtime").JSX.Element>;
16
+ export declare function ItemForm({ context, config, listKey, mode, itemId, basePath, serverAction, }: ItemFormProps): Promise<import("react/jsx-runtime").JSX.Element>;
17
17
  //# sourceMappingURL=ItemForm.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ItemForm.d.ts","sourceRoot":"","sources":["../../src/components/ItemForm.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,aAAa,EAAuB,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAGzF,MAAM,WAAW,aAAa,CAAC,OAAO;IACpC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAC7D;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,EACtC,OAAO,EACP,MAAM,EACN,OAAO,EACP,IAAI,EACJ,MAAM,EACN,QAAmB,EACnB,YAAY,GACb,EAAE,aAAa,CAAC,OAAO,CAAC,oDAmKxB"}
1
+ {"version":3,"file":"ItemForm.d.ts","sourceRoot":"","sources":["../../src/components/ItemForm.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,KAAK,aAAa,EAAuB,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAG9F,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAC7D;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,EAC7B,OAAO,EACP,MAAM,EACN,OAAO,EACP,IAAI,EACJ,MAAM,EACN,QAAmB,EACnB,YAAY,GACb,EAAE,aAAa,oDAuKf"}
@@ -27,10 +27,13 @@ export async function ItemForm({ context, config, listKey, mode, itemId, basePat
27
27
  }
28
28
  }
29
29
  // Fetch item with relationships included
30
- itemData = await context.db[getDbKey(listKey)].findUnique({
31
- where: { id: itemId },
32
- ...(Object.keys(includeRelationships).length > 0 && { include: includeRelationships }),
33
- });
30
+ const delegate = context.db[getDbKey(listKey)];
31
+ if (delegate?.findUnique) {
32
+ itemData = await delegate.findUnique({
33
+ where: { id: itemId },
34
+ ...(Object.keys(includeRelationships).length > 0 && { include: includeRelationships }),
35
+ });
36
+ }
34
37
  }
35
38
  catch (error) {
36
39
  console.error(`Failed to fetch item ${itemId}:`, error);
@@ -53,7 +56,8 @@ export async function ItemForm({ context, config, listKey, mode, itemId, basePat
53
56
  if (relatedListConfig) {
54
57
  try {
55
58
  const dbContext = context.db;
56
- const relatedItems = await dbContext[getDbKey(relatedListName)].findMany({});
59
+ const delegate = dbContext[getDbKey(relatedListName)];
60
+ const relatedItems = delegate?.findMany ? await delegate.findMany({}) : [];
57
61
  // Use 'name' field as label if it exists, otherwise use 'id'
58
62
  relationshipData[fieldName] = relatedItems.map((item) => ({
59
63
  id: item.id,
@@ -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
  };
@@ -1,6 +1,6 @@
1
- import { AccessContext, OpenSaasConfig, type PrismaClientLike } from '@opensaas/stack-core';
2
- export interface ListViewProps<TPrisma extends PrismaClientLike = PrismaClientLike> {
3
- context: AccessContext<TPrisma>;
1
+ import { type AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
2
+ export interface ListViewProps {
3
+ context: AccessContext<unknown>;
4
4
  config: OpenSaasConfig;
5
5
  listKey: string;
6
6
  basePath?: string;
@@ -13,5 +13,5 @@ export interface ListViewProps<TPrisma extends PrismaClientLike = PrismaClientLi
13
13
  * List view component - displays items in a table
14
14
  * Server Component that fetches data and renders client table
15
15
  */
16
- export declare function ListView<TPrisma extends PrismaClientLike = PrismaClientLike>({ context, config, listKey, basePath, columns, page, pageSize, search, }: ListViewProps<TPrisma>): Promise<import("react/jsx-runtime").JSX.Element>;
16
+ export declare function ListView({ context, config, listKey, basePath, columns, page, pageSize, search, }: ListViewProps): Promise<import("react/jsx-runtime").JSX.Element>;
17
17
  //# sourceMappingURL=ListView.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ListView.d.ts","sourceRoot":"","sources":["../../src/components/ListView.tsx"],"names":[],"mappings":"AAGA,OAAO,EACL,aAAa,EAGb,cAAc,EACd,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAA;AAE7B,MAAM,WAAW,aAAa,CAAC,OAAO,SAAS,gBAAgB,GAAG,gBAAgB;IAChF,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,SAAS,gBAAgB,GAAG,gBAAgB,EAAE,EAClF,OAAO,EACP,MAAM,EACN,OAAO,EACP,QAAmB,EACnB,OAAO,EACP,IAAQ,EACR,QAAa,EACb,MAAM,GACP,EAAE,aAAa,CAAC,OAAO,CAAC,oDAqHxB"}
1
+ {"version":3,"file":"ListView.d.ts","sourceRoot":"","sources":["../../src/components/ListView.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,aAAa,EAAuB,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAE9F,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,EAC7B,OAAO,EACP,MAAM,EACN,OAAO,EACP,QAAmB,EACnB,OAAO,EACP,IAAQ,EACR,QAAa,EACb,MAAM,GACP,EAAE,aAAa,oDA6Hf"}
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import Link from 'next/link.js';
3
3
  import { ListViewClient } from './ListViewClient.js';
4
4
  import { formatListName } from '../lib/utils.js';
5
- import { getDbKey, getUrlKey, } from '@opensaas/stack-core';
5
+ import { getDbKey, getUrlKey } from '@opensaas/stack-core';
6
6
  /**
7
7
  * List view component - displays items in a table
8
8
  * Server Component that fetches data and renders client table
@@ -47,15 +47,19 @@ export async function ListView({ context, config, listKey, basePath = '/admin',
47
47
  include[fieldName] = true;
48
48
  }
49
49
  });
50
- [items, total] = await Promise.all([
51
- dbContext[key].findMany({
52
- where,
53
- skip,
54
- take: pageSize,
55
- ...(Object.keys(include).length > 0 ? { include } : {}),
56
- }),
57
- dbContext[key].count({ where }),
58
- ]);
50
+ const delegate = dbContext[key];
51
+ if (delegate?.findMany && delegate?.count) {
52
+ ;
53
+ [items, total] = await Promise.all([
54
+ delegate.findMany({
55
+ where,
56
+ skip,
57
+ take: pageSize,
58
+ ...(Object.keys(include).length > 0 ? { include } : {}),
59
+ }),
60
+ delegate.count({ where }),
61
+ ]);
62
+ }
59
63
  }
60
64
  catch (error) {
61
65
  console.error(`Failed to fetch ${listKey}:`, error);
@@ -65,7 +69,10 @@ export async function ListView({ context, config, listKey, basePath = '/admin',
65
69
  // Extract only the relationship refs needed by client (don't send entire config)
66
70
  const relationshipRefs = {};
67
71
  Object.entries(listConfig.fields).forEach(([fieldName, field]) => {
68
- if ('type' in field && field.type === 'relationship' && 'ref' in field && field.ref) {
72
+ if ('type' in field &&
73
+ field.type === 'relationship' &&
74
+ 'ref' in field &&
75
+ typeof field.ref === 'string') {
69
76
  relationshipRefs[fieldName] = field.ref;
70
77
  }
71
78
  });
@@ -1,13 +1,14 @@
1
- import { AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
2
- export interface NavigationProps<TPrisma> {
3
- context: AccessContext<TPrisma>;
1
+ import { type AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
2
+ export interface NavigationProps {
3
+ context: AccessContext<unknown>;
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({ context, config, basePath, currentPath, onSignOut, }: NavigationProps): 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,KAAK,aAAa,EAAa,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAGpF,MAAM,WAAW,eAAe;IAC9B,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,EACzB,OAAO,EACP,MAAM,EACN,QAAmB,EACnB,WAAgB,EAChB,SAAS,GACV,EAAE,eAAe,2CAkFjB"}
@@ -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"}