@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,159 @@
1
+ 'use client'
2
+
3
+ import { Input } from '../../primitives/input.js'
4
+ import { Label } from '../../primitives/label.js'
5
+ import { Button } from '../../primitives/button.js'
6
+ import { cn } from '../../lib/utils.js'
7
+ import { useState } from 'react'
8
+
9
+ export interface PasswordFieldProps {
10
+ name: string
11
+ value: string | { isSet: boolean }
12
+ onChange: (value: string | { isSet: boolean } | undefined) => void
13
+ label: string
14
+ placeholder?: string
15
+ error?: string
16
+ disabled?: boolean
17
+ required?: boolean
18
+ mode?: 'read' | 'edit'
19
+ showConfirm?: boolean
20
+ }
21
+
22
+ export function PasswordField({
23
+ name,
24
+ value,
25
+ onChange,
26
+ label,
27
+ placeholder,
28
+ error,
29
+ disabled,
30
+ required,
31
+ mode = 'edit',
32
+ showConfirm = true,
33
+ }: PasswordFieldProps) {
34
+ // Check if value is the isSet object
35
+ const isSetObject = typeof value === 'object' && value !== null && 'isSet' in value
36
+ const isPasswordSet = isSetObject ? value.isSet : false
37
+
38
+ const [isChangingPassword, setIsChangingPassword] = useState(false)
39
+ const [passwordValue, setPasswordValue] = useState('')
40
+ const [confirmValue, setConfirmValue] = useState('')
41
+ const [showPassword, setShowPassword] = useState(false)
42
+
43
+ if (mode === 'read') {
44
+ return (
45
+ <div className="space-y-1">
46
+ <Label className="text-muted-foreground">{label}</Label>
47
+ <p className="text-sm">{isPasswordSet ? '••••••••' : 'Not set'}</p>
48
+ </div>
49
+ )
50
+ }
51
+
52
+ // If not changing password and it's set, show the button
53
+ if (!isChangingPassword && isSetObject) {
54
+ return (
55
+ <div className="space-y-2">
56
+ <Label>{label}</Label>
57
+ <div>
58
+ <Button
59
+ type="button"
60
+ variant="outline"
61
+ onClick={() => setIsChangingPassword(true)}
62
+ disabled={disabled}
63
+ >
64
+ {isPasswordSet ? 'Change Password' : 'Set Password'}
65
+ </Button>
66
+ </div>
67
+ </div>
68
+ )
69
+ }
70
+
71
+ const confirmError =
72
+ showConfirm && passwordValue !== confirmValue && confirmValue !== ''
73
+ ? 'Passwords do not match'
74
+ : undefined
75
+
76
+ const handleCancel = () => {
77
+ setIsChangingPassword(false)
78
+ setPasswordValue('')
79
+ setConfirmValue('')
80
+ setShowPassword(false)
81
+ // Reset to the isSet object
82
+ if (isSetObject) {
83
+ onChange(value)
84
+ }
85
+ }
86
+
87
+ const handlePasswordChange = (newValue: string) => {
88
+ setPasswordValue(newValue)
89
+ // Update the parent with the actual password string
90
+ onChange(newValue || undefined)
91
+ }
92
+
93
+ return (
94
+ <div className="space-y-4">
95
+ <div className="space-y-2">
96
+ <Label htmlFor={name}>
97
+ {label}
98
+ {required && !isPasswordSet && <span className="text-destructive ml-1">*</span>}
99
+ </Label>
100
+ <div className="relative">
101
+ <Input
102
+ id={name}
103
+ name={name}
104
+ type={showPassword ? 'text' : 'password'}
105
+ value={passwordValue}
106
+ onChange={(e) => handlePasswordChange(e.target.value)}
107
+ placeholder={placeholder}
108
+ disabled={disabled}
109
+ required={required && !isPasswordSet}
110
+ className={cn('pr-10', error && 'border-destructive')}
111
+ />
112
+ <button
113
+ type="button"
114
+ onClick={() => setShowPassword(!showPassword)}
115
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
116
+ >
117
+ {showPassword ? '👁️' : '👁️‍🗨️'}
118
+ </button>
119
+ </div>
120
+ {error && <p className="text-sm text-destructive">{error}</p>}
121
+ </div>
122
+
123
+ {showConfirm && (
124
+ <div className="space-y-2">
125
+ <Label htmlFor={`${name}-confirm`}>
126
+ Confirm {label}
127
+ {required && !isPasswordSet && <span className="text-destructive ml-1">*</span>}
128
+ </Label>
129
+ <Input
130
+ id={`${name}-confirm`}
131
+ name={`${name}-confirm`}
132
+ type={showPassword ? 'text' : 'password'}
133
+ value={confirmValue}
134
+ onChange={(e) => setConfirmValue(e.target.value)}
135
+ placeholder={`Confirm ${placeholder || label.toLowerCase()}`}
136
+ disabled={disabled}
137
+ required={required && !isPasswordSet}
138
+ className={cn(confirmError && 'border-destructive')}
139
+ />
140
+ {confirmError && <p className="text-sm text-destructive">{confirmError}</p>}
141
+ </div>
142
+ )}
143
+
144
+ {isSetObject && (
145
+ <div>
146
+ <Button
147
+ type="button"
148
+ variant="ghost"
149
+ size="sm"
150
+ onClick={handleCancel}
151
+ disabled={disabled}
152
+ >
153
+ Cancel
154
+ </Button>
155
+ </div>
156
+ )}
157
+ </div>
158
+ )
159
+ }
@@ -0,0 +1,71 @@
1
+ 'use client'
2
+
3
+ import { ComboboxField } from './ComboboxField.js'
4
+ import { RelationshipManager } from './RelationshipManager.js'
5
+
6
+ export interface RelationshipFieldProps {
7
+ name: string
8
+ value: string | string[] | null
9
+ onChange: (value: string | string[] | null) => void
10
+ label: string
11
+ items: Array<{ id: string; label: string }>
12
+ error?: string
13
+ disabled?: boolean
14
+ required?: boolean
15
+ mode?: 'read' | 'edit'
16
+ isLoading?: boolean
17
+ many?: boolean
18
+ relatedListKey?: string
19
+ basePath?: string
20
+ }
21
+
22
+ export function RelationshipField({
23
+ name,
24
+ value,
25
+ onChange,
26
+ label,
27
+ items,
28
+ error,
29
+ disabled,
30
+ required,
31
+ mode = 'edit',
32
+ isLoading = false,
33
+ many = false,
34
+ relatedListKey,
35
+ basePath,
36
+ }: RelationshipFieldProps) {
37
+ // Delegate to specialized components based on cardinality
38
+ if (many) {
39
+ return (
40
+ <RelationshipManager
41
+ name={name}
42
+ value={Array.isArray(value) ? value : []}
43
+ onChange={onChange}
44
+ label={label}
45
+ items={items}
46
+ error={error}
47
+ disabled={disabled}
48
+ required={required}
49
+ mode={mode}
50
+ isLoading={isLoading}
51
+ relatedListKey={relatedListKey}
52
+ basePath={basePath}
53
+ />
54
+ )
55
+ }
56
+
57
+ return (
58
+ <ComboboxField
59
+ name={name}
60
+ value={typeof value === 'string' ? value : null}
61
+ onChange={onChange}
62
+ label={label}
63
+ items={items}
64
+ error={error}
65
+ disabled={disabled}
66
+ required={required}
67
+ mode={mode}
68
+ isLoading={isLoading}
69
+ />
70
+ )
71
+ }
@@ -0,0 +1,189 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useState } from 'react'
5
+ import Link from 'next/link'
6
+ import {
7
+ Table,
8
+ TableBody,
9
+ TableCell,
10
+ TableHead,
11
+ TableHeader,
12
+ TableRow,
13
+ } from '../../primitives/table.js'
14
+ import {
15
+ Combobox,
16
+ ComboboxTrigger,
17
+ ComboboxContent,
18
+ ComboboxSearch,
19
+ ComboboxList,
20
+ ComboboxEmpty,
21
+ ComboboxItem,
22
+ } from '../../primitives/combobox.js'
23
+ import { Button } from '../../primitives/button.js'
24
+
25
+ export interface RelationshipManagerProps {
26
+ name: string
27
+ value: string[]
28
+ onChange: (value: string[]) => void
29
+ label: string
30
+ items: Array<{ id: string; label: string }>
31
+ error?: string
32
+ disabled?: boolean
33
+ required?: boolean
34
+ mode?: 'read' | 'edit'
35
+ isLoading?: boolean
36
+ relatedListKey?: string
37
+ basePath?: string
38
+ }
39
+
40
+ export function RelationshipManager({
41
+ name: _name,
42
+ value,
43
+ onChange,
44
+ label,
45
+ items,
46
+ error,
47
+ disabled,
48
+ required,
49
+ mode = 'edit',
50
+ isLoading = false,
51
+ relatedListKey,
52
+ basePath = '/admin',
53
+ }: RelationshipManagerProps) {
54
+ const [showConnectModal, setShowConnectModal] = useState(false)
55
+ const [searchQuery, setSearchQuery] = useState('')
56
+
57
+ const selectedIds = Array.isArray(value) ? value : []
58
+ const selectedItems = items.filter((item) => selectedIds.includes(item.id))
59
+ const availableItems = items.filter((item) => !selectedIds.includes(item.id))
60
+
61
+ // Read mode
62
+ if (mode === 'read') {
63
+ return (
64
+ <div className="space-y-1">
65
+ <label className="text-sm font-medium text-muted-foreground">{label}</label>
66
+ <p className="text-sm">
67
+ {selectedItems.length > 0 ? selectedItems.map((item) => item.label).join(', ') : '-'}
68
+ </p>
69
+ </div>
70
+ )
71
+ }
72
+
73
+ // Filter available items based on search
74
+ const filteredAvailableItems = searchQuery
75
+ ? availableItems.filter((item) => item.label.toLowerCase().includes(searchQuery.toLowerCase()))
76
+ : availableItems
77
+
78
+ const handleRemove = (itemId: string) => {
79
+ onChange(selectedIds.filter((id) => id !== itemId))
80
+ }
81
+
82
+ const handleConnect = (itemId: string) => {
83
+ onChange([...selectedIds, itemId])
84
+ setShowConnectModal(false)
85
+ setSearchQuery('')
86
+ }
87
+
88
+ return (
89
+ <div className="space-y-2">
90
+ <label className="text-sm font-medium">
91
+ {label}
92
+ {required && <span className="text-destructive ml-1">*</span>}
93
+ </label>
94
+
95
+ {/* Selected Items Table */}
96
+ {selectedItems.length > 0 ? (
97
+ <div className="rounded-md border border-input">
98
+ <Table>
99
+ <TableHeader>
100
+ <TableRow>
101
+ <TableHead>Name</TableHead>
102
+ <TableHead className="w-[100px] text-right">Actions</TableHead>
103
+ </TableRow>
104
+ </TableHeader>
105
+ <TableBody>
106
+ {selectedItems.map((item) => (
107
+ <TableRow key={item.id}>
108
+ <TableCell>
109
+ {relatedListKey ? (
110
+ <Link
111
+ href={`${basePath}/${relatedListKey}/${item.id}`}
112
+ className="text-primary hover:underline"
113
+ >
114
+ {item.label}
115
+ </Link>
116
+ ) : (
117
+ item.label
118
+ )}
119
+ </TableCell>
120
+ <TableCell className="text-right">
121
+ <Button
122
+ type="button"
123
+ variant="ghost"
124
+ size="sm"
125
+ onClick={() => handleRemove(item.id)}
126
+ disabled={disabled}
127
+ >
128
+ Remove
129
+ </Button>
130
+ </TableCell>
131
+ </TableRow>
132
+ ))}
133
+ </TableBody>
134
+ </Table>
135
+ </div>
136
+ ) : (
137
+ <div className="rounded-md border border-input border-dashed p-8 text-center">
138
+ <p className="text-sm text-muted-foreground">
139
+ No items connected. Click &quot;Connect Existing&quot; to add items.
140
+ </p>
141
+ </div>
142
+ )}
143
+
144
+ {/* Action Buttons */}
145
+ <div className="flex gap-2">
146
+ <Combobox open={showConnectModal} onOpenChange={setShowConnectModal}>
147
+ <ComboboxTrigger
148
+ disabled={disabled || isLoading || availableItems.length === 0}
149
+ className="h-9 px-3"
150
+ >
151
+ <span>{isLoading ? 'Loading...' : 'Connect Existing'}</span>
152
+ </ComboboxTrigger>
153
+ <ComboboxContent>
154
+ <ComboboxSearch
155
+ placeholder="Search..."
156
+ value={searchQuery}
157
+ onChange={(e) => setSearchQuery(e.target.value)}
158
+ onKeyDown={(e) => {
159
+ if (e.key === 'Enter') {
160
+ e.preventDefault()
161
+ }
162
+ }}
163
+ />
164
+ <ComboboxList>
165
+ {filteredAvailableItems.length === 0 ? (
166
+ <ComboboxEmpty>
167
+ {availableItems.length === 0
168
+ ? 'All items are already connected'
169
+ : 'No results found'}
170
+ </ComboboxEmpty>
171
+ ) : (
172
+ filteredAvailableItems.map((item) => (
173
+ <ComboboxItem key={item.id} onClick={() => handleConnect(item.id)}>
174
+ {item.label}
175
+ </ComboboxItem>
176
+ ))
177
+ )}
178
+ </ComboboxList>
179
+ </ComboboxContent>
180
+ </Combobox>
181
+
182
+ {/* Note: "Create New" functionality would require additional props for the related list's fields
183
+ and form rendering logic. For now, we'll leave it as a placeholder or implement in a future iteration */}
184
+ </div>
185
+
186
+ {error && <p className="text-sm text-destructive mt-2">{error}</p>}
187
+ </div>
188
+ )
189
+ }
@@ -0,0 +1,71 @@
1
+ 'use client'
2
+
3
+ import {
4
+ Select,
5
+ SelectContent,
6
+ SelectItem,
7
+ SelectTrigger,
8
+ SelectValue,
9
+ } from '../../primitives/select.js'
10
+ import { Label } from '../../primitives/label.js'
11
+
12
+ export interface SelectFieldProps {
13
+ name: string
14
+ value: string | null
15
+ onChange: (value: string | null) => void
16
+ label: string
17
+ options: Array<{ label: string; value: string }>
18
+ error?: string
19
+ disabled?: boolean
20
+ required?: boolean
21
+ mode?: 'read' | 'edit'
22
+ }
23
+
24
+ export function SelectField({
25
+ name,
26
+ value,
27
+ onChange,
28
+ label,
29
+ options,
30
+ error,
31
+ disabled,
32
+ required,
33
+ mode = 'edit',
34
+ }: SelectFieldProps) {
35
+ if (mode === 'read') {
36
+ const selectedOption = options.find((opt) => opt.value === value)
37
+ return (
38
+ <div className="space-y-1">
39
+ <Label className="text-muted-foreground">{label}</Label>
40
+ <p className="text-sm">{selectedOption?.label || '-'}</p>
41
+ </div>
42
+ )
43
+ }
44
+
45
+ return (
46
+ <div className="space-y-2">
47
+ <Label htmlFor={name}>
48
+ {label}
49
+ {required && <span className="text-destructive ml-1">*</span>}
50
+ </Label>
51
+ <Select
52
+ value={value || undefined}
53
+ onValueChange={(val) => onChange(val || null)}
54
+ disabled={disabled}
55
+ required={required}
56
+ >
57
+ <SelectTrigger id={name} className={error ? 'border-destructive' : ''}>
58
+ <SelectValue placeholder="Select an option..." />
59
+ </SelectTrigger>
60
+ <SelectContent>
61
+ {options.map((option) => (
62
+ <SelectItem key={option.value} value={option.value}>
63
+ {option.label}
64
+ </SelectItem>
65
+ ))}
66
+ </SelectContent>
67
+ </Select>
68
+ {error && <p className="text-sm text-destructive">{error}</p>}
69
+ </div>
70
+ )
71
+ }
@@ -0,0 +1,59 @@
1
+ 'use client'
2
+
3
+ import { Input } from '../../primitives/input.js'
4
+ import { Label } from '../../primitives/label.js'
5
+ import { cn } from '../../lib/utils.js'
6
+
7
+ export interface TextFieldProps {
8
+ name: string
9
+ value: string
10
+ onChange: (value: string) => void
11
+ label: string
12
+ placeholder?: string
13
+ error?: string
14
+ disabled?: boolean
15
+ required?: boolean
16
+ mode?: 'read' | 'edit'
17
+ }
18
+
19
+ export function TextField({
20
+ name,
21
+ value,
22
+ onChange,
23
+ label,
24
+ placeholder,
25
+ error,
26
+ disabled,
27
+ required,
28
+ mode = 'edit',
29
+ }: TextFieldProps) {
30
+ if (mode === 'read') {
31
+ return (
32
+ <div className="space-y-1">
33
+ <Label className="text-muted-foreground">{label}</Label>
34
+ <p className="text-sm">{value || '-'}</p>
35
+ </div>
36
+ )
37
+ }
38
+
39
+ return (
40
+ <div className="space-y-2">
41
+ <Label htmlFor={name}>
42
+ {label}
43
+ {required && <span className="text-destructive ml-1">*</span>}
44
+ </Label>
45
+ <Input
46
+ id={name}
47
+ name={name}
48
+ type="text"
49
+ value={value || ''}
50
+ onChange={(e) => onChange(e.target.value)}
51
+ placeholder={placeholder}
52
+ disabled={disabled}
53
+ required={required}
54
+ className={cn(error && 'border-destructive')}
55
+ />
56
+ {error && <p className="text-sm text-destructive">{error}</p>}
57
+ </div>
58
+ )
59
+ }
@@ -0,0 +1,49 @@
1
+ 'use client'
2
+
3
+ import { Label } from '../../primitives/label.js'
4
+ import { DateTimePicker } from '../../primitives/datetime-picker.js'
5
+ import { format } from 'date-fns'
6
+
7
+ export interface TimestampFieldProps {
8
+ name: string
9
+ value: Date | string | null
10
+ onChange: (value: Date | null) => void
11
+ label: string
12
+ error?: string
13
+ disabled?: boolean
14
+ required?: boolean
15
+ mode?: 'read' | 'edit'
16
+ }
17
+
18
+ export function TimestampField({
19
+ name,
20
+ value,
21
+ onChange,
22
+ label,
23
+ error,
24
+ disabled,
25
+ required,
26
+ mode = 'edit',
27
+ }: TimestampFieldProps) {
28
+ const dateValue = value ? new Date(value) : null
29
+
30
+ if (mode === 'read') {
31
+ return (
32
+ <div className="space-y-1">
33
+ <Label className="text-muted-foreground">{label}</Label>
34
+ <p className="text-sm">{dateValue ? format(dateValue, 'PPpp') : '-'}</p>
35
+ </div>
36
+ )
37
+ }
38
+
39
+ return (
40
+ <div className="space-y-2">
41
+ <Label htmlFor={name}>
42
+ {label}
43
+ {required && <span className="text-destructive ml-1">*</span>}
44
+ </Label>
45
+ <DateTimePicker value={dateValue} onChange={onChange} disabled={disabled} />
46
+ {error && <p className="text-sm text-destructive">{error}</p>}
47
+ </div>
48
+ )
49
+ }
@@ -0,0 +1,27 @@
1
+ // Field components
2
+ export { TextField } from './TextField.js'
3
+ export { IntegerField } from './IntegerField.js'
4
+ export { CheckboxField } from './CheckboxField.js'
5
+ export { SelectField } from './SelectField.js'
6
+ export { TimestampField } from './TimestampField.js'
7
+ export { PasswordField } from './PasswordField.js'
8
+ export { RelationshipField } from './RelationshipField.js'
9
+ export { ComboboxField } from './ComboboxField.js'
10
+ export { RelationshipManager } from './RelationshipManager.js'
11
+ export { FieldRenderer } from './FieldRenderer.js'
12
+
13
+ // Registry for custom field types
14
+ export { fieldComponentRegistry, registerFieldComponent, getFieldComponent } from './registry.js'
15
+
16
+ // Re-export types
17
+ export type { TextFieldProps } from './TextField.js'
18
+ export type { IntegerFieldProps } from './IntegerField.js'
19
+ export type { CheckboxFieldProps } from './CheckboxField.js'
20
+ export type { SelectFieldProps } from './SelectField.js'
21
+ export type { TimestampFieldProps } from './TimestampField.js'
22
+ export type { PasswordFieldProps } from './PasswordField.js'
23
+ export type { RelationshipFieldProps } from './RelationshipField.js'
24
+ export type { ComboboxFieldProps } from './ComboboxField.js'
25
+ export type { RelationshipManagerProps } from './RelationshipManager.js'
26
+ export type { FieldRendererProps } from './FieldRenderer.js'
27
+ export type { FieldComponent, FieldComponentProps } from './registry.js'
@@ -0,0 +1,72 @@
1
+ import type { ComponentType } from 'react'
2
+ import { TextField } from './TextField.js'
3
+ import { IntegerField } from './IntegerField.js'
4
+ import { CheckboxField } from './CheckboxField.js'
5
+ import { SelectField } from './SelectField.js'
6
+ import { TimestampField } from './TimestampField.js'
7
+ import { PasswordField } from './PasswordField.js'
8
+ import { RelationshipField } from './RelationshipField.js'
9
+
10
+ /**
11
+ * Base props that all field components must accept
12
+ * Field components can extend this with additional field-specific props
13
+ */
14
+ export type FieldComponentProps = {
15
+ name: string
16
+ value: unknown
17
+ onChange: (value: unknown) => void
18
+ label: string
19
+ error?: string
20
+ disabled?: boolean
21
+ required?: boolean
22
+ mode?: 'read' | 'edit'
23
+ }
24
+
25
+ /**
26
+ * Type for field component
27
+ * Field components must accept props that extend FieldComponentProps.
28
+ * The registry uses ComponentType<any> because components have different
29
+ * specific prop types (e.g., value: string vs value: number), but all
30
+ * must include the base FieldComponentProps structure.
31
+ */
32
+ export type FieldComponent = ComponentType<FieldComponentProps & Record<string, unknown>>
33
+
34
+ /**
35
+ * Registry mapping field types to their default UI components
36
+ * This can be extended for custom field types
37
+ * Uses ComponentType<any> to allow components with more specific prop types
38
+ */
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
+ export const fieldComponentRegistry: Record<string, ComponentType<any>> = {
41
+ text: TextField,
42
+ integer: IntegerField,
43
+ checkbox: CheckboxField,
44
+ select: SelectField,
45
+ timestamp: TimestampField,
46
+ password: PasswordField,
47
+ relationship: RelationshipField,
48
+ }
49
+
50
+ /**
51
+ * Register a custom field component for a field type
52
+ * Useful for adding support for custom field types
53
+ *
54
+ * @param fieldType - The field type identifier
55
+ * @param component - A React component that accepts FieldComponentProps (and optionally additional props)
56
+ */
57
+ export function registerFieldComponent(
58
+ fieldType: string,
59
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
+ component: ComponentType<any>,
61
+ ): void {
62
+ fieldComponentRegistry[fieldType] = component
63
+ }
64
+
65
+ /**
66
+ * Get the component for a field type
67
+ * Returns undefined if no component is registered
68
+ */
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ export function getFieldComponent(fieldType: string): ComponentType<any> | undefined {
71
+ return fieldComponentRegistry[fieldType]
72
+ }