@actuate-media/cms-admin 0.10.0 → 0.12.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/LICENSE +21 -21
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +8 -5
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/fields/component-block-helpers.test.d.ts +7 -0
- package/dist/__tests__/fields/component-block-helpers.test.d.ts.map +1 -0
- package/dist/__tests__/fields/component-block-helpers.test.js +592 -0
- package/dist/__tests__/fields/component-block-helpers.test.js.map +1 -0
- package/dist/__tests__/layout/primitives.test.d.ts +2 -0
- package/dist/__tests__/layout/primitives.test.d.ts.map +1 -0
- package/dist/__tests__/layout/primitives.test.js +34 -0
- package/dist/__tests__/layout/primitives.test.js.map +1 -0
- package/dist/__tests__/lib/cv.test.d.ts +2 -0
- package/dist/__tests__/lib/cv.test.d.ts.map +1 -0
- package/dist/__tests__/lib/cv.test.js +66 -0
- package/dist/__tests__/lib/cv.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/assets/actuate-logo.d.ts +36 -0
- package/dist/assets/actuate-logo.d.ts.map +1 -0
- package/dist/assets/actuate-logo.js +15 -0
- package/dist/assets/actuate-logo.js.map +1 -0
- package/dist/components/Breadcrumbs.js +2 -2
- package/dist/components/CommandPalette.js +10 -10
- package/dist/components/ContentOverviewChart.js +3 -3
- package/dist/components/ErrorBoundary.js +1 -1
- package/dist/components/FocalPointPicker.js +2 -2
- package/dist/components/FolderTree.js +20 -20
- package/dist/components/LivePreview.js +3 -3
- package/dist/components/LocaleSwitcher.js +1 -1
- package/dist/components/MediaPickerModal.js +4 -4
- package/dist/components/PresenceIndicator.js +1 -1
- package/dist/components/SEOConfigPanel.d.ts +2 -0
- package/dist/components/SEOConfigPanel.d.ts.map +1 -0
- package/dist/components/SEOConfigPanel.js +174 -0
- package/dist/components/SEOConfigPanel.js.map +1 -0
- package/dist/components/SEOPanel.js +9 -9
- package/dist/components/SEOPerformance.js +2 -2
- package/dist/components/SchedulePublishDialog.js +1 -1
- package/dist/components/SharePreviewLinkDialog.js +1 -1
- package/dist/components/TipTapEditor.js +5 -5
- package/dist/components/VersionHistory.js +2 -2
- package/dist/components/ui/Badge.d.ts +33 -3
- package/dist/components/ui/Badge.d.ts.map +1 -1
- package/dist/components/ui/Badge.js +42 -8
- package/dist/components/ui/Badge.js.map +1 -1
- package/dist/components/ui/Button.d.ts +19 -8
- package/dist/components/ui/Button.d.ts.map +1 -1
- package/dist/components/ui/Button.js +35 -14
- package/dist/components/ui/Button.js.map +1 -1
- package/dist/components/ui/Card.d.ts +26 -0
- package/dist/components/ui/Card.d.ts.map +1 -0
- package/dist/components/ui/Card.js +45 -0
- package/dist/components/ui/Card.js.map +1 -0
- package/dist/components/ui/DataTable.js +1 -1
- package/dist/components/ui/Input.d.ts +15 -0
- package/dist/components/ui/Input.d.ts.map +1 -0
- package/dist/components/ui/Input.js +23 -0
- package/dist/components/ui/Input.js.map +1 -0
- package/dist/components/ui/SearchInput.js +1 -1
- package/dist/components/ui/Select.d.ts +16 -0
- package/dist/components/ui/Select.d.ts.map +1 -0
- package/dist/components/ui/Select.js +25 -0
- package/dist/components/ui/Select.js.map +1 -0
- package/dist/components/ui/Toast.js +1 -1
- package/dist/components/ui/index.d.ts +10 -4
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +5 -2
- package/dist/components/ui/index.js.map +1 -1
- package/dist/fields/BlockBuilderField.js +3 -3
- package/dist/fields/ComponentBlockField.d.ts +25 -0
- package/dist/fields/ComponentBlockField.d.ts.map +1 -0
- package/dist/fields/ComponentBlockField.js +74 -0
- package/dist/fields/ComponentBlockField.js.map +1 -0
- package/dist/fields/DateField.js +1 -1
- package/dist/fields/FieldRenderer.d.ts +3 -0
- package/dist/fields/FieldRenderer.d.ts.map +1 -1
- package/dist/fields/FieldRenderer.js +3 -1
- package/dist/fields/FieldRenderer.js.map +1 -1
- package/dist/fields/PropInput.d.ts +14 -0
- package/dist/fields/PropInput.d.ts.map +1 -0
- package/dist/fields/PropInput.js +163 -0
- package/dist/fields/PropInput.js.map +1 -0
- package/dist/fields/RelationshipField.js +3 -3
- package/dist/fields/TextField.js +1 -1
- package/dist/fields/component-block-helpers.d.ts +96 -0
- package/dist/fields/component-block-helpers.d.ts.map +1 -0
- package/dist/fields/component-block-helpers.js +323 -0
- package/dist/fields/component-block-helpers.js.map +1 -0
- package/dist/fields/index.d.ts +4 -0
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +2 -0
- package/dist/fields/index.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/layout/Header.js +1 -1
- package/dist/layout/Layout.d.ts +14 -0
- package/dist/layout/Layout.d.ts.map +1 -1
- package/dist/layout/Layout.js +17 -11
- package/dist/layout/Layout.js.map +1 -1
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +21 -11
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/layout/primitives/AdminShell.d.ts +43 -0
- package/dist/layout/primitives/AdminShell.d.ts.map +1 -0
- package/dist/layout/primitives/AdminShell.js +51 -0
- package/dist/layout/primitives/AdminShell.js.map +1 -0
- package/dist/layout/primitives/Box.d.ts +19 -0
- package/dist/layout/primitives/Box.d.ts.map +1 -0
- package/dist/layout/primitives/Box.js +12 -0
- package/dist/layout/primitives/Box.js.map +1 -0
- package/dist/layout/primitives/Cluster.d.ts +27 -0
- package/dist/layout/primitives/Cluster.d.ts.map +1 -0
- package/dist/layout/primitives/Cluster.js +37 -0
- package/dist/layout/primitives/Cluster.js.map +1 -0
- package/dist/layout/primitives/Grid.d.ts +45 -0
- package/dist/layout/primitives/Grid.d.ts.map +1 -0
- package/dist/layout/primitives/Grid.js +59 -0
- package/dist/layout/primitives/Grid.js.map +1 -0
- package/dist/layout/primitives/PageContainer.d.ts +36 -0
- package/dist/layout/primitives/PageContainer.d.ts.map +1 -0
- package/dist/layout/primitives/PageContainer.js +41 -0
- package/dist/layout/primitives/PageContainer.js.map +1 -0
- package/dist/layout/primitives/Split.d.ts +34 -0
- package/dist/layout/primitives/Split.d.ts.map +1 -0
- package/dist/layout/primitives/Split.js +27 -0
- package/dist/layout/primitives/Split.js.map +1 -0
- package/dist/layout/primitives/Stack.d.ts +23 -0
- package/dist/layout/primitives/Stack.d.ts.map +1 -0
- package/dist/layout/primitives/Stack.js +34 -0
- package/dist/layout/primitives/Stack.js.map +1 -0
- package/dist/layout/primitives/index.d.ts +30 -0
- package/dist/layout/primitives/index.d.ts.map +1 -0
- package/dist/layout/primitives/index.js +22 -0
- package/dist/layout/primitives/index.js.map +1 -0
- package/dist/layout/primitives/tokens.d.ts +48 -0
- package/dist/layout/primitives/tokens.d.ts.map +1 -0
- package/dist/layout/primitives/tokens.js +54 -0
- package/dist/layout/primitives/tokens.js.map +1 -0
- package/dist/lib/cv.d.ts +53 -0
- package/dist/lib/cv.d.ts.map +1 -0
- package/dist/lib/cv.js +39 -0
- package/dist/lib/cv.js.map +1 -0
- package/dist/views/ApiKeys.js +7 -7
- package/dist/views/CollectionList.js +8 -8
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +333 -78
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/DocumentEdit.js +3 -3
- package/dist/views/ForgotPassword.js +2 -2
- package/dist/views/FormEditor.js +5 -5
- package/dist/views/FormSubmissions.js +6 -6
- package/dist/views/Forms.js +2 -2
- package/dist/views/Login.d.ts +16 -1
- package/dist/views/Login.d.ts.map +1 -1
- package/dist/views/Login.js +17 -7
- package/dist/views/Login.js.map +1 -1
- package/dist/views/MediaBrowser.js +16 -16
- package/dist/views/PageEditor.js +2 -2
- package/dist/views/Pages.js +10 -10
- package/dist/views/PostEditor.js +2 -2
- package/dist/views/Posts.js +4 -4
- package/dist/views/Redirects.js +4 -4
- package/dist/views/ResetPassword.js +2 -2
- package/dist/views/SEO.js +6 -6
- package/dist/views/ScriptTagEditor.js +4 -4
- package/dist/views/ScriptTags.js +2 -2
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +9 -8
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/SetupWizard.js +2 -2
- package/dist/views/Users.js +4 -4
- package/dist/views/page-builder/AIBlockAssist.js +1 -1
- package/dist/views/page-builder/AIGenerateDialog.js +10 -10
- package/dist/views/page-builder/BlockEditor.js +10 -10
- package/dist/views/page-builder/BlockPicker.js +4 -4
- package/dist/views/page-builder/BottomBar.js +1 -1
- package/dist/views/page-builder/BuilderToolbar.js +2 -2
- package/dist/views/page-builder/ContextPanel.js +2 -2
- package/dist/views/page-builder/DesignScore.js +9 -9
- package/dist/views/page-builder/NodeSettings.js +8 -8
- package/dist/views/page-builder/PageBuilder.js +3 -3
- package/dist/views/page-builder/PageSettings.js +1 -1
- package/dist/views/page-builder/PageTemplates.js +2 -2
- package/dist/views/page-builder/SEOPanel.js +13 -13
- package/dist/views/page-builder/SavedSections.js +5 -5
- package/dist/views/page-builder/TemplatePicker.js +2 -2
- package/dist/views/page-builder/block-renderers/CTAPreview.js +5 -5
- package/dist/views/page-builder/block-renderers/CardsPreview.js +1 -1
- package/dist/views/page-builder/block-renderers/CodePreview.js +1 -1
- package/dist/views/page-builder/block-renderers/FAQPreview.js +3 -3
- package/dist/views/page-builder/block-renderers/FallbackPreview.js +1 -1
- package/dist/views/page-builder/block-renderers/FormPreview.js +3 -3
- package/dist/views/page-builder/block-renderers/GalleryPreview.js +5 -5
- package/dist/views/page-builder/block-renderers/HeroPreview.js +3 -3
- package/dist/views/page-builder/block-renderers/ImagePreview.js +3 -3
- package/dist/views/page-builder/block-renderers/TextPreview.js +3 -3
- package/dist/views/page-builder/block-renderers/VideoPreview.js +4 -4
- package/dist/views/page-builder/canvas/BlockRenderer.js +1 -1
- package/dist/views/page-builder/canvas/BuilderCanvas.js +3 -3
- package/dist/views/page-builder/canvas/ColumnRenderer.js +2 -2
- package/dist/views/page-builder/canvas/ContainerRenderer.js +2 -2
- package/dist/views/page-builder/canvas/RowRenderer.js +2 -2
- package/dist/views/page-builder/canvas/SectionRenderer.js +2 -2
- package/package.json +14 -3
- package/src/AdminRoot.tsx +21 -11
- package/src/__tests__/fields/component-block-helpers.test.ts +674 -0
- package/src/__tests__/layout/primitives.test.ts +37 -0
- package/src/__tests__/lib/cv.test.ts +74 -0
- package/src/assets/actuate-logo.tsx +72 -0
- package/src/components/Breadcrumbs.tsx +6 -6
- package/src/components/CommandPalette.tsx +34 -34
- package/src/components/ContentOverviewChart.tsx +3 -3
- package/src/components/ErrorBoundary.tsx +3 -3
- package/src/components/FocalPointPicker.tsx +4 -4
- package/src/components/FolderTree.tsx +38 -38
- package/src/components/LivePreview.tsx +16 -16
- package/src/components/LocaleSwitcher.tsx +7 -7
- package/src/components/MediaPickerModal.tsx +21 -21
- package/src/components/PresenceIndicator.tsx +2 -2
- package/src/components/SEOConfigPanel.tsx +582 -0
- package/src/components/SEOPanel.tsx +46 -46
- package/src/components/SEOPerformance.tsx +21 -21
- package/src/components/SchedulePublishDialog.tsx +4 -4
- package/src/components/SharePreviewLinkDialog.tsx +1 -1
- package/src/components/TipTapEditor.tsx +33 -33
- package/src/components/VersionHistory.tsx +16 -16
- package/src/components/ui/Badge.tsx +66 -14
- package/src/components/ui/Button.tsx +70 -33
- package/src/components/ui/Card.tsx +101 -0
- package/src/components/ui/DataTable.tsx +1 -1
- package/src/components/ui/Input.tsx +35 -0
- package/src/components/ui/SearchInput.tsx +4 -4
- package/src/components/ui/Select.tsx +56 -0
- package/src/components/ui/Toast.tsx +1 -1
- package/src/components/ui/index.ts +18 -4
- package/src/fields/BlockBuilderField.tsx +3 -3
- package/src/fields/ComponentBlockField.tsx +179 -0
- package/src/fields/DateField.tsx +1 -1
- package/src/fields/FieldRenderer.tsx +8 -0
- package/src/fields/PropInput.tsx +552 -0
- package/src/fields/RelationshipField.tsx +10 -10
- package/src/fields/TextField.tsx +1 -1
- package/src/fields/component-block-helpers.ts +341 -0
- package/src/fields/index.ts +4 -0
- package/src/index.ts +35 -0
- package/src/layout/Header.tsx +28 -28
- package/src/layout/Layout.tsx +39 -46
- package/src/layout/Sidebar.tsx +37 -64
- package/src/layout/primitives/AdminShell.tsx +118 -0
- package/src/layout/primitives/Box.tsx +30 -0
- package/src/layout/primitives/Cluster.tsx +74 -0
- package/src/layout/primitives/Grid.tsx +120 -0
- package/src/layout/primitives/PageContainer.tsx +96 -0
- package/src/layout/primitives/Split.tsx +73 -0
- package/src/layout/primitives/Stack.tsx +67 -0
- package/src/layout/primitives/index.ts +36 -0
- package/src/layout/primitives/tokens.ts +76 -0
- package/src/lib/cv.ts +96 -0
- package/src/styles/build-input.css +1 -1
- package/src/views/ApiKeys.tsx +57 -57
- package/src/views/CollectionList.tsx +30 -30
- package/src/views/Dashboard.tsx +737 -186
- package/src/views/DocumentEdit.tsx +9 -9
- package/src/views/ForgotPassword.tsx +18 -18
- package/src/views/FormEditor.tsx +75 -75
- package/src/views/FormSubmissions.tsx +76 -76
- package/src/views/Forms.tsx +27 -27
- package/src/views/Login.tsx +65 -25
- package/src/views/MediaBrowser.tsx +127 -127
- package/src/views/PageEditor.tsx +25 -25
- package/src/views/Pages.tsx +59 -59
- package/src/views/PostEditor.tsx +37 -37
- package/src/views/Posts.tsx +48 -48
- package/src/views/Redirects.tsx +21 -21
- package/src/views/ResetPassword.tsx +28 -28
- package/src/views/SEO.tsx +144 -144
- package/src/views/ScriptTagEditor.tsx +24 -24
- package/src/views/ScriptTags.tsx +10 -10
- package/src/views/Settings.tsx +88 -80
- package/src/views/SetupWizard.tsx +28 -28
- package/src/views/Users.tsx +20 -20
- package/src/views/page-builder/AIBlockAssist.tsx +1 -1
- package/src/views/page-builder/AIGenerateDialog.tsx +63 -63
- package/src/views/page-builder/BlockEditor.tsx +26 -26
- package/src/views/page-builder/BlockPicker.tsx +22 -22
- package/src/views/page-builder/BottomBar.tsx +8 -8
- package/src/views/page-builder/BuilderToolbar.tsx +17 -17
- package/src/views/page-builder/ContextPanel.tsx +3 -3
- package/src/views/page-builder/DesignScore.tsx +21 -21
- package/src/views/page-builder/NodeSettings.tsx +27 -27
- package/src/views/page-builder/PageBuilder.tsx +11 -11
- package/src/views/page-builder/PageSettings.tsx +4 -4
- package/src/views/page-builder/PageTemplates.tsx +18 -18
- package/src/views/page-builder/SEOPanel.tsx +53 -53
- package/src/views/page-builder/SavedSections.tsx +37 -37
- package/src/views/page-builder/TemplatePicker.tsx +17 -17
- package/src/views/page-builder/block-renderers/CTAPreview.tsx +13 -13
- package/src/views/page-builder/block-renderers/CardsPreview.tsx +5 -5
- package/src/views/page-builder/block-renderers/CodePreview.tsx +6 -6
- package/src/views/page-builder/block-renderers/FAQPreview.tsx +13 -13
- package/src/views/page-builder/block-renderers/FallbackPreview.tsx +3 -3
- package/src/views/page-builder/block-renderers/FormPreview.tsx +20 -20
- package/src/views/page-builder/block-renderers/GalleryPreview.tsx +8 -8
- package/src/views/page-builder/block-renderers/HeroPreview.tsx +16 -16
- package/src/views/page-builder/block-renderers/ImagePreview.tsx +4 -4
- package/src/views/page-builder/block-renderers/TextPreview.tsx +14 -14
- package/src/views/page-builder/block-renderers/VideoPreview.tsx +12 -12
- package/src/views/page-builder/canvas/BlockRenderer.tsx +4 -4
- package/src/views/page-builder/canvas/BuilderCanvas.tsx +6 -6
- package/src/views/page-builder/canvas/ColumnRenderer.tsx +3 -3
- package/src/views/page-builder/canvas/ContainerRenderer.tsx +2 -2
- package/src/views/page-builder/canvas/RowRenderer.tsx +2 -2
- package/src/views/page-builder/canvas/SectionRenderer.tsx +2 -2
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Recursive input dispatcher for component-block props. Given a
|
|
5
|
+
* {@link PropSpec} and the currently stored value, renders the correct
|
|
6
|
+
* editor:
|
|
7
|
+
*
|
|
8
|
+
* | PropType.kind | Editor |
|
|
9
|
+
* | --------------------- | ---------------------------------------------- |
|
|
10
|
+
* | `string` | single-line text input |
|
|
11
|
+
* | `number` | native number input |
|
|
12
|
+
* | `boolean` | switch (ToggleField look) |
|
|
13
|
+
* | `enum` | select with the literal values |
|
|
14
|
+
* | `literal` | readonly display (it's the only allowed value) |
|
|
15
|
+
* | `array` | add/remove list of recursive PropInputs |
|
|
16
|
+
* | `object` | bordered group of recursive PropInputs |
|
|
17
|
+
* | `union` (discriminated) | variant picker + recursive object form |
|
|
18
|
+
* | `union` (other) | JSON textarea + hint |
|
|
19
|
+
* | `reference` | JSON textarea + "unresolved" hint |
|
|
20
|
+
* | `unknown` | JSON textarea + warning |
|
|
21
|
+
*
|
|
22
|
+
* The component is intentionally small and self-contained — no calls
|
|
23
|
+
* into cms-core; the parent {@link ComponentBlockField} runs validation
|
|
24
|
+
* via `validateComponentBlockValue` and pipes per-prop errors back in
|
|
25
|
+
* via the `errors` prop.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
detectDiscriminator,
|
|
30
|
+
findVariant,
|
|
31
|
+
} from '@actuate-media/component-blocks/discriminated-union'
|
|
32
|
+
import type { DiscriminatedUnion } from '@actuate-media/component-blocks/discriminated-union'
|
|
33
|
+
import type { PropSpec, PropType } from '@actuate-media/component-blocks'
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
defaultForType,
|
|
37
|
+
parseEnumSelection,
|
|
38
|
+
safeJsonStringify,
|
|
39
|
+
switchUnionVariant,
|
|
40
|
+
} from './component-block-helpers.js'
|
|
41
|
+
|
|
42
|
+
export { defaultForType }
|
|
43
|
+
|
|
44
|
+
export interface PropInputProps {
|
|
45
|
+
prop: PropSpec
|
|
46
|
+
value: unknown
|
|
47
|
+
onChange: (value: unknown) => void
|
|
48
|
+
/** Map keyed by `propPath` (dot-joined) for nested error display. */
|
|
49
|
+
errors?: Record<string, string>
|
|
50
|
+
/** Dot-separated path for the field; used as the error map key. */
|
|
51
|
+
path?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function PropInput({ prop, value, onChange, errors, path }: PropInputProps) {
|
|
55
|
+
const fullPath = path ?? prop.name
|
|
56
|
+
const safeErrors = errors ?? EMPTY_ERRORS
|
|
57
|
+
const error = safeErrors[fullPath]
|
|
58
|
+
return (
|
|
59
|
+
<div className="space-y-1">
|
|
60
|
+
<PropInputBody
|
|
61
|
+
prop={prop}
|
|
62
|
+
value={value}
|
|
63
|
+
onChange={onChange}
|
|
64
|
+
errors={safeErrors}
|
|
65
|
+
path={fullPath}
|
|
66
|
+
/>
|
|
67
|
+
{prop.description && !error ? (
|
|
68
|
+
<p className="text-xs text-[var(--muted-foreground)]">{prop.description}</p>
|
|
69
|
+
) : null}
|
|
70
|
+
{error ? <p className="text-xs text-[var(--destructive)]">{error}</p> : null}
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const EMPTY_ERRORS: Record<string, string> = Object.freeze({}) as Record<string, string>
|
|
76
|
+
|
|
77
|
+
interface PropInputBodyProps {
|
|
78
|
+
prop: PropSpec
|
|
79
|
+
value: unknown
|
|
80
|
+
onChange: (value: unknown) => void
|
|
81
|
+
errors: Record<string, string>
|
|
82
|
+
path: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function PropInputBody({ prop, value, onChange, errors, path }: PropInputBodyProps) {
|
|
86
|
+
const type = prop.type
|
|
87
|
+
switch (type.kind) {
|
|
88
|
+
case 'string':
|
|
89
|
+
return <StringInput label={fieldLabel(prop)} value={value} onChange={onChange} />
|
|
90
|
+
case 'number':
|
|
91
|
+
return <NumberInputControl label={fieldLabel(prop)} value={value} onChange={onChange} />
|
|
92
|
+
case 'boolean':
|
|
93
|
+
return <BooleanInput label={fieldLabel(prop)} value={value} onChange={onChange} />
|
|
94
|
+
case 'enum':
|
|
95
|
+
return (
|
|
96
|
+
<EnumInput
|
|
97
|
+
label={fieldLabel(prop)}
|
|
98
|
+
options={type.values}
|
|
99
|
+
value={value}
|
|
100
|
+
onChange={onChange}
|
|
101
|
+
/>
|
|
102
|
+
)
|
|
103
|
+
case 'literal':
|
|
104
|
+
return <LiteralDisplay label={fieldLabel(prop)} literal={type.value} />
|
|
105
|
+
case 'array':
|
|
106
|
+
return (
|
|
107
|
+
<ArrayInput
|
|
108
|
+
label={fieldLabel(prop)}
|
|
109
|
+
itemType={type.itemType}
|
|
110
|
+
itemPath={path}
|
|
111
|
+
value={value}
|
|
112
|
+
onChange={onChange}
|
|
113
|
+
errors={errors}
|
|
114
|
+
/>
|
|
115
|
+
)
|
|
116
|
+
case 'object':
|
|
117
|
+
return (
|
|
118
|
+
<ObjectInput
|
|
119
|
+
label={fieldLabel(prop)}
|
|
120
|
+
fields={type.fields}
|
|
121
|
+
value={value}
|
|
122
|
+
onChange={onChange}
|
|
123
|
+
errors={errors}
|
|
124
|
+
path={path}
|
|
125
|
+
/>
|
|
126
|
+
)
|
|
127
|
+
case 'union': {
|
|
128
|
+
const detected = detectDiscriminator(type)
|
|
129
|
+
if (detected) {
|
|
130
|
+
return (
|
|
131
|
+
<DiscriminatedUnionInput
|
|
132
|
+
label={fieldLabel(prop)}
|
|
133
|
+
union={detected}
|
|
134
|
+
value={value}
|
|
135
|
+
onChange={onChange}
|
|
136
|
+
errors={errors}
|
|
137
|
+
path={path}
|
|
138
|
+
/>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
return (
|
|
142
|
+
<JsonFallback
|
|
143
|
+
label={fieldLabel(prop)}
|
|
144
|
+
value={value}
|
|
145
|
+
onChange={onChange}
|
|
146
|
+
hint={fallbackHint(type)}
|
|
147
|
+
/>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
case 'reference':
|
|
151
|
+
case 'unknown':
|
|
152
|
+
default:
|
|
153
|
+
return (
|
|
154
|
+
<JsonFallback
|
|
155
|
+
label={fieldLabel(prop)}
|
|
156
|
+
value={value}
|
|
157
|
+
onChange={onChange}
|
|
158
|
+
hint={fallbackHint(type)}
|
|
159
|
+
/>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function fieldLabel(prop: PropSpec): React.ReactNode {
|
|
165
|
+
return (
|
|
166
|
+
<>
|
|
167
|
+
<span className="font-medium">{prop.name}</span>
|
|
168
|
+
{prop.required ? <span className="ml-0.5 text-[var(--destructive)]">*</span> : null}
|
|
169
|
+
</>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function fallbackHint(type: PropType): string {
|
|
174
|
+
if (type.kind === 'reference') {
|
|
175
|
+
return `Type '${type.targetType}' could not be resolved — paste a raw JSON value for now.`
|
|
176
|
+
}
|
|
177
|
+
if (type.kind === 'union') {
|
|
178
|
+
return 'Non-discriminated union — paste a raw JSON value (the form renders a picker for unions with a shared literal tag field like `kind`).'
|
|
179
|
+
}
|
|
180
|
+
return 'Unknown type — paste a raw JSON value.'
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─── Primitive editors ───────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
function StringInput({
|
|
186
|
+
label,
|
|
187
|
+
value,
|
|
188
|
+
onChange,
|
|
189
|
+
}: {
|
|
190
|
+
label: React.ReactNode
|
|
191
|
+
value: unknown
|
|
192
|
+
onChange: (v: unknown) => void
|
|
193
|
+
}) {
|
|
194
|
+
const stringVal = typeof value === 'string' ? value : ''
|
|
195
|
+
return (
|
|
196
|
+
<label className="block text-sm">
|
|
197
|
+
<div className="mb-1">{label}</div>
|
|
198
|
+
<input
|
|
199
|
+
type="text"
|
|
200
|
+
value={stringVal}
|
|
201
|
+
onChange={(e) => onChange(e.target.value)}
|
|
202
|
+
className="w-full rounded-md border border-[var(--border)] bg-[var(--input-background)] px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-[var(--ring)]"
|
|
203
|
+
/>
|
|
204
|
+
</label>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function NumberInputControl({
|
|
209
|
+
label,
|
|
210
|
+
value,
|
|
211
|
+
onChange,
|
|
212
|
+
}: {
|
|
213
|
+
label: React.ReactNode
|
|
214
|
+
value: unknown
|
|
215
|
+
onChange: (v: unknown) => void
|
|
216
|
+
}) {
|
|
217
|
+
const numericVal = typeof value === 'number' ? value : ''
|
|
218
|
+
return (
|
|
219
|
+
<label className="block text-sm">
|
|
220
|
+
<div className="mb-1">{label}</div>
|
|
221
|
+
<input
|
|
222
|
+
type="number"
|
|
223
|
+
value={numericVal === '' ? '' : numericVal}
|
|
224
|
+
onChange={(e) => {
|
|
225
|
+
const raw = e.target.value
|
|
226
|
+
if (raw === '') {
|
|
227
|
+
onChange(undefined)
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
const parsed = Number(raw)
|
|
231
|
+
onChange(Number.isNaN(parsed) ? raw : parsed)
|
|
232
|
+
}}
|
|
233
|
+
className="w-full rounded-md border border-[var(--border)] bg-[var(--input-background)] px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-[var(--ring)]"
|
|
234
|
+
/>
|
|
235
|
+
</label>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function BooleanInput({
|
|
240
|
+
label,
|
|
241
|
+
value,
|
|
242
|
+
onChange,
|
|
243
|
+
}: {
|
|
244
|
+
label: React.ReactNode
|
|
245
|
+
value: unknown
|
|
246
|
+
onChange: (v: unknown) => void
|
|
247
|
+
}) {
|
|
248
|
+
const boolVal = value === true
|
|
249
|
+
return (
|
|
250
|
+
<div className="flex items-center justify-between text-sm">
|
|
251
|
+
<span>{label}</span>
|
|
252
|
+
<button
|
|
253
|
+
type="button"
|
|
254
|
+
role="switch"
|
|
255
|
+
aria-checked={boolVal}
|
|
256
|
+
onClick={() => onChange(!boolVal)}
|
|
257
|
+
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors ${
|
|
258
|
+
boolVal ? 'bg-[var(--primary)]' : 'bg-[var(--muted)]'
|
|
259
|
+
}`}
|
|
260
|
+
>
|
|
261
|
+
<span
|
|
262
|
+
className={`pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow-sm ring-0 transition-transform ${
|
|
263
|
+
boolVal ? 'translate-x-5' : 'translate-x-0'
|
|
264
|
+
}`}
|
|
265
|
+
/>
|
|
266
|
+
</button>
|
|
267
|
+
</div>
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function EnumInput({
|
|
272
|
+
label,
|
|
273
|
+
options,
|
|
274
|
+
value,
|
|
275
|
+
onChange,
|
|
276
|
+
}: {
|
|
277
|
+
label: React.ReactNode
|
|
278
|
+
options: (string | number)[]
|
|
279
|
+
value: unknown
|
|
280
|
+
onChange: (v: unknown) => void
|
|
281
|
+
}) {
|
|
282
|
+
const currentIdx = options.findIndex((o) => o === value)
|
|
283
|
+
return (
|
|
284
|
+
<label className="block text-sm">
|
|
285
|
+
<div className="mb-1">{label}</div>
|
|
286
|
+
<select
|
|
287
|
+
value={currentIdx === -1 ? '' : String(currentIdx)}
|
|
288
|
+
onChange={(e) => {
|
|
289
|
+
onChange(parseEnumSelection(e.target.value, options))
|
|
290
|
+
}}
|
|
291
|
+
className="w-full rounded-md border border-[var(--border)] bg-[var(--input-background)] px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-[var(--ring)]"
|
|
292
|
+
>
|
|
293
|
+
<option value="">Select…</option>
|
|
294
|
+
{options.map((opt, idx) => (
|
|
295
|
+
<option key={String(opt)} value={idx}>
|
|
296
|
+
{String(opt)}
|
|
297
|
+
</option>
|
|
298
|
+
))}
|
|
299
|
+
</select>
|
|
300
|
+
</label>
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function LiteralDisplay({
|
|
305
|
+
label,
|
|
306
|
+
literal,
|
|
307
|
+
}: {
|
|
308
|
+
label: React.ReactNode
|
|
309
|
+
literal: string | number | boolean
|
|
310
|
+
}) {
|
|
311
|
+
return (
|
|
312
|
+
<div className="text-sm">
|
|
313
|
+
<div className="mb-1">{label}</div>
|
|
314
|
+
<div className="rounded-md border border-dashed border-[var(--border)] bg-[var(--muted)] px-3 py-2 text-xs text-[var(--muted-foreground)]">
|
|
315
|
+
Fixed: <code>{String(literal)}</code>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ─── Composite editors ───────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
function ObjectInput({
|
|
324
|
+
label,
|
|
325
|
+
fields,
|
|
326
|
+
value,
|
|
327
|
+
onChange,
|
|
328
|
+
errors,
|
|
329
|
+
path,
|
|
330
|
+
}: {
|
|
331
|
+
label: React.ReactNode
|
|
332
|
+
fields: PropSpec[]
|
|
333
|
+
value: unknown
|
|
334
|
+
onChange: (v: unknown) => void
|
|
335
|
+
errors: Record<string, string>
|
|
336
|
+
path: string
|
|
337
|
+
}) {
|
|
338
|
+
const obj = isPlainObject(value) ? value : {}
|
|
339
|
+
return (
|
|
340
|
+
<fieldset className="rounded-md border border-[var(--border)] bg-[var(--card)] p-3 text-sm">
|
|
341
|
+
<legend className="px-1 text-xs tracking-wide text-[var(--muted-foreground)] uppercase">
|
|
342
|
+
{label}
|
|
343
|
+
</legend>
|
|
344
|
+
<div className="space-y-3">
|
|
345
|
+
{fields.map((field) => (
|
|
346
|
+
<PropInput
|
|
347
|
+
key={field.name}
|
|
348
|
+
prop={field}
|
|
349
|
+
value={obj[field.name]}
|
|
350
|
+
onChange={(next) => onChange({ ...obj, [field.name]: next })}
|
|
351
|
+
errors={errors}
|
|
352
|
+
path={`${path}.${field.name}`}
|
|
353
|
+
/>
|
|
354
|
+
))}
|
|
355
|
+
</div>
|
|
356
|
+
</fieldset>
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function ArrayInput({
|
|
361
|
+
label,
|
|
362
|
+
itemType,
|
|
363
|
+
itemPath,
|
|
364
|
+
value,
|
|
365
|
+
onChange,
|
|
366
|
+
errors,
|
|
367
|
+
}: {
|
|
368
|
+
label: React.ReactNode
|
|
369
|
+
itemType: PropType
|
|
370
|
+
itemPath: string
|
|
371
|
+
value: unknown
|
|
372
|
+
onChange: (v: unknown) => void
|
|
373
|
+
errors: Record<string, string>
|
|
374
|
+
}) {
|
|
375
|
+
const arr = Array.isArray(value) ? value : []
|
|
376
|
+
return (
|
|
377
|
+
<div className="text-sm">
|
|
378
|
+
<div className="mb-2 flex items-center justify-between">
|
|
379
|
+
<span>{label}</span>
|
|
380
|
+
<button
|
|
381
|
+
type="button"
|
|
382
|
+
onClick={() => onChange([...arr, defaultForType(itemType)])}
|
|
383
|
+
className="rounded-md border border-[var(--border)] bg-[var(--input-background)] px-2 py-1 text-xs text-[var(--foreground)] hover:bg-[var(--accent)]"
|
|
384
|
+
>
|
|
385
|
+
+ Add
|
|
386
|
+
</button>
|
|
387
|
+
</div>
|
|
388
|
+
{arr.length === 0 ? (
|
|
389
|
+
<div className="rounded-md border border-dashed border-[var(--border)] px-3 py-4 text-center text-xs text-[var(--muted-foreground)]">
|
|
390
|
+
No items yet
|
|
391
|
+
</div>
|
|
392
|
+
) : (
|
|
393
|
+
<ol className="space-y-2">
|
|
394
|
+
{arr.map((item, idx) => (
|
|
395
|
+
<li key={idx} className="rounded-md border border-[var(--border)] bg-[var(--card)] p-3">
|
|
396
|
+
<div className="mb-2 flex items-center justify-between text-xs text-[var(--muted-foreground)]">
|
|
397
|
+
<span>Item {idx + 1}</span>
|
|
398
|
+
<button
|
|
399
|
+
type="button"
|
|
400
|
+
onClick={() => onChange(arr.filter((_, i) => i !== idx))}
|
|
401
|
+
className="text-[var(--destructive)] hover:underline"
|
|
402
|
+
>
|
|
403
|
+
Remove
|
|
404
|
+
</button>
|
|
405
|
+
</div>
|
|
406
|
+
<PropInput
|
|
407
|
+
prop={{ name: `[${idx}]`, type: itemType, required: true }}
|
|
408
|
+
value={item}
|
|
409
|
+
onChange={(next) => onChange(arr.map((v, i) => (i === idx ? next : v)))}
|
|
410
|
+
errors={errors}
|
|
411
|
+
path={`${itemPath}[${idx}]`}
|
|
412
|
+
/>
|
|
413
|
+
</li>
|
|
414
|
+
))}
|
|
415
|
+
</ol>
|
|
416
|
+
)}
|
|
417
|
+
</div>
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function DiscriminatedUnionInput({
|
|
422
|
+
label,
|
|
423
|
+
union,
|
|
424
|
+
value,
|
|
425
|
+
onChange,
|
|
426
|
+
errors,
|
|
427
|
+
path,
|
|
428
|
+
}: {
|
|
429
|
+
label: React.ReactNode
|
|
430
|
+
union: DiscriminatedUnion
|
|
431
|
+
value: unknown
|
|
432
|
+
onChange: (v: unknown) => void
|
|
433
|
+
errors: Record<string, string>
|
|
434
|
+
path: string
|
|
435
|
+
}) {
|
|
436
|
+
const currentVariant = findVariant(value, union)
|
|
437
|
+
const obj = isPlainObject(value) ? value : {}
|
|
438
|
+
|
|
439
|
+
function switchTo(variantIdx: number) {
|
|
440
|
+
const variant = union.variants[variantIdx]
|
|
441
|
+
if (!variant) return
|
|
442
|
+
onChange(switchUnionVariant(value, union, variant))
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const currentIdx = currentVariant
|
|
446
|
+
? union.variants.findIndex((v) => v.value === currentVariant.value)
|
|
447
|
+
: -1
|
|
448
|
+
|
|
449
|
+
return (
|
|
450
|
+
<fieldset className="rounded-md border border-[var(--border)] bg-[var(--card)] p-3 text-sm">
|
|
451
|
+
<legend className="px-1 text-xs tracking-wide text-[var(--muted-foreground)] uppercase">
|
|
452
|
+
{label}
|
|
453
|
+
</legend>
|
|
454
|
+
<div className="space-y-3">
|
|
455
|
+
<label className="block text-sm">
|
|
456
|
+
<div className="mb-1">
|
|
457
|
+
<span className="font-medium">{union.field}</span>
|
|
458
|
+
<span className="ml-0.5 text-[var(--destructive)]">*</span>
|
|
459
|
+
<span className="ml-2 text-xs text-[var(--muted-foreground)]">(variant)</span>
|
|
460
|
+
</div>
|
|
461
|
+
<select
|
|
462
|
+
value={currentIdx === -1 ? '' : String(currentIdx)}
|
|
463
|
+
onChange={(e) => {
|
|
464
|
+
const idx = Number(e.target.value)
|
|
465
|
+
if (Number.isInteger(idx) && idx >= 0 && idx < union.variants.length) {
|
|
466
|
+
switchTo(idx)
|
|
467
|
+
}
|
|
468
|
+
}}
|
|
469
|
+
className="w-full rounded-md border border-[var(--border)] bg-[var(--input-background)] px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-[var(--ring)]"
|
|
470
|
+
>
|
|
471
|
+
<option value="" disabled>
|
|
472
|
+
Select a variant…
|
|
473
|
+
</option>
|
|
474
|
+
{union.variants.map((variant, idx) => (
|
|
475
|
+
<option key={String(variant.value)} value={idx}>
|
|
476
|
+
{String(variant.value)}
|
|
477
|
+
</option>
|
|
478
|
+
))}
|
|
479
|
+
</select>
|
|
480
|
+
</label>
|
|
481
|
+
{currentVariant && currentVariant.remainingFields.length > 0 ? (
|
|
482
|
+
<div className="space-y-3">
|
|
483
|
+
{currentVariant.remainingFields.map((field) => (
|
|
484
|
+
<PropInput
|
|
485
|
+
key={field.name}
|
|
486
|
+
prop={field}
|
|
487
|
+
value={obj[field.name]}
|
|
488
|
+
onChange={(next) => onChange({ ...obj, [field.name]: next })}
|
|
489
|
+
errors={errors}
|
|
490
|
+
path={`${path}.${field.name}`}
|
|
491
|
+
/>
|
|
492
|
+
))}
|
|
493
|
+
</div>
|
|
494
|
+
) : null}
|
|
495
|
+
{!currentVariant ? (
|
|
496
|
+
<p className="text-xs text-[var(--muted-foreground)]">
|
|
497
|
+
Pick a variant to configure its fields.
|
|
498
|
+
</p>
|
|
499
|
+
) : null}
|
|
500
|
+
</div>
|
|
501
|
+
</fieldset>
|
|
502
|
+
)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function JsonFallback({
|
|
506
|
+
label,
|
|
507
|
+
value,
|
|
508
|
+
onChange,
|
|
509
|
+
hint,
|
|
510
|
+
}: {
|
|
511
|
+
label: React.ReactNode
|
|
512
|
+
value: unknown
|
|
513
|
+
onChange: (v: unknown) => void
|
|
514
|
+
hint: string
|
|
515
|
+
}) {
|
|
516
|
+
// Treat both `undefined` and `null` as "empty" — `null` arrives as
|
|
517
|
+
// the structural default for kinds we can't seed (union / reference /
|
|
518
|
+
// unknown) and also from a JSON round-trip on `undefined`. Rendering
|
|
519
|
+
// the literal string "null" would surface as broken UX.
|
|
520
|
+
const text = value === undefined || value === null ? '' : safeJsonStringify(value)
|
|
521
|
+
return (
|
|
522
|
+
<label className="block text-sm">
|
|
523
|
+
<div className="mb-1">{label}</div>
|
|
524
|
+
<textarea
|
|
525
|
+
value={text}
|
|
526
|
+
onChange={(e) => {
|
|
527
|
+
const raw = e.target.value
|
|
528
|
+
if (raw.trim() === '') {
|
|
529
|
+
onChange(undefined)
|
|
530
|
+
return
|
|
531
|
+
}
|
|
532
|
+
try {
|
|
533
|
+
onChange(JSON.parse(raw))
|
|
534
|
+
} catch {
|
|
535
|
+
// Keep the raw string so the user can finish editing without
|
|
536
|
+
// losing focus; the validator will catch it on save.
|
|
537
|
+
onChange(raw)
|
|
538
|
+
}
|
|
539
|
+
}}
|
|
540
|
+
rows={3}
|
|
541
|
+
className="w-full rounded-md border border-[var(--border)] bg-[var(--input-background)] px-3 py-2 font-mono text-xs outline-none focus:ring-2 focus:ring-[var(--ring)]"
|
|
542
|
+
/>
|
|
543
|
+
<p className="mt-1 text-xs text-[var(--muted-foreground)]">{hint}</p>
|
|
544
|
+
</label>
|
|
545
|
+
)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
549
|
+
|
|
550
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
551
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
552
|
+
}
|
|
@@ -158,12 +158,12 @@ export function RelationshipField({
|
|
|
158
158
|
onClick={() => handleToggle(opt.id)}
|
|
159
159
|
className={`flex w-full items-center gap-2 px-3 py-2 text-sm hover:bg-[var(--accent)] ${isSelected ? 'font-medium' : ''}`}
|
|
160
160
|
>
|
|
161
|
-
<div className="
|
|
161
|
+
<div className="min-w-0 flex-1 text-left">
|
|
162
162
|
<div className="flex items-center gap-2">
|
|
163
163
|
<span className="truncate">{getDocTitle(opt)}</span>
|
|
164
164
|
{isSelected && <span className="shrink-0 text-[var(--primary)]">✓</span>}
|
|
165
165
|
</div>
|
|
166
|
-
<div className="flex items-center gap-2
|
|
166
|
+
<div className="mt-0.5 flex items-center gap-2">
|
|
167
167
|
<span className="text-[10px] text-[var(--muted-foreground)]">{collection}</span>
|
|
168
168
|
{opt.updatedAt && (
|
|
169
169
|
<span className="text-[10px] text-[var(--muted-foreground)]">
|
|
@@ -201,7 +201,7 @@ export function RelationshipField({
|
|
|
201
201
|
onClick={() => handleRemove(item.id)}
|
|
202
202
|
className="hover:text-[var(--destructive)]"
|
|
203
203
|
>
|
|
204
|
-
<X className="
|
|
204
|
+
<X className="h-3 w-3" />
|
|
205
205
|
</button>
|
|
206
206
|
</span>
|
|
207
207
|
))}
|
|
@@ -209,26 +209,26 @@ export function RelationshipField({
|
|
|
209
209
|
)}
|
|
210
210
|
|
|
211
211
|
<div className="relative">
|
|
212
|
-
<Search className="absolute left-2.5
|
|
212
|
+
<Search className="pointer-events-none absolute top-1/2 left-2.5 h-4 w-4 -translate-y-1/2 text-[var(--muted-foreground)]" />
|
|
213
213
|
<input
|
|
214
214
|
type="text"
|
|
215
215
|
value={searchTerm}
|
|
216
216
|
onChange={(e) => handleSearch(e.target.value)}
|
|
217
217
|
onFocus={() => setOpen(true)}
|
|
218
218
|
placeholder={`Search ${relationTo}...`}
|
|
219
|
-
className="w-full rounded-md border border-[var(--border)] bg-[var(--input-background)]
|
|
219
|
+
className="w-full rounded-md border border-[var(--border)] bg-[var(--input-background)] py-2 pr-3 pl-8 text-sm outline-none focus:ring-2 focus:ring-[var(--ring)]"
|
|
220
220
|
/>
|
|
221
221
|
{loading && (
|
|
222
|
-
<Loader2 className="absolute right-2.5
|
|
222
|
+
<Loader2 className="absolute top-1/2 right-2.5 h-4 w-4 -translate-y-1/2 animate-spin text-[var(--muted-foreground)]" />
|
|
223
223
|
)}
|
|
224
224
|
|
|
225
225
|
{open && (
|
|
226
|
-
<ul className="absolute z-50 mt-1
|
|
226
|
+
<ul className="absolute z-50 mt-1 max-h-60 w-full overflow-y-auto rounded-md border border-[var(--border)] bg-[var(--popover)] py-1 shadow-lg">
|
|
227
227
|
{unselectedItems.map((opt) => (
|
|
228
228
|
<li key={opt.id}>{renderOption(opt, false)}</li>
|
|
229
229
|
))}
|
|
230
230
|
{selectedItems.length > 0 && unselectedItems.length > 0 && (
|
|
231
|
-
<li className="border-t border-[var(--border)]
|
|
231
|
+
<li className="my-1 border-t border-[var(--border)]" />
|
|
232
232
|
)}
|
|
233
233
|
{selectedItems.map((opt) => (
|
|
234
234
|
<li key={opt.id}>{renderOption(opt, true)}</li>
|
|
@@ -236,7 +236,7 @@ export function RelationshipField({
|
|
|
236
236
|
{options.length === 0 && !loading && (
|
|
237
237
|
<li className="px-3 py-2 text-sm text-[var(--muted-foreground)]">No results</li>
|
|
238
238
|
)}
|
|
239
|
-
<li className="border-t border-[var(--border)]
|
|
239
|
+
<li className="mt-1 border-t border-[var(--border)]">
|
|
240
240
|
<button
|
|
241
241
|
type="button"
|
|
242
242
|
onClick={() => {
|
|
@@ -245,7 +245,7 @@ export function RelationshipField({
|
|
|
245
245
|
}}
|
|
246
246
|
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-[var(--primary)] hover:bg-[var(--accent)]"
|
|
247
247
|
>
|
|
248
|
-
<Plus className="
|
|
248
|
+
<Plus className="h-3.5 w-3.5" />
|
|
249
249
|
Create New
|
|
250
250
|
</button>
|
|
251
251
|
</li>
|
package/src/fields/TextField.tsx
CHANGED
|
@@ -43,7 +43,7 @@ export function TextField({
|
|
|
43
43
|
onChange={(e) => onChange(e.target.value)}
|
|
44
44
|
required={required}
|
|
45
45
|
maxLength={maxLength}
|
|
46
|
-
className={`w-full rounded-md border bg-[var(--input-background)] px-3 py-2 text-sm outline-none
|
|
46
|
+
className={`w-full rounded-md border bg-[var(--input-background)] px-3 py-2 text-sm transition-colors outline-none focus:ring-2 focus:ring-[var(--ring)] ${
|
|
47
47
|
hasError ? 'border-[var(--destructive)]' : 'border-[var(--border)]'
|
|
48
48
|
}`}
|
|
49
49
|
/>
|