@easyops-cn/a2ui-react 0.0.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/.claude/commands/speckit.analyze.md +184 -0
- package/.claude/commands/speckit.checklist.md +294 -0
- package/.claude/commands/speckit.clarify.md +181 -0
- package/.claude/commands/speckit.constitution.md +82 -0
- package/.claude/commands/speckit.implement.md +135 -0
- package/.claude/commands/speckit.plan.md +89 -0
- package/.claude/commands/speckit.specify.md +256 -0
- package/.claude/commands/speckit.tasks.md +137 -0
- package/.claude/commands/speckit.taskstoissues.md +30 -0
- package/.github/workflows/deploy.yml +69 -0
- package/.husky/pre-commit +1 -0
- package/.prettierignore +4 -0
- package/.prettierrc +7 -0
- package/.specify/memory/constitution.md +73 -0
- package/.specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.specify/scripts/bash/common.sh +156 -0
- package/.specify/scripts/bash/create-new-feature.sh +297 -0
- package/.specify/scripts/bash/setup-plan.sh +61 -0
- package/.specify/scripts/bash/update-agent-context.sh +799 -0
- package/.specify/templates/agent-file-template.md +28 -0
- package/.specify/templates/checklist-template.md +40 -0
- package/.specify/templates/plan-template.md +105 -0
- package/.specify/templates/spec-template.md +115 -0
- package/.specify/templates/tasks-template.md +250 -0
- package/CLAUDE.md +105 -0
- package/CONTRIBUTING.md +97 -0
- package/README.md +126 -0
- package/components.json +21 -0
- package/eslint.config.js +25 -0
- package/netlify.toml +50 -0
- package/package.json +94 -0
- package/playground/README.md +75 -0
- package/playground/index.html +22 -0
- package/playground/package.json +32 -0
- package/playground/public/favicon.svg +8 -0
- package/playground/src/App.css +256 -0
- package/playground/src/App.tsx +115 -0
- package/playground/src/assets/react.svg +1 -0
- package/playground/src/components/ErrorDisplay.tsx +13 -0
- package/playground/src/components/ExampleSelector.tsx +64 -0
- package/playground/src/components/Header.tsx +47 -0
- package/playground/src/components/JsonEditor.tsx +32 -0
- package/playground/src/components/Preview.tsx +78 -0
- package/playground/src/components/ThemeToggle.tsx +19 -0
- package/playground/src/data/examples.ts +1571 -0
- package/playground/src/hooks/useTheme.ts +55 -0
- package/playground/src/index.css +220 -0
- package/playground/src/main.tsx +10 -0
- package/playground/tsconfig.app.json +34 -0
- package/playground/tsconfig.json +13 -0
- package/playground/tsconfig.node.json +26 -0
- package/playground/vite.config.ts +31 -0
- package/specs/001-a2ui-renderer/checklists/requirements.md +41 -0
- package/specs/001-a2ui-renderer/data-model.md +140 -0
- package/specs/001-a2ui-renderer/plan.md +123 -0
- package/specs/001-a2ui-renderer/quickstart.md +141 -0
- package/specs/001-a2ui-renderer/research.md +140 -0
- package/specs/001-a2ui-renderer/spec.md +165 -0
- package/specs/001-a2ui-renderer/tasks.md +310 -0
- package/specs/002-playground/checklists/requirements.md +37 -0
- package/specs/002-playground/contracts/components.md +120 -0
- package/specs/002-playground/data-model.md +149 -0
- package/specs/002-playground/plan.md +73 -0
- package/specs/002-playground/quickstart.md +158 -0
- package/specs/002-playground/research.md +117 -0
- package/specs/002-playground/spec.md +109 -0
- package/specs/002-playground/tasks.md +224 -0
- package/src/0.8/A2UIRender.test.tsx +793 -0
- package/src/0.8/A2UIRender.tsx +142 -0
- package/src/0.8/components/ComponentRenderer.test.tsx +373 -0
- package/src/0.8/components/ComponentRenderer.tsx +163 -0
- package/src/0.8/components/UnknownComponent.tsx +49 -0
- package/src/0.8/components/display/AudioPlayerComponent.tsx +37 -0
- package/src/0.8/components/display/DividerComponent.tsx +23 -0
- package/src/0.8/components/display/IconComponent.tsx +137 -0
- package/src/0.8/components/display/ImageComponent.tsx +57 -0
- package/src/0.8/components/display/TextComponent.tsx +56 -0
- package/src/0.8/components/display/VideoComponent.tsx +31 -0
- package/src/0.8/components/display/display.test.tsx +660 -0
- package/src/0.8/components/display/index.ts +10 -0
- package/src/0.8/components/index.ts +14 -0
- package/src/0.8/components/interactive/ButtonComponent.tsx +44 -0
- package/src/0.8/components/interactive/CheckBoxComponent.tsx +45 -0
- package/src/0.8/components/interactive/DateTimeInputComponent.tsx +176 -0
- package/src/0.8/components/interactive/MultipleChoiceComponent.tsx +157 -0
- package/src/0.8/components/interactive/SliderComponent.tsx +53 -0
- package/src/0.8/components/interactive/TextFieldComponent.tsx +65 -0
- package/src/0.8/components/interactive/index.ts +10 -0
- package/src/0.8/components/interactive/interactive.test.tsx +618 -0
- package/src/0.8/components/layout/CardComponent.tsx +30 -0
- package/src/0.8/components/layout/ColumnComponent.tsx +93 -0
- package/src/0.8/components/layout/ListComponent.tsx +81 -0
- package/src/0.8/components/layout/ModalComponent.tsx +41 -0
- package/src/0.8/components/layout/RowComponent.tsx +94 -0
- package/src/0.8/components/layout/TabsComponent.tsx +59 -0
- package/src/0.8/components/layout/index.ts +10 -0
- package/src/0.8/components/layout/layout.test.tsx +558 -0
- package/src/0.8/contexts/A2UIProvider.test.tsx +226 -0
- package/src/0.8/contexts/A2UIProvider.tsx +54 -0
- package/src/0.8/contexts/ActionContext.test.tsx +242 -0
- package/src/0.8/contexts/ActionContext.tsx +105 -0
- package/src/0.8/contexts/ComponentsMapContext.tsx +125 -0
- package/src/0.8/contexts/DataModelContext.test.tsx +335 -0
- package/src/0.8/contexts/DataModelContext.tsx +184 -0
- package/src/0.8/contexts/SurfaceContext.test.tsx +339 -0
- package/src/0.8/contexts/SurfaceContext.tsx +197 -0
- package/src/0.8/hooks/useA2UIMessageHandler.test.tsx +399 -0
- package/src/0.8/hooks/useA2UIMessageHandler.ts +123 -0
- package/src/0.8/hooks/useComponent.test.tsx +148 -0
- package/src/0.8/hooks/useComponent.ts +39 -0
- package/src/0.8/hooks/useDataBinding.test.tsx +334 -0
- package/src/0.8/hooks/useDataBinding.ts +99 -0
- package/src/0.8/hooks/useDispatchAction.test.tsx +83 -0
- package/src/0.8/hooks/useDispatchAction.ts +35 -0
- package/src/0.8/hooks/useSurface.test.tsx +114 -0
- package/src/0.8/hooks/useSurface.ts +34 -0
- package/src/0.8/index.ts +38 -0
- package/src/0.8/schemas/client_to_server.json +50 -0
- package/src/0.8/schemas/server_to_client.json +148 -0
- package/src/0.8/schemas/standard_catalog_definition.json +661 -0
- package/src/0.8/types/index.ts +448 -0
- package/src/0.8/utils/dataBinding.test.ts +443 -0
- package/src/0.8/utils/dataBinding.ts +212 -0
- package/src/0.8/utils/pathUtils.test.ts +353 -0
- package/src/0.8/utils/pathUtils.ts +200 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/calendar.tsx +220 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/dialog.tsx +141 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +22 -0
- package/src/components/ui/native-select.tsx +53 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/select.tsx +188 -0
- package/src/components/ui/separator.tsx +26 -0
- package/src/components/ui/slider.tsx +61 -0
- package/src/components/ui/tabs.tsx +64 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/index.ts +1 -0
- package/src/lib/utils.ts +6 -0
- package/tsconfig.json +28 -0
- package/vite.config.ts +29 -0
- package/vitest.config.ts +22 -0
- package/vitest.setup.ts +8 -0
- package/website/README.md +4 -0
- package/website/assets/favicon.svg +8 -0
- package/website/content/.gitkeep +0 -0
- package/website/content/index.md +122 -0
- package/website/global.d.ts +9 -0
- package/website/package.json +17 -0
- package/website/plain.config.js +28 -0
- package/website/serve.json +6 -0
- package/website/src/client/color-mode-switch.css +47 -0
- package/website/src/client/index.js +61 -0
- package/website/src/client/moon.svg +1 -0
- package/website/src/client/sun.svg +1 -0
- package/website/src/components/Footer.jsx +9 -0
- package/website/src/components/Header.jsx +44 -0
- package/website/src/components/Page.jsx +28 -0
- package/website/src/global.css +423 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ButtonComponent - Clickable button that triggers actions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { memo, useCallback } from 'react'
|
|
6
|
+
import type { ButtonComponentProps } from '@/0.8/types'
|
|
7
|
+
import { useDispatchAction } from '@/0.8/hooks/useDispatchAction'
|
|
8
|
+
import { Button } from '@/components/ui/button'
|
|
9
|
+
import { ComponentRenderer } from '../ComponentRenderer'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Button component - triggers actions on click.
|
|
13
|
+
*/
|
|
14
|
+
export const ButtonComponent = memo(function ButtonComponent({
|
|
15
|
+
surfaceId,
|
|
16
|
+
componentId,
|
|
17
|
+
child,
|
|
18
|
+
primary = false,
|
|
19
|
+
action,
|
|
20
|
+
}: ButtonComponentProps) {
|
|
21
|
+
const dispatchAction = useDispatchAction()
|
|
22
|
+
|
|
23
|
+
const handleClick = useCallback(() => {
|
|
24
|
+
if (action) {
|
|
25
|
+
dispatchAction(surfaceId, componentId, action)
|
|
26
|
+
}
|
|
27
|
+
}, [dispatchAction, surfaceId, componentId, action])
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Button
|
|
31
|
+
variant={primary ? 'default' : 'outline'}
|
|
32
|
+
onClick={handleClick}
|
|
33
|
+
className="inline-flex items-center justify-center"
|
|
34
|
+
>
|
|
35
|
+
{child ? (
|
|
36
|
+
<ComponentRenderer surfaceId={surfaceId} componentId={child} />
|
|
37
|
+
) : (
|
|
38
|
+
'Button'
|
|
39
|
+
)}
|
|
40
|
+
</Button>
|
|
41
|
+
)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
ButtonComponent.displayName = 'A2UI.Button'
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CheckBoxComponent - Checkbox input with two-way binding.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { memo, useCallback } from 'react'
|
|
6
|
+
import type { CheckBoxComponentProps } from '@/0.8/types'
|
|
7
|
+
import { useDataBinding, useFormBinding } from '@/0.8/hooks/useDataBinding'
|
|
8
|
+
import { Checkbox } from '@/components/ui/checkbox'
|
|
9
|
+
import { Label } from '@/components/ui/label'
|
|
10
|
+
import { cn } from '@/lib/utils'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* CheckBox component - checkbox input with label.
|
|
14
|
+
*/
|
|
15
|
+
export const CheckBoxComponent = memo(function CheckBoxComponent({
|
|
16
|
+
surfaceId,
|
|
17
|
+
componentId,
|
|
18
|
+
label,
|
|
19
|
+
value,
|
|
20
|
+
}: CheckBoxComponentProps) {
|
|
21
|
+
const labelText = useDataBinding<string>(surfaceId, label, '')
|
|
22
|
+
const [checked, setChecked] = useFormBinding<boolean>(surfaceId, value, false)
|
|
23
|
+
|
|
24
|
+
const handleChange = useCallback(
|
|
25
|
+
(newChecked: boolean) => {
|
|
26
|
+
setChecked(newChecked)
|
|
27
|
+
},
|
|
28
|
+
[setChecked]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const id = `checkbox-${componentId}`
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className={cn('flex items-center gap-3')}>
|
|
35
|
+
<Checkbox id={id} checked={checked} onCheckedChange={handleChange} />
|
|
36
|
+
{labelText && (
|
|
37
|
+
<Label htmlFor={id} className="cursor-pointer">
|
|
38
|
+
{labelText}
|
|
39
|
+
</Label>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
CheckBoxComponent.displayName = 'A2UI.CheckBox'
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DateTimeInputComponent - Date and/or time input with two-way binding.
|
|
3
|
+
* Uses shadcn/ui Calendar and Popover components.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { memo, useCallback, useMemo } from 'react'
|
|
7
|
+
import { CalendarIcon } from 'lucide-react'
|
|
8
|
+
import { format, parse, isValid } from 'date-fns'
|
|
9
|
+
import type { DateTimeInputComponentProps } from '@/0.8/types'
|
|
10
|
+
import { useFormBinding } from '@/0.8/hooks/useDataBinding'
|
|
11
|
+
import { cn } from '@/lib/utils'
|
|
12
|
+
import { Button } from '@/components/ui/button'
|
|
13
|
+
import { Calendar } from '@/components/ui/calendar'
|
|
14
|
+
import {
|
|
15
|
+
Popover,
|
|
16
|
+
PopoverContent,
|
|
17
|
+
PopoverTrigger,
|
|
18
|
+
} from '@/components/ui/popover'
|
|
19
|
+
import { Input } from '@/components/ui/input'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* DateTimeInput component - date/time picker using Calendar and Popover.
|
|
23
|
+
*/
|
|
24
|
+
export const DateTimeInputComponent = memo(function DateTimeInputComponent({
|
|
25
|
+
surfaceId,
|
|
26
|
+
componentId,
|
|
27
|
+
value,
|
|
28
|
+
enableDate = true,
|
|
29
|
+
enableTime = false,
|
|
30
|
+
}: DateTimeInputComponentProps) {
|
|
31
|
+
const [dateValue, setDateValue] = useFormBinding<string>(surfaceId, value, '')
|
|
32
|
+
|
|
33
|
+
// Parse the string value to Date object
|
|
34
|
+
const selectedDate = useMemo(() => {
|
|
35
|
+
if (!dateValue) return undefined
|
|
36
|
+
|
|
37
|
+
let date: Date | undefined
|
|
38
|
+
if (enableDate && enableTime) {
|
|
39
|
+
// datetime-local format: "YYYY-MM-DDTHH:mm"
|
|
40
|
+
date = parse(dateValue, "yyyy-MM-dd'T'HH:mm", new Date())
|
|
41
|
+
} else if (enableDate) {
|
|
42
|
+
// date format: "YYYY-MM-DD"
|
|
43
|
+
date = parse(dateValue, 'yyyy-MM-dd', new Date())
|
|
44
|
+
} else if (enableTime) {
|
|
45
|
+
// time format: "HH:mm" - create a date with today's date
|
|
46
|
+
date = parse(dateValue, 'HH:mm', new Date())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return date && isValid(date) ? date : undefined
|
|
50
|
+
}, [dateValue, enableDate, enableTime])
|
|
51
|
+
|
|
52
|
+
// Extract time parts for time input
|
|
53
|
+
const timeValue = useMemo(() => {
|
|
54
|
+
if (!selectedDate || !enableTime) return ''
|
|
55
|
+
return format(selectedDate, 'HH:mm')
|
|
56
|
+
}, [selectedDate, enableTime])
|
|
57
|
+
|
|
58
|
+
// Handle date selection from calendar
|
|
59
|
+
const handleDateSelect = useCallback(
|
|
60
|
+
(date: Date | undefined) => {
|
|
61
|
+
if (!date) {
|
|
62
|
+
setDateValue('')
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (enableDate && enableTime) {
|
|
67
|
+
// Preserve existing time if any
|
|
68
|
+
const existingTime = selectedDate
|
|
69
|
+
? format(selectedDate, 'HH:mm')
|
|
70
|
+
: '00:00'
|
|
71
|
+
const [hours, minutes] = existingTime.split(':').map(Number)
|
|
72
|
+
date.setHours(hours, minutes)
|
|
73
|
+
setDateValue(format(date, "yyyy-MM-dd'T'HH:mm"))
|
|
74
|
+
} else {
|
|
75
|
+
setDateValue(format(date, 'yyyy-MM-dd'))
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
[setDateValue, enableDate, enableTime, selectedDate]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
// Handle time change
|
|
82
|
+
const handleTimeChange = useCallback(
|
|
83
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
84
|
+
const newTime = e.target.value
|
|
85
|
+
if (!newTime) return
|
|
86
|
+
|
|
87
|
+
const [hours, minutes] = newTime.split(':').map(Number)
|
|
88
|
+
|
|
89
|
+
if (enableDate && enableTime) {
|
|
90
|
+
// Update time on existing date or use today
|
|
91
|
+
const baseDate = selectedDate || new Date()
|
|
92
|
+
baseDate.setHours(hours, minutes)
|
|
93
|
+
setDateValue(format(baseDate, "yyyy-MM-dd'T'HH:mm"))
|
|
94
|
+
} else if (enableTime && !enableDate) {
|
|
95
|
+
// Time only mode
|
|
96
|
+
setDateValue(newTime)
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
[setDateValue, enableDate, enableTime, selectedDate]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
// Format display text
|
|
103
|
+
const displayText = useMemo(() => {
|
|
104
|
+
if (!selectedDate) {
|
|
105
|
+
if (enableDate && enableTime) return '选择日期和时间'
|
|
106
|
+
if (enableDate) return '选择日期'
|
|
107
|
+
return '选择时间'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (enableDate && enableTime) {
|
|
111
|
+
return format(selectedDate, 'yyyy-MM-dd HH:mm')
|
|
112
|
+
} else if (enableDate) {
|
|
113
|
+
return format(selectedDate, 'yyyy-MM-dd')
|
|
114
|
+
} else {
|
|
115
|
+
return format(selectedDate, 'HH:mm')
|
|
116
|
+
}
|
|
117
|
+
}, [selectedDate, enableDate, enableTime])
|
|
118
|
+
|
|
119
|
+
const id = `datetime-${componentId}`
|
|
120
|
+
|
|
121
|
+
// Time-only mode: just show time input
|
|
122
|
+
if (enableTime && !enableDate) {
|
|
123
|
+
return (
|
|
124
|
+
<div className={cn('flex flex-col gap-2')}>
|
|
125
|
+
<Input
|
|
126
|
+
id={id}
|
|
127
|
+
type="time"
|
|
128
|
+
value={dateValue}
|
|
129
|
+
onChange={handleTimeChange}
|
|
130
|
+
className="w-full"
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div className={cn('flex flex-col gap-2')}>
|
|
138
|
+
<Popover>
|
|
139
|
+
<PopoverTrigger asChild>
|
|
140
|
+
<Button
|
|
141
|
+
id={id}
|
|
142
|
+
variant="outline"
|
|
143
|
+
className={cn(
|
|
144
|
+
'w-full justify-start text-left font-normal',
|
|
145
|
+
!selectedDate && 'text-muted-foreground'
|
|
146
|
+
)}
|
|
147
|
+
>
|
|
148
|
+
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
149
|
+
{displayText}
|
|
150
|
+
</Button>
|
|
151
|
+
</PopoverTrigger>
|
|
152
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
153
|
+
<Calendar
|
|
154
|
+
mode="single"
|
|
155
|
+
selected={selectedDate}
|
|
156
|
+
onSelect={handleDateSelect}
|
|
157
|
+
captionLayout="dropdown"
|
|
158
|
+
initialFocus
|
|
159
|
+
/>
|
|
160
|
+
{enableTime && (
|
|
161
|
+
<div className="border-t p-3">
|
|
162
|
+
<Input
|
|
163
|
+
type="time"
|
|
164
|
+
value={timeValue}
|
|
165
|
+
onChange={handleTimeChange}
|
|
166
|
+
className="w-full"
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
</PopoverContent>
|
|
171
|
+
</Popover>
|
|
172
|
+
</div>
|
|
173
|
+
)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
DateTimeInputComponent.displayName = 'A2UI.DateTimeInput'
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MultipleChoiceComponent - Dropdown/Select input with two-way binding.
|
|
3
|
+
* Supports both single selection (dropdown) and multi-selection (checkboxes).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { memo, useCallback } from 'react'
|
|
7
|
+
import type { MultipleChoiceComponentProps, ValueSource } from '@/0.8/types'
|
|
8
|
+
import { useDataBinding, useFormBinding } from '@/0.8/hooks/useDataBinding'
|
|
9
|
+
import {
|
|
10
|
+
Select,
|
|
11
|
+
SelectContent,
|
|
12
|
+
SelectItem,
|
|
13
|
+
SelectTrigger,
|
|
14
|
+
SelectValue,
|
|
15
|
+
} from '@/components/ui/select'
|
|
16
|
+
import { Checkbox } from '@/components/ui/checkbox'
|
|
17
|
+
import { Label } from '@/components/ui/label'
|
|
18
|
+
import { cn } from '@/lib/utils'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* MultipleChoice component - dropdown/select input.
|
|
22
|
+
* When maxAllowedSelections === 1, renders as a dropdown.
|
|
23
|
+
* When maxAllowedSelections > 1 or undefined, renders as checkboxes for multi-select.
|
|
24
|
+
*/
|
|
25
|
+
export const MultipleChoiceComponent = memo(function MultipleChoiceComponent({
|
|
26
|
+
surfaceId,
|
|
27
|
+
componentId,
|
|
28
|
+
selections,
|
|
29
|
+
options,
|
|
30
|
+
maxAllowedSelections,
|
|
31
|
+
}: MultipleChoiceComponentProps) {
|
|
32
|
+
const [selectedValue, setSelectedValue] = useFormBinding<string | string[]>(
|
|
33
|
+
surfaceId,
|
|
34
|
+
selections,
|
|
35
|
+
maxAllowedSelections === 1 ? '' : []
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const handleSingleChange = useCallback(
|
|
39
|
+
(value: string) => {
|
|
40
|
+
setSelectedValue(value)
|
|
41
|
+
},
|
|
42
|
+
[setSelectedValue]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const handleMultiChange = useCallback(
|
|
46
|
+
(value: string, checked: boolean) => {
|
|
47
|
+
const currentSelections = Array.isArray(selectedValue)
|
|
48
|
+
? selectedValue
|
|
49
|
+
: selectedValue
|
|
50
|
+
? [selectedValue]
|
|
51
|
+
: []
|
|
52
|
+
|
|
53
|
+
if (checked) {
|
|
54
|
+
// Check if we've reached the max allowed selections
|
|
55
|
+
if (
|
|
56
|
+
maxAllowedSelections !== undefined &&
|
|
57
|
+
currentSelections.length >= maxAllowedSelections
|
|
58
|
+
) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
setSelectedValue([...currentSelections, value])
|
|
62
|
+
} else {
|
|
63
|
+
setSelectedValue(currentSelections.filter((v) => v !== value))
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
[selectedValue, setSelectedValue, maxAllowedSelections]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const id = `multiplechoice-${componentId}`
|
|
70
|
+
|
|
71
|
+
if (!options || options.length === 0) {
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Single selection mode - use dropdown
|
|
76
|
+
if (maxAllowedSelections === 1) {
|
|
77
|
+
const currentValue = Array.isArray(selectedValue)
|
|
78
|
+
? selectedValue[0] || ''
|
|
79
|
+
: selectedValue
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className={cn('flex flex-col gap-2')}>
|
|
83
|
+
<Select value={currentValue} onValueChange={handleSingleChange}>
|
|
84
|
+
<SelectTrigger id={id}>
|
|
85
|
+
<SelectValue placeholder="Select an option" />
|
|
86
|
+
</SelectTrigger>
|
|
87
|
+
<SelectContent>
|
|
88
|
+
{options.map((option) => (
|
|
89
|
+
<SelectItem key={option.value} value={option.value}>
|
|
90
|
+
<OptionLabel surfaceId={surfaceId} label={option.label} />
|
|
91
|
+
</SelectItem>
|
|
92
|
+
))}
|
|
93
|
+
</SelectContent>
|
|
94
|
+
</Select>
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Multi-selection mode - use checkboxes
|
|
100
|
+
const currentSelections = Array.isArray(selectedValue)
|
|
101
|
+
? selectedValue
|
|
102
|
+
: selectedValue
|
|
103
|
+
? [selectedValue]
|
|
104
|
+
: []
|
|
105
|
+
|
|
106
|
+
const isMaxReached =
|
|
107
|
+
maxAllowedSelections !== undefined &&
|
|
108
|
+
currentSelections.length >= maxAllowedSelections
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div className={cn('flex flex-col gap-2')}>
|
|
112
|
+
{options.map((option) => {
|
|
113
|
+
const isChecked = currentSelections.includes(option.value)
|
|
114
|
+
const isDisabled = !isChecked && isMaxReached
|
|
115
|
+
const checkboxId = `${id}-${option.value}`
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div key={option.value} className="flex items-center gap-2">
|
|
119
|
+
<Checkbox
|
|
120
|
+
id={checkboxId}
|
|
121
|
+
checked={isChecked}
|
|
122
|
+
disabled={isDisabled}
|
|
123
|
+
onCheckedChange={(checked) =>
|
|
124
|
+
handleMultiChange(option.value, checked === true)
|
|
125
|
+
}
|
|
126
|
+
/>
|
|
127
|
+
<Label
|
|
128
|
+
htmlFor={checkboxId}
|
|
129
|
+
className={cn(
|
|
130
|
+
'cursor-pointer',
|
|
131
|
+
isDisabled && 'cursor-not-allowed opacity-50'
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
<OptionLabel surfaceId={surfaceId} label={option.label} />
|
|
135
|
+
</Label>
|
|
136
|
+
</div>
|
|
137
|
+
)
|
|
138
|
+
})}
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Helper component to resolve option labels.
|
|
145
|
+
*/
|
|
146
|
+
function OptionLabel({
|
|
147
|
+
surfaceId,
|
|
148
|
+
label,
|
|
149
|
+
}: {
|
|
150
|
+
surfaceId: string
|
|
151
|
+
label: ValueSource | undefined
|
|
152
|
+
}) {
|
|
153
|
+
const labelText = useDataBinding<string>(surfaceId, label, '')
|
|
154
|
+
return <>{labelText}</>
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
MultipleChoiceComponent.displayName = 'A2UI.MultipleChoice'
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SliderComponent - Slider input with two-way binding.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { memo, useCallback } from 'react'
|
|
6
|
+
import type { SliderComponentProps } from '@/0.8/types'
|
|
7
|
+
import { useFormBinding } from '@/0.8/hooks/useDataBinding'
|
|
8
|
+
import { Slider } from '@/components/ui/slider'
|
|
9
|
+
import { cn } from '@/lib/utils'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Slider component - range slider input.
|
|
13
|
+
*/
|
|
14
|
+
export const SliderComponent = memo(function SliderComponent({
|
|
15
|
+
surfaceId,
|
|
16
|
+
value,
|
|
17
|
+
minValue = 0,
|
|
18
|
+
maxValue = 100,
|
|
19
|
+
}: SliderComponentProps) {
|
|
20
|
+
const [sliderValue, setSliderValue] = useFormBinding<number>(
|
|
21
|
+
surfaceId,
|
|
22
|
+
value,
|
|
23
|
+
minValue
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const handleChange = useCallback(
|
|
27
|
+
(values: number[]) => {
|
|
28
|
+
if (values.length > 0) {
|
|
29
|
+
setSliderValue(values[0])
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
[setSliderValue]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className={cn('flex flex-col gap-2 py-2')}>
|
|
37
|
+
<Slider
|
|
38
|
+
value={[sliderValue]}
|
|
39
|
+
onValueChange={handleChange}
|
|
40
|
+
min={minValue}
|
|
41
|
+
max={maxValue}
|
|
42
|
+
step={1}
|
|
43
|
+
/>
|
|
44
|
+
<div className="flex justify-between text-sm text-muted-foreground">
|
|
45
|
+
<span>{minValue}</span>
|
|
46
|
+
<span className="font-medium text-foreground">{sliderValue}</span>
|
|
47
|
+
<span>{maxValue}</span>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
SliderComponent.displayName = 'A2UI.Slider'
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TextFieldComponent - Text input field with two-way binding.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { memo, useCallback } from 'react'
|
|
6
|
+
import type { TextFieldComponentProps } from '@/0.8/types'
|
|
7
|
+
import { useDataBinding, useFormBinding } from '@/0.8/hooks/useDataBinding'
|
|
8
|
+
import { Input } from '@/components/ui/input'
|
|
9
|
+
import { Textarea } from '@/components/ui/textarea'
|
|
10
|
+
import { Label } from '@/components/ui/label'
|
|
11
|
+
import { cn } from '@/lib/utils'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Maps textFieldType to HTML input type.
|
|
15
|
+
*/
|
|
16
|
+
const inputTypeMap: Record<string, string> = {
|
|
17
|
+
shortText: 'text',
|
|
18
|
+
longText: 'text', // Uses textarea
|
|
19
|
+
number: 'number',
|
|
20
|
+
date: 'date',
|
|
21
|
+
obscured: 'password',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* TextField component - text input with label.
|
|
26
|
+
*/
|
|
27
|
+
export const TextFieldComponent = memo(function TextFieldComponent({
|
|
28
|
+
surfaceId,
|
|
29
|
+
componentId,
|
|
30
|
+
label,
|
|
31
|
+
text,
|
|
32
|
+
textFieldType = 'shortText',
|
|
33
|
+
}: TextFieldComponentProps) {
|
|
34
|
+
const labelText = useDataBinding<string>(surfaceId, label, '')
|
|
35
|
+
const [value, setValue] = useFormBinding<string>(surfaceId, text, '')
|
|
36
|
+
|
|
37
|
+
const handleChange = useCallback(
|
|
38
|
+
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
39
|
+
setValue(e.target.value)
|
|
40
|
+
},
|
|
41
|
+
[setValue]
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const id = `textfield-${componentId}`
|
|
45
|
+
const inputType = inputTypeMap[textFieldType] || 'text'
|
|
46
|
+
const isLongText = textFieldType === 'longText'
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className={cn('flex flex-col gap-2')}>
|
|
50
|
+
{labelText && <Label htmlFor={id}>{labelText}</Label>}
|
|
51
|
+
{isLongText ? (
|
|
52
|
+
<Textarea
|
|
53
|
+
id={id}
|
|
54
|
+
value={value}
|
|
55
|
+
onChange={handleChange}
|
|
56
|
+
className="min-h-[100px]"
|
|
57
|
+
/>
|
|
58
|
+
) : (
|
|
59
|
+
<Input id={id} type={inputType} value={value} onChange={handleChange} />
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
TextFieldComponent.displayName = 'A2UI.TextField'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2UI React Renderer - Interactive Components
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { ButtonComponent } from './ButtonComponent'
|
|
6
|
+
export { CheckBoxComponent } from './CheckBoxComponent'
|
|
7
|
+
export { TextFieldComponent } from './TextFieldComponent'
|
|
8
|
+
export { DateTimeInputComponent } from './DateTimeInputComponent'
|
|
9
|
+
export { MultipleChoiceComponent } from './MultipleChoiceComponent'
|
|
10
|
+
export { SliderComponent } from './SliderComponent'
|