@opensaas/stack-ui 0.3.0 → 0.5.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.
@@ -1,11 +1,11 @@
1
1
 
2
- > @opensaas/stack-ui@0.3.0 build /home/runner/work/stack/stack/packages/ui
2
+ > @opensaas/stack-ui@0.5.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
6
  npm warn Unknown env config "npm-globalconfig". This will stop working in the next major version of npm.
7
7
  npm warn Unknown env config "_jsr-registry". This will stop working in the next major version of npm.
8
8
 
9
- > @opensaas/stack-ui@0.3.0 build:css
9
+ > @opensaas/stack-ui@0.5.0 build:css
10
10
  > mkdir -p dist/styles && postcss ./src/styles/globals.css -o ./dist/styles/globals.css
11
11
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,143 @@
1
1
  # @opensaas/stack-ui
2
2
 
3
+ ## 0.5.0
4
+
5
+ ## 0.4.0
6
+
7
+ ### Minor Changes
8
+
9
+ - [#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
10
+
11
+ 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.
12
+
13
+ **New Features:**
14
+ - Added `virtual()` field type for defining computed fields in your schema
15
+ - Virtual fields are automatically excluded from database schema and input types
16
+ - Virtual fields appear in output types with full TypeScript autocomplete
17
+ - Virtual fields support `resolveOutput` hooks for custom computation logic
18
+
19
+ **Type System Improvements:**
20
+ - Generated Context type now properly extends AccessContext from core
21
+ - Separate Input and Output types (e.g., `UserOutput` includes virtual fields, `UserCreateInput` does not)
22
+ - UI components now accept `AccessContext<any>` for better compatibility with custom context types
23
+ - Type aliases provide convenience (e.g., `User = UserOutput`)
24
+
25
+ **Example Usage:**
26
+
27
+ ```typescript
28
+ import { list, text, virtual } from '@opensaas/stack-core'
29
+
30
+ export default config({
31
+ lists: {
32
+
33
+ fields: {
34
+ name: text(),
35
+ email: text(),
36
+ displayName: virtual({
37
+ type: 'string',
38
+ hooks: {
39
+ resolveOutput: async ({ item }) => {
40
+ return `${item.name} (${item.email})`
41
+ },
42
+ },
43
+ }),
44
+ },
45
+ }),
46
+ },
47
+ })
48
+ ```
49
+
50
+ 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.
51
+
52
+ ### Patch Changes
53
+
54
+ - [#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
55
+
56
+ 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.
57
+
58
+ ## Field Type Inference Improvements
59
+
60
+ 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.
61
+
62
+ **Example:**
63
+
64
+ ```typescript
65
+ // Before
66
+
67
+ fields: {
68
+ displayName: virtual<Lists.User.TypeInfo>({
69
+ type: 'string',
70
+ hooks: {
71
+ resolveOutput: ({ item }) => `${item.name} (${item.email})`,
72
+ },
73
+ }),
74
+ },
75
+ })
76
+
77
+ // After
78
+
79
+ fields: {
80
+ displayName: virtual({
81
+ type: 'string',
82
+ hooks: {
83
+ resolveOutput: ({ item }) => `${item.name} (${item.email})`,
84
+ },
85
+ }),
86
+ },
87
+ })
88
+ ```
89
+
90
+ ## List-Level Hooks Type Inference Fix
91
+
92
+ 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:
93
+ 1. `TOutput` - The item type (what's stored in DB)
94
+ 2. `TCreateInput` - Prisma create input type
95
+ 3. `TUpdateInput` - Prisma update input type
96
+
97
+ **Impact:**
98
+ - `resolveInput` now receives proper Prisma input types (e.g., `PostCreateInput`, `PostUpdateInput`)
99
+ - `validateInput` has access to properly typed input data
100
+ - `beforeOperation` and `afterOperation` have correct item types
101
+ - All list-level hook callbacks now get full IntelliSense and type checking
102
+
103
+ **Example:**
104
+
105
+ ```typescript
106
+ Post: list<Lists.Post.TypeInfo>({
107
+ fields: { title: text(), content: text() },
108
+ hooks: {
109
+ resolveInput: async ({ operation, resolvedData }) => {
110
+ // ✅ resolvedData is now properly typed as PostCreateInput or PostUpdateInput
111
+ // ✅ Full autocomplete for title, content, etc.
112
+ if (operation === 'create') {
113
+ console.log(resolvedData.title) // TypeScript knows this is string | undefined
114
+ }
115
+ return resolvedData
116
+ },
117
+ beforeOperation: async ({ operation, item }) => {
118
+ // ✅ item is now properly typed as Post with all fields
119
+ if (operation === 'update' && item) {
120
+ console.log(item.title) // TypeScript knows this is string
121
+ console.log(item.createdAt) // TypeScript knows this is Date
122
+ }
123
+ },
124
+ },
125
+ })
126
+ ```
127
+
128
+ ## Breaking Changes
129
+ - Field types now accept full `TTypeInfo extends TypeInfo` instead of just `TItem`
130
+ - `FieldsWithItemType` utility replaced with `FieldsWithTypeInfo`
131
+ - All field builders updated to use new type signature
132
+ - List-level hooks now receive properly typed parameters (may reveal existing type errors)
133
+
134
+ ## Benefits
135
+ - ✨ Cleaner code without manual type parameter repetition
136
+ - 🎯 Better type inference in both field-level and list-level hooks
137
+ - 🔄 Consistent type flow from list configuration down to individual fields
138
+ - 🛡️ Maintained full type safety with improved DX
139
+ - 💡 Full IntelliSense support in all hook callbacks
140
+
3
141
  ## 0.3.0
4
142
 
5
143
  ## 0.2.0
@@ -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?: {
@@ -21,5 +21,5 @@ export interface AdminUIProps<TPrisma> {
21
21
  * - [list, 'create'] → ItemForm (create)
22
22
  * - [list, id] → ItemForm (edit)
23
23
  */
24
- export declare function AdminUI<TPrisma>({ context, config, params, searchParams, basePath, serverAction, onSignOut, }: 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;
25
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;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"}
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"}
@@ -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,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,6 +1,6 @@
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;
@@ -10,5 +10,5 @@ export interface NavigationProps<TPrisma> {
10
10
  * Navigation sidebar showing all lists
11
11
  * Server Component
12
12
  */
13
- export declare function Navigation<TPrisma>({ context, config, basePath, currentPath, onSignOut, }: 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;
14
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;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"}
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"}
@@ -1 +1 @@
1
- {"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../src/primitives/button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAIjE,QAAA,MAAM,cAAc;;;8EAwBnB,CAAA;AAED,MAAM,WAAW,WACf,SAAQ,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EACnD,YAAY,CAAC,OAAO,cAAc,CAAC;IACrC,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,QAAA,MAAM,MAAM,uFAOX,CAAA;AAGD,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA"}
1
+ {"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../src/primitives/button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAIjE,QAAA,MAAM,cAAc;;;8EAwBnB,CAAA;AAED,MAAM,WAAW,WACf,SAAQ,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,YAAY,CAAC,OAAO,cAAc,CAAC;IAC1F,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,QAAA,MAAM,MAAM,uFAOX,CAAA;AAGD,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA"}
@@ -1,4 +1,4 @@
1
- /*! tailwindcss v4.1.17 | MIT License | https://tailwindcss.com */
1
+ /*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */
2
2
  @layer properties;
3
3
  @layer theme, base, components, utilities;
4
4
  @layer theme {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opensaas/stack-ui",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Composable React UI components for OpenSaas Stack",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -60,41 +60,41 @@
60
60
  "@radix-ui/react-checkbox": "^1.3.3",
61
61
  "@radix-ui/react-dialog": "^1.1.15",
62
62
  "@radix-ui/react-dropdown-menu": "^2.1.16",
63
- "@radix-ui/react-label": "^2.1.7",
63
+ "@radix-ui/react-label": "^2.1.8",
64
64
  "@radix-ui/react-popover": "^1.1.15",
65
65
  "@radix-ui/react-select": "^2.2.6",
66
- "@radix-ui/react-separator": "^1.1.7",
67
- "@radix-ui/react-slot": "^1.2.3",
66
+ "@radix-ui/react-separator": "^1.1.8",
67
+ "@radix-ui/react-slot": "^1.2.4",
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.554.0",
72
- "react-hook-form": "^7.54.2",
73
- "tailwind-merge": "^3.3.1"
71
+ "lucide-react": "^0.561.0",
72
+ "react-hook-form": "^7.68.0",
73
+ "tailwind-merge": "^3.4.0"
74
74
  },
75
75
  "devDependencies": {
76
- "@tailwindcss/postcss": "^4.0.0",
76
+ "@tailwindcss/postcss": "^4.1.17",
77
77
  "@testing-library/jest-dom": "^6.9.1",
78
- "@testing-library/react": "^16.1.0",
79
- "@testing-library/user-event": "^14.5.2",
80
- "@types/node": "^24.7.2",
81
- "@types/react": "^19.2.2",
82
- "@types/react-dom": "^19.2.2",
83
- "@vitejs/plugin-react": "^5.0.0",
84
- "@vitest/browser": "^4.0.9",
85
- "@vitest/browser-playwright": "^4.0.9",
86
- "@vitest/coverage-v8": "^4.0.4",
87
- "happy-dom": "^20.0.0",
88
- "next": "^16.0.1",
89
- "playwright": "^1.56.1",
90
- "postcss": "^8.4.49",
91
- "postcss-cli": "^11.0.0",
92
- "react": "^19.2.0",
93
- "react-dom": "^19.2.0",
94
- "tailwindcss": "^4.0.0",
78
+ "@testing-library/react": "^16.3.0",
79
+ "@testing-library/user-event": "^14.6.1",
80
+ "@types/node": "^24.10.1",
81
+ "@types/react": "^19.2.7",
82
+ "@types/react-dom": "^19.2.3",
83
+ "@vitejs/plugin-react": "^5.1.1",
84
+ "@vitest/browser": "^4.0.15",
85
+ "@vitest/browser-playwright": "^4.0.15",
86
+ "@vitest/coverage-v8": "^4.0.15",
87
+ "happy-dom": "^20.0.11",
88
+ "next": "^16.0.7",
89
+ "playwright": "^1.57.0",
90
+ "postcss": "^8.5.6",
91
+ "postcss-cli": "^11.0.1",
92
+ "react": "^19.2.1",
93
+ "react-dom": "^19.2.1",
94
+ "tailwindcss": "^4.1.17",
95
95
  "typescript": "^5.9.3",
96
- "vitest": "^4.0.0",
97
- "@opensaas/stack-core": "0.3.0"
96
+ "vitest": "^4.0.15",
97
+ "@opensaas/stack-core": "0.5.0"
98
98
  },
99
99
  "scripts": {
100
100
  "build": "tsc && npm run build:css",
@@ -4,11 +4,11 @@ import { Dashboard } from './Dashboard.js'
4
4
  import { ListView } from './ListView.js'
5
5
  import { ItemForm } from './ItemForm.js'
6
6
  import type { ServerActionInput } from '../server/types.js'
7
- import { AccessContext, getListKeyFromUrl, OpenSaasConfig } from '@opensaas/stack-core'
7
+ import { type AccessContext, getListKeyFromUrl, OpenSaasConfig } from '@opensaas/stack-core'
8
8
  import { generateThemeCSS } from '../lib/theme.js'
9
9
 
10
- export interface AdminUIProps<TPrisma> {
11
- context: AccessContext<TPrisma>
10
+ export interface AdminUIProps {
11
+ context: AccessContext<unknown>
12
12
  config: OpenSaasConfig
13
13
  params?: string[]
14
14
  searchParams?: { [key: string]: string | string[] | undefined }
@@ -28,7 +28,7 @@ export interface AdminUIProps<TPrisma> {
28
28
  * - [list, 'create'] → ItemForm (create)
29
29
  * - [list, id] → ItemForm (edit)
30
30
  */
31
- export function AdminUI<TPrisma>({
31
+ export function AdminUI({
32
32
  context,
33
33
  config,
34
34
  params = [],
@@ -36,7 +36,7 @@ export function AdminUI<TPrisma>({
36
36
  basePath = '/admin',
37
37
  serverAction,
38
38
  onSignOut,
39
- }: AdminUIProps<TPrisma>) {
39
+ }: AdminUIProps) {
40
40
  // Parse route from params
41
41
  const [urlSegment, action] = params
42
42
 
@@ -1,10 +1,10 @@
1
1
  import Link from 'next/link.js'
2
2
  import { formatListName } from '../lib/utils.js'
3
- import { AccessContext, getDbKey, getUrlKey, OpenSaasConfig } from '@opensaas/stack-core'
3
+ import { type AccessContext, getDbKey, getUrlKey, OpenSaasConfig } from '@opensaas/stack-core'
4
4
  import { Card, CardContent, CardHeader, CardTitle } from '../primitives/card.js'
5
5
 
6
- export interface DashboardProps<TPrisma> {
7
- context: AccessContext<TPrisma>
6
+ export interface DashboardProps {
7
+ context: AccessContext<unknown>
8
8
  config: OpenSaasConfig
9
9
  basePath?: string
10
10
  }
@@ -13,19 +13,16 @@ export interface DashboardProps<TPrisma> {
13
13
  * Dashboard landing page showing all available lists
14
14
  * Server Component
15
15
  */
16
- export async function Dashboard<TPrisma>({
17
- context,
18
- config,
19
- basePath = '/admin',
20
- }: DashboardProps<TPrisma>) {
16
+ export async function Dashboard({ context, config, basePath = '/admin' }: DashboardProps) {
21
17
  const lists = Object.keys(config.lists || {})
22
18
 
23
19
  // Get counts for each list
24
20
  const listCounts = await Promise.all(
25
21
  lists.map(async (listKey) => {
26
22
  try {
27
- const count = await context.db[getDbKey(listKey)]?.count()
28
- return { listKey, count: count || 0 }
23
+ const delegate = context.db[getDbKey(listKey)]
24
+ const count = delegate?.count ? await delegate.count() : 0
25
+ return { listKey, count }
29
26
  } catch (error) {
30
27
  console.error(`Failed to get count for ${listKey}:`, error)
31
28
  return { listKey, count: 0 }
@@ -3,11 +3,11 @@ import Link from 'next/link.js'
3
3
  import { ItemFormClient } from './ItemFormClient.js'
4
4
  import { formatListName } from '../lib/utils.js'
5
5
  import type { ServerActionInput } from '../server/types.js'
6
- import { AccessContext, getDbKey, getUrlKey, OpenSaasConfig } from '@opensaas/stack-core'
6
+ import { type AccessContext, getDbKey, getUrlKey, OpenSaasConfig } from '@opensaas/stack-core'
7
7
  import { serializeFieldConfigs } from '../lib/serializeFieldConfig.js'
8
8
 
9
- export interface ItemFormProps<TPrisma> {
10
- context: AccessContext<TPrisma>
9
+ export interface ItemFormProps {
10
+ context: AccessContext<unknown>
11
11
  config: OpenSaasConfig
12
12
  listKey: string
13
13
  mode: 'create' | 'edit'
@@ -21,7 +21,7 @@ export interface ItemFormProps<TPrisma> {
21
21
  * Item form component - create or edit an item
22
22
  * Server Component that fetches data and sets up actions
23
23
  */
24
- export async function ItemForm<TPrisma>({
24
+ export async function ItemForm({
25
25
  context,
26
26
  config,
27
27
  listKey,
@@ -29,7 +29,7 @@ export async function ItemForm<TPrisma>({
29
29
  itemId,
30
30
  basePath = '/admin',
31
31
  serverAction,
32
- }: ItemFormProps<TPrisma>) {
32
+ }: ItemFormProps) {
33
33
  const listConfig = config.lists[listKey]
34
34
  const urlKey = getUrlKey(listKey)
35
35
 
@@ -58,10 +58,13 @@ export async function ItemForm<TPrisma>({
58
58
  }
59
59
 
60
60
  // Fetch item with relationships included
61
- itemData = await context.db[getDbKey(listKey)].findUnique({
62
- where: { id: itemId },
63
- ...(Object.keys(includeRelationships).length > 0 && { include: includeRelationships }),
64
- })
61
+ const delegate = context.db[getDbKey(listKey)]
62
+ if (delegate?.findUnique) {
63
+ itemData = await delegate.findUnique({
64
+ where: { id: itemId },
65
+ ...(Object.keys(includeRelationships).length > 0 && { include: includeRelationships }),
66
+ })
67
+ }
65
68
  } catch (error) {
66
69
  console.error(`Failed to fetch item ${itemId}:`, error)
67
70
  }
@@ -102,7 +105,8 @@ export async function ItemForm<TPrisma>({
102
105
  if (relatedListConfig) {
103
106
  try {
104
107
  const dbContext = context.db
105
- const relatedItems = await dbContext[getDbKey(relatedListName)].findMany({})
108
+ const delegate = dbContext[getDbKey(relatedListName)]
109
+ const relatedItems = delegate?.findMany ? await delegate.findMany({}) : []
106
110
 
107
111
  // Use 'name' field as label if it exists, otherwise use 'id'
108
112
  relationshipData[fieldName] = relatedItems.map((item: Record<string, unknown>) => ({
@@ -1,16 +1,10 @@
1
1
  import Link from 'next/link.js'
2
2
  import { ListViewClient } from './ListViewClient.js'
3
3
  import { formatListName } from '../lib/utils.js'
4
- import {
5
- AccessContext,
6
- getDbKey,
7
- getUrlKey,
8
- OpenSaasConfig,
9
- type PrismaClientLike,
10
- } from '@opensaas/stack-core'
4
+ import { type AccessContext, getDbKey, getUrlKey, OpenSaasConfig } from '@opensaas/stack-core'
11
5
 
12
- export interface ListViewProps<TPrisma extends PrismaClientLike = PrismaClientLike> {
13
- context: AccessContext<TPrisma>
6
+ export interface ListViewProps {
7
+ context: AccessContext<unknown>
14
8
  config: OpenSaasConfig
15
9
  listKey: string
16
10
  basePath?: string
@@ -24,7 +18,7 @@ export interface ListViewProps<TPrisma extends PrismaClientLike = PrismaClientLi
24
18
  * List view component - displays items in a table
25
19
  * Server Component that fetches data and renders client table
26
20
  */
27
- export async function ListView<TPrisma extends PrismaClientLike = PrismaClientLike>({
21
+ export async function ListView({
28
22
  context,
29
23
  config,
30
24
  listKey,
@@ -33,7 +27,7 @@ export async function ListView<TPrisma extends PrismaClientLike = PrismaClientLi
33
27
  page = 1,
34
28
  pageSize = 50,
35
29
  search,
36
- }: ListViewProps<TPrisma>) {
30
+ }: ListViewProps) {
37
31
  const key = getDbKey(listKey)
38
32
  const urlKey = getUrlKey(listKey)
39
33
  const listConfig = config.lists[listKey]
@@ -86,15 +80,18 @@ export async function ListView<TPrisma extends PrismaClientLike = PrismaClientLi
86
80
  include[fieldName] = true
87
81
  }
88
82
  })
89
- ;[items, total] = await Promise.all([
90
- dbContext[key].findMany({
91
- where,
92
- skip,
93
- take: pageSize,
94
- ...(Object.keys(include).length > 0 ? { include } : {}),
95
- }),
96
- dbContext[key].count({ where }),
97
- ])
83
+ const delegate = dbContext[key]
84
+ if (delegate?.findMany && delegate?.count) {
85
+ ;[items, total] = await Promise.all([
86
+ delegate.findMany({
87
+ where,
88
+ skip,
89
+ take: pageSize,
90
+ ...(Object.keys(include).length > 0 ? { include } : {}),
91
+ }),
92
+ delegate.count({ where }),
93
+ ])
94
+ }
98
95
  } catch (error) {
99
96
  console.error(`Failed to fetch ${listKey}:`, error)
100
97
  }
@@ -105,7 +102,12 @@ export async function ListView<TPrisma extends PrismaClientLike = PrismaClientLi
105
102
  // Extract only the relationship refs needed by client (don't send entire config)
106
103
  const relationshipRefs: Record<string, string> = {}
107
104
  Object.entries(listConfig.fields).forEach(([fieldName, field]) => {
108
- if ('type' in field && field.type === 'relationship' && 'ref' in field && field.ref) {
105
+ if (
106
+ 'type' in field &&
107
+ field.type === 'relationship' &&
108
+ 'ref' in field &&
109
+ typeof field.ref === 'string'
110
+ ) {
109
111
  relationshipRefs[fieldName] = field.ref
110
112
  }
111
113
  })
@@ -1,10 +1,10 @@
1
1
  import Link from 'next/link.js'
2
2
  import { formatListName } from '../lib/utils.js'
3
- import { AccessContext, getUrlKey, OpenSaasConfig } from '@opensaas/stack-core'
3
+ import { type AccessContext, getUrlKey, OpenSaasConfig } from '@opensaas/stack-core'
4
4
  import { UserMenu } from './UserMenu.js'
5
5
 
6
- export interface NavigationProps<TPrisma> {
7
- context: AccessContext<TPrisma>
6
+ export interface NavigationProps {
7
+ context: AccessContext<unknown>
8
8
  config: OpenSaasConfig
9
9
  basePath?: string
10
10
  currentPath?: string
@@ -15,13 +15,13 @@ export interface NavigationProps<TPrisma> {
15
15
  * Navigation sidebar showing all lists
16
16
  * Server Component
17
17
  */
18
- export function Navigation<TPrisma>({
18
+ export function Navigation({
19
19
  context,
20
20
  config,
21
21
  basePath = '/admin',
22
22
  currentPath = '',
23
23
  onSignOut,
24
- }: NavigationProps<TPrisma>) {
24
+ }: NavigationProps) {
25
25
  const lists = Object.keys(config.lists || {})
26
26
 
27
27
  return (
@@ -31,8 +31,7 @@ const buttonVariants = cva(
31
31
  )
32
32
 
33
33
  export interface ButtonProps
34
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
35
- VariantProps<typeof buttonVariants> {
34
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
36
35
  asChild?: boolean
37
36
  }
38
37