@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,86 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useState } from 'react'
5
+ import { Input } from '../../primitives/input.js'
6
+ import { Button } from '../../primitives/button.js'
7
+ import { Card } from '../../primitives/card.js'
8
+ import { usePathname, useRouter } from 'next/navigation'
9
+
10
+ export interface SearchBarProps {
11
+ onSearch?: (query: string) => void
12
+ onClear?: () => void
13
+ placeholder?: string
14
+ defaultValue?: string
15
+ searchLabel?: string
16
+ className?: string
17
+ }
18
+
19
+ /**
20
+ * Standalone search bar component
21
+ * Can be embedded in any custom page
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * <SearchBar
26
+ * onSearch={(query) => {
27
+ * setSearchQuery(query);
28
+ * fetchPosts({ search: query });
29
+ * }}
30
+ * onClear={() => {
31
+ * setSearchQuery('');
32
+ * fetchPosts({});
33
+ * }}
34
+ * placeholder="Search posts..."
35
+ * />
36
+ * ```
37
+ */
38
+ export function SearchBar({
39
+ onSearch,
40
+ onClear,
41
+ placeholder = 'Search...',
42
+ defaultValue = '',
43
+ searchLabel = 'Search',
44
+ className,
45
+ }: SearchBarProps) {
46
+ const router = useRouter()
47
+ const pathname = usePathname()
48
+ const [searchInput, setSearchInput] = useState(defaultValue)
49
+
50
+ const handleSubmit = (e: React.FormEvent) => {
51
+ e.preventDefault()
52
+ if (onSearch) onSearch(searchInput.trim())
53
+ else router.push(`${pathname}?search=${searchInput.trim()}`)
54
+ }
55
+
56
+ const handleClear = () => {
57
+ setSearchInput('')
58
+ onClear?.()
59
+ }
60
+
61
+ return (
62
+ <Card className={`p-4 ${className || ''}`}>
63
+ <form onSubmit={handleSubmit} className="flex gap-2">
64
+ <div className="flex-1 relative">
65
+ <Input
66
+ type="text"
67
+ value={searchInput}
68
+ onChange={(e) => setSearchInput(e.target.value)}
69
+ placeholder={placeholder}
70
+ className="pr-10"
71
+ />
72
+ {searchInput && (
73
+ <button
74
+ type="button"
75
+ onClick={handleClear}
76
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
77
+ >
78
+
79
+ </button>
80
+ )}
81
+ </div>
82
+ <Button type="submit">{searchLabel}</Button>
83
+ </form>
84
+ </Card>
85
+ )
86
+ }
@@ -0,0 +1,13 @@
1
+ // Standalone composable components
2
+ export { ItemCreateForm } from './ItemCreateForm.js'
3
+ export { ItemEditForm } from './ItemEditForm.js'
4
+ export { ListTable } from './ListTable.js'
5
+ export { SearchBar } from './SearchBar.js'
6
+ export { DeleteButton } from './DeleteButton.js'
7
+
8
+ // Types
9
+ export type { ItemCreateFormProps } from './ItemCreateForm.js'
10
+ export type { ItemEditFormProps } from './ItemEditForm.js'
11
+ export type { ListTableProps } from './ListTable.js'
12
+ export type { SearchBarProps } from './SearchBar.js'
13
+ export type { DeleteButtonProps } from './DeleteButton.js'
package/src/index.ts ADDED
@@ -0,0 +1,74 @@
1
+ // Main components
2
+ export { AdminUI } from './components/AdminUI.js'
3
+ export { Dashboard } from './components/Dashboard.js'
4
+ export { Navigation } from './components/Navigation.js'
5
+ export { ListView } from './components/ListView.js'
6
+ export { ListViewClient } from './components/ListViewClient.js'
7
+ export { ItemForm } from './components/ItemForm.js'
8
+ export { ItemFormClient } from './components/ItemFormClient.js'
9
+ export { ConfirmDialog } from './components/ConfirmDialog.js'
10
+ export { LoadingSpinner } from './components/LoadingSpinner.js'
11
+ export { SkeletonLoader, TableSkeleton, FormSkeleton } from './components/SkeletonLoader.js'
12
+
13
+ // Field components
14
+ export {
15
+ TextField,
16
+ IntegerField,
17
+ CheckboxField,
18
+ SelectField,
19
+ TimestampField,
20
+ PasswordField,
21
+ RelationshipField,
22
+ FieldRenderer,
23
+ fieldComponentRegistry,
24
+ registerFieldComponent,
25
+ getFieldComponent,
26
+ } from './components/fields/index.js'
27
+
28
+ // Types
29
+ export type { AdminUIProps } from './components/AdminUI.js'
30
+ export type { DashboardProps } from './components/Dashboard.js'
31
+ export type { NavigationProps } from './components/Navigation.js'
32
+ export type { ListViewProps } from './components/ListView.js'
33
+ export type { ListViewClientProps } from './components/ListViewClient.js'
34
+ export type { ItemFormProps } from './components/ItemForm.js'
35
+ export type { ItemFormClientProps } from './components/ItemFormClient.js'
36
+ export type { ConfirmDialogProps } from './components/ConfirmDialog.js'
37
+ export type { LoadingSpinnerProps } from './components/LoadingSpinner.js'
38
+ export type { SkeletonLoaderProps } from './components/SkeletonLoader.js'
39
+
40
+ export type {
41
+ TextFieldProps,
42
+ IntegerFieldProps,
43
+ CheckboxFieldProps,
44
+ SelectFieldProps,
45
+ TimestampFieldProps,
46
+ PasswordFieldProps,
47
+ RelationshipFieldProps,
48
+ FieldRendererProps,
49
+ FieldComponent,
50
+ FieldComponentProps,
51
+ } from './components/fields/index.js'
52
+
53
+ // Standalone composable components
54
+ export {
55
+ ItemCreateForm,
56
+ ItemEditForm,
57
+ ListTable,
58
+ SearchBar,
59
+ DeleteButton,
60
+ } from './components/standalone/index.js'
61
+
62
+ export type {
63
+ ItemCreateFormProps,
64
+ ItemEditFormProps,
65
+ ListTableProps,
66
+ SearchBarProps,
67
+ DeleteButtonProps,
68
+ } from './components/standalone/index.js'
69
+
70
+ // Utility functions
71
+ export { cn, formatListName, formatFieldName, getFieldDisplayValue } from './lib/utils.js'
72
+
73
+ // Theme utilities
74
+ export { generateThemeCSS, getThemeStyleTag, presetThemes } from './lib/theme.js'
@@ -0,0 +1,88 @@
1
+ import type { FieldConfig } from '@opensaas/stack-core'
2
+ import type { ComponentType } from 'react'
3
+
4
+ /**
5
+ * Serializable field config for client components
6
+ * Strips out functions and non-serializable properties
7
+ */
8
+ export type SerializableFieldConfig = {
9
+ type: string
10
+ label?: string
11
+ validation?: {
12
+ isRequired?: boolean
13
+ length?: { min?: number; max?: number }
14
+ min?: number
15
+ max?: number
16
+ }
17
+ options?: Array<{ label: string; value: string }>
18
+ many?: boolean
19
+ ref?: string
20
+ ui?: {
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ component?: ComponentType<any>
23
+ fieldType?: string
24
+ [key: string]: unknown
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Extract only serializable properties from a single field config
30
+ * Removes functions (getZodSchema, getPrismaType, getTypeScriptType)
31
+ * and non-serializable properties (access, hooks, typePatch, valueForClientSerialization)
32
+ */
33
+ export function serializeFieldConfig(fieldConfig: FieldConfig): SerializableFieldConfig {
34
+ const config: SerializableFieldConfig = {
35
+ type: fieldConfig.type,
36
+ }
37
+
38
+ // Process ui options, excluding the valueForClientSerialization function
39
+ if (fieldConfig.ui) {
40
+ const { valueForClientSerialization: _valueForClientSerialization, ...serializableUi } =
41
+ fieldConfig.ui
42
+ config.ui = serializableUi
43
+ }
44
+
45
+ // Extract label if present
46
+ if ('label' in fieldConfig && fieldConfig.label !== undefined) {
47
+ config.label = fieldConfig.label as string
48
+ }
49
+
50
+ // Extract validation if present
51
+ if ('validation' in fieldConfig && fieldConfig.validation !== undefined) {
52
+ config.validation = fieldConfig.validation as SerializableFieldConfig['validation']
53
+ }
54
+
55
+ // Extract options for select fields
56
+ if ('options' in fieldConfig && fieldConfig.options !== undefined) {
57
+ config.options = fieldConfig.options as Array<{ label: string; value: string }>
58
+ }
59
+
60
+ // Extract many for relationship fields
61
+ if ('many' in fieldConfig && fieldConfig.many !== undefined) {
62
+ config.many = fieldConfig.many as boolean
63
+ }
64
+
65
+ // Extract ref for relationship fields
66
+ if ('ref' in fieldConfig && fieldConfig.ref !== undefined) {
67
+ config.ref = fieldConfig.ref as string
68
+ }
69
+
70
+ return config
71
+ }
72
+
73
+ /**
74
+ * Extract only serializable properties from field configs
75
+ * Removes functions (getZodSchema, getPrismaType, getTypeScriptType)
76
+ * and non-serializable properties (access, hooks, typePatch)
77
+ */
78
+ export function serializeFieldConfigs(
79
+ fields: Record<string, FieldConfig>,
80
+ ): Record<string, SerializableFieldConfig> {
81
+ const serialized: Record<string, SerializableFieldConfig> = {}
82
+
83
+ for (const [fieldName, fieldConfig] of Object.entries(fields)) {
84
+ serialized[fieldName] = serializeFieldConfig(fieldConfig)
85
+ }
86
+
87
+ return serialized
88
+ }
@@ -0,0 +1,202 @@
1
+ import type { ThemeColors, ThemeConfig, ThemePreset } from '@opensaas/stack-core'
2
+
3
+ /**
4
+ * Preset theme definitions
5
+ */
6
+ export const presetThemes: Record<ThemePreset, { light: ThemeColors; dark: ThemeColors }> = {
7
+ modern: {
8
+ light: {
9
+ background: '220 20% 97%',
10
+ foreground: '220 30% 10%',
11
+ card: '0 0% 100%',
12
+ cardForeground: '220 30% 10%',
13
+ popover: '0 0% 100%',
14
+ popoverForeground: '220 30% 10%',
15
+ primary: '190 95% 55%',
16
+ primaryForeground: '220 30% 10%',
17
+ secondary: '220 15% 94%',
18
+ secondaryForeground: '220 30% 10%',
19
+ muted: '220 15% 94%',
20
+ mutedForeground: '220 15% 45%',
21
+ accent: '280 85% 65%',
22
+ accentForeground: '0 0% 100%',
23
+ destructive: '0 84% 60%',
24
+ destructiveForeground: '0 0% 100%',
25
+ border: '220 15% 88%',
26
+ input: '220 15% 88%',
27
+ ring: '190 95% 55%',
28
+ gradientFrom: '190 95% 55%',
29
+ gradientTo: '280 85% 65%',
30
+ },
31
+ dark: {
32
+ background: '220 25% 8%',
33
+ foreground: '220 15% 95%',
34
+ card: '220 20% 12%',
35
+ cardForeground: '220 15% 95%',
36
+ popover: '220 20% 12%',
37
+ popoverForeground: '220 15% 95%',
38
+ primary: '190 100% 60%',
39
+ primaryForeground: '220 30% 10%',
40
+ secondary: '220 20% 18%',
41
+ secondaryForeground: '220 15% 95%',
42
+ muted: '220 20% 18%',
43
+ mutedForeground: '220 10% 55%',
44
+ accent: '280 90% 70%',
45
+ accentForeground: '220 30% 10%',
46
+ destructive: '0 84% 65%',
47
+ destructiveForeground: '0 0% 100%',
48
+ border: '220 20% 22%',
49
+ input: '220 20% 22%',
50
+ ring: '190 100% 60%',
51
+ gradientFrom: '190 100% 60%',
52
+ gradientTo: '280 90% 70%',
53
+ },
54
+ },
55
+ classic: {
56
+ light: {
57
+ background: '0 0% 100%',
58
+ foreground: '222.2 84% 4.9%',
59
+ card: '0 0% 100%',
60
+ cardForeground: '222.2 84% 4.9%',
61
+ popover: '0 0% 100%',
62
+ popoverForeground: '222.2 84% 4.9%',
63
+ primary: '221.2 83.2% 53.3%',
64
+ primaryForeground: '210 40% 98%',
65
+ secondary: '210 40% 96.1%',
66
+ secondaryForeground: '222.2 47.4% 11.2%',
67
+ muted: '210 40% 96.1%',
68
+ mutedForeground: '215.4 16.3% 46.9%',
69
+ accent: '210 40% 96.1%',
70
+ accentForeground: '222.2 47.4% 11.2%',
71
+ destructive: '0 84.2% 60.2%',
72
+ destructiveForeground: '210 40% 98%',
73
+ border: '214.3 31.8% 91.4%',
74
+ input: '214.3 31.8% 91.4%',
75
+ ring: '221.2 83.2% 53.3%',
76
+ gradientFrom: '221.2 83.2% 53.3%',
77
+ gradientTo: '221.2 83.2% 53.3%',
78
+ },
79
+ dark: {
80
+ background: '222.2 84% 4.9%',
81
+ foreground: '210 40% 98%',
82
+ card: '222.2 84% 8%',
83
+ cardForeground: '210 40% 98%',
84
+ popover: '222.2 84% 8%',
85
+ popoverForeground: '210 40% 98%',
86
+ primary: '221.2 83.2% 53.3%',
87
+ primaryForeground: '210 40% 98%',
88
+ secondary: '217.2 32.6% 17.5%',
89
+ secondaryForeground: '210 40% 98%',
90
+ muted: '217.2 32.6% 17.5%',
91
+ mutedForeground: '215 20.2% 65.1%',
92
+ accent: '217.2 32.6% 17.5%',
93
+ accentForeground: '210 40% 98%',
94
+ destructive: '0 84.2% 60.2%',
95
+ destructiveForeground: '210 40% 98%',
96
+ border: '217.2 32.6% 17.5%',
97
+ input: '217.2 32.6% 17.5%',
98
+ ring: '221.2 83.2% 53.3%',
99
+ gradientFrom: '221.2 83.2% 53.3%',
100
+ gradientTo: '221.2 83.2% 53.3%',
101
+ },
102
+ },
103
+ neon: {
104
+ light: {
105
+ background: '0 0% 100%',
106
+ foreground: '240 10% 5%',
107
+ card: '0 0% 100%',
108
+ cardForeground: '240 10% 5%',
109
+ popover: '0 0% 100%',
110
+ popoverForeground: '240 10% 5%',
111
+ primary: '330 100% 50%',
112
+ primaryForeground: '0 0% 100%',
113
+ secondary: '240 5% 96%',
114
+ secondaryForeground: '240 10% 5%',
115
+ muted: '240 5% 96%',
116
+ mutedForeground: '240 5% 45%',
117
+ accent: '155 100% 50%',
118
+ accentForeground: '240 10% 5%',
119
+ destructive: '0 100% 50%',
120
+ destructiveForeground: '0 0% 100%',
121
+ border: '240 5.9% 90%',
122
+ input: '240 5.9% 90%',
123
+ ring: '330 100% 50%',
124
+ gradientFrom: '330 100% 50%',
125
+ gradientTo: '155 100% 50%',
126
+ },
127
+ dark: {
128
+ background: '240 10% 5%',
129
+ foreground: '0 0% 98%',
130
+ card: '240 10% 8%',
131
+ cardForeground: '0 0% 98%',
132
+ popover: '240 10% 8%',
133
+ popoverForeground: '0 0% 98%',
134
+ primary: '330 100% 60%',
135
+ primaryForeground: '240 10% 5%',
136
+ secondary: '240 5% 15%',
137
+ secondaryForeground: '0 0% 98%',
138
+ muted: '240 5% 15%',
139
+ mutedForeground: '240 5% 60%',
140
+ accent: '155 100% 60%',
141
+ accentForeground: '240 10% 5%',
142
+ destructive: '0 100% 60%',
143
+ destructiveForeground: '0 0% 100%',
144
+ border: '240 5% 20%',
145
+ input: '240 5% 20%',
146
+ ring: '330 100% 60%',
147
+ gradientFrom: '330 100% 60%',
148
+ gradientTo: '155 100% 60%',
149
+ },
150
+ },
151
+ }
152
+
153
+ /**
154
+ * Generate CSS variables from theme configuration
155
+ */
156
+ export function generateThemeCSS(config?: ThemeConfig): string {
157
+ const preset = config?.preset || 'modern'
158
+ const radius = config?.radius ?? 0.75
159
+
160
+ // Get base colors from preset
161
+ const baseLight = presetThemes[preset].light
162
+ const baseDark = presetThemes[preset].dark
163
+
164
+ // Merge with custom overrides
165
+ const lightColors = { ...baseLight, ...config?.colors }
166
+ const darkColors = { ...baseDark, ...config?.darkColors }
167
+
168
+ // Convert colors to CSS variables
169
+ const colorToCSSVar = (key: string, value: string) => {
170
+ // Convert camelCase to kebab-case with --color- prefix
171
+ const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase()
172
+ return ` --color-${cssKey}: ${value};`
173
+ }
174
+
175
+ const lightVars = Object.entries(lightColors)
176
+ .map(([key, value]) => colorToCSSVar(key, value as string))
177
+ .join('\n')
178
+
179
+ const darkVars = Object.entries(darkColors)
180
+ .map(([key, value]) => colorToCSSVar(key, value as string))
181
+ .join('\n')
182
+
183
+ return `
184
+ :root {
185
+ ${lightVars}
186
+ --radius: ${radius}rem;
187
+ }
188
+
189
+ @media (prefers-color-scheme: dark) {
190
+ :root {
191
+ ${darkVars}
192
+ }
193
+ }
194
+ `.trim()
195
+ }
196
+
197
+ /**
198
+ * Get theme CSS as a style tag content
199
+ */
200
+ export function getThemeStyleTag(config?: ThemeConfig): string {
201
+ return `<style id="opensaas-theme">${generateThemeCSS(config)}</style>`
202
+ }
@@ -0,0 +1,81 @@
1
+ import { type ClassValue, clsx } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ /**
5
+ * Merge Tailwind CSS classes with clsx
6
+ */
7
+ export function cn(...inputs: ClassValue[]) {
8
+ return twMerge(clsx(inputs))
9
+ }
10
+
11
+ /**
12
+ * Format a list name for display (PascalCase → Title Case)
13
+ */
14
+ export function formatListName(name: string): string {
15
+ return name
16
+ .replace(/([A-Z])/g, ' $1')
17
+ .trim()
18
+ .replace(/^./, (str) => str.toUpperCase())
19
+ }
20
+
21
+ /**
22
+ * Format a field name for display (camelCase → Title Case)
23
+ */
24
+ export function formatFieldName(name: string): string {
25
+ return name
26
+ .replace(/([A-Z])/g, ' $1')
27
+ .replace(/^./, (str) => str.toUpperCase())
28
+ .trim()
29
+ }
30
+
31
+ /**
32
+ * Get the display value for a field
33
+ */
34
+ export function getFieldDisplayValue(value: unknown, fieldType: string): string {
35
+ if (value === null || value === undefined) {
36
+ return '-'
37
+ }
38
+
39
+ switch (fieldType) {
40
+ case 'checkbox':
41
+ return value ? 'Yes' : 'No'
42
+ case 'timestamp':
43
+ return new Date(value as string | number | Date).toLocaleString()
44
+ case 'password':
45
+ return '••••••••'
46
+ case 'relationship':
47
+ return getRelationshipDisplayValue(value)
48
+ default:
49
+ return String(value)
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Get display value for a relationship field
55
+ * Tries to display: name → title → label → id
56
+ */
57
+ function getRelationshipDisplayValue(value: unknown): string {
58
+ if (!value || typeof value !== 'object') {
59
+ return '-'
60
+ }
61
+
62
+ // Handle array of relationships (many: true)
63
+ if (Array.isArray(value)) {
64
+ if (value.length === 0) return '-'
65
+ return value.map((item) => getRelationshipDisplayValue(item)).join(', ')
66
+ }
67
+
68
+ // Handle single relationship object
69
+ let displayValue: unknown
70
+ if ('name' in value) {
71
+ displayValue = value.name
72
+ } else if ('title' in value) {
73
+ displayValue = value.title
74
+ } else if ('label' in value) {
75
+ displayValue = value.label
76
+ } else if ('id' in value) {
77
+ displayValue = value.id
78
+ }
79
+
80
+ return displayValue ? String(displayValue) : '-'
81
+ }
@@ -0,0 +1,49 @@
1
+ import * as React from 'react'
2
+ import { Slot } from '@radix-ui/react-slot'
3
+ import { cva, type VariantProps } from 'class-variance-authority'
4
+
5
+ import { cn } from '../lib/utils.js'
6
+
7
+ const buttonVariants = cva(
8
+ 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13
+ destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
14
+ outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
15
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
16
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
17
+ link: 'text-primary underline-offset-4 hover:underline',
18
+ },
19
+ size: {
20
+ default: 'h-10 px-4 py-2',
21
+ sm: 'h-9 rounded-md px-3',
22
+ lg: 'h-11 rounded-md px-8',
23
+ icon: 'h-10 w-10',
24
+ },
25
+ },
26
+ defaultVariants: {
27
+ variant: 'default',
28
+ size: 'default',
29
+ },
30
+ },
31
+ )
32
+
33
+ export interface ButtonProps
34
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
35
+ VariantProps<typeof buttonVariants> {
36
+ asChild?: boolean
37
+ }
38
+
39
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
40
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
41
+ const Comp = asChild ? Slot : 'button'
42
+ return (
43
+ <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
44
+ )
45
+ },
46
+ )
47
+ Button.displayName = 'Button'
48
+
49
+ export { Button, buttonVariants }