@opensaas/stack-ui 0.1.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 (203) hide show
  1. package/.turbo/turbo-build.log +8 -0
  2. package/README.md +286 -0
  3. package/dist/components/AdminUI.d.ts +24 -0
  4. package/dist/components/AdminUI.d.ts.map +1 -0
  5. package/dist/components/AdminUI.js +48 -0
  6. package/dist/components/ConfirmDialog.d.ts +16 -0
  7. package/dist/components/ConfirmDialog.d.ts.map +1 -0
  8. package/dist/components/ConfirmDialog.js +11 -0
  9. package/dist/components/Dashboard.d.ts +12 -0
  10. package/dist/components/Dashboard.d.ts.map +1 -0
  11. package/dist/components/Dashboard.js +30 -0
  12. package/dist/components/ItemForm.d.ts +17 -0
  13. package/dist/components/ItemForm.d.ts.map +1 -0
  14. package/dist/components/ItemForm.js +97 -0
  15. package/dist/components/ItemFormClient.d.ts +22 -0
  16. package/dist/components/ItemFormClient.d.ts.map +1 -0
  17. package/dist/components/ItemFormClient.js +127 -0
  18. package/dist/components/ListView.d.ts +17 -0
  19. package/dist/components/ListView.d.ts.map +1 -0
  20. package/dist/components/ListView.js +76 -0
  21. package/dist/components/ListViewClient.d.ts +19 -0
  22. package/dist/components/ListViewClient.d.ts.map +1 -0
  23. package/dist/components/ListViewClient.js +108 -0
  24. package/dist/components/LoadingSpinner.d.ts +10 -0
  25. package/dist/components/LoadingSpinner.d.ts.map +1 -0
  26. package/dist/components/LoadingSpinner.js +14 -0
  27. package/dist/components/Navigation.d.ts +13 -0
  28. package/dist/components/Navigation.d.ts.map +1 -0
  29. package/dist/components/Navigation.js +20 -0
  30. package/dist/components/SkeletonLoader.d.ts +22 -0
  31. package/dist/components/SkeletonLoader.d.ts.map +1 -0
  32. package/dist/components/SkeletonLoader.js +25 -0
  33. package/dist/components/fields/CheckboxField.d.ts +11 -0
  34. package/dist/components/fields/CheckboxField.d.ts.map +1 -0
  35. package/dist/components/fields/CheckboxField.js +10 -0
  36. package/dist/components/fields/ComboboxField.d.ts +18 -0
  37. package/dist/components/fields/ComboboxField.d.ts.map +1 -0
  38. package/dist/components/fields/ComboboxField.js +32 -0
  39. package/dist/components/fields/FieldRenderer.d.ts +22 -0
  40. package/dist/components/fields/FieldRenderer.d.ts.map +1 -0
  41. package/dist/components/fields/FieldRenderer.js +81 -0
  42. package/dist/components/fields/IntegerField.d.ts +15 -0
  43. package/dist/components/fields/IntegerField.d.ts.map +1 -0
  44. package/dist/components/fields/IntegerField.js +14 -0
  45. package/dist/components/fields/PasswordField.d.ts +18 -0
  46. package/dist/components/fields/PasswordField.d.ts.map +1 -0
  47. package/dist/components/fields/PasswordField.js +42 -0
  48. package/dist/components/fields/RelationshipField.d.ts +20 -0
  49. package/dist/components/fields/RelationshipField.d.ts.map +1 -0
  50. package/dist/components/fields/RelationshipField.js +11 -0
  51. package/dist/components/fields/RelationshipManager.d.ts +19 -0
  52. package/dist/components/fields/RelationshipManager.d.ts.map +1 -0
  53. package/dist/components/fields/RelationshipManager.js +37 -0
  54. package/dist/components/fields/SelectField.d.ts +16 -0
  55. package/dist/components/fields/SelectField.d.ts.map +1 -0
  56. package/dist/components/fields/SelectField.js +11 -0
  57. package/dist/components/fields/TextField.d.ts +13 -0
  58. package/dist/components/fields/TextField.d.ts.map +1 -0
  59. package/dist/components/fields/TextField.js +11 -0
  60. package/dist/components/fields/TimestampField.d.ts +12 -0
  61. package/dist/components/fields/TimestampField.d.ts.map +1 -0
  62. package/dist/components/fields/TimestampField.js +12 -0
  63. package/dist/components/fields/index.d.ts +23 -0
  64. package/dist/components/fields/index.d.ts.map +1 -0
  65. package/dist/components/fields/index.js +13 -0
  66. package/dist/components/fields/registry.d.ts +43 -0
  67. package/dist/components/fields/registry.d.ts.map +1 -0
  68. package/dist/components/fields/registry.js +42 -0
  69. package/dist/components/standalone/DeleteButton.d.ts +35 -0
  70. package/dist/components/standalone/DeleteButton.d.ts.map +1 -0
  71. package/dist/components/standalone/DeleteButton.js +46 -0
  72. package/dist/components/standalone/ItemCreateForm.d.ts +34 -0
  73. package/dist/components/standalone/ItemCreateForm.d.ts.map +1 -0
  74. package/dist/components/standalone/ItemCreateForm.js +91 -0
  75. package/dist/components/standalone/ItemEditForm.d.ts +37 -0
  76. package/dist/components/standalone/ItemEditForm.d.ts.map +1 -0
  77. package/dist/components/standalone/ItemEditForm.js +112 -0
  78. package/dist/components/standalone/ListTable.d.ts +33 -0
  79. package/dist/components/standalone/ListTable.d.ts.map +1 -0
  80. package/dist/components/standalone/ListTable.js +94 -0
  81. package/dist/components/standalone/SearchBar.d.ts +29 -0
  82. package/dist/components/standalone/SearchBar.d.ts.map +1 -0
  83. package/dist/components/standalone/SearchBar.js +43 -0
  84. package/dist/components/standalone/index.d.ts +11 -0
  85. package/dist/components/standalone/index.d.ts.map +1 -0
  86. package/dist/components/standalone/index.js +6 -0
  87. package/dist/index.d.ts +27 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +19 -0
  90. package/dist/lib/serializeFieldConfig.d.ts +43 -0
  91. package/dist/lib/serializeFieldConfig.d.ts.map +1 -0
  92. package/dist/lib/serializeFieldConfig.js +48 -0
  93. package/dist/lib/theme.d.ts +17 -0
  94. package/dist/lib/theme.d.ts.map +1 -0
  95. package/dist/lib/theme.js +192 -0
  96. package/dist/lib/utils.d.ts +18 -0
  97. package/dist/lib/utils.d.ts.map +1 -0
  98. package/dist/lib/utils.js +76 -0
  99. package/dist/primitives/button.d.ts +12 -0
  100. package/dist/primitives/button.d.ts.map +1 -0
  101. package/dist/primitives/button.js +33 -0
  102. package/dist/primitives/calendar.d.ts +9 -0
  103. package/dist/primitives/calendar.d.ts.map +1 -0
  104. package/dist/primitives/calendar.js +48 -0
  105. package/dist/primitives/card.d.ts +9 -0
  106. package/dist/primitives/card.d.ts.map +1 -0
  107. package/dist/primitives/card.js +16 -0
  108. package/dist/primitives/checkbox.d.ts +5 -0
  109. package/dist/primitives/checkbox.d.ts.map +1 -0
  110. package/dist/primitives/checkbox.js +7 -0
  111. package/dist/primitives/combobox.d.ts +14 -0
  112. package/dist/primitives/combobox.d.ts.map +1 -0
  113. package/dist/primitives/combobox.js +20 -0
  114. package/dist/primitives/datetime-picker.d.ts +9 -0
  115. package/dist/primitives/datetime-picker.d.ts.map +1 -0
  116. package/dist/primitives/datetime-picker.js +42 -0
  117. package/dist/primitives/dialog.d.ts +20 -0
  118. package/dist/primitives/dialog.d.ts.map +1 -0
  119. package/dist/primitives/dialog.js +21 -0
  120. package/dist/primitives/index.d.ts +14 -0
  121. package/dist/primitives/index.d.ts.map +1 -0
  122. package/dist/primitives/index.js +14 -0
  123. package/dist/primitives/input.d.ts +5 -0
  124. package/dist/primitives/input.d.ts.map +1 -0
  125. package/dist/primitives/input.js +8 -0
  126. package/dist/primitives/label.d.ts +6 -0
  127. package/dist/primitives/label.d.ts.map +1 -0
  128. package/dist/primitives/label.js +9 -0
  129. package/dist/primitives/popover.d.ts +7 -0
  130. package/dist/primitives/popover.d.ts.map +1 -0
  131. package/dist/primitives/popover.js +10 -0
  132. package/dist/primitives/select.d.ts +14 -0
  133. package/dist/primitives/select.d.ts.map +1 -0
  134. package/dist/primitives/select.js +24 -0
  135. package/dist/primitives/table.d.ts +11 -0
  136. package/dist/primitives/table.d.ts.map +1 -0
  137. package/dist/primitives/table.js +20 -0
  138. package/dist/primitives/time-picker.d.ts +8 -0
  139. package/dist/primitives/time-picker.d.ts.map +1 -0
  140. package/dist/primitives/time-picker.js +27 -0
  141. package/dist/server/index.d.ts +2 -0
  142. package/dist/server/index.d.ts.map +1 -0
  143. package/dist/server/index.js +2 -0
  144. package/dist/server/types.d.ts +15 -0
  145. package/dist/server/types.d.ts.map +1 -0
  146. package/dist/server/types.js +1 -0
  147. package/dist/styles/globals.css +1896 -0
  148. package/package.json +91 -0
  149. package/postcss.config.cjs +5 -0
  150. package/src/components/AdminUI.tsx +112 -0
  151. package/src/components/ConfirmDialog.tsx +56 -0
  152. package/src/components/Dashboard.tsx +134 -0
  153. package/src/components/ItemForm.tsx +195 -0
  154. package/src/components/ItemFormClient.tsx +237 -0
  155. package/src/components/ListView.tsx +153 -0
  156. package/src/components/ListViewClient.tsx +282 -0
  157. package/src/components/LoadingSpinner.tsx +32 -0
  158. package/src/components/Navigation.tsx +117 -0
  159. package/src/components/SkeletonLoader.tsx +82 -0
  160. package/src/components/fields/CheckboxField.tsx +54 -0
  161. package/src/components/fields/ComboboxField.tsx +127 -0
  162. package/src/components/fields/FieldRenderer.tsx +132 -0
  163. package/src/components/fields/IntegerField.tsx +68 -0
  164. package/src/components/fields/PasswordField.tsx +159 -0
  165. package/src/components/fields/RelationshipField.tsx +71 -0
  166. package/src/components/fields/RelationshipManager.tsx +189 -0
  167. package/src/components/fields/SelectField.tsx +71 -0
  168. package/src/components/fields/TextField.tsx +59 -0
  169. package/src/components/fields/TimestampField.tsx +49 -0
  170. package/src/components/fields/index.ts +27 -0
  171. package/src/components/fields/registry.ts +72 -0
  172. package/src/components/standalone/DeleteButton.tsx +114 -0
  173. package/src/components/standalone/ItemCreateForm.tsx +161 -0
  174. package/src/components/standalone/ItemEditForm.tsx +193 -0
  175. package/src/components/standalone/ListTable.tsx +211 -0
  176. package/src/components/standalone/SearchBar.tsx +86 -0
  177. package/src/components/standalone/index.ts +13 -0
  178. package/src/index.ts +74 -0
  179. package/src/lib/serializeFieldConfig.ts +88 -0
  180. package/src/lib/theme.ts +202 -0
  181. package/src/lib/utils.ts +81 -0
  182. package/src/primitives/button.tsx +49 -0
  183. package/src/primitives/calendar.tsx +160 -0
  184. package/src/primitives/card.tsx +58 -0
  185. package/src/primitives/checkbox.tsx +27 -0
  186. package/src/primitives/combobox.tsx +159 -0
  187. package/src/primitives/datetime-picker.tsx +130 -0
  188. package/src/primitives/dialog.tsx +108 -0
  189. package/src/primitives/index.ts +54 -0
  190. package/src/primitives/input.tsx +24 -0
  191. package/src/primitives/label.tsx +19 -0
  192. package/src/primitives/popover.tsx +31 -0
  193. package/src/primitives/select.tsx +158 -0
  194. package/src/primitives/table.tsx +91 -0
  195. package/src/primitives/time-picker.tsx +65 -0
  196. package/src/server/index.ts +3 -0
  197. package/src/server/types.ts +15 -0
  198. package/src/styles/globals.css +123 -0
  199. package/tailwind.config.ts +3 -0
  200. package/tests/components/TextField.test.tsx +94 -0
  201. package/tests/setup.ts +11 -0
  202. package/tsconfig.json +26 -0
  203. package/vitest.config.ts +22 -0
@@ -0,0 +1,127 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState, useTransition } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { FieldRenderer } from './fields/FieldRenderer.js';
6
+ import { ConfirmDialog } from './ConfirmDialog.js';
7
+ import { LoadingSpinner } from './LoadingSpinner.js';
8
+ import { Button } from '../primitives/button.js';
9
+ /**
10
+ * Client component for interactive form
11
+ * Handles form state, validation, and submission
12
+ */
13
+ export function ItemFormClient({ listKey, urlKey, mode, fields, initialData = {}, itemId, basePath, serverAction, relationshipData = {}, }) {
14
+ const router = useRouter();
15
+ const [isPending, startTransition] = useTransition();
16
+ const [formData, setFormData] = useState(initialData);
17
+ const [errors, setErrors] = useState({});
18
+ const [generalError, setGeneralError] = useState(null);
19
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
20
+ const handleFieldChange = (fieldName, value) => {
21
+ setFormData((prev) => ({ ...prev, [fieldName]: value }));
22
+ // Clear error for this field when user starts typing
23
+ if (errors[fieldName]) {
24
+ setErrors((prev) => {
25
+ const newErrors = { ...prev };
26
+ delete newErrors[fieldName];
27
+ return newErrors;
28
+ });
29
+ }
30
+ };
31
+ const handleSubmit = async (e) => {
32
+ e.preventDefault();
33
+ setErrors({});
34
+ setGeneralError(null);
35
+ startTransition(async () => {
36
+ try {
37
+ // Transform relationship fields to Prisma format
38
+ // Filter out password fields with isSet objects (unchanged passwords)
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
+ };
55
+ }
56
+ }
57
+ else {
58
+ // Single relationship: use connect format
59
+ if (value) {
60
+ transformedData[fieldName] = {
61
+ connect: { id: value },
62
+ };
63
+ }
64
+ }
65
+ }
66
+ else {
67
+ // Non-relationship field: pass through
68
+ transformedData[fieldName] = value;
69
+ }
70
+ }
71
+ const result = mode === 'create'
72
+ ? await serverAction({
73
+ listKey,
74
+ action: 'create',
75
+ data: transformedData,
76
+ })
77
+ : await serverAction({
78
+ listKey,
79
+ action: 'update',
80
+ id: itemId,
81
+ data: transformedData,
82
+ });
83
+ if (result) {
84
+ // Navigate back to list view
85
+ router.push(`${basePath}/${urlKey}`);
86
+ router.refresh();
87
+ }
88
+ else {
89
+ setGeneralError('Access denied or operation failed');
90
+ }
91
+ }
92
+ catch (error) {
93
+ const errorMessage = error instanceof Error ? error.message : 'Failed to save item';
94
+ setGeneralError(errorMessage);
95
+ }
96
+ });
97
+ };
98
+ const handleDelete = async () => {
99
+ if (!itemId)
100
+ return;
101
+ setGeneralError(null);
102
+ setShowDeleteConfirm(false);
103
+ startTransition(async () => {
104
+ try {
105
+ const result = await serverAction({
106
+ listKey,
107
+ action: 'delete',
108
+ id: itemId,
109
+ });
110
+ if (result) {
111
+ router.push(`${basePath}/${urlKey}`);
112
+ router.refresh();
113
+ }
114
+ else {
115
+ setGeneralError('Access denied or failed to delete item');
116
+ }
117
+ }
118
+ catch (error) {
119
+ const errorMessage = error instanceof Error ? error.message : 'Failed to delete item';
120
+ setGeneralError(errorMessage);
121
+ }
122
+ });
123
+ };
124
+ // Filter out system fields
125
+ const editableFields = Object.entries(fields).filter(([key]) => !['id', 'createdAt', 'updatedAt'].includes(key));
126
+ return (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-6", children: [generalError && (_jsx("div", { className: "bg-destructive/10 border border-destructive text-destructive rounded-lg p-4", children: _jsx("p", { className: "text-sm font-medium", children: generalError }) })), _jsx("div", { className: "space-y-6", children: editableFields.map(([fieldName, fieldConfig]) => (_jsx(FieldRenderer, { fieldName: fieldName, fieldConfig: fieldConfig, value: formData[fieldName], onChange: (value) => handleFieldChange(fieldName, value), error: errors[fieldName], disabled: isPending, mode: "edit", relationshipItems: relationshipData[fieldName] || [], relationshipLoading: false, basePath: basePath }, fieldName))) }), _jsxs("div", { className: "flex items-center justify-between pt-6 border-t border-border", children: [_jsxs("div", { className: "flex gap-3", children: [_jsxs(Button, { type: "submit", disabled: isPending, className: "gap-2", children: [isPending && (_jsx(LoadingSpinner, { size: "sm", className: "border-primary-foreground border-t-transparent" })), isPending ? 'Saving...' : mode === 'create' ? 'Create' : 'Save'] }), _jsx(Button, { type: "button", variant: "secondary", onClick: () => router.push(`${basePath}/${urlKey}`), disabled: isPending, children: "Cancel" })] }), mode === 'edit' && itemId && (_jsx(Button, { type: "button", variant: "destructive", onClick: () => setShowDeleteConfirm(true), disabled: isPending, children: "Delete" }))] }), _jsx(ConfirmDialog, { isOpen: showDeleteConfirm, title: "Delete Item", message: "Are you sure you want to delete this item? This action cannot be undone.", confirmLabel: "Delete", cancelLabel: "Cancel", variant: "danger", onConfirm: handleDelete, onCancel: () => setShowDeleteConfirm(false) })] }));
127
+ }
@@ -0,0 +1,17 @@
1
+ import { AccessContext, OpenSaasConfig, type PrismaClientLike } from '@opensaas/stack-core';
2
+ export interface ListViewProps<TPrisma extends PrismaClientLike = PrismaClientLike> {
3
+ context: AccessContext<TPrisma>;
4
+ config: OpenSaasConfig;
5
+ listKey: string;
6
+ basePath?: string;
7
+ columns?: string[];
8
+ page?: number;
9
+ pageSize?: number;
10
+ search?: string;
11
+ }
12
+ /**
13
+ * List view component - displays items in a table
14
+ * Server Component that fetches data and renders client table
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>;
17
+ //# sourceMappingURL=ListView.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Link from 'next/link';
3
+ import { ListViewClient } from './ListViewClient.js';
4
+ import { formatListName } from '../lib/utils.js';
5
+ import { getDbKey, getUrlKey, } from '@opensaas/stack-core';
6
+ /**
7
+ * List view component - displays items in a table
8
+ * Server Component that fetches data and renders client table
9
+ */
10
+ export async function ListView({ context, config, listKey, basePath = '/admin', columns, page = 1, pageSize = 50, search, }) {
11
+ const key = getDbKey(listKey);
12
+ const urlKey = getUrlKey(listKey);
13
+ const listConfig = config.lists[listKey];
14
+ if (!listConfig) {
15
+ return (_jsx("div", { className: "p-8", children: _jsxs("div", { className: "bg-destructive/10 border border-destructive text-destructive rounded-lg p-6", children: [_jsx("h2", { className: "text-lg font-semibold mb-2", children: "List not found" }), _jsxs("p", { children: ["The list \"", listKey, "\" does not exist in your configuration."] })] }) }));
16
+ }
17
+ // Fetch items using access-controlled context
18
+ const skip = (page - 1) * pageSize;
19
+ let items = [];
20
+ let total = 0;
21
+ try {
22
+ const dbContext = context.db;
23
+ if (!dbContext || !dbContext[key]) {
24
+ throw new Error(`Context for ${listKey} not found`);
25
+ }
26
+ // Build search filter if search term provided
27
+ let where = undefined;
28
+ if (search && search.trim()) {
29
+ // Find all text fields to search across
30
+ const searchableFields = Object.entries(listConfig.fields)
31
+ .filter(([_, field]) => field.type === 'text')
32
+ .map(([fieldName]) => fieldName);
33
+ if (searchableFields.length > 0) {
34
+ where = {
35
+ OR: searchableFields.map((fieldName) => ({
36
+ [fieldName]: {
37
+ contains: search.trim(),
38
+ },
39
+ })),
40
+ };
41
+ }
42
+ }
43
+ // Build include object for relationship fields
44
+ const include = {};
45
+ Object.entries(listConfig.fields).forEach(([fieldName, field]) => {
46
+ if (field.type === 'relationship') {
47
+ include[fieldName] = true;
48
+ }
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
+ ]);
59
+ }
60
+ catch (error) {
61
+ console.error(`Failed to fetch ${listKey}:`, error);
62
+ }
63
+ // Serialize items for client component (convert Dates, etc to JSON-safe format)
64
+ const serializedItems = JSON.parse(JSON.stringify(items));
65
+ // Extract only the relationship refs needed by client (don't send entire config)
66
+ const relationshipRefs = {};
67
+ Object.entries(listConfig.fields).forEach(([fieldName, field]) => {
68
+ if ('type' in field && field.type === 'relationship' && 'ref' in field && field.ref) {
69
+ relationshipRefs[fieldName] = field.ref;
70
+ }
71
+ });
72
+ return (_jsxs("div", { className: "p-8", children: [_jsxs("div", { className: "flex items-center justify-between mb-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-bold mb-2", children: formatListName(listKey) }), _jsxs("p", { className: "text-muted-foreground", children: [total, " ", total === 1 ? 'item' : 'items'] })] }), _jsxs(Link, { href: `${basePath}/${urlKey}/create`, className: "inline-flex items-center px-4 py-2 bg-primary text-primary-foreground rounded-md font-medium hover:bg-primary/90 transition-colors", children: [_jsx("span", { className: "mr-2", children: "+" }), "Create ", formatListName(listKey)] })] }), _jsx(ListViewClient, { items: serializedItems || [], fieldTypes: Object.fromEntries(Object.entries(listConfig.fields).map(([key, field]) => [
73
+ key,
74
+ field.type,
75
+ ])), relationshipRefs: relationshipRefs, columns: columns, listKey: listKey, urlKey: urlKey, basePath: basePath, page: page, pageSize: pageSize, total: total || 0, search: search })] }));
76
+ }
@@ -0,0 +1,19 @@
1
+ export interface ListViewClientProps {
2
+ items: Array<Record<string, unknown>>;
3
+ fieldTypes: Record<string, string>;
4
+ relationshipRefs: Record<string, string>;
5
+ columns?: string[];
6
+ listKey: string;
7
+ urlKey: string;
8
+ basePath: string;
9
+ page: number;
10
+ pageSize: number;
11
+ total: number;
12
+ search?: string;
13
+ }
14
+ /**
15
+ * Client component for interactive list table
16
+ * Handles sorting, pagination, and row interactions
17
+ */
18
+ export declare function ListViewClient({ items, fieldTypes, relationshipRefs, columns, urlKey, basePath, page, pageSize, total, search: initialSearch, }: ListViewClientProps): import("react/jsx-runtime").JSX.Element;
19
+ //# sourceMappingURL=ListViewClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ListViewClient.d.ts","sourceRoot":"","sources":["../../src/components/ListViewClient.tsx"],"names":[],"mappings":"AAoBA,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IACrC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACxC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,UAAU,EACV,gBAAgB,EAChB,OAAO,EACP,MAAM,EACN,QAAQ,EACR,IAAI,EACJ,QAAQ,EACR,KAAK,EACL,MAAM,EAAE,aAAa,GACtB,EAAE,mBAAmB,2CAwOrB"}
@@ -0,0 +1,108 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from 'react';
4
+ import { useState } from 'react';
5
+ import Link from 'next/link';
6
+ import { useRouter } from 'next/navigation';
7
+ import { formatFieldName, getFieldDisplayValue } from '../lib/utils.js';
8
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '../primitives/table.js';
9
+ import { Input } from '../primitives/input.js';
10
+ import { Button } from '../primitives/button.js';
11
+ import { Card } from '../primitives/card.js';
12
+ import { getUrlKey } from '@opensaas/stack-core';
13
+ /**
14
+ * Client component for interactive list table
15
+ * Handles sorting, pagination, and row interactions
16
+ */
17
+ export function ListViewClient({ items, fieldTypes, relationshipRefs, columns, urlKey, basePath, page, pageSize, total, search: initialSearch, }) {
18
+ const router = useRouter();
19
+ const [sortBy, setSortBy] = useState(null);
20
+ const [sortOrder, setSortOrder] = useState('asc');
21
+ const [searchInput, setSearchInput] = useState(initialSearch || '');
22
+ // Determine which columns to show
23
+ const displayColumns = columns ||
24
+ Object.keys(fieldTypes).filter((key) => !['password', 'createdAt', 'updatedAt'].includes(key));
25
+ // Sort items if needed
26
+ const sortedItems = [...items];
27
+ if (sortBy) {
28
+ sortedItems.sort((a, b) => {
29
+ const aVal = a[sortBy];
30
+ const bVal = b[sortBy];
31
+ if (aVal === bVal)
32
+ return 0;
33
+ // Handle unknown types for comparison - convert to string for safety
34
+ const aStr = String(aVal ?? '');
35
+ const bStr = String(bVal ?? '');
36
+ const comparison = aStr > bStr ? 1 : -1;
37
+ return sortOrder === 'asc' ? comparison : -comparison;
38
+ });
39
+ }
40
+ const totalPages = Math.ceil(total / pageSize);
41
+ const hasNextPage = page < totalPages;
42
+ const hasPrevPage = page > 1;
43
+ const handleSort = (column) => {
44
+ if (sortBy === column) {
45
+ setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
46
+ }
47
+ else {
48
+ setSortBy(column);
49
+ setSortOrder('asc');
50
+ }
51
+ };
52
+ const handleSearch = (e) => {
53
+ e.preventDefault();
54
+ const params = new URLSearchParams();
55
+ if (searchInput.trim()) {
56
+ params.set('search', searchInput.trim());
57
+ }
58
+ params.set('page', '1'); // Reset to page 1 on new search
59
+ router.push(`${basePath}/${urlKey}?${params.toString()}`);
60
+ };
61
+ const handleClearSearch = () => {
62
+ setSearchInput('');
63
+ router.push(`${basePath}/${urlKey}`);
64
+ };
65
+ const buildPaginationUrl = (newPage) => {
66
+ const params = new URLSearchParams();
67
+ if (initialSearch) {
68
+ params.set('search', initialSearch);
69
+ }
70
+ params.set('page', newPage.toString());
71
+ return `${basePath}/${urlKey}?${params.toString()}`;
72
+ };
73
+ /**
74
+ * Render a relationship field as a clickable link or links
75
+ */
76
+ const renderRelationshipCell = (value, fieldName) => {
77
+ const ref = relationshipRefs[fieldName];
78
+ if (!ref) {
79
+ return getFieldDisplayValue(value, 'relationship');
80
+ }
81
+ // Parse ref to get related list name
82
+ const [relatedListKey] = ref.split('.');
83
+ const relatedUrlKey = getUrlKey(relatedListKey);
84
+ if (!value || typeof value !== 'object') {
85
+ return _jsx("span", { className: "text-muted-foreground", children: "-" });
86
+ }
87
+ // Handle array of relationships (many: true)
88
+ if (Array.isArray(value)) {
89
+ if (value.length === 0)
90
+ return _jsx("span", { className: "text-muted-foreground", children: "-" });
91
+ return (_jsx("span", { className: "flex flex-wrap gap-1", children: value.map((item, idx) => {
92
+ if (!item || typeof item !== 'object')
93
+ return null;
94
+ const displayValue = getFieldDisplayValue(item, 'relationship');
95
+ const itemId = 'id' in item ? item.id : null;
96
+ const key = itemId || idx;
97
+ return (_jsxs(React.Fragment, { children: [idx > 0 && _jsx("span", { className: "text-muted-foreground", children: ", " }), _jsx(Link, { href: `${basePath}/${relatedUrlKey}/${itemId}`, className: "text-primary hover:underline", onClick: (e) => e.stopPropagation(), children: displayValue })] }, key));
98
+ }) }));
99
+ }
100
+ // Handle single relationship
101
+ const itemId = 'id' in value ? value.id : null;
102
+ const displayValue = getFieldDisplayValue(value, 'relationship');
103
+ return (_jsx(Link, { href: `${basePath}/${relatedUrlKey}/${itemId}`, className: "text-primary hover:underline", onClick: (e) => e.stopPropagation(), children: displayValue }));
104
+ };
105
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx(Card, { className: "p-4", children: _jsxs("form", { onSubmit: handleSearch, className: "flex gap-2", children: [_jsxs("div", { className: "flex-1 relative", children: [_jsx(Input, { type: "text", value: searchInput, onChange: (e) => setSearchInput(e.target.value), placeholder: "Search...", className: "pr-10" }), searchInput && (_jsx("button", { type: "button", onClick: handleClearSearch, className: "absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground", children: "\u2715" }))] }), _jsx(Button, { type: "submit", children: "Search" })] }) }), _jsx("div", { className: "rounded-lg border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [displayColumns.map((column) => (_jsx(TableHead, { className: "cursor-pointer hover:bg-muted/70 transition-colors", onClick: () => handleSort(column), children: _jsxs("div", { className: "flex items-center space-x-1", children: [_jsx("span", { children: formatFieldName(column) }), sortBy === column && (_jsx("span", { className: "text-primary", children: sortOrder === 'asc' ? '↑' : '↓' }))] }) }, column))), _jsx(TableHead, { className: "text-right", children: "Actions" })] }) }), _jsx(TableBody, { children: sortedItems.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: displayColumns.length + 1, className: "h-24 text-center", children: "No items found" }) })) : (sortedItems.map((item) => (_jsxs(TableRow, { children: [displayColumns.map((column) => (_jsx(TableCell, { children: fieldTypes[column] === 'relationship'
106
+ ? renderRelationshipCell(item[column], column)
107
+ : getFieldDisplayValue(item[column], fieldTypes[column]) }, column))), _jsx(TableCell, { className: "text-right", children: _jsx(Link, { href: `${basePath}/${urlKey}/${item.id}`, className: "text-primary hover:underline", children: "Edit" }) })] }, String(item.id))))) })] }) }), totalPages > 1 && (_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("p", { className: "text-sm text-muted-foreground", children: ["Showing ", (page - 1) * pageSize + 1, " to ", Math.min(page * pageSize, total), " of ", total, ' ', "results"] }), _jsxs("div", { className: "flex items-center space-x-2", children: [_jsx(Button, { variant: "outline", onClick: () => router.push(buildPaginationUrl(page - 1)), disabled: !hasPrevPage, children: "Previous" }), _jsxs("span", { className: "text-sm text-muted-foreground", children: ["Page ", page, " of ", totalPages] }), _jsx(Button, { variant: "outline", onClick: () => router.push(buildPaginationUrl(page + 1)), disabled: !hasNextPage, children: "Next" })] })] }))] }));
108
+ }
@@ -0,0 +1,10 @@
1
+ export interface LoadingSpinnerProps {
2
+ size?: 'sm' | 'md' | 'lg';
3
+ className?: string;
4
+ }
5
+ /**
6
+ * Loading spinner component
7
+ * Used to indicate loading states
8
+ */
9
+ export declare function LoadingSpinner({ size, className }: LoadingSpinnerProps): import("react/jsx-runtime").JSX.Element;
10
+ //# sourceMappingURL=LoadingSpinner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LoadingSpinner.d.ts","sourceRoot":"","sources":["../../src/components/LoadingSpinner.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAAE,IAAW,EAAE,SAAS,EAAE,EAAE,mBAAmB,2CAoB7E"}
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cn } from '../lib/utils.js';
3
+ /**
4
+ * Loading spinner component
5
+ * Used to indicate loading states
6
+ */
7
+ export function LoadingSpinner({ size = 'md', className }) {
8
+ const sizeClasses = {
9
+ sm: 'h-4 w-4 border-2',
10
+ md: 'h-8 w-8 border-2',
11
+ lg: 'h-12 w-12 border-3',
12
+ };
13
+ return (_jsx("div", { className: cn('animate-spin rounded-full border-primary border-t-transparent', sizeClasses[size], className), role: "status", "aria-label": "Loading", children: _jsx("span", { className: "sr-only", children: "Loading..." }) }));
14
+ }
@@ -0,0 +1,13 @@
1
+ import { AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
2
+ export interface NavigationProps<TPrisma> {
3
+ context: AccessContext<TPrisma>;
4
+ config: OpenSaasConfig;
5
+ basePath?: string;
6
+ currentPath?: string;
7
+ }
8
+ /**
9
+ * Navigation sidebar showing all lists
10
+ * Server Component
11
+ */
12
+ export declare function Navigation<TPrisma>({ context, config, basePath, currentPath, }: NavigationProps<TPrisma>): import("react/jsx-runtime").JSX.Element;
13
+ //# sourceMappingURL=Navigation.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import Link from 'next/link';
3
+ import { formatListName } from '../lib/utils.js';
4
+ import { getUrlKey } from '@opensaas/stack-core';
5
+ /**
6
+ * Navigation sidebar showing all lists
7
+ * Server Component
8
+ */
9
+ export function Navigation({ context, config, basePath = '/admin', currentPath = '', }) {
10
+ const lists = Object.keys(config.lists || {});
11
+ 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
+ ? 'bg-gradient-to-r from-primary to-accent text-primary-foreground shadow-lg shadow-primary/25'
13
+ : 'text-foreground hover:bg-accent/50 hover:text-accent-foreground'}`, children: [currentPath === '' && (_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: currentPath === '' ? 'text-lg' : 'text-base', children: "\uD83D\uDCCA" }), "Dashboard"] })] }), lists.length > 0 && (_jsxs(_Fragment, { children: [_jsx("div", { className: "pt-4 pb-2 px-3", children: _jsx("p", { className: "text-xs font-semibold text-muted-foreground uppercase tracking-wider", children: "Lists" }) }), lists.map((listKey) => {
14
+ const urlKey = getUrlKey(listKey);
15
+ const isActive = currentPath.startsWith(`/${urlKey}`);
16
+ 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
+ ? 'bg-gradient-to-r from-primary to-accent text-primary-foreground shadow-lg shadow-primary/25'
18
+ : '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
+ }
@@ -0,0 +1,22 @@
1
+ export interface SkeletonLoaderProps {
2
+ className?: string;
3
+ variant?: 'text' | 'circular' | 'rectangular';
4
+ }
5
+ /**
6
+ * Skeleton loader component for content placeholders
7
+ */
8
+ export declare function SkeletonLoader({ className, variant }: SkeletonLoaderProps): import("react/jsx-runtime").JSX.Element;
9
+ /**
10
+ * Table skeleton loader
11
+ */
12
+ export declare function TableSkeleton({ rows, columns }: {
13
+ rows?: number;
14
+ columns?: number;
15
+ }): import("react/jsx-runtime").JSX.Element;
16
+ /**
17
+ * Form skeleton loader
18
+ */
19
+ export declare function FormSkeleton({ fields }: {
20
+ fields?: number;
21
+ }): import("react/jsx-runtime").JSX.Element;
22
+ //# sourceMappingURL=SkeletonLoader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SkeletonLoader.d.ts","sourceRoot":"","sources":["../../src/components/SkeletonLoader.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,aAAa,CAAA;CAC9C;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,EAAE,SAAS,EAAE,OAAuB,EAAE,EAAE,mBAAmB,2CAazF;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,EAAE,IAAQ,EAAE,OAAW,EAAE,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,2CA6B3F;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,EAAE,MAAU,EAAE,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,2CAmB/D"}
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from '../lib/utils.js';
3
+ /**
4
+ * Skeleton loader component for content placeholders
5
+ */
6
+ export function SkeletonLoader({ className, variant = 'rectangular' }) {
7
+ const variantClasses = {
8
+ text: 'h-4 rounded',
9
+ circular: 'rounded-full',
10
+ rectangular: 'rounded-md',
11
+ };
12
+ return (_jsx("div", { className: cn('animate-pulse bg-muted', variantClasses[variant], className), "aria-hidden": "true" }));
13
+ }
14
+ /**
15
+ * Table skeleton loader
16
+ */
17
+ export function TableSkeleton({ rows = 5, columns = 4 }) {
18
+ return (_jsx("div", { className: "bg-card border border-border rounded-lg overflow-hidden", children: _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full", children: [_jsx("thead", { className: "bg-muted/50 border-b border-border", children: _jsx("tr", { children: Array.from({ length: columns }).map((_, i) => (_jsx("th", { className: "px-6 py-3", children: _jsx(SkeletonLoader, { variant: "text", className: "h-4 w-24" }) }, i))) }) }), _jsx("tbody", { className: "divide-y divide-border", children: Array.from({ length: rows }).map((_, rowIndex) => (_jsx("tr", { children: Array.from({ length: columns }).map((_, colIndex) => (_jsx("td", { className: "px-6 py-4", children: _jsx(SkeletonLoader, { variant: "text", className: "h-4 w-32" }) }, colIndex))) }, rowIndex))) })] }) }) }));
19
+ }
20
+ /**
21
+ * Form skeleton loader
22
+ */
23
+ export function FormSkeleton({ fields = 4 }) {
24
+ return (_jsx("div", { className: "bg-card border border-border rounded-lg p-6", children: _jsxs("div", { className: "space-y-6", children: [Array.from({ length: fields }).map((_, i) => (_jsxs("div", { className: "space-y-2", children: [_jsx(SkeletonLoader, { variant: "text", className: "h-4 w-24" }), _jsx(SkeletonLoader, { variant: "rectangular", className: "h-10 w-full" })] }, i))), _jsx("div", { className: "flex items-center justify-between pt-6 border-t border-border", children: _jsxs("div", { className: "flex gap-3", children: [_jsx(SkeletonLoader, { variant: "rectangular", className: "h-10 w-20" }), _jsx(SkeletonLoader, { variant: "rectangular", className: "h-10 w-20" })] }) })] }) }));
25
+ }
@@ -0,0 +1,11 @@
1
+ export interface CheckboxFieldProps {
2
+ name: string;
3
+ value: boolean;
4
+ onChange: (value: boolean) => void;
5
+ label: string;
6
+ error?: string;
7
+ disabled?: boolean;
8
+ mode?: 'read' | 'edit';
9
+ }
10
+ export declare function CheckboxField({ name, value, onChange, label, error, disabled, mode, }: CheckboxFieldProps): import("react/jsx-runtime").JSX.Element;
11
+ //# sourceMappingURL=CheckboxField.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CheckboxField.d.ts","sourceRoot":"","sources":["../../../src/components/fields/CheckboxField.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CACvB;AAED,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,KAAK,EACL,KAAK,EACL,QAAQ,EACR,IAAa,GACd,EAAE,kBAAkB,2CA8BpB"}
@@ -0,0 +1,10 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Checkbox } from '../../primitives/checkbox.js';
4
+ import { Label } from '../../primitives/label.js';
5
+ export function CheckboxField({ name, value, onChange, label, error, disabled, mode = 'edit', }) {
6
+ if (mode === 'read') {
7
+ return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-muted-foreground", children: label }), _jsx("p", { className: "text-sm", children: value ? 'Yes' : 'No' })] }));
8
+ }
9
+ return (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center space-x-2", children: [_jsx(Checkbox, { id: name, name: name, checked: value || false, onCheckedChange: (checked) => onChange(checked === true), disabled: disabled }), _jsx(Label, { htmlFor: name, className: "leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", children: label })] }), error && _jsx("p", { className: "text-sm text-destructive", children: error })] }));
10
+ }
@@ -0,0 +1,18 @@
1
+ export interface ComboboxFieldProps {
2
+ name: string;
3
+ value: string | null;
4
+ onChange: (value: string | null) => void;
5
+ label: string;
6
+ items: Array<{
7
+ id: string;
8
+ label: string;
9
+ }>;
10
+ error?: string;
11
+ disabled?: boolean;
12
+ required?: boolean;
13
+ mode?: 'read' | 'edit';
14
+ isLoading?: boolean;
15
+ placeholder?: string;
16
+ }
17
+ export declare function ComboboxField({ name, value, onChange, label, items, error, disabled, required, mode, isLoading, placeholder, }: ComboboxFieldProps): import("react/jsx-runtime").JSX.Element;
18
+ //# sourceMappingURL=ComboboxField.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ComboboxField.d.ts","sourceRoot":"","sources":["../../../src/components/fields/ComboboxField.tsx"],"names":[],"mappings":"AAcA,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACxC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC3C,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,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,KAAK,EACL,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,IAAa,EACb,SAAiB,EACjB,WAAyB,GAC1B,EAAE,kBAAkB,2CAsFpB"}
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useState } from 'react';
4
+ import { Combobox, ComboboxTrigger, ComboboxContent, ComboboxSearch, ComboboxList, ComboboxEmpty, ComboboxItem, } from '../../primitives/combobox.js';
5
+ export function ComboboxField({ name, value, onChange, label, items, error, disabled, required, mode = 'edit', isLoading = false, placeholder = 'Select...', }) {
6
+ const [open, setOpen] = useState(false);
7
+ const [searchQuery, setSearchQuery] = useState('');
8
+ // Read mode
9
+ if (mode === 'read') {
10
+ const selectedItem = items.find((item) => item.id === value);
11
+ return (_jsxs("div", { className: "space-y-1", children: [_jsx("label", { className: "text-sm font-medium text-muted-foreground", children: label }), _jsx("p", { className: "text-sm", children: selectedItem?.label || '-' })] }));
12
+ }
13
+ // Filter items based on search query
14
+ const filteredItems = searchQuery
15
+ ? items.filter((item) => item.label.toLowerCase().includes(searchQuery.toLowerCase()))
16
+ : items;
17
+ const selectedItem = items.find((item) => item.id === value);
18
+ return (_jsxs("div", { className: "space-y-2", children: [_jsxs("label", { htmlFor: name, className: "text-sm font-medium", children: [label, required && _jsx("span", { className: "text-destructive ml-1", children: "*" })] }), _jsxs(Combobox, { open: open, onOpenChange: setOpen, children: [_jsx(ComboboxTrigger, { disabled: disabled || isLoading, children: _jsx("span", { className: !selectedItem ? 'text-muted-foreground' : '', children: isLoading ? 'Loading...' : selectedItem ? selectedItem.label : placeholder }) }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxSearch, { placeholder: "Search...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), onKeyDown: (e) => {
19
+ // Prevent form submission on Enter
20
+ if (e.key === 'Enter') {
21
+ e.preventDefault();
22
+ }
23
+ } }), _jsx(ComboboxList, { children: filteredItems.length === 0 ? (_jsx(ComboboxEmpty, {})) : (_jsxs(_Fragment, { children: [!required && value && (_jsxs(_Fragment, { children: [_jsx(ComboboxItem, { onClick: () => {
24
+ onChange(null);
25
+ setOpen(false);
26
+ setSearchQuery('');
27
+ }, children: _jsx("span", { className: "text-muted-foreground italic", children: "Clear selection" }) }), _jsx("div", { className: "-mx-1 my-1 h-px bg-border" })] })), filteredItems.map((item) => (_jsx(ComboboxItem, { selected: item.id === value, onClick: () => {
28
+ onChange(item.id);
29
+ setOpen(false);
30
+ setSearchQuery('');
31
+ }, children: item.label }, item.id)))] })) })] })] }), error && _jsx("p", { className: "text-sm text-destructive", children: error })] }));
32
+ }
@@ -0,0 +1,22 @@
1
+ import type { SerializableFieldConfig } from '../../lib/serializeFieldConfig.js';
2
+ export interface FieldRendererProps {
3
+ fieldName: string;
4
+ fieldConfig: SerializableFieldConfig;
5
+ value: unknown;
6
+ onChange: (value: unknown) => void;
7
+ error?: string;
8
+ disabled?: boolean;
9
+ mode?: 'read' | 'edit';
10
+ relationshipItems?: Array<{
11
+ id: string;
12
+ label: string;
13
+ }>;
14
+ relationshipLoading?: boolean;
15
+ basePath?: string;
16
+ }
17
+ /**
18
+ * Factory component that renders the appropriate field type
19
+ * based on the field configuration and component registry
20
+ */
21
+ export declare function FieldRenderer(props: FieldRendererProps): import("react/jsx-runtime").JSX.Element | null;
22
+ //# sourceMappingURL=FieldRenderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FieldRenderer.d.ts","sourceRoot":"","sources":["../../../src/components/fields/FieldRenderer.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAA;AAEhF,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,uBAAuB,CAAA;IACpC,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAClC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,iBAAiB,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACxD,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AA6ED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,kDA+BtD"}