@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.
- package/.turbo/turbo-build.log +8 -0
- package/README.md +286 -0
- package/dist/components/AdminUI.d.ts +24 -0
- package/dist/components/AdminUI.d.ts.map +1 -0
- package/dist/components/AdminUI.js +48 -0
- package/dist/components/ConfirmDialog.d.ts +16 -0
- package/dist/components/ConfirmDialog.d.ts.map +1 -0
- package/dist/components/ConfirmDialog.js +11 -0
- package/dist/components/Dashboard.d.ts +12 -0
- package/dist/components/Dashboard.d.ts.map +1 -0
- package/dist/components/Dashboard.js +30 -0
- package/dist/components/ItemForm.d.ts +17 -0
- package/dist/components/ItemForm.d.ts.map +1 -0
- package/dist/components/ItemForm.js +97 -0
- package/dist/components/ItemFormClient.d.ts +22 -0
- package/dist/components/ItemFormClient.d.ts.map +1 -0
- package/dist/components/ItemFormClient.js +127 -0
- package/dist/components/ListView.d.ts +17 -0
- package/dist/components/ListView.d.ts.map +1 -0
- package/dist/components/ListView.js +76 -0
- package/dist/components/ListViewClient.d.ts +19 -0
- package/dist/components/ListViewClient.d.ts.map +1 -0
- package/dist/components/ListViewClient.js +108 -0
- package/dist/components/LoadingSpinner.d.ts +10 -0
- package/dist/components/LoadingSpinner.d.ts.map +1 -0
- package/dist/components/LoadingSpinner.js +14 -0
- package/dist/components/Navigation.d.ts +13 -0
- package/dist/components/Navigation.d.ts.map +1 -0
- package/dist/components/Navigation.js +20 -0
- package/dist/components/SkeletonLoader.d.ts +22 -0
- package/dist/components/SkeletonLoader.d.ts.map +1 -0
- package/dist/components/SkeletonLoader.js +25 -0
- package/dist/components/fields/CheckboxField.d.ts +11 -0
- package/dist/components/fields/CheckboxField.d.ts.map +1 -0
- package/dist/components/fields/CheckboxField.js +10 -0
- package/dist/components/fields/ComboboxField.d.ts +18 -0
- package/dist/components/fields/ComboboxField.d.ts.map +1 -0
- package/dist/components/fields/ComboboxField.js +32 -0
- package/dist/components/fields/FieldRenderer.d.ts +22 -0
- package/dist/components/fields/FieldRenderer.d.ts.map +1 -0
- package/dist/components/fields/FieldRenderer.js +81 -0
- package/dist/components/fields/IntegerField.d.ts +15 -0
- package/dist/components/fields/IntegerField.d.ts.map +1 -0
- package/dist/components/fields/IntegerField.js +14 -0
- package/dist/components/fields/PasswordField.d.ts +18 -0
- package/dist/components/fields/PasswordField.d.ts.map +1 -0
- package/dist/components/fields/PasswordField.js +42 -0
- package/dist/components/fields/RelationshipField.d.ts +20 -0
- package/dist/components/fields/RelationshipField.d.ts.map +1 -0
- package/dist/components/fields/RelationshipField.js +11 -0
- package/dist/components/fields/RelationshipManager.d.ts +19 -0
- package/dist/components/fields/RelationshipManager.d.ts.map +1 -0
- package/dist/components/fields/RelationshipManager.js +37 -0
- package/dist/components/fields/SelectField.d.ts +16 -0
- package/dist/components/fields/SelectField.d.ts.map +1 -0
- package/dist/components/fields/SelectField.js +11 -0
- package/dist/components/fields/TextField.d.ts +13 -0
- package/dist/components/fields/TextField.d.ts.map +1 -0
- package/dist/components/fields/TextField.js +11 -0
- package/dist/components/fields/TimestampField.d.ts +12 -0
- package/dist/components/fields/TimestampField.d.ts.map +1 -0
- package/dist/components/fields/TimestampField.js +12 -0
- package/dist/components/fields/index.d.ts +23 -0
- package/dist/components/fields/index.d.ts.map +1 -0
- package/dist/components/fields/index.js +13 -0
- package/dist/components/fields/registry.d.ts +43 -0
- package/dist/components/fields/registry.d.ts.map +1 -0
- package/dist/components/fields/registry.js +42 -0
- package/dist/components/standalone/DeleteButton.d.ts +35 -0
- package/dist/components/standalone/DeleteButton.d.ts.map +1 -0
- package/dist/components/standalone/DeleteButton.js +46 -0
- package/dist/components/standalone/ItemCreateForm.d.ts +34 -0
- package/dist/components/standalone/ItemCreateForm.d.ts.map +1 -0
- package/dist/components/standalone/ItemCreateForm.js +91 -0
- package/dist/components/standalone/ItemEditForm.d.ts +37 -0
- package/dist/components/standalone/ItemEditForm.d.ts.map +1 -0
- package/dist/components/standalone/ItemEditForm.js +112 -0
- package/dist/components/standalone/ListTable.d.ts +33 -0
- package/dist/components/standalone/ListTable.d.ts.map +1 -0
- package/dist/components/standalone/ListTable.js +94 -0
- package/dist/components/standalone/SearchBar.d.ts +29 -0
- package/dist/components/standalone/SearchBar.d.ts.map +1 -0
- package/dist/components/standalone/SearchBar.js +43 -0
- package/dist/components/standalone/index.d.ts +11 -0
- package/dist/components/standalone/index.d.ts.map +1 -0
- package/dist/components/standalone/index.js +6 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/lib/serializeFieldConfig.d.ts +43 -0
- package/dist/lib/serializeFieldConfig.d.ts.map +1 -0
- package/dist/lib/serializeFieldConfig.js +48 -0
- package/dist/lib/theme.d.ts +17 -0
- package/dist/lib/theme.d.ts.map +1 -0
- package/dist/lib/theme.js +192 -0
- package/dist/lib/utils.d.ts +18 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +76 -0
- package/dist/primitives/button.d.ts +12 -0
- package/dist/primitives/button.d.ts.map +1 -0
- package/dist/primitives/button.js +33 -0
- package/dist/primitives/calendar.d.ts +9 -0
- package/dist/primitives/calendar.d.ts.map +1 -0
- package/dist/primitives/calendar.js +48 -0
- package/dist/primitives/card.d.ts +9 -0
- package/dist/primitives/card.d.ts.map +1 -0
- package/dist/primitives/card.js +16 -0
- package/dist/primitives/checkbox.d.ts +5 -0
- package/dist/primitives/checkbox.d.ts.map +1 -0
- package/dist/primitives/checkbox.js +7 -0
- package/dist/primitives/combobox.d.ts +14 -0
- package/dist/primitives/combobox.d.ts.map +1 -0
- package/dist/primitives/combobox.js +20 -0
- package/dist/primitives/datetime-picker.d.ts +9 -0
- package/dist/primitives/datetime-picker.d.ts.map +1 -0
- package/dist/primitives/datetime-picker.js +42 -0
- package/dist/primitives/dialog.d.ts +20 -0
- package/dist/primitives/dialog.d.ts.map +1 -0
- package/dist/primitives/dialog.js +21 -0
- package/dist/primitives/index.d.ts +14 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +14 -0
- package/dist/primitives/input.d.ts +5 -0
- package/dist/primitives/input.d.ts.map +1 -0
- package/dist/primitives/input.js +8 -0
- package/dist/primitives/label.d.ts +6 -0
- package/dist/primitives/label.d.ts.map +1 -0
- package/dist/primitives/label.js +9 -0
- package/dist/primitives/popover.d.ts +7 -0
- package/dist/primitives/popover.d.ts.map +1 -0
- package/dist/primitives/popover.js +10 -0
- package/dist/primitives/select.d.ts +14 -0
- package/dist/primitives/select.d.ts.map +1 -0
- package/dist/primitives/select.js +24 -0
- package/dist/primitives/table.d.ts +11 -0
- package/dist/primitives/table.d.ts.map +1 -0
- package/dist/primitives/table.js +20 -0
- package/dist/primitives/time-picker.d.ts +8 -0
- package/dist/primitives/time-picker.d.ts.map +1 -0
- package/dist/primitives/time-picker.js +27 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +2 -0
- package/dist/server/types.d.ts +15 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +1 -0
- package/dist/styles/globals.css +1896 -0
- package/package.json +91 -0
- package/postcss.config.cjs +5 -0
- package/src/components/AdminUI.tsx +112 -0
- package/src/components/ConfirmDialog.tsx +56 -0
- package/src/components/Dashboard.tsx +134 -0
- package/src/components/ItemForm.tsx +195 -0
- package/src/components/ItemFormClient.tsx +237 -0
- package/src/components/ListView.tsx +153 -0
- package/src/components/ListViewClient.tsx +282 -0
- package/src/components/LoadingSpinner.tsx +32 -0
- package/src/components/Navigation.tsx +117 -0
- package/src/components/SkeletonLoader.tsx +82 -0
- package/src/components/fields/CheckboxField.tsx +54 -0
- package/src/components/fields/ComboboxField.tsx +127 -0
- package/src/components/fields/FieldRenderer.tsx +132 -0
- package/src/components/fields/IntegerField.tsx +68 -0
- package/src/components/fields/PasswordField.tsx +159 -0
- package/src/components/fields/RelationshipField.tsx +71 -0
- package/src/components/fields/RelationshipManager.tsx +189 -0
- package/src/components/fields/SelectField.tsx +71 -0
- package/src/components/fields/TextField.tsx +59 -0
- package/src/components/fields/TimestampField.tsx +49 -0
- package/src/components/fields/index.ts +27 -0
- package/src/components/fields/registry.ts +72 -0
- package/src/components/standalone/DeleteButton.tsx +114 -0
- package/src/components/standalone/ItemCreateForm.tsx +161 -0
- package/src/components/standalone/ItemEditForm.tsx +193 -0
- package/src/components/standalone/ListTable.tsx +211 -0
- package/src/components/standalone/SearchBar.tsx +86 -0
- package/src/components/standalone/index.ts +13 -0
- package/src/index.ts +74 -0
- package/src/lib/serializeFieldConfig.ts +88 -0
- package/src/lib/theme.ts +202 -0
- package/src/lib/utils.ts +81 -0
- package/src/primitives/button.tsx +49 -0
- package/src/primitives/calendar.tsx +160 -0
- package/src/primitives/card.tsx +58 -0
- package/src/primitives/checkbox.tsx +27 -0
- package/src/primitives/combobox.tsx +159 -0
- package/src/primitives/datetime-picker.tsx +130 -0
- package/src/primitives/dialog.tsx +108 -0
- package/src/primitives/index.ts +54 -0
- package/src/primitives/input.tsx +24 -0
- package/src/primitives/label.tsx +19 -0
- package/src/primitives/popover.tsx +31 -0
- package/src/primitives/select.tsx +158 -0
- package/src/primitives/table.tsx +91 -0
- package/src/primitives/time-picker.tsx +65 -0
- package/src/server/index.ts +3 -0
- package/src/server/types.ts +15 -0
- package/src/styles/globals.css +123 -0
- package/tailwind.config.ts +3 -0
- package/tests/components/TextField.test.tsx +94 -0
- package/tests/setup.ts +11 -0
- package/tsconfig.json +26 -0
- 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
|
+
}
|
package/src/lib/theme.ts
ADDED
|
@@ -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
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -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 }
|