@cohesivesystems/presentation-react-shadcn 0.1.0-alpha.1

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 (262) hide show
  1. package/LICENSE +55 -0
  2. package/dist/document-workspace-surface-slot-renderers.d.ts +58 -0
  3. package/dist/document-workspace-surface-slot-renderers.d.ts.map +1 -0
  4. package/dist/document-workspace-surface-slot-renderers.js +206 -0
  5. package/dist/document-workspace-surface-slot-renderers.js.map +1 -0
  6. package/dist/index.d.ts +53 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +53 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/navigation-shell-frame-renderer-registry.d.ts +8 -0
  11. package/dist/navigation-shell-frame-renderer-registry.d.ts.map +1 -0
  12. package/dist/navigation-shell-frame-renderer-registry.js +2 -0
  13. package/dist/navigation-shell-frame-renderer-registry.js.map +1 -0
  14. package/dist/navigation-shell-slot-renderer-registry.d.ts +9 -0
  15. package/dist/navigation-shell-slot-renderer-registry.d.ts.map +1 -0
  16. package/dist/navigation-shell-slot-renderer-registry.js +2 -0
  17. package/dist/navigation-shell-slot-renderer-registry.js.map +1 -0
  18. package/dist/page-host-unavailable-renderer.d.ts +10 -0
  19. package/dist/page-host-unavailable-renderer.d.ts.map +1 -0
  20. package/dist/page-host-unavailable-renderer.js +27 -0
  21. package/dist/page-host-unavailable-renderer.js.map +1 -0
  22. package/dist/presentation-action-group.d.ts +99 -0
  23. package/dist/presentation-action-group.d.ts.map +1 -0
  24. package/dist/presentation-action-group.js +214 -0
  25. package/dist/presentation-action-group.js.map +1 -0
  26. package/dist/presentation-badge-target-interpreter.d.ts +26 -0
  27. package/dist/presentation-badge-target-interpreter.d.ts.map +1 -0
  28. package/dist/presentation-badge-target-interpreter.js +17 -0
  29. package/dist/presentation-badge-target-interpreter.js.map +1 -0
  30. package/dist/presentation-component-groups.d.ts +968 -0
  31. package/dist/presentation-component-groups.d.ts.map +1 -0
  32. package/dist/presentation-component-groups.js +505 -0
  33. package/dist/presentation-component-groups.js.map +1 -0
  34. package/dist/presentation-component-system-diagnostics.d.ts +27 -0
  35. package/dist/presentation-component-system-diagnostics.d.ts.map +1 -0
  36. package/dist/presentation-component-system-diagnostics.js +399 -0
  37. package/dist/presentation-component-system-diagnostics.js.map +1 -0
  38. package/dist/presentation-component-system.d.ts +31 -0
  39. package/dist/presentation-component-system.d.ts.map +1 -0
  40. package/dist/presentation-component-system.js +61 -0
  41. package/dist/presentation-component-system.js.map +1 -0
  42. package/dist/presentation-icon-diagnostics.d.ts +12 -0
  43. package/dist/presentation-icon-diagnostics.d.ts.map +1 -0
  44. package/dist/presentation-icon-diagnostics.js +17 -0
  45. package/dist/presentation-icon-diagnostics.js.map +1 -0
  46. package/dist/presentation-icon-registry.d.ts +28 -0
  47. package/dist/presentation-icon-registry.d.ts.map +1 -0
  48. package/dist/presentation-icon-registry.js +80 -0
  49. package/dist/presentation-icon-registry.js.map +1 -0
  50. package/dist/presentation-projection-trace-panel.d.ts +10 -0
  51. package/dist/presentation-projection-trace-panel.d.ts.map +1 -0
  52. package/dist/presentation-projection-trace-panel.js +48 -0
  53. package/dist/presentation-projection-trace-panel.js.map +1 -0
  54. package/dist/presentation-routed-surface-host.d.ts +19 -0
  55. package/dist/presentation-routed-surface-host.d.ts.map +1 -0
  56. package/dist/presentation-routed-surface-host.js +12 -0
  57. package/dist/presentation-routed-surface-host.js.map +1 -0
  58. package/dist/presentation-shadcn-component-system.d.ts +22 -0
  59. package/dist/presentation-shadcn-component-system.d.ts.map +1 -0
  60. package/dist/presentation-shadcn-component-system.js +2 -0
  61. package/dist/presentation-shadcn-component-system.js.map +1 -0
  62. package/dist/presentation-standard-renderers.d.ts +168 -0
  63. package/dist/presentation-standard-renderers.d.ts.map +1 -0
  64. package/dist/presentation-standard-renderers.js +440 -0
  65. package/dist/presentation-standard-renderers.js.map +1 -0
  66. package/dist/presentation-surface-renderer.d.ts +23 -0
  67. package/dist/presentation-surface-renderer.d.ts.map +1 -0
  68. package/dist/presentation-surface-renderer.js +119 -0
  69. package/dist/presentation-surface-renderer.js.map +1 -0
  70. package/dist/presentation-value-renderers.d.ts +10 -0
  71. package/dist/presentation-value-renderers.d.ts.map +1 -0
  72. package/dist/presentation-value-renderers.js +14 -0
  73. package/dist/presentation-value-renderers.js.map +1 -0
  74. package/dist/process-task-icons.d.ts +49 -0
  75. package/dist/process-task-icons.d.ts.map +1 -0
  76. package/dist/process-task-icons.js +154 -0
  77. package/dist/process-task-icons.js.map +1 -0
  78. package/dist/projected-activity-state.d.ts +17 -0
  79. package/dist/projected-activity-state.d.ts.map +1 -0
  80. package/dist/projected-activity-state.js +22 -0
  81. package/dist/projected-activity-state.js.map +1 -0
  82. package/dist/projected-collection-action-runtime-diagnostics.d.ts +4 -0
  83. package/dist/projected-collection-action-runtime-diagnostics.d.ts.map +1 -0
  84. package/dist/projected-collection-action-runtime-diagnostics.js +9 -0
  85. package/dist/projected-collection-action-runtime-diagnostics.js.map +1 -0
  86. package/dist/projected-collection-view.d.ts +90 -0
  87. package/dist/projected-collection-view.d.ts.map +1 -0
  88. package/dist/projected-collection-view.js +532 -0
  89. package/dist/projected-collection-view.js.map +1 -0
  90. package/dist/projected-document-action-status-notices.d.ts +15 -0
  91. package/dist/projected-document-action-status-notices.d.ts.map +1 -0
  92. package/dist/projected-document-action-status-notices.js +33 -0
  93. package/dist/projected-document-action-status-notices.js.map +1 -0
  94. package/dist/projected-document-badges.d.ts +24 -0
  95. package/dist/projected-document-badges.d.ts.map +1 -0
  96. package/dist/projected-document-badges.js +53 -0
  97. package/dist/projected-document-badges.js.map +1 -0
  98. package/dist/projected-document-editor-surface.d.ts +46 -0
  99. package/dist/projected-document-editor-surface.d.ts.map +1 -0
  100. package/dist/projected-document-editor-surface.js +399 -0
  101. package/dist/projected-document-editor-surface.js.map +1 -0
  102. package/dist/projected-document-workspace-notices.d.ts +24 -0
  103. package/dist/projected-document-workspace-notices.d.ts.map +1 -0
  104. package/dist/projected-document-workspace-notices.js +14 -0
  105. package/dist/projected-document-workspace-notices.js.map +1 -0
  106. package/dist/projected-document-workspace-prompt-child-view-registry.d.ts +25 -0
  107. package/dist/projected-document-workspace-prompt-child-view-registry.d.ts.map +1 -0
  108. package/dist/projected-document-workspace-prompt-child-view-registry.js +41 -0
  109. package/dist/projected-document-workspace-prompt-child-view-registry.js.map +1 -0
  110. package/dist/projected-document-workspace-prompt-child-view-renderers.d.ts +32 -0
  111. package/dist/projected-document-workspace-prompt-child-view-renderers.d.ts.map +1 -0
  112. package/dist/projected-document-workspace-prompt-child-view-renderers.js +77 -0
  113. package/dist/projected-document-workspace-prompt-child-view-renderers.js.map +1 -0
  114. package/dist/projected-document-workspace-shell.d.ts +100 -0
  115. package/dist/projected-document-workspace-shell.d.ts.map +1 -0
  116. package/dist/projected-document-workspace-shell.js +153 -0
  117. package/dist/projected-document-workspace-shell.js.map +1 -0
  118. package/dist/projected-document-workspace.d.ts +39 -0
  119. package/dist/projected-document-workspace.d.ts.map +1 -0
  120. package/dist/projected-document-workspace.js +63 -0
  121. package/dist/projected-document-workspace.js.map +1 -0
  122. package/dist/projected-field-value-rendering.d.ts +94 -0
  123. package/dist/projected-field-value-rendering.d.ts.map +1 -0
  124. package/dist/projected-field-value-rendering.js +377 -0
  125. package/dist/projected-field-value-rendering.js.map +1 -0
  126. package/dist/projected-inline-list.d.ts +20 -0
  127. package/dist/projected-inline-list.d.ts.map +1 -0
  128. package/dist/projected-inline-list.js +149 -0
  129. package/dist/projected-inline-list.js.map +1 -0
  130. package/dist/projected-input-form.d.ts +143 -0
  131. package/dist/projected-input-form.d.ts.map +1 -0
  132. package/dist/projected-input-form.js +612 -0
  133. package/dist/projected-input-form.js.map +1 -0
  134. package/dist/projected-metric-dashboard.d.ts +26 -0
  135. package/dist/projected-metric-dashboard.d.ts.map +1 -0
  136. package/dist/projected-metric-dashboard.js +20 -0
  137. package/dist/projected-metric-dashboard.js.map +1 -0
  138. package/dist/projected-metric-strip.d.ts +20 -0
  139. package/dist/projected-metric-strip.d.ts.map +1 -0
  140. package/dist/projected-metric-strip.js +131 -0
  141. package/dist/projected-metric-strip.js.map +1 -0
  142. package/dist/projected-navigation-shell-view-host.d.ts +34 -0
  143. package/dist/projected-navigation-shell-view-host.d.ts.map +1 -0
  144. package/dist/projected-navigation-shell-view-host.js +75 -0
  145. package/dist/projected-navigation-shell-view-host.js.map +1 -0
  146. package/dist/projected-navigation-shell.d.ts +33 -0
  147. package/dist/projected-navigation-shell.d.ts.map +1 -0
  148. package/dist/projected-navigation-shell.js +112 -0
  149. package/dist/projected-navigation-shell.js.map +1 -0
  150. package/dist/projected-presentation-badges.d.ts +34 -0
  151. package/dist/projected-presentation-badges.d.ts.map +1 -0
  152. package/dist/projected-presentation-badges.js +22 -0
  153. package/dist/projected-presentation-badges.js.map +1 -0
  154. package/dist/projected-presentation-diagnostics.d.ts +10 -0
  155. package/dist/projected-presentation-diagnostics.d.ts.map +1 -0
  156. package/dist/projected-presentation-diagnostics.js +40 -0
  157. package/dist/projected-presentation-diagnostics.js.map +1 -0
  158. package/dist/projected-presentation-flow-layer.d.ts +33 -0
  159. package/dist/projected-presentation-flow-layer.d.ts.map +1 -0
  160. package/dist/projected-presentation-flow-layer.js +71 -0
  161. package/dist/projected-presentation-flow-layer.js.map +1 -0
  162. package/dist/projected-process-task-notices.d.ts +37 -0
  163. package/dist/projected-process-task-notices.d.ts.map +1 -0
  164. package/dist/projected-process-task-notices.js +225 -0
  165. package/dist/projected-process-task-notices.js.map +1 -0
  166. package/dist/projected-prompt-child-view-renderer.d.ts +101 -0
  167. package/dist/projected-prompt-child-view-renderer.d.ts.map +1 -0
  168. package/dist/projected-prompt-child-view-renderer.js +442 -0
  169. package/dist/projected-prompt-child-view-renderer.js.map +1 -0
  170. package/dist/projected-prompt-document-preview.d.ts +39 -0
  171. package/dist/projected-prompt-document-preview.d.ts.map +1 -0
  172. package/dist/projected-prompt-document-preview.js +50 -0
  173. package/dist/projected-prompt-document-preview.js.map +1 -0
  174. package/dist/projected-prompt-view.d.ts +35 -0
  175. package/dist/projected-prompt-view.d.ts.map +1 -0
  176. package/dist/projected-prompt-view.js +292 -0
  177. package/dist/projected-prompt-view.js.map +1 -0
  178. package/dist/projected-query-form.d.ts +56 -0
  179. package/dist/projected-query-form.d.ts.map +1 -0
  180. package/dist/projected-query-form.js +100 -0
  181. package/dist/projected-query-form.js.map +1 -0
  182. package/dist/projected-record-details.d.ts +20 -0
  183. package/dist/projected-record-details.d.ts.map +1 -0
  184. package/dist/projected-record-details.js +60 -0
  185. package/dist/projected-record-details.js.map +1 -0
  186. package/dist/projected-routed-surface-runtime.d.ts +86 -0
  187. package/dist/projected-routed-surface-runtime.d.ts.map +1 -0
  188. package/dist/projected-routed-surface-runtime.js +332 -0
  189. package/dist/projected-routed-surface-runtime.js.map +1 -0
  190. package/dist/projected-tabs-view.d.ts +21 -0
  191. package/dist/projected-tabs-view.d.ts.map +1 -0
  192. package/dist/projected-tabs-view.js +140 -0
  193. package/dist/projected-tabs-view.js.map +1 -0
  194. package/dist/projected-view-chrome.d.ts +15 -0
  195. package/dist/projected-view-chrome.d.ts.map +1 -0
  196. package/dist/projected-view-chrome.js +67 -0
  197. package/dist/projected-view-chrome.js.map +1 -0
  198. package/dist/projected-view-surface.d.ts +34 -0
  199. package/dist/projected-view-surface.d.ts.map +1 -0
  200. package/dist/projected-view-surface.js +40 -0
  201. package/dist/projected-view-surface.js.map +1 -0
  202. package/dist/standard-collection-chrome-slot-renderers.d.ts +26 -0
  203. package/dist/standard-collection-chrome-slot-renderers.d.ts.map +1 -0
  204. package/dist/standard-collection-chrome-slot-renderers.js +250 -0
  205. package/dist/standard-collection-chrome-slot-renderers.js.map +1 -0
  206. package/dist/standard-view-chrome-slot-renderers.d.ts +57 -0
  207. package/dist/standard-view-chrome-slot-renderers.d.ts.map +1 -0
  208. package/dist/standard-view-chrome-slot-renderers.js +614 -0
  209. package/dist/standard-view-chrome-slot-renderers.js.map +1 -0
  210. package/package.json +68 -0
  211. package/src/document-workspace-surface-slot-renderers.tsx +395 -0
  212. package/src/index.ts +53 -0
  213. package/src/navigation-shell-frame-renderer-registry.ts +29 -0
  214. package/src/navigation-shell-slot-renderer-registry.ts +46 -0
  215. package/src/page-host-unavailable-renderer.tsx +79 -0
  216. package/src/presentation-action-group.tsx +475 -0
  217. package/src/presentation-badge-target-interpreter.ts +54 -0
  218. package/src/presentation-component-groups.ts +2505 -0
  219. package/src/presentation-component-system-diagnostics.ts +848 -0
  220. package/src/presentation-component-system.ts +365 -0
  221. package/src/presentation-icon-diagnostics.ts +53 -0
  222. package/src/presentation-icon-registry.tsx +181 -0
  223. package/src/presentation-projection-trace-panel.tsx +140 -0
  224. package/src/presentation-routed-surface-host.tsx +48 -0
  225. package/src/presentation-shadcn-component-system.ts +102 -0
  226. package/src/presentation-standard-renderers.tsx +1290 -0
  227. package/src/presentation-surface-renderer.tsx +263 -0
  228. package/src/presentation-value-renderers.tsx +40 -0
  229. package/src/process-task-icons.tsx +258 -0
  230. package/src/projected-activity-state.tsx +63 -0
  231. package/src/projected-collection-action-runtime-diagnostics.ts +22 -0
  232. package/src/projected-collection-view.tsx +1273 -0
  233. package/src/projected-document-action-status-notices.tsx +66 -0
  234. package/src/projected-document-badges.tsx +134 -0
  235. package/src/projected-document-editor-surface.tsx +808 -0
  236. package/src/projected-document-workspace-notices.tsx +77 -0
  237. package/src/projected-document-workspace-prompt-child-view-registry.ts +99 -0
  238. package/src/projected-document-workspace-prompt-child-view-renderers.tsx +205 -0
  239. package/src/projected-document-workspace-shell.tsx +427 -0
  240. package/src/projected-document-workspace.tsx +216 -0
  241. package/src/projected-field-value-rendering.tsx +704 -0
  242. package/src/projected-inline-list.tsx +434 -0
  243. package/src/projected-input-form.tsx +1218 -0
  244. package/src/projected-metric-dashboard.tsx +106 -0
  245. package/src/projected-metric-strip.tsx +244 -0
  246. package/src/projected-navigation-shell-view-host.tsx +177 -0
  247. package/src/projected-navigation-shell.tsx +219 -0
  248. package/src/projected-presentation-badges.tsx +95 -0
  249. package/src/projected-presentation-diagnostics.tsx +120 -0
  250. package/src/projected-presentation-flow-layer.tsx +186 -0
  251. package/src/projected-process-task-notices.tsx +501 -0
  252. package/src/projected-prompt-child-view-renderer.tsx +698 -0
  253. package/src/projected-prompt-document-preview.tsx +197 -0
  254. package/src/projected-prompt-view.tsx +592 -0
  255. package/src/projected-query-form.tsx +286 -0
  256. package/src/projected-record-details.tsx +133 -0
  257. package/src/projected-routed-surface-runtime.tsx +713 -0
  258. package/src/projected-tabs-view.tsx +308 -0
  259. package/src/projected-view-chrome.tsx +148 -0
  260. package/src/projected-view-surface.tsx +131 -0
  261. package/src/standard-collection-chrome-slot-renderers.tsx +391 -0
  262. package/src/standard-view-chrome-slot-renderers.tsx +1064 -0
@@ -0,0 +1,1218 @@
1
+ import { Fragment, useMemo, type ReactNode } from 'react'
2
+
3
+ import {
4
+ createDefaultDateTimeFilter,
5
+ createPresentationEnumDiscriminator,
6
+ createPresentationProjectionDiagnostic,
7
+ defaultPresentationComponentSet,
8
+ findPresentationAction,
9
+ findPresentationField,
10
+ isViewChromeSlotKind,
11
+ matchesPresentationEnum,
12
+ normalizeDateTimeFilter,
13
+ readObjectPath,
14
+ resolvePresentationFieldValueLabel,
15
+ resolvePresentationFieldValueTone,
16
+ type ActionPlacementDefinition,
17
+ type DataSourceQueryDefinition,
18
+ type DataSourceQueryFieldDefinition,
19
+ type DateTimeFilterValue,
20
+ type FieldPresentationDefinition,
21
+ type InputFormDefinition,
22
+ type InputFormFieldDefinition,
23
+ type PresentationModuleDefinition,
24
+ type PresentationDataSourceResolver,
25
+ type PresentationProjectionDiagnostic,
26
+ type ProjectedInputFormRuntime,
27
+ type ProjectedInputFormTargetContext,
28
+ type QueryFormDefinition,
29
+ type ViewChromeSlotDefinition,
30
+ type ViewDefinition,
31
+ } from '@cohesivesystems/presentation-core'
32
+ import type {
33
+ PresentationShadcnComponentSystem as PresentationComponentSystem,
34
+ } from './presentation-shadcn-component-system'
35
+ import type { PresentationDesignSystem } from '@cohesivesystems/presentation-tailwind'
36
+ import {
37
+ projectPresentationActionIconDiagnostics,
38
+ } from './presentation-icon-diagnostics'
39
+ import {
40
+ renderPresentationIcon,
41
+ } from './presentation-icon-registry'
42
+ import { ProjectedStatusBlock } from './projected-activity-state'
43
+ import {
44
+ ProjectedViewSurface,
45
+ type ProjectedViewSurfaceChromeSlotRenderer,
46
+ } from './projected-view-surface'
47
+ import {
48
+ useRegisterPresentationProjectionDiagnostics,
49
+ } from '@cohesivesystems/presentation-react'
50
+ import {
51
+ inputFormChoiceDefaultSelections,
52
+ inputFormFieldControlKinds,
53
+ inputFormFieldKinds,
54
+ viewChromeSlotKinds,
55
+ } from '@cohesivesystems/presentation-contracts'
56
+
57
+ export type {
58
+ ProjectedInputFormActionContext,
59
+ ProjectedInputFormRuntime,
60
+ ProjectedInputFormTargetContext,
61
+ ProjectedInputFormValueChangeContext,
62
+ } from '@cohesivesystems/presentation-core'
63
+
64
+ /**
65
+ * Context supplied to a domain-specific input-form field renderer.
66
+ */
67
+ export interface ProjectedInputFormFieldRenderContext {
68
+ /** Optional resolver that maps a choice and tone to toggle styling. */
69
+ readonly choiceToggleClassName?: ProjectedInputFormChoiceClassNameResolver
70
+
71
+ /** Optional resolver that normalizes domain choice metadata into a presentation tone. */
72
+ readonly choiceTone?: ProjectedInputFormChoiceToneResolver
73
+
74
+ /** Choices resolved from the field's semantic choice source. */
75
+ readonly choices: readonly ProjectedInputFormChoice[]
76
+
77
+ /** Component implementation set used for projected form controls. */
78
+ readonly componentSystem: PresentationComponentSystem
79
+
80
+ /** Design-system class and semantic resolvers used for projected form layout. */
81
+ readonly designSystem: PresentationDesignSystem
82
+
83
+ /** Input-form field being rendered. */
84
+ readonly field: InputFormFieldDefinition
85
+
86
+ /** Input form that owns the field. */
87
+ readonly inputForm: InputFormDefinition
88
+
89
+ /** Presentation field metadata referenced by the input-form field, when available. */
90
+ readonly presentationField: FieldPresentationDefinition | null
91
+
92
+ /** Data-source query field supplied when a relation-query target specializes the input form. */
93
+ readonly queryField: DataSourceQueryFieldDefinition | null
94
+
95
+ /** Query-form binding supplied when a relation-query target specializes the input form. */
96
+ readonly queryForm: QueryFormDefinition | null
97
+
98
+ /** Shared or target state id affected by the input form. */
99
+ readonly stateId: string
100
+
101
+ /** Target interpretation attached to this form instance. */
102
+ readonly target: ProjectedInputFormTargetContext
103
+
104
+ /** Updates the field value at its declared value path. */
105
+ readonly setFieldValue: (value: unknown) => void
106
+
107
+ /** Current value read from the caller-owned form value at the field value path. */
108
+ readonly value: unknown
109
+ }
110
+
111
+ /**
112
+ * Override renderer for an input-form field.
113
+ */
114
+ export type ProjectedInputFormFieldRenderer = (
115
+ context: ProjectedInputFormFieldRenderContext,
116
+ ) => ReactNode
117
+
118
+ /**
119
+ * Normalized choice projected from an input-form choice source.
120
+ */
121
+ export interface ProjectedInputFormChoice {
122
+ /** User-facing choice label. */
123
+ readonly label: string
124
+
125
+ /** Optional semantic tone supplied by the choice source or caller resolver. */
126
+ readonly tone?: string | null
127
+
128
+ /** Stable choice value written into input-form state. */
129
+ readonly value: string
130
+ }
131
+
132
+ /**
133
+ * Context supplied when resolving choice tone or styling.
134
+ */
135
+ export interface ProjectedInputFormChoiceRenderContext {
136
+ /** Choice being rendered. */
137
+ readonly choice: ProjectedInputFormChoice
138
+
139
+ /** Input-form field that owns the choice. */
140
+ readonly field: InputFormFieldDefinition
141
+
142
+ /** Input form that owns the field. */
143
+ readonly inputForm: InputFormDefinition
144
+
145
+ /** Presentation field metadata referenced by the input-form field, when available. */
146
+ readonly presentationField: FieldPresentationDefinition | null
147
+
148
+ /** Data-source query field supplied when a relation-query target specializes the input form. */
149
+ readonly queryField: DataSourceQueryFieldDefinition | null
150
+
151
+ /** Query-form binding supplied when a relation-query target specializes the input form. */
152
+ readonly queryForm: QueryFormDefinition | null
153
+
154
+ /** Shared or target state id affected by the input form. */
155
+ readonly stateId: string
156
+
157
+ /** Target interpretation attached to this form instance. */
158
+ readonly target: ProjectedInputFormTargetContext
159
+ }
160
+
161
+ /**
162
+ * Choice styling context after tone resolution has completed.
163
+ */
164
+ export interface ProjectedInputFormChoiceClassNameContext
165
+ extends ProjectedInputFormChoiceRenderContext {
166
+ /** Resolved presentation tone for the choice, or null when no tone applies. */
167
+ readonly tone: string | null
168
+ }
169
+
170
+ /**
171
+ * Resolves a semantic choice into a presentation tone understood by the local design system.
172
+ */
173
+ export type ProjectedInputFormChoiceToneResolver = (
174
+ context: ProjectedInputFormChoiceRenderContext,
175
+ ) => string | null | undefined
176
+
177
+ /**
178
+ * Resolves final toggle classes for a choice after tone resolution.
179
+ */
180
+ export type ProjectedInputFormChoiceClassNameResolver = (
181
+ context: ProjectedInputFormChoiceClassNameContext,
182
+ ) => string | undefined
183
+
184
+ /**
185
+ * Props required to project a backend-declared input form into React controls.
186
+ *
187
+ * @typeParam TValue - Shape of the caller-owned form state object.
188
+ */
189
+ export interface ProjectedInputFormProps<TValue extends object = object> {
190
+ /** Optional resolver that maps a choice and tone to toggle styling. */
191
+ readonly choiceToggleClassName?: ProjectedInputFormChoiceClassNameResolver
192
+
193
+ /** Optional resolver that normalizes domain choice metadata into a presentation tone. */
194
+ readonly choiceTone?: ProjectedInputFormChoiceToneResolver
195
+
196
+ /** Class name applied to the projected view surface. */
197
+ readonly className?: string
198
+
199
+ /** Class name applied to projected view chrome after the form body. */
200
+ readonly chromeAfterContentClassName?: string
201
+
202
+ /** Class name applied to projected view chrome before the form body. */
203
+ readonly chromeBeforeContentClassName?: string
204
+
205
+ /** Class name applied to projected view chrome in the surface footer. */
206
+ readonly chromeFooterClassName?: string
207
+
208
+ /** Class name applied to projected view chrome in the surface header. */
209
+ readonly chromeHeaderClassName?: string
210
+
211
+ /** Class name applied to the projected view surface content area. */
212
+ readonly contentClassName?: string
213
+
214
+ /** Component implementation set used for projected form controls. */
215
+ readonly componentSystem: PresentationComponentSystem
216
+
217
+ /** Presentation target component set used to resolve declared icons. */
218
+ readonly componentSet?: string
219
+
220
+ /** Design-system class and semantic resolvers used for projected form layout. */
221
+ readonly designSystem: PresentationDesignSystem
222
+
223
+ /** Resolver for data sources used by field choice sources. */
224
+ readonly dataSourceResolver: PresentationDataSourceResolver
225
+
226
+ /** Field renderer overrides keyed by input-form field id or presentation field id. */
227
+ readonly fieldRenderers?: Readonly<Record<string, ProjectedInputFormFieldRenderer>>
228
+
229
+ /** Input form to project; null renders a diagnostic status block. */
230
+ readonly inputForm: InputFormDefinition | null
231
+
232
+ /** Presentation module that owns the input form, fields, and actions. */
233
+ readonly module: PresentationModuleDefinition
234
+
235
+ /** Runtime state and action handlers; null renders a diagnostic status block. */
236
+ readonly runtime: ProjectedInputFormRuntime<TValue> | null
237
+
238
+ /** Interprets non-form view chrome slots declared by the hosting view. */
239
+ readonly renderChromeSlot?: ProjectedViewSurfaceChromeSlotRenderer
240
+
241
+ /** Optional target specialization attached to this input form instance. */
242
+ readonly target?: ProjectedInputFormTargetContext | null
243
+
244
+ /** View that hosts the input form and supplies surface identity. */
245
+ readonly view: ViewDefinition
246
+ }
247
+
248
+ interface ProjectedInputFormFieldProjection {
249
+ readonly inputField: InputFormFieldDefinition
250
+ readonly queryField: DataSourceQueryFieldDefinition | null
251
+ }
252
+
253
+ /**
254
+ * Projects a semantic input-form definition into concrete React form controls.
255
+ *
256
+ * Generic input kinds are handled locally; domain-specific fields, choice tones,
257
+ * styling, and action behavior stay injectable through renderer props and runtime.
258
+ */
259
+ export function ProjectedInputForm<TValue extends object = object>({
260
+ choiceToggleClassName,
261
+ choiceTone,
262
+ className,
263
+ chromeAfterContentClassName,
264
+ chromeBeforeContentClassName,
265
+ chromeFooterClassName,
266
+ chromeHeaderClassName,
267
+ componentSet = defaultPresentationComponentSet,
268
+ componentSystem,
269
+ contentClassName,
270
+ dataSourceResolver,
271
+ designSystem,
272
+ fieldRenderers,
273
+ inputForm,
274
+ module,
275
+ renderChromeSlot,
276
+ runtime,
277
+ target,
278
+ view,
279
+ }: ProjectedInputFormProps<TValue>) {
280
+ const actionIconPlacements = useMemo(
281
+ () => resolveInputFormActionIconPlacements(inputForm, view),
282
+ [inputForm, view],
283
+ )
284
+ const actionIconDiagnostics = useMemo(
285
+ () => projectPresentationActionIconDiagnostics({
286
+ actionPlacements: actionIconPlacements,
287
+ componentSet,
288
+ module,
289
+ source: `projected-input-form-icons:${view.Id}:${inputForm?.Id ?? 'missing'}`,
290
+ surfaceId: view.Id,
291
+ surfaceName: inputForm?.Name ?? view.Name,
292
+ }),
293
+ [
294
+ actionIconPlacements,
295
+ componentSet,
296
+ inputForm?.Id,
297
+ inputForm?.Name,
298
+ module,
299
+ view.Id,
300
+ view.Name,
301
+ ],
302
+ )
303
+ useRegisterPresentationProjectionDiagnostics(
304
+ `projected-input-form-icons:${view.Id}:${inputForm?.Id ?? 'missing'}`,
305
+ actionIconDiagnostics,
306
+ )
307
+ const runtimeDiagnosticsSource =
308
+ `projected-input-form-runtime:${view.Id}:${inputForm?.Id ?? 'missing'}`
309
+ const runtimeDiagnostics = useMemo(
310
+ () => createInputFormRuntimeDiagnostics({
311
+ hasRuntime: runtime !== null,
312
+ inputForm,
313
+ source: runtimeDiagnosticsSource,
314
+ view,
315
+ }),
316
+ [inputForm, runtime, runtimeDiagnosticsSource, view],
317
+ )
318
+ useRegisterPresentationProjectionDiagnostics(
319
+ runtimeDiagnosticsSource,
320
+ runtimeDiagnostics,
321
+ )
322
+
323
+ if (!inputForm) {
324
+ return <ProjectedStatusBlock label={`Presentation view '${view.Name}' has no input form binding.`} />
325
+ }
326
+
327
+ if (!runtime) {
328
+ return null
329
+ }
330
+
331
+ const formTarget = target ?? createDefaultInputFormTarget(inputForm)
332
+ const projectedFields = projectInputFormFields(inputForm, formTarget.queryDefinition ?? null)
333
+ const projectedFieldsById = new Map(
334
+ projectedFields.map((field) => [field.inputField.Id, field] as const),
335
+ )
336
+ const choiceValuesByFieldId = Object.fromEntries(
337
+ projectedFields.map(({ inputField, queryField }) => [
338
+ inputField.Id,
339
+ readInputFormChoices(
340
+ dataSourceResolver,
341
+ inputField,
342
+ queryField,
343
+ findPresentationField<FieldPresentationDefinition>(
344
+ module,
345
+ queryField?.FieldId ?? inputField.FieldId,
346
+ ) ?? null,
347
+ ).map((choice) => choice.value),
348
+ ]),
349
+ )
350
+ const renderFormChromeSlot = (slot: ViewChromeSlotDefinition, chromeView: ViewDefinition | null) => {
351
+ if (isViewChromeSlotKind(slot, viewChromeSlotKinds.actions)) {
352
+ return renderInputFormActionRow({
353
+ choiceValuesByFieldId,
354
+ componentSet,
355
+ componentSystem,
356
+ designSystem,
357
+ inputForm,
358
+ module,
359
+ placements: slot.Actions.length > 0 ? slot.Actions : inputForm.Actions,
360
+ runtime,
361
+ target: formTarget,
362
+ })
363
+ }
364
+
365
+ return renderChromeSlot?.(slot, chromeView) ?? null
366
+ }
367
+
368
+ return (
369
+ <ProjectedViewSurface
370
+ className={className}
371
+ chromeAfterContentClassName={chromeAfterContentClassName}
372
+ chromeBeforeContentClassName={chromeBeforeContentClassName}
373
+ chromeFooterClassName={chromeFooterClassName}
374
+ chromeHeaderClassName={chromeHeaderClassName}
375
+ componentSystem={componentSystem}
376
+ contentClassName={cn(designSystem.classNames.formSurface.content({}), contentClassName)}
377
+ collapsible
378
+ renderChromeSlot={renderFormChromeSlot}
379
+ title={view.Name}
380
+ view={view}
381
+ >
382
+ {componentSystem.forms.InputForm({
383
+ form: inputForm,
384
+ viewId: view.Id,
385
+ children: (
386
+ <>
387
+ {componentSystem.forms.InputFormGroups({
388
+ className: designSystem.classNames.formSurface.groups({}),
389
+ form: inputForm,
390
+ children: inputForm.Groups.map((group) => {
391
+ const groupFields = group.FieldIds.flatMap((fieldId) => {
392
+ const field = projectedFieldsById.get(fieldId)
393
+ return field ? [field] : []
394
+ })
395
+ if (groupFields.length === 0) {
396
+ return null
397
+ }
398
+
399
+ return (
400
+ <Fragment key={group.Id}>
401
+ {componentSystem.forms.InputFormGroup({
402
+ className: designSystem.classNames.formSurface.group({ group }),
403
+ group,
404
+ children: groupFields.map((field) => (
405
+ <Fragment key={field.inputField.Id}>
406
+ {renderInputFormField({
407
+ choiceToggleClassName,
408
+ choiceTone,
409
+ componentSystem,
410
+ dataSourceResolver,
411
+ designSystem,
412
+ fieldRenderers,
413
+ choiceValuesByFieldId,
414
+ inputField: field.inputField,
415
+ inputForm,
416
+ module,
417
+ queryField: field.queryField,
418
+ runtime,
419
+ target: formTarget,
420
+ })}
421
+ </Fragment>
422
+ )),
423
+ })}
424
+ </Fragment>
425
+ )
426
+ }),
427
+ })}
428
+
429
+ {hasActionsChromeSlot(view) ? null : renderInputFormActionRow({
430
+ choiceValuesByFieldId,
431
+ componentSet,
432
+ componentSystem,
433
+ designSystem,
434
+ inputForm,
435
+ module,
436
+ placements: inputForm.Actions,
437
+ runtime,
438
+ target: formTarget,
439
+ })}
440
+ </>
441
+ ),
442
+ })}
443
+ </ProjectedViewSurface>
444
+ )
445
+ }
446
+
447
+ function createInputFormRuntimeDiagnostics({
448
+ hasRuntime,
449
+ inputForm,
450
+ source,
451
+ view,
452
+ }: {
453
+ readonly hasRuntime: boolean
454
+ readonly inputForm: InputFormDefinition | null
455
+ readonly source: string
456
+ readonly view: ViewDefinition
457
+ }): readonly PresentationProjectionDiagnostic[] {
458
+ if (!inputForm || hasRuntime) {
459
+ return []
460
+ }
461
+
462
+ return [
463
+ createPresentationProjectionDiagnostic({
464
+ category: 'missing-binding',
465
+ id: `input-form.${inputForm.Id}.runtime.missing-binding`,
466
+ interpretation: {
467
+ status: 'unbound',
468
+ target: 'input-form-runtime-binding',
469
+ },
470
+ message: `Input form '${inputForm.Name}' has no runtime binding.`,
471
+ severity: 'warning',
472
+ source,
473
+ subject: {
474
+ id: inputForm.Id,
475
+ kind: 'input-form',
476
+ name: inputForm.Name,
477
+ },
478
+ suggestedNextStep:
479
+ `Bind input form '${inputForm.Id}' to a frontend runtime adapter or remove it from view '${view.Id}'.`,
480
+ }),
481
+ ]
482
+ }
483
+
484
+ function renderInputFormField<TValue extends object>({
485
+ choiceToggleClassName,
486
+ choiceTone,
487
+ componentSystem,
488
+ dataSourceResolver,
489
+ designSystem,
490
+ fieldRenderers,
491
+ choiceValuesByFieldId,
492
+ inputField,
493
+ inputForm,
494
+ module,
495
+ queryField,
496
+ runtime,
497
+ target,
498
+ }: {
499
+ readonly choiceToggleClassName?: ProjectedInputFormChoiceClassNameResolver
500
+ readonly choiceTone?: ProjectedInputFormChoiceToneResolver
501
+ readonly choiceValuesByFieldId: Readonly<Record<string, readonly string[]>>
502
+ readonly componentSystem: PresentationComponentSystem
503
+ readonly dataSourceResolver: PresentationDataSourceResolver
504
+ readonly designSystem: PresentationDesignSystem
505
+ readonly fieldRenderers?: Readonly<Record<string, ProjectedInputFormFieldRenderer>>
506
+ readonly inputField: InputFormFieldDefinition
507
+ readonly inputForm: InputFormDefinition
508
+ readonly module: PresentationModuleDefinition
509
+ readonly queryField: DataSourceQueryFieldDefinition | null
510
+ readonly runtime: ProjectedInputFormRuntime<TValue>
511
+ readonly target: ProjectedInputFormTargetContext
512
+ }) {
513
+ const fieldId = queryField?.FieldId ?? inputField.FieldId
514
+ const valuePath = queryField?.ValuePath ?? inputField.ValuePath
515
+ const presentationField = findPresentationField<FieldPresentationDefinition>(module, fieldId) ?? null
516
+ const value = readFormValue(runtime.value, valuePath)
517
+ const choices = readInputFormChoices(dataSourceResolver, inputField, queryField, presentationField)
518
+ const setFieldValue = (nextValue: unknown) => {
519
+ runtime.setValue(
520
+ (current) => setFormValue(current, valuePath, nextValue),
521
+ {
522
+ choiceValuesByFieldId,
523
+ inputForm,
524
+ target,
525
+ value: runtime.value,
526
+ },
527
+ )
528
+ }
529
+ const renderOverride = findInputFieldRenderer(fieldRenderers, inputField, queryField)
530
+ const fieldContext = {
531
+ choiceToggleClassName,
532
+ choiceTone,
533
+ choices,
534
+ componentSystem,
535
+ designSystem,
536
+ field: inputField,
537
+ inputForm,
538
+ presentationField,
539
+ queryField,
540
+ queryForm: target.queryForm ?? null,
541
+ setFieldValue,
542
+ stateId: target.stateId,
543
+ target,
544
+ value,
545
+ } satisfies ProjectedInputFormFieldRenderContext
546
+ const control = renderOverride
547
+ ? renderOverride(fieldContext)
548
+ : renderDefaultFieldControl({
549
+ ...fieldContext,
550
+ label: presentationField?.Label ?? queryField?.Name ?? inputField.Name,
551
+ })
552
+ const label = presentationField?.Label ?? queryField?.Name ?? inputField.Name
553
+
554
+ return componentSystem.forms.InputFormField({
555
+ className: designSystem.classNames.formSurface.field({}),
556
+ control: componentSystem.forms.InputFormControlSlot({
557
+ field: inputField,
558
+ children: control,
559
+ }),
560
+ field: inputField,
561
+ label: componentSystem.forms.FormFieldLabel({
562
+ children: label,
563
+ }),
564
+ })
565
+ }
566
+
567
+ function renderDefaultFieldControl({
568
+ choiceToggleClassName,
569
+ choiceTone,
570
+ choices,
571
+ componentSystem,
572
+ designSystem,
573
+ field,
574
+ inputForm,
575
+ label,
576
+ presentationField,
577
+ queryField,
578
+ queryForm,
579
+ setFieldValue,
580
+ stateId,
581
+ target,
582
+ value,
583
+ }: ProjectedInputFormFieldRenderContext & { readonly label: string }) {
584
+ if (isMultiSelectInputField(field, queryField)) {
585
+ const selected = readSelectedChoiceValues(field, queryField, choices, value)
586
+ return choices.length > 0 ? (
587
+ componentSystem.forms.ChoiceToggleGroup({
588
+ 'aria-label': label,
589
+ onValueChange: setFieldValue,
590
+ value: selected,
591
+ children: choices.map((choice) => (
592
+ <Fragment key={choice.value}>
593
+ {componentSystem.forms.ChoiceToggleItem({
594
+ 'aria-label': choice.label,
595
+ className: resolveChoiceToggleClassName({
596
+ choice,
597
+ choiceToggleClassName,
598
+ choiceTone,
599
+ field,
600
+ inputForm,
601
+ presentationField,
602
+ queryField,
603
+ queryForm,
604
+ stateId,
605
+ target,
606
+ }),
607
+ value: choice.value,
608
+ children: choice.label,
609
+ })}
610
+ </Fragment>
611
+ )),
612
+ })
613
+ ) : (
614
+ componentSystem.forms.InputFormFieldMessage({
615
+ field,
616
+ children: `No ${label.toLocaleLowerCase()} choices are available.`,
617
+ })
618
+ )
619
+ }
620
+
621
+ if (isInputFormFieldKind(field, 'select', 'Select')) {
622
+ return componentSystem.forms.SelectControl({
623
+ 'aria-label': label,
624
+ onValueChange: setFieldValue,
625
+ options: [
626
+ { label: 'Any', value: '' },
627
+ ...choices.map((choice) => ({
628
+ label: choice.label,
629
+ value: choice.value,
630
+ })),
631
+ ],
632
+ value: typeof value === 'string' ? value : '',
633
+ })
634
+ }
635
+
636
+ if (isInputFormFieldKind(field, 'dateTimeRange', 'DateTimeRange')) {
637
+ if (isDateTimeFilterControl(field)) {
638
+ return componentSystem.forms.DateTimeFilterControl({
639
+ emptyLabel: field.Display?.EmptyValueLabel ?? undefined,
640
+ incrementMinutes: field.Display?.IncrementMinutes ?? undefined,
641
+ label,
642
+ onValueChange: setFieldValue,
643
+ showTimezone: field.Display?.ShowTimezone ?? undefined,
644
+ value: coerceDateTimeFilterValue(value),
645
+ })
646
+ }
647
+
648
+ const range = value && typeof value === 'object'
649
+ ? value as Record<string, unknown>
650
+ : {}
651
+ return componentSystem.forms.InputFormControlGroup({
652
+ className: designSystem.classNames.formSurface.rangeGroup({}),
653
+ field,
654
+ children: (
655
+ <>
656
+ {componentSystem.forms.TextInputControl({
657
+ 'aria-label': `${label} after`,
658
+ onValueChange: (nextValue) =>
659
+ setFieldValue({ ...range, after: localDateTimeToIso(nextValue) }),
660
+ type: 'datetime-local',
661
+ value: isoToLocalDateTime(readOptionalString(range.after)),
662
+ })}
663
+ {componentSystem.forms.TextInputControl({
664
+ 'aria-label': `${label} before`,
665
+ onValueChange: (nextValue) =>
666
+ setFieldValue({ ...range, before: localDateTimeToIso(nextValue) }),
667
+ type: 'datetime-local',
668
+ value: isoToLocalDateTime(readOptionalString(range.before)),
669
+ })}
670
+ </>
671
+ ),
672
+ })
673
+ }
674
+
675
+ if (isInputFormFieldKind(field, 'dateTime', 'DateTime')) {
676
+ return componentSystem.forms.TextInputControl({
677
+ 'aria-label': label,
678
+ onValueChange: (nextValue) => setFieldValue(localDateTimeToIso(nextValue)),
679
+ type: 'datetime-local',
680
+ value: isoToLocalDateTime(readOptionalString(value)),
681
+ })
682
+ }
683
+
684
+ if (isInputFormFieldKind(field, 'date', 'Date')) {
685
+ return componentSystem.forms.TextInputControl({
686
+ 'aria-label': label,
687
+ onValueChange: (nextValue) => setFieldValue(nextValue || null),
688
+ type: 'date',
689
+ value: typeof value === 'string' ? value.slice(0, 10) : '',
690
+ })
691
+ }
692
+
693
+ if (isInputFormFieldKind(field, 'number', 'Number')) {
694
+ return componentSystem.forms.TextInputControl({
695
+ 'aria-label': label,
696
+ onValueChange: (nextValue) =>
697
+ setFieldValue(nextValue === '' ? null : Number.parseFloat(nextValue)),
698
+ type: 'number',
699
+ value: typeof value === 'number' && Number.isFinite(value) ? String(value) : '',
700
+ })
701
+ }
702
+
703
+ if (isInputFormFieldKind(field, 'numberRange', 'NumberRange')) {
704
+ const range = value && typeof value === 'object'
705
+ ? value as Record<string, unknown>
706
+ : {}
707
+ return componentSystem.forms.InputFormControlGroup({
708
+ className: designSystem.classNames.formSurface.rangeGroup({}),
709
+ field,
710
+ children: (
711
+ <>
712
+ {componentSystem.forms.TextInputControl({
713
+ 'aria-label': `${label} minimum`,
714
+ onValueChange: (nextValue) =>
715
+ setFieldValue({ ...range, minimum: readOptionalNumber(nextValue) }),
716
+ type: 'number',
717
+ value: typeof range.minimum === 'number' ? String(range.minimum) : '',
718
+ })}
719
+ {componentSystem.forms.TextInputControl({
720
+ 'aria-label': `${label} maximum`,
721
+ onValueChange: (nextValue) =>
722
+ setFieldValue({ ...range, maximum: readOptionalNumber(nextValue) }),
723
+ type: 'number',
724
+ value: typeof range.maximum === 'number' ? String(range.maximum) : '',
725
+ })}
726
+ </>
727
+ ),
728
+ })
729
+ }
730
+
731
+ if (isInputFormFieldKind(field, 'boolean', 'Boolean')) {
732
+ return componentSystem.forms.CheckboxControl({
733
+ 'aria-label': label,
734
+ checked: value === true,
735
+ onCheckedChange: setFieldValue,
736
+ })
737
+ }
738
+
739
+ return componentSystem.forms.TextInputControl({
740
+ 'aria-label': label,
741
+ onValueChange: setFieldValue,
742
+ placeholder: field.Placeholder ?? undefined,
743
+ value: typeof value === 'string' ? value : '',
744
+ })
745
+ }
746
+
747
+ function renderInputFormActionRow<TValue extends object>({
748
+ choiceValuesByFieldId,
749
+ componentSet,
750
+ componentSystem,
751
+ designSystem,
752
+ inputForm,
753
+ module,
754
+ placements,
755
+ runtime,
756
+ target,
757
+ }: {
758
+ readonly choiceValuesByFieldId: Readonly<Record<string, readonly string[]>>
759
+ readonly componentSet: string
760
+ readonly componentSystem: PresentationComponentSystem
761
+ readonly designSystem: PresentationDesignSystem
762
+ readonly inputForm: InputFormDefinition
763
+ readonly module: PresentationModuleDefinition
764
+ readonly placements: readonly ActionPlacementDefinition[]
765
+ readonly runtime: ProjectedInputFormRuntime<TValue>
766
+ readonly target: ProjectedInputFormTargetContext
767
+ }) {
768
+ if (placements.length === 0) {
769
+ return null
770
+ }
771
+
772
+ return componentSystem.forms.InputFormActionRow({
773
+ className: designSystem.classNames.formSurface.actionRow({}),
774
+ form: inputForm,
775
+ children: placements.map((placement) => (
776
+ <Fragment key={`${placement.Region}:${placement.ActionId}`}>
777
+ {renderInputFormAction({
778
+ choiceValuesByFieldId,
779
+ componentSet,
780
+ componentSystem,
781
+ inputForm,
782
+ module,
783
+ placement,
784
+ runtime,
785
+ target,
786
+ })}
787
+ </Fragment>
788
+ )),
789
+ })
790
+ }
791
+
792
+ function renderInputFormAction<TValue extends object>({
793
+ choiceValuesByFieldId,
794
+ componentSystem,
795
+ componentSet,
796
+ inputForm,
797
+ module,
798
+ placement,
799
+ runtime,
800
+ target,
801
+ }: {
802
+ readonly choiceValuesByFieldId: Readonly<Record<string, readonly string[]>>
803
+ readonly componentSet: string
804
+ readonly componentSystem: PresentationComponentSystem
805
+ readonly inputForm: InputFormDefinition
806
+ readonly module: PresentationModuleDefinition
807
+ readonly placement: ActionPlacementDefinition
808
+ readonly runtime: ProjectedInputFormRuntime<TValue>
809
+ readonly target: ProjectedInputFormTargetContext
810
+ }) {
811
+ const action = findPresentationAction(module, placement.ActionId)
812
+ const label = placement.Label ?? action?.Name ?? placement.ActionId
813
+ return componentSystem.forms.FormActionButton({
814
+ 'data-presentation-action-id': placement.ActionId,
815
+ 'data-presentation-view-id': target.queryForm?.Id ?? inputForm.Id,
816
+ onClick: () =>
817
+ runtime.invokeAction({
818
+ action,
819
+ choiceValuesByFieldId,
820
+ inputForm,
821
+ placement,
822
+ target,
823
+ value: runtime.value,
824
+ }),
825
+ size: 'sm',
826
+ type: 'button',
827
+ variant: placement.Intent === 'primary' ? 'default' : 'outline',
828
+ children: (
829
+ <>
830
+ {renderPresentationIcon({
831
+ componentSet,
832
+ icon: placement.Icon,
833
+ module,
834
+ subject: placement,
835
+ })}
836
+ {label}
837
+ </>
838
+ ),
839
+ })
840
+ }
841
+
842
+ function hasActionsChromeSlot(view: ViewDefinition) {
843
+ return view.Chrome?.Slots.some((slot) =>
844
+ isViewChromeSlotKind(slot, viewChromeSlotKinds.actions)) ?? false
845
+ }
846
+
847
+ function resolveInputFormActionIconPlacements(
848
+ inputForm: InputFormDefinition | null,
849
+ view: ViewDefinition,
850
+ ) {
851
+ if (!inputForm) {
852
+ return []
853
+ }
854
+
855
+ const actionSlots = view.Chrome?.Slots.filter((slot) =>
856
+ isViewChromeSlotKind(slot, viewChromeSlotKinds.actions)) ?? []
857
+ if (actionSlots.length === 0) {
858
+ return inputForm.Actions
859
+ }
860
+
861
+ return actionSlots.flatMap((slot) =>
862
+ slot.Actions.length > 0 ? slot.Actions : inputForm.Actions)
863
+ }
864
+
865
+ function createDefaultInputFormTarget(inputForm: InputFormDefinition): ProjectedInputFormTargetContext {
866
+ return {
867
+ queryDefinition: null,
868
+ queryForm: null,
869
+ stateId: inputForm.SharedStateId ?? inputForm.Target.Id ?? inputForm.StateDataSourceId,
870
+ }
871
+ }
872
+
873
+ function projectInputFormFields(
874
+ inputForm: InputFormDefinition,
875
+ query: DataSourceQueryDefinition | null,
876
+ ): ProjectedInputFormFieldProjection[] {
877
+ if (!query) {
878
+ return inputForm.Fields.map((field): ProjectedInputFormFieldProjection => ({
879
+ inputField: field,
880
+ queryField: null,
881
+ }))
882
+ }
883
+
884
+ const queryFieldsById = new Map(query.Fields.map((field) => [field.Id, field]))
885
+ const queryFieldsByFieldId = new Map(
886
+ query.Fields.flatMap((field) => field.FieldId ? [[field.FieldId, field]] : []),
887
+ )
888
+ const queryFieldsByValuePath = new Map(
889
+ query.Fields.flatMap((field) => field.ValuePath ? [[field.ValuePath, field]] : []),
890
+ )
891
+
892
+ return inputForm.Fields.map((field): ProjectedInputFormFieldProjection => {
893
+ const queryField =
894
+ queryFieldsById.get(field.Id) ??
895
+ queryFieldsByFieldId.get(field.FieldId) ??
896
+ queryFieldsByValuePath.get(field.ValuePath)
897
+ return {
898
+ inputField: field,
899
+ queryField: queryField ?? null,
900
+ }
901
+ })
902
+ }
903
+
904
+ function findInputFieldRenderer(
905
+ renderers: Readonly<Record<string, ProjectedInputFormFieldRenderer>> | undefined,
906
+ field: InputFormFieldDefinition,
907
+ queryField: DataSourceQueryFieldDefinition | null,
908
+ ) {
909
+ if (!renderers) {
910
+ return undefined
911
+ }
912
+
913
+ return (
914
+ (queryField?.Id ? renderers[queryField.Id] : undefined) ??
915
+ renderers[field.Id] ??
916
+ (queryField?.FieldId ? renderers[queryField.FieldId] : undefined) ??
917
+ renderers[field.FieldId] ??
918
+ (queryField?.ValuePath ? renderers[queryField.ValuePath] : undefined) ??
919
+ renderers[field.ValuePath]
920
+ )
921
+ }
922
+
923
+ function readInputFormChoices(
924
+ dataSourceResolver: PresentationDataSourceResolver,
925
+ field: InputFormFieldDefinition,
926
+ queryField: DataSourceQueryFieldDefinition | null,
927
+ presentationField: FieldPresentationDefinition | null,
928
+ ): readonly ProjectedInputFormChoice[] {
929
+ if (queryField?.ChoiceDataSourceId) {
930
+ const value = dataSourceResolver.readPath(
931
+ queryField.ChoiceDataSourceId,
932
+ queryField.ChoiceItemsPath,
933
+ )
934
+ return readChoiceItems(value, {
935
+ labelPath: 'Label',
936
+ presentationField,
937
+ tonePath: 'Tone',
938
+ valuePath: 'Id',
939
+ })
940
+ }
941
+
942
+ const source = field.ChoiceSource
943
+ if (!source) {
944
+ return []
945
+ }
946
+
947
+ const sourcePath = source.CollectionPath ?? source.FactsPath
948
+ const value = source.DataSourceId
949
+ ? dataSourceResolver.readPath(source.DataSourceId, sourcePath)
950
+ : []
951
+ if (!Array.isArray(value)) {
952
+ return []
953
+ }
954
+
955
+ return readChoiceItems(value, {
956
+ labelPath: source.LabelPath,
957
+ presentationField,
958
+ tonePath: source.TonePath,
959
+ valuePath: source.ValuePath,
960
+ })
961
+ }
962
+
963
+ function readChoiceItems(
964
+ value: unknown,
965
+ paths: {
966
+ readonly labelPath: string
967
+ readonly presentationField: FieldPresentationDefinition | null
968
+ readonly tonePath?: string | null
969
+ readonly valuePath: string
970
+ },
971
+ ): readonly ProjectedInputFormChoice[] {
972
+ if (!Array.isArray(value)) {
973
+ return []
974
+ }
975
+
976
+ return value.flatMap((item): ProjectedInputFormChoice[] => {
977
+ const choiceValue = readObjectPath(item, paths.valuePath)
978
+ if (choiceValue === null || choiceValue === undefined || choiceValue === '') {
979
+ return []
980
+ }
981
+
982
+ const label = readObjectPath(item, paths.labelPath)
983
+ const tone = paths.tonePath
984
+ ? readObjectPath(item, paths.tonePath)
985
+ : null
986
+ const resolvedLabel = label === null || label === undefined || label === ''
987
+ ? resolvePresentationFieldValueLabel(paths.presentationField, choiceValue)
988
+ : String(label)
989
+ return [{
990
+ label: resolvedLabel ?? String(choiceValue),
991
+ tone: tone === null || tone === undefined
992
+ ? resolvePresentationFieldValueTone(paths.presentationField, choiceValue)
993
+ : String(tone),
994
+ value: String(choiceValue),
995
+ }]
996
+ })
997
+ }
998
+
999
+ function readSelectedChoiceValues(
1000
+ field: InputFormFieldDefinition,
1001
+ queryField: DataSourceQueryFieldDefinition | null,
1002
+ choices: readonly ProjectedInputFormChoice[],
1003
+ value: unknown,
1004
+ ) {
1005
+ if (Array.isArray(value) && value.length > 0) {
1006
+ return value.map(String)
1007
+ }
1008
+
1009
+ if (isInputFormChoiceDefaultSelection(field, 'all', 'All')) {
1010
+ return choices.map((choice) => choice.value)
1011
+ }
1012
+
1013
+ if (isInputFormChoiceDefaultSelection(field, 'first', 'First')) {
1014
+ return choices[0] ? [choices[0].value] : []
1015
+ }
1016
+
1017
+ if (queryField?.ChoiceDataSourceId && isMultiSelectInputField(field, queryField)) {
1018
+ return choices.map((choice) => choice.value)
1019
+ }
1020
+
1021
+ return []
1022
+ }
1023
+
1024
+ function resolveChoiceToggleClassName({
1025
+ choice,
1026
+ choiceToggleClassName,
1027
+ choiceTone,
1028
+ field,
1029
+ inputForm,
1030
+ presentationField,
1031
+ queryField,
1032
+ queryForm,
1033
+ stateId,
1034
+ target,
1035
+ }: {
1036
+ readonly choice: ProjectedInputFormChoice
1037
+ readonly choiceToggleClassName?: ProjectedInputFormChoiceClassNameResolver
1038
+ readonly choiceTone?: ProjectedInputFormChoiceToneResolver
1039
+ readonly field: InputFormFieldDefinition
1040
+ readonly inputForm: InputFormDefinition
1041
+ readonly presentationField: FieldPresentationDefinition | null
1042
+ readonly queryField: DataSourceQueryFieldDefinition | null
1043
+ readonly queryForm: QueryFormDefinition | null
1044
+ readonly stateId: string
1045
+ readonly target: ProjectedInputFormTargetContext
1046
+ }) {
1047
+ const choiceContext = {
1048
+ choice,
1049
+ field,
1050
+ inputForm,
1051
+ presentationField,
1052
+ queryField,
1053
+ queryForm,
1054
+ stateId,
1055
+ target,
1056
+ } satisfies ProjectedInputFormChoiceRenderContext
1057
+ const tone =
1058
+ choiceTone?.(choiceContext) ??
1059
+ choice.tone ??
1060
+ resolvePresentationFieldValueTone(presentationField, choice.value) ??
1061
+ null
1062
+ return choiceToggleClassName?.({ ...choiceContext, tone }) ??
1063
+ getDefaultChoiceClassName(tone)
1064
+ }
1065
+
1066
+ function readFormValue(value: object, path: string) {
1067
+ return readObjectPath(value, path)
1068
+ }
1069
+
1070
+ function setFormValue<TValue extends object>(
1071
+ value: TValue,
1072
+ path: string,
1073
+ nextValue: unknown,
1074
+ ): TValue {
1075
+ const segments = path.split('.').filter(Boolean)
1076
+ if (segments.length === 0) {
1077
+ return value
1078
+ }
1079
+
1080
+ return setObjectPath(value, segments, nextValue) as TValue
1081
+ }
1082
+
1083
+ function setObjectPath(value: unknown, segments: readonly string[], nextValue: unknown): unknown {
1084
+ const [head, ...tail] = segments
1085
+ if (!head) {
1086
+ return nextValue
1087
+ }
1088
+
1089
+ const record = value && typeof value === 'object'
1090
+ ? value as Record<string, unknown>
1091
+ : {}
1092
+
1093
+ return {
1094
+ ...record,
1095
+ [head]: tail.length === 0
1096
+ ? nextValue
1097
+ : setObjectPath(record[head], tail, nextValue),
1098
+ }
1099
+ }
1100
+
1101
+ function isMultiSelectInputField(
1102
+ field: InputFormFieldDefinition,
1103
+ queryField: DataSourceQueryFieldDefinition | null,
1104
+ ) {
1105
+ return isInputFormFieldKind(field, 'multiSelect', 'MultiSelect') ||
1106
+ queryField?.Operators.some((operator) => operator.toLocaleLowerCase() === 'in') === true
1107
+ }
1108
+
1109
+ function isInputFormFieldKind(
1110
+ field: InputFormFieldDefinition,
1111
+ kind: keyof typeof inputFormFieldKinds,
1112
+ label: string,
1113
+ ) {
1114
+ return matchesPresentationEnum(
1115
+ field.Kind,
1116
+ createPresentationEnumDiscriminator(inputFormFieldKinds, kind, label),
1117
+ )
1118
+ }
1119
+
1120
+ function isInputFormChoiceDefaultSelection(
1121
+ field: InputFormFieldDefinition,
1122
+ selection: keyof typeof inputFormChoiceDefaultSelections,
1123
+ label: string,
1124
+ ) {
1125
+ const defaultSelection = field.ChoiceSource?.DefaultSelection
1126
+ return defaultSelection !== null &&
1127
+ defaultSelection !== undefined &&
1128
+ matchesPresentationEnum(
1129
+ defaultSelection,
1130
+ createPresentationEnumDiscriminator(inputFormChoiceDefaultSelections, selection, label),
1131
+ )
1132
+ }
1133
+
1134
+ function getDefaultChoiceClassName(tone: string | null) {
1135
+ if (tone === 'success') {
1136
+ return 'data-[state=off]:border-teal-700/15 data-[state=off]:bg-white data-[state=off]:text-slate-500 data-[state=on]:border-teal-700/15 data-[state=on]:bg-teal-50 data-[state=on]:text-teal-700'
1137
+ }
1138
+
1139
+ if (tone === 'danger') {
1140
+ return 'data-[state=off]:border-red-700/15 data-[state=off]:bg-white data-[state=off]:text-slate-500 data-[state=on]:border-red-700/15 data-[state=on]:bg-red-50 data-[state=on]:text-red-700'
1141
+ }
1142
+
1143
+ if (tone === 'warning') {
1144
+ return 'data-[state=off]:border-amber-700/15 data-[state=off]:bg-white data-[state=off]:text-slate-500 data-[state=on]:border-amber-700/15 data-[state=on]:bg-amber-50 data-[state=on]:text-amber-700'
1145
+ }
1146
+
1147
+ if (tone === 'accent') {
1148
+ return 'data-[state=off]:border-fuchsia-700/15 data-[state=off]:bg-white data-[state=off]:text-slate-500 data-[state=on]:border-fuchsia-700/15 data-[state=on]:bg-fuchsia-50 data-[state=on]:text-fuchsia-700'
1149
+ }
1150
+
1151
+ return 'data-[state=off]:border-sky-700/15 data-[state=off]:bg-white data-[state=off]:text-slate-500 data-[state=on]:border-sky-700/15 data-[state=on]:bg-sky-50 data-[state=on]:text-sky-700'
1152
+ }
1153
+
1154
+ function isDateTimeFilterControl(field: InputFormFieldDefinition) {
1155
+ const control = field.Display?.Control
1156
+ return control === inputFormFieldControlKinds.dateTimeFilter ||
1157
+ control?.toString().toLocaleLowerCase() === 'datetimefilter'
1158
+ }
1159
+
1160
+ function coerceDateTimeFilterValue(value: unknown): DateTimeFilterValue {
1161
+ const fallback = createDefaultDateTimeFilter()
1162
+ if (!value || typeof value !== 'object') {
1163
+ return fallback
1164
+ }
1165
+
1166
+ const record = value as Record<string, unknown>
1167
+ return normalizeDateTimeFilter({
1168
+ afterLocal: readOptionalString(record.afterLocal) || fallback.afterLocal,
1169
+ beforeLocal: readOptionalString(record.beforeLocal) || fallback.beforeLocal,
1170
+ mode: record.mode === 'range' ? 'range' : 'preset',
1171
+ preset: readOptionalString(record.preset) as DateTimeFilterValue['preset'],
1172
+ timezone: readOptionalString(record.timezone) || fallback.timezone,
1173
+ })
1174
+ }
1175
+
1176
+ function readOptionalString(value: unknown) {
1177
+ return typeof value === 'string' ? value : ''
1178
+ }
1179
+
1180
+ function readOptionalNumber(value: string) {
1181
+ if (!value) {
1182
+ return null
1183
+ }
1184
+
1185
+ const numberValue = Number(value)
1186
+ return Number.isFinite(numberValue) ? numberValue : null
1187
+ }
1188
+
1189
+ function isoToLocalDateTime(value: string) {
1190
+ if (!value) {
1191
+ return ''
1192
+ }
1193
+
1194
+ const parsed = new Date(value)
1195
+ if (Number.isNaN(parsed.getTime())) {
1196
+ return ''
1197
+ }
1198
+
1199
+ const year = parsed.getFullYear()
1200
+ const month = `${parsed.getMonth() + 1}`.padStart(2, '0')
1201
+ const day = `${parsed.getDate()}`.padStart(2, '0')
1202
+ const hours = `${parsed.getHours()}`.padStart(2, '0')
1203
+ const minutes = `${parsed.getMinutes()}`.padStart(2, '0')
1204
+ return `${year}-${month}-${day}T${hours}:${minutes}`
1205
+ }
1206
+
1207
+ function localDateTimeToIso(value: string) {
1208
+ if (!value) {
1209
+ return null
1210
+ }
1211
+
1212
+ const parsed = new Date(value)
1213
+ return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString()
1214
+ }
1215
+
1216
+ function cn(...values: readonly (false | null | string | undefined)[]) {
1217
+ return values.filter(Boolean).join(' ')
1218
+ }