@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.
Files changed (161) hide show
  1. package/.claude/commands/speckit.analyze.md +184 -0
  2. package/.claude/commands/speckit.checklist.md +294 -0
  3. package/.claude/commands/speckit.clarify.md +181 -0
  4. package/.claude/commands/speckit.constitution.md +82 -0
  5. package/.claude/commands/speckit.implement.md +135 -0
  6. package/.claude/commands/speckit.plan.md +89 -0
  7. package/.claude/commands/speckit.specify.md +256 -0
  8. package/.claude/commands/speckit.tasks.md +137 -0
  9. package/.claude/commands/speckit.taskstoissues.md +30 -0
  10. package/.github/workflows/deploy.yml +69 -0
  11. package/.husky/pre-commit +1 -0
  12. package/.prettierignore +4 -0
  13. package/.prettierrc +7 -0
  14. package/.specify/memory/constitution.md +73 -0
  15. package/.specify/scripts/bash/check-prerequisites.sh +166 -0
  16. package/.specify/scripts/bash/common.sh +156 -0
  17. package/.specify/scripts/bash/create-new-feature.sh +297 -0
  18. package/.specify/scripts/bash/setup-plan.sh +61 -0
  19. package/.specify/scripts/bash/update-agent-context.sh +799 -0
  20. package/.specify/templates/agent-file-template.md +28 -0
  21. package/.specify/templates/checklist-template.md +40 -0
  22. package/.specify/templates/plan-template.md +105 -0
  23. package/.specify/templates/spec-template.md +115 -0
  24. package/.specify/templates/tasks-template.md +250 -0
  25. package/CLAUDE.md +105 -0
  26. package/CONTRIBUTING.md +97 -0
  27. package/README.md +126 -0
  28. package/components.json +21 -0
  29. package/eslint.config.js +25 -0
  30. package/netlify.toml +50 -0
  31. package/package.json +94 -0
  32. package/playground/README.md +75 -0
  33. package/playground/index.html +22 -0
  34. package/playground/package.json +32 -0
  35. package/playground/public/favicon.svg +8 -0
  36. package/playground/src/App.css +256 -0
  37. package/playground/src/App.tsx +115 -0
  38. package/playground/src/assets/react.svg +1 -0
  39. package/playground/src/components/ErrorDisplay.tsx +13 -0
  40. package/playground/src/components/ExampleSelector.tsx +64 -0
  41. package/playground/src/components/Header.tsx +47 -0
  42. package/playground/src/components/JsonEditor.tsx +32 -0
  43. package/playground/src/components/Preview.tsx +78 -0
  44. package/playground/src/components/ThemeToggle.tsx +19 -0
  45. package/playground/src/data/examples.ts +1571 -0
  46. package/playground/src/hooks/useTheme.ts +55 -0
  47. package/playground/src/index.css +220 -0
  48. package/playground/src/main.tsx +10 -0
  49. package/playground/tsconfig.app.json +34 -0
  50. package/playground/tsconfig.json +13 -0
  51. package/playground/tsconfig.node.json +26 -0
  52. package/playground/vite.config.ts +31 -0
  53. package/specs/001-a2ui-renderer/checklists/requirements.md +41 -0
  54. package/specs/001-a2ui-renderer/data-model.md +140 -0
  55. package/specs/001-a2ui-renderer/plan.md +123 -0
  56. package/specs/001-a2ui-renderer/quickstart.md +141 -0
  57. package/specs/001-a2ui-renderer/research.md +140 -0
  58. package/specs/001-a2ui-renderer/spec.md +165 -0
  59. package/specs/001-a2ui-renderer/tasks.md +310 -0
  60. package/specs/002-playground/checklists/requirements.md +37 -0
  61. package/specs/002-playground/contracts/components.md +120 -0
  62. package/specs/002-playground/data-model.md +149 -0
  63. package/specs/002-playground/plan.md +73 -0
  64. package/specs/002-playground/quickstart.md +158 -0
  65. package/specs/002-playground/research.md +117 -0
  66. package/specs/002-playground/spec.md +109 -0
  67. package/specs/002-playground/tasks.md +224 -0
  68. package/src/0.8/A2UIRender.test.tsx +793 -0
  69. package/src/0.8/A2UIRender.tsx +142 -0
  70. package/src/0.8/components/ComponentRenderer.test.tsx +373 -0
  71. package/src/0.8/components/ComponentRenderer.tsx +163 -0
  72. package/src/0.8/components/UnknownComponent.tsx +49 -0
  73. package/src/0.8/components/display/AudioPlayerComponent.tsx +37 -0
  74. package/src/0.8/components/display/DividerComponent.tsx +23 -0
  75. package/src/0.8/components/display/IconComponent.tsx +137 -0
  76. package/src/0.8/components/display/ImageComponent.tsx +57 -0
  77. package/src/0.8/components/display/TextComponent.tsx +56 -0
  78. package/src/0.8/components/display/VideoComponent.tsx +31 -0
  79. package/src/0.8/components/display/display.test.tsx +660 -0
  80. package/src/0.8/components/display/index.ts +10 -0
  81. package/src/0.8/components/index.ts +14 -0
  82. package/src/0.8/components/interactive/ButtonComponent.tsx +44 -0
  83. package/src/0.8/components/interactive/CheckBoxComponent.tsx +45 -0
  84. package/src/0.8/components/interactive/DateTimeInputComponent.tsx +176 -0
  85. package/src/0.8/components/interactive/MultipleChoiceComponent.tsx +157 -0
  86. package/src/0.8/components/interactive/SliderComponent.tsx +53 -0
  87. package/src/0.8/components/interactive/TextFieldComponent.tsx +65 -0
  88. package/src/0.8/components/interactive/index.ts +10 -0
  89. package/src/0.8/components/interactive/interactive.test.tsx +618 -0
  90. package/src/0.8/components/layout/CardComponent.tsx +30 -0
  91. package/src/0.8/components/layout/ColumnComponent.tsx +93 -0
  92. package/src/0.8/components/layout/ListComponent.tsx +81 -0
  93. package/src/0.8/components/layout/ModalComponent.tsx +41 -0
  94. package/src/0.8/components/layout/RowComponent.tsx +94 -0
  95. package/src/0.8/components/layout/TabsComponent.tsx +59 -0
  96. package/src/0.8/components/layout/index.ts +10 -0
  97. package/src/0.8/components/layout/layout.test.tsx +558 -0
  98. package/src/0.8/contexts/A2UIProvider.test.tsx +226 -0
  99. package/src/0.8/contexts/A2UIProvider.tsx +54 -0
  100. package/src/0.8/contexts/ActionContext.test.tsx +242 -0
  101. package/src/0.8/contexts/ActionContext.tsx +105 -0
  102. package/src/0.8/contexts/ComponentsMapContext.tsx +125 -0
  103. package/src/0.8/contexts/DataModelContext.test.tsx +335 -0
  104. package/src/0.8/contexts/DataModelContext.tsx +184 -0
  105. package/src/0.8/contexts/SurfaceContext.test.tsx +339 -0
  106. package/src/0.8/contexts/SurfaceContext.tsx +197 -0
  107. package/src/0.8/hooks/useA2UIMessageHandler.test.tsx +399 -0
  108. package/src/0.8/hooks/useA2UIMessageHandler.ts +123 -0
  109. package/src/0.8/hooks/useComponent.test.tsx +148 -0
  110. package/src/0.8/hooks/useComponent.ts +39 -0
  111. package/src/0.8/hooks/useDataBinding.test.tsx +334 -0
  112. package/src/0.8/hooks/useDataBinding.ts +99 -0
  113. package/src/0.8/hooks/useDispatchAction.test.tsx +83 -0
  114. package/src/0.8/hooks/useDispatchAction.ts +35 -0
  115. package/src/0.8/hooks/useSurface.test.tsx +114 -0
  116. package/src/0.8/hooks/useSurface.ts +34 -0
  117. package/src/0.8/index.ts +38 -0
  118. package/src/0.8/schemas/client_to_server.json +50 -0
  119. package/src/0.8/schemas/server_to_client.json +148 -0
  120. package/src/0.8/schemas/standard_catalog_definition.json +661 -0
  121. package/src/0.8/types/index.ts +448 -0
  122. package/src/0.8/utils/dataBinding.test.ts +443 -0
  123. package/src/0.8/utils/dataBinding.ts +212 -0
  124. package/src/0.8/utils/pathUtils.test.ts +353 -0
  125. package/src/0.8/utils/pathUtils.ts +200 -0
  126. package/src/components/ui/button.tsx +62 -0
  127. package/src/components/ui/calendar.tsx +220 -0
  128. package/src/components/ui/card.tsx +92 -0
  129. package/src/components/ui/checkbox.tsx +30 -0
  130. package/src/components/ui/dialog.tsx +141 -0
  131. package/src/components/ui/input.tsx +21 -0
  132. package/src/components/ui/label.tsx +22 -0
  133. package/src/components/ui/native-select.tsx +53 -0
  134. package/src/components/ui/popover.tsx +46 -0
  135. package/src/components/ui/select.tsx +188 -0
  136. package/src/components/ui/separator.tsx +26 -0
  137. package/src/components/ui/slider.tsx +61 -0
  138. package/src/components/ui/tabs.tsx +64 -0
  139. package/src/components/ui/textarea.tsx +18 -0
  140. package/src/index.ts +1 -0
  141. package/src/lib/utils.ts +6 -0
  142. package/tsconfig.json +28 -0
  143. package/vite.config.ts +29 -0
  144. package/vitest.config.ts +22 -0
  145. package/vitest.setup.ts +8 -0
  146. package/website/README.md +4 -0
  147. package/website/assets/favicon.svg +8 -0
  148. package/website/content/.gitkeep +0 -0
  149. package/website/content/index.md +122 -0
  150. package/website/global.d.ts +9 -0
  151. package/website/package.json +17 -0
  152. package/website/plain.config.js +28 -0
  153. package/website/serve.json +6 -0
  154. package/website/src/client/color-mode-switch.css +47 -0
  155. package/website/src/client/index.js +61 -0
  156. package/website/src/client/moon.svg +1 -0
  157. package/website/src/client/sun.svg +1 -0
  158. package/website/src/components/Footer.jsx +9 -0
  159. package/website/src/components/Header.jsx +44 -0
  160. package/website/src/components/Page.jsx +28 -0
  161. 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'