@skopon-cool/form-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +82 -0
  2. package/dist/adapter/a2uiAdapter.d.ts +21 -0
  3. package/dist/adapter/a2uiAdapter.d.ts.map +1 -0
  4. package/dist/adapter/extractSurfaceValues.d.ts +8 -0
  5. package/dist/adapter/extractSurfaceValues.d.ts.map +1 -0
  6. package/dist/adapter/formFileAccept.d.ts +17 -0
  7. package/dist/adapter/formFileAccept.d.ts.map +1 -0
  8. package/dist/adapter/formFilePlaceholderIcon.d.ts +5 -0
  9. package/dist/adapter/formFilePlaceholderIcon.d.ts.map +1 -0
  10. package/dist/adapter/formMedia.d.ts +7 -0
  11. package/dist/adapter/formMedia.d.ts.map +1 -0
  12. package/dist/adapter/formSchema.d.ts +6 -0
  13. package/dist/adapter/formSchema.d.ts.map +1 -0
  14. package/dist/adapter/id.d.ts +4 -0
  15. package/dist/adapter/id.d.ts.map +1 -0
  16. package/dist/adapter/resolveSurface.d.ts +6 -0
  17. package/dist/adapter/resolveSurface.d.ts.map +1 -0
  18. package/dist/catalog/a2uiCustomCatalog.d.ts +10 -0
  19. package/dist/catalog/a2uiCustomCatalog.d.ts.map +1 -0
  20. package/dist/catalog/a2uiPreviewContext.d.ts +11 -0
  21. package/dist/catalog/a2uiPreviewContext.d.ts.map +1 -0
  22. package/dist/catalog/useSkoponBoundField.d.ts +10 -0
  23. package/dist/catalog/useSkoponBoundField.d.ts.map +1 -0
  24. package/dist/client/formClient.d.ts +22 -0
  25. package/dist/client/formClient.d.ts.map +1 -0
  26. package/dist/components/AskUserFormCard.d.ts +13 -0
  27. package/dist/components/AskUserFormCard.d.ts.map +1 -0
  28. package/dist/components/CurlSubmitBlock.d.ts +10 -0
  29. package/dist/components/CurlSubmitBlock.d.ts.map +1 -0
  30. package/dist/components/SkoponA2uiStreamRenderer.d.ts +11 -0
  31. package/dist/components/SkoponA2uiStreamRenderer.d.ts.map +1 -0
  32. package/dist/components/SkoponFormRenderer.d.ts +16 -0
  33. package/dist/components/SkoponFormRenderer.d.ts.map +1 -0
  34. package/dist/form-sdk.css +1 -0
  35. package/dist/icons/FilePlaceholderIcon.d.ts +10 -0
  36. package/dist/icons/FilePlaceholderIcon.d.ts.map +1 -0
  37. package/dist/index.d.ts +20 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +1332 -0
  40. package/dist/submit/buildCurlStatement.d.ts +2 -0
  41. package/dist/submit/buildCurlStatement.d.ts.map +1 -0
  42. package/dist/submit/intersectPayloadWithForm.d.ts +17 -0
  43. package/dist/submit/intersectPayloadWithForm.d.ts.map +1 -0
  44. package/dist/submit/submitFormJson.d.ts +12 -0
  45. package/dist/submit/submitFormJson.d.ts.map +1 -0
  46. package/dist/types/index.d.ts +76 -0
  47. package/dist/types/index.d.ts.map +1 -0
  48. package/package.json +53 -0
  49. package/src/adapter/a2uiAdapter.test.ts +150 -0
  50. package/src/adapter/a2uiAdapter.ts +490 -0
  51. package/src/adapter/extractSurfaceValues.ts +25 -0
  52. package/src/adapter/formFileAccept.ts +198 -0
  53. package/src/adapter/formFilePlaceholderIcon.ts +33 -0
  54. package/src/adapter/formMedia.ts +50 -0
  55. package/src/adapter/formSchema.ts +139 -0
  56. package/src/adapter/id.ts +24 -0
  57. package/src/adapter/resolveSurface.ts +66 -0
  58. package/src/catalog/a2uiCustomCatalog.tsx +548 -0
  59. package/src/catalog/a2uiPreviewContext.tsx +26 -0
  60. package/src/catalog/useSkoponBoundField.ts +57 -0
  61. package/src/client/formClient.ts +72 -0
  62. package/src/components/AskUserFormCard.tsx +155 -0
  63. package/src/components/CurlSubmitBlock.tsx +60 -0
  64. package/src/components/SkoponA2uiStreamRenderer.tsx +70 -0
  65. package/src/components/SkoponFormRenderer.tsx +100 -0
  66. package/src/icons/FilePlaceholderIcon.tsx +40 -0
  67. package/src/index.ts +67 -0
  68. package/src/styles/a2ui-preview.css +345 -0
  69. package/src/styles/index.css +190 -0
  70. package/src/submit/buildCurlStatement.ts +13 -0
  71. package/src/submit/intersectPayloadWithForm.ts +54 -0
  72. package/src/submit/submit.test.ts +63 -0
  73. package/src/submit/submitFormJson.ts +50 -0
  74. package/src/types/index.ts +139 -0
@@ -0,0 +1,345 @@
1
+ /**
2
+ * A2UI v0.9 预览样式(对齐 design/ui/UI规范.md + form-block-preview)
3
+ * 预构建 @a2ui/react 的 CSS Modules 类名为空,用结构选择器补全外观。
4
+ */
5
+ .a2ui-surface.a2ui-container {
6
+ --a2ui-color-border: var(--color-border);
7
+ --a2ui-color-border-hover: var(--color-border-strong);
8
+ --a2ui-color-input: var(--color-surface);
9
+ --a2ui-color-on-input: var(--color-text);
10
+ --a2ui-color-on-background: var(--color-text);
11
+ --a2ui-color-on-surface: var(--color-text);
12
+ --a2ui-color-primary: var(--color-primary);
13
+ --a2ui-color-primary-hover: var(--color-primary-hover);
14
+ --a2ui-color-on-primary: var(--color-on-primary);
15
+ --a2ui-color-surface: var(--color-surface);
16
+ --a2ui-color-secondary-hover: var(--color-bg-subtle);
17
+ --a2ui-border-radius: var(--radius-md);
18
+ --a2ui-border-width: 1px;
19
+ --a2ui-column-gap: var(--space-form-item);
20
+ --a2ui-row-gap: var(--space-3);
21
+ --a2ui-spacing-xs: var(--space-1);
22
+ --a2ui-spacing-s: var(--space-2);
23
+ --a2ui-spacing-m: var(--space-3);
24
+ --a2ui-spacing-l: var(--space-4);
25
+ --a2ui-font-size-xs: var(--font-size-xs);
26
+ --a2ui-font-size-s: var(--font-size-sm);
27
+ --a2ui-font-size-m: var(--font-size-base);
28
+ --a2ui-font-size-l: var(--font-size-lg);
29
+ --a2ui-font-size-xl: var(--font-size-xl);
30
+ --a2ui-font-size-2xl: var(--font-size-2xl);
31
+ --a2ui-line-height-headings: var(--line-height-tight);
32
+ --a2ui-line-height-body: var(--line-height-base);
33
+ width: 100%;
34
+ color: var(--color-text);
35
+ font-family: var(--font-sans);
36
+ font-size: var(--font-size-base);
37
+ line-height: var(--line-height-base);
38
+ }
39
+
40
+ /* 标题 / 说明块 */
41
+ .a2ui-surface.a2ui-container h1,
42
+ .a2ui-surface.a2ui-container h2,
43
+ .a2ui-surface.a2ui-container h3,
44
+ .a2ui-surface.a2ui-container h4,
45
+ .a2ui-surface.a2ui-container h5 {
46
+ margin: 0 0 var(--space-4);
47
+ font-family: var(--font-sans);
48
+ font-weight: var(--font-weight-semibold);
49
+ line-height: var(--line-height-tight);
50
+ color: var(--color-text);
51
+ }
52
+
53
+ .a2ui-surface.a2ui-container h1 {
54
+ font-size: var(--font-size-2xl);
55
+ }
56
+
57
+ .a2ui-surface.a2ui-container h2 {
58
+ font-size: var(--font-size-xl);
59
+ }
60
+
61
+ .a2ui-surface.a2ui-container h3 {
62
+ font-size: var(--font-size-lg);
63
+ }
64
+
65
+ .a2ui-surface.a2ui-container h4 {
66
+ font-size: var(--font-size-base);
67
+ }
68
+
69
+ .a2ui-surface.a2ui-container p {
70
+ margin: 0 0 var(--space-4);
71
+ font-size: var(--font-size-base);
72
+ line-height: var(--line-height-relaxed);
73
+ color: var(--color-text-secondary);
74
+ }
75
+
76
+ /* TextField / DateTime:label 在 input 或 textarea 上方 */
77
+ .a2ui-surface.a2ui-container label:not(:has(input)):has(
78
+ + input:is(
79
+ [type='text'],
80
+ [type='number'],
81
+ [type='email'],
82
+ [type='tel'],
83
+ [type='url'],
84
+ [type='password'],
85
+ [type='date'],
86
+ [type='time'],
87
+ [type='datetime-local']
88
+ )
89
+ ),
90
+ .a2ui-surface.a2ui-container label:not(:has(input)):has(+ textarea) {
91
+ display: block;
92
+ margin: 0 0 var(--space-2);
93
+ font-size: var(--font-size-lg);
94
+ font-weight: var(--font-weight-medium);
95
+ line-height: var(--line-height-base);
96
+ color: var(--color-text);
97
+ }
98
+
99
+ /* ChoicePicker 分组标题 */
100
+ .a2ui-surface.a2ui-container strong {
101
+ display: block;
102
+ margin: 0 0 var(--space-2);
103
+ font-size: var(--font-size-lg);
104
+ font-weight: var(--font-weight-medium);
105
+ line-height: var(--line-height-base);
106
+ color: var(--color-text);
107
+ }
108
+
109
+ /* CheckBox 组件:input 与 label 为兄弟节点 */
110
+ .a2ui-surface.a2ui-container div:has(> input[type='checkbox']:first-child + label) {
111
+ display: flex;
112
+ align-items: center;
113
+ gap: var(--space-2);
114
+ }
115
+
116
+ .a2ui-surface.a2ui-container input[type='checkbox']:not(label input) + label,
117
+ .a2ui-surface.a2ui-container input[type='radio']:not(label input) + label {
118
+ display: inline;
119
+ margin: 0;
120
+ font-size: var(--font-size-base);
121
+ font-weight: var(--font-weight-normal);
122
+ line-height: var(--line-height-base);
123
+ color: var(--color-text);
124
+ cursor: pointer;
125
+ }
126
+
127
+ /* ChoicePicker 选项列表 */
128
+ .a2ui-surface.a2ui-container div:has(> label > input[type='checkbox']),
129
+ .a2ui-surface.a2ui-container div:has(> label > input[type='radio']) {
130
+ display: flex;
131
+ flex-direction: column;
132
+ align-items: flex-start;
133
+ gap: var(--space-2);
134
+ width: 100%;
135
+ }
136
+
137
+ /* ChoicePicker 单行选项 */
138
+ .a2ui-surface.a2ui-container label:has(> input[type='checkbox']),
139
+ .a2ui-surface.a2ui-container label:has(> input[type='radio']) {
140
+ display: inline-flex;
141
+ align-items: center;
142
+ gap: var(--space-2);
143
+ margin: 0;
144
+ width: auto;
145
+ max-width: 100%;
146
+ font-size: var(--font-size-base);
147
+ font-weight: var(--font-weight-normal);
148
+ line-height: var(--line-height-base);
149
+ color: var(--color-text);
150
+ cursor: pointer;
151
+ }
152
+
153
+ .a2ui-surface.a2ui-container label:has(> input[type='checkbox']) > span,
154
+ .a2ui-surface.a2ui-container label:has(> input[type='radio']) > span {
155
+ font-size: var(--font-size-base);
156
+ line-height: var(--line-height-base);
157
+ color: var(--color-text);
158
+ }
159
+
160
+ /* 文本输入 */
161
+ .a2ui-surface.a2ui-container input[type='text'],
162
+ .a2ui-surface.a2ui-container input[type='number'],
163
+ .a2ui-surface.a2ui-container input[type='email'],
164
+ .a2ui-surface.a2ui-container input[type='tel'],
165
+ .a2ui-surface.a2ui-container input[type='url'],
166
+ .a2ui-surface.a2ui-container input[type='password'],
167
+ .a2ui-surface.a2ui-container input[type='date'],
168
+ .a2ui-surface.a2ui-container input[type='time'],
169
+ .a2ui-surface.a2ui-container input[type='datetime-local'],
170
+ .a2ui-surface.a2ui-container textarea {
171
+ display: block;
172
+ width: 100%;
173
+ min-height: var(--button-height-md);
174
+ box-sizing: border-box;
175
+ padding: var(--space-2) var(--space-3);
176
+ font-family: var(--font-sans);
177
+ font-size: var(--font-size-base);
178
+ line-height: var(--line-height-base);
179
+ color: var(--color-text);
180
+ background-color: var(--color-surface);
181
+ border: 1px solid var(--color-border);
182
+ border-radius: var(--radius-md);
183
+ transition:
184
+ border-color var(--duration-fast) var(--ease-default),
185
+ box-shadow var(--duration-fast) var(--ease-default);
186
+ }
187
+
188
+ .a2ui-surface.a2ui-container textarea {
189
+ min-height: calc(var(--button-height-md) * 2);
190
+ padding-top: var(--space-2);
191
+ padding-bottom: var(--space-2);
192
+ resize: vertical;
193
+ }
194
+
195
+ .a2ui-surface.a2ui-container input[type='text']:hover,
196
+ .a2ui-surface.a2ui-container input[type='number']:hover,
197
+ .a2ui-surface.a2ui-container input[type='email']:hover,
198
+ .a2ui-surface.a2ui-container input[type='tel']:hover,
199
+ .a2ui-surface.a2ui-container input[type='url']:hover,
200
+ .a2ui-surface.a2ui-container input[type='password']:hover,
201
+ .a2ui-surface.a2ui-container input[type='date']:hover,
202
+ .a2ui-surface.a2ui-container input[type='time']:hover,
203
+ .a2ui-surface.a2ui-container input[type='datetime-local']:hover,
204
+ .a2ui-surface.a2ui-container textarea:hover {
205
+ border-color: var(--color-border-strong);
206
+ }
207
+
208
+ .a2ui-surface.a2ui-container input[type='text']:focus,
209
+ .a2ui-surface.a2ui-container input[type='number']:focus,
210
+ .a2ui-surface.a2ui-container input[type='email']:focus,
211
+ .a2ui-surface.a2ui-container input[type='tel']:focus,
212
+ .a2ui-surface.a2ui-container input[type='url']:focus,
213
+ .a2ui-surface.a2ui-container input[type='password']:focus,
214
+ .a2ui-surface.a2ui-container input[type='date']:focus,
215
+ .a2ui-surface.a2ui-container input[type='time']:focus,
216
+ .a2ui-surface.a2ui-container input[type='datetime-local']:focus,
217
+ .a2ui-surface.a2ui-container textarea:focus {
218
+ outline: none;
219
+ border-color: var(--color-primary);
220
+ box-shadow: var(--shadow-focus);
221
+ }
222
+
223
+ .a2ui-surface.a2ui-container input[type='checkbox']:not(.ant-checkbox-input),
224
+ .a2ui-surface.a2ui-container input[type='radio']:not(.ant-radio-input) {
225
+ width: 16px;
226
+ height: 16px;
227
+ min-height: 0;
228
+ min-width: 16px;
229
+ margin: 0;
230
+ padding: 0;
231
+ flex-shrink: 0;
232
+ border: none;
233
+ border-radius: var(--radius-sm);
234
+ background: transparent;
235
+ box-shadow: none;
236
+ accent-color: var(--color-primary);
237
+ cursor: pointer;
238
+ vertical-align: middle;
239
+ }
240
+
241
+ .a2ui-surface.a2ui-container input[type='checkbox']:not(.ant-checkbox-input):not(label input),
242
+ .a2ui-surface.a2ui-container input[type='radio']:not(.ant-radio-input):not(label input) {
243
+ border: 1px solid var(--color-border);
244
+ background: var(--color-surface);
245
+ }
246
+
247
+ .a2ui-surface.a2ui-container input[type='radio']:not(.ant-radio-input):not(label input) {
248
+ border-radius: 50%;
249
+ }
250
+
251
+ .a2ui-surface.a2ui-container input[type='checkbox']:not(.ant-checkbox-input):focus,
252
+ .a2ui-surface.a2ui-container input[type='radio']:not(.ant-radio-input):focus {
253
+ outline: none;
254
+ box-shadow: var(--shadow-focus);
255
+ }
256
+
257
+ /* 原生 A2UI 按钮/chip;勿作用于 Ant Design(Switch、Select、Picker 等) */
258
+ .a2ui-surface.a2ui-container button:not([class*='ant-']) {
259
+ min-height: var(--button-height-md);
260
+ padding: 0 var(--button-padding-x-md);
261
+ font-family: var(--font-sans);
262
+ font-size: var(--button-font-size-md);
263
+ font-weight: var(--button-font-weight);
264
+ line-height: var(--line-height-base);
265
+ color: var(--color-text);
266
+ background: var(--color-surface);
267
+ border: 1px solid var(--color-border);
268
+ border-radius: var(--radius-pill);
269
+ cursor: pointer;
270
+ transition:
271
+ background-color var(--duration-fast) var(--ease-default),
272
+ border-color var(--duration-fast) var(--ease-default),
273
+ color var(--duration-fast) var(--ease-default);
274
+ }
275
+
276
+ .a2ui-surface.a2ui-container button:not([class*='ant-']):hover:not(:disabled) {
277
+ border-color: var(--color-border-strong);
278
+ background: var(--color-bg-subtle);
279
+ }
280
+
281
+ .a2ui-surface.a2ui-container button:not([class*='ant-']):disabled {
282
+ cursor: not-allowed;
283
+ opacity: 0.45;
284
+ }
285
+
286
+ .a2ui-surface.a2ui-container button.chip,
287
+ .a2ui-surface.a2ui-container button.chip.selected,
288
+ .a2ui-surface.a2ui-container button.selected {
289
+ min-height: var(--button-height-sm);
290
+ padding: 0 var(--button-padding-x-sm);
291
+ font-size: var(--button-font-size-sm);
292
+ }
293
+
294
+ .a2ui-surface.a2ui-container button.chip.selected,
295
+ .a2ui-surface.a2ui-container button.selected {
296
+ color: var(--color-on-primary);
297
+ background-color: var(--color-primary-solid);
298
+ border-color: var(--color-primary-solid);
299
+ }
300
+
301
+ .a2ui-surface.a2ui-container button.chip.selected:hover,
302
+ .a2ui-surface.a2ui-container button.selected:hover {
303
+ background-color: var(--color-primary-hover);
304
+ border-color: var(--color-primary-hover);
305
+ }
306
+
307
+ .a2ui-surface.a2ui-container span[style*='color: red'],
308
+ .a2ui-surface.a2ui-container span[style*='color:red'] {
309
+ display: block;
310
+ margin-top: var(--space-1);
311
+ font-size: var(--font-size-xs);
312
+ line-height: var(--line-height-relaxed);
313
+ color: var(--color-danger-text) !important;
314
+ }
315
+
316
+ .a2ui-surface.a2ui-container img:not(.form-media-item) {
317
+ max-width: 100%;
318
+ height: auto;
319
+ border-radius: var(--radius-md);
320
+ }
321
+
322
+ /* skopon catalog 预览块:与编辑区 form-block-preview 对齐 */
323
+ .a2ui-surface.a2ui-container .form-block-preview {
324
+ width: 100%;
325
+ }
326
+
327
+ .a2ui-surface.a2ui-container .form-block-preview + .form-block-preview,
328
+ .a2ui-surface.a2ui-container .form-block-preview-label + .form-block-preview-control,
329
+ .a2ui-surface.a2ui-container .form-block-preview-label + .form-media-preview,
330
+ .a2ui-surface.a2ui-container .form-block-preview-label + .form-media-list,
331
+ .a2ui-surface.a2ui-container .form-block-preview-label + .form-file-upload-preview {
332
+ margin-top: var(--space-2);
333
+ }
334
+
335
+ .a2ui-surface.a2ui-container .form-block-preview .ant-upload-drag {
336
+ border-radius: var(--radius-md);
337
+ }
338
+
339
+ .a2ui-surface.a2ui-container .form-block-preview-control.ant-picker {
340
+ width: 100%;
341
+ }
342
+
343
+ .a2ui-surface.a2ui-container .form-block-preview-control.ant-switch {
344
+ margin-top: var(--space-1);
345
+ }
@@ -0,0 +1,190 @@
1
+ @import './a2ui-preview.css';
2
+
3
+ /* form-block-preview(与 admin form-tally 预览对齐的子集) */
4
+ .form-block-preview {
5
+ padding: var(--space-1) 0 var(--space-2);
6
+ }
7
+
8
+ .form-block-preview-label {
9
+ font-size: var(--font-size-lg);
10
+ font-weight: var(--font-weight-medium);
11
+ margin-bottom: var(--space-2);
12
+ color: var(--color-text);
13
+ }
14
+
15
+ .form-block-preview-control {
16
+ margin-top: var(--space-1);
17
+ }
18
+
19
+ .form-block-preview-help {
20
+ display: block;
21
+ margin-top: var(--space-2);
22
+ font-size: var(--font-size-xs);
23
+ color: var(--color-text-muted);
24
+ }
25
+
26
+ .form-media-preview {
27
+ max-width: 100%;
28
+ border-radius: var(--radius-md);
29
+ overflow: hidden;
30
+ }
31
+
32
+ .form-media-list {
33
+ display: flex;
34
+ flex-direction: row;
35
+ flex-wrap: wrap;
36
+ align-items: center;
37
+ gap: var(--space-2);
38
+ max-width: 100%;
39
+ }
40
+
41
+ .form-media-item {
42
+ display: block;
43
+ flex-shrink: 0;
44
+ object-fit: cover;
45
+ border-radius: var(--radius-md);
46
+ }
47
+
48
+ .form-media-list.form-media-size-huge .form-media-item {
49
+ width: 480px;
50
+ height: 480px;
51
+ }
52
+
53
+ .form-media-list.form-media-size-huge .form-media-item.form-media-item--audio {
54
+ width: 480px;
55
+ height: 48px;
56
+ }
57
+
58
+ .form-media-list.form-media-size-large .form-media-item {
59
+ width: 320px;
60
+ height: 320px;
61
+ }
62
+
63
+ .form-media-list.form-media-size-large .form-media-item.form-media-item--audio {
64
+ width: 320px;
65
+ height: 48px;
66
+ }
67
+
68
+ .form-media-list.form-media-size-medium .form-media-item {
69
+ width: 160px;
70
+ height: 160px;
71
+ }
72
+
73
+ .form-media-list.form-media-size-medium .form-media-item.form-media-item--audio {
74
+ width: 160px;
75
+ height: 48px;
76
+ }
77
+
78
+ .form-media-list.form-media-size-small .form-media-item {
79
+ width: 80px;
80
+ height: 80px;
81
+ }
82
+
83
+ .form-media-list.form-media-size-small .form-media-item.form-media-item--audio {
84
+ width: 80px;
85
+ height: 48px;
86
+ }
87
+
88
+ .form-media-list.form-media-size-icon .form-media-item {
89
+ width: 48px;
90
+ height: 48px;
91
+ }
92
+
93
+ .form-media-preview--empty {
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: center;
97
+ min-height: 120px;
98
+ padding: var(--space-4);
99
+ border: 1px dashed var(--color-border);
100
+ border-radius: var(--radius-md);
101
+ background: var(--color-bg-subtle);
102
+ color: var(--color-text-muted);
103
+ font-size: var(--font-size-sm);
104
+ text-align: center;
105
+ }
106
+
107
+ .form-file-upload-preview {
108
+ margin-top: var(--space-1);
109
+ }
110
+
111
+ .form-file-upload-preview .ant-upload-drag {
112
+ border-color: var(--color-border);
113
+ background: var(--color-bg-subtle);
114
+ }
115
+
116
+ .form-file-upload-preview-icon {
117
+ margin: 0 0 var(--space-2);
118
+ color: var(--color-primary);
119
+ }
120
+
121
+ .form-file-upload-preview-text {
122
+ margin: 0;
123
+ font-size: var(--font-size-sm);
124
+ color: var(--color-text-secondary);
125
+ }
126
+
127
+ /* Ask User 卡片 */
128
+ .ask-user-form-card {
129
+ display: flex;
130
+ flex-direction: column;
131
+ gap: var(--space-3);
132
+ min-width: 0;
133
+ }
134
+
135
+ .ask-user-form-actions {
136
+ display: flex;
137
+ justify-content: flex-end;
138
+ }
139
+
140
+ .ask-user-curl-card {
141
+ display: flex;
142
+ flex-direction: column;
143
+ gap: var(--space-1);
144
+ min-width: 0;
145
+ padding: var(--space-2);
146
+ border: 1px solid var(--color-border-subtle);
147
+ border-radius: var(--radius-md);
148
+ background: var(--color-bg-subtle, rgba(0, 0, 0, 0.03));
149
+ }
150
+
151
+ .ask-user-curl-card-header {
152
+ display: flex;
153
+ align-items: center;
154
+ justify-content: space-between;
155
+ gap: var(--space-2);
156
+ }
157
+
158
+ .ask-user-curl-card-header-title {
159
+ display: flex;
160
+ align-items: center;
161
+ gap: var(--space-2);
162
+ min-width: 0;
163
+ flex: 1 1 auto;
164
+ }
165
+
166
+ .ask-user-curl-unpublished-tag {
167
+ margin: 0;
168
+ flex-shrink: 0;
169
+ color: var(--color-warning, #d48806);
170
+ background: rgba(212, 136, 6, 0.12);
171
+ border-color: transparent;
172
+ }
173
+
174
+ .ask-user-curl-incomplete-tag {
175
+ margin: 0;
176
+ flex-shrink: 0;
177
+ color: var(--color-primary);
178
+ background: var(--color-primary-subtle);
179
+ border-color: transparent;
180
+ }
181
+
182
+ .skopon-form-curl-json,
183
+ .flow-artifact-generate-chat-json {
184
+ margin: 0;
185
+ padding: var(--space-2);
186
+ font-size: 12px;
187
+ line-height: 1.5;
188
+ white-space: pre-wrap;
189
+ word-break: break-all;
190
+ }
@@ -0,0 +1,13 @@
1
+ function escapeSingleQuotes(text: string): string {
2
+ return text.replace(/'/g, `'\\''`)
3
+ }
4
+
5
+ export function buildCurlStatement(payload: unknown, callbackUrl?: string | null): string {
6
+ const url = (callbackUrl ?? '').trim() || '<callback_url>'
7
+ const body = JSON.stringify(payload ?? {}, null, 2)
8
+ return [
9
+ `curl -X POST '${url}' \\`,
10
+ ` -H 'Content-Type: application/json' \\`,
11
+ ` -d '${escapeSingleQuotes(body)}'`,
12
+ ].join('\n')
13
+ }
@@ -0,0 +1,54 @@
1
+ import type { FormBlock, FormSchema } from '../types/index'
2
+ import { isInputBlockType } from '../types/index'
3
+
4
+ /** payload 单个字段的元数据约定(见 docs/ask/ payload.json) */
5
+ export interface AskUserPayloadField {
6
+ 用途描述?: string
7
+ 字段类型?: string
8
+ 字段格式?: string
9
+ 字段枚举?: string
10
+ [key: string]: unknown
11
+ }
12
+
13
+ export type AskUserPayload = Record<string, AskUserPayloadField | unknown>
14
+
15
+ export interface PayloadFormIntersection {
16
+ matchedBlocks: FormBlock[]
17
+ remainderPayload: AskUserPayload
18
+ }
19
+
20
+ export function getPayloadKeys(payload: unknown): string[] {
21
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return []
22
+ return Object.keys(payload as Record<string, unknown>)
23
+ }
24
+
25
+ export function intersectPayloadWithForm(
26
+ payload: unknown,
27
+ formDefinition: FormSchema | undefined,
28
+ ): PayloadFormIntersection {
29
+ const keys = getPayloadKeys(payload)
30
+ const source = (payload ?? {}) as Record<string, unknown>
31
+
32
+ const blocks = formDefinition?.blocks ?? []
33
+ const inputBlockNames = new Set(
34
+ blocks
35
+ .filter((block) => isInputBlockType(block.type) && block.name?.trim())
36
+ .map((block) => block.name!.trim()),
37
+ )
38
+
39
+ const matchedBlocks = blocks.filter(
40
+ (block) =>
41
+ isInputBlockType(block.type) &&
42
+ block.name?.trim() &&
43
+ keys.includes(block.name.trim()),
44
+ )
45
+
46
+ const remainderPayload: AskUserPayload = {}
47
+ for (const key of keys) {
48
+ if (!inputBlockNames.has(key)) {
49
+ remainderPayload[key] = source[key]
50
+ }
51
+ }
52
+
53
+ return { matchedBlocks, remainderPayload }
54
+ }
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { buildCurlStatement } from '../submit/buildCurlStatement'
3
+ import { intersectPayloadWithForm } from '../submit/intersectPayloadWithForm'
4
+ import { submitFormJson } from '../submit/submitFormJson'
5
+ import type { FormSchema } from '../types/index'
6
+
7
+ describe('buildCurlStatement', () => {
8
+ it('builds POST curl with escaped single quotes', () => {
9
+ const curl = buildCurlStatement({ name: "O'Brien" }, 'https://example.com/hook')
10
+ expect(curl).toContain("curl -X POST 'https://example.com/hook'")
11
+ expect(curl).toContain("O'\\''Brien")
12
+ })
13
+
14
+ it('uses placeholder when callback url missing', () => {
15
+ expect(buildCurlStatement({ a: 1 }, null)).toContain('<callback_url>')
16
+ })
17
+ })
18
+
19
+ describe('intersectPayloadWithForm', () => {
20
+ const formDef: FormSchema = {
21
+ title: 't',
22
+ description: '',
23
+ blocks: [
24
+ { id: '1', type: 'text', name: 'video_title', label: 'Title' },
25
+ { id: '2', type: 'text', name: 'other', label: 'Other' },
26
+ ],
27
+ jsonSchema: {},
28
+ }
29
+
30
+ it('matches payload keys to form blocks', () => {
31
+ const result = intersectPayloadWithForm(
32
+ { video_title: { type: 'text' }, extra: { type: 'text' } },
33
+ formDef,
34
+ )
35
+ expect(result.matchedBlocks.map((b) => b.name)).toEqual(['video_title'])
36
+ expect(Object.keys(result.remainderPayload)).toEqual(['extra'])
37
+ })
38
+ })
39
+
40
+ describe('submitFormJson', () => {
41
+ it('POSTs JSON payload', async () => {
42
+ const fetchMock = vi.fn(async () => ({
43
+ ok: true,
44
+ status: 200,
45
+ headers: { get: () => 'application/json' },
46
+ json: async () => ({ ok: true }),
47
+ })) as unknown as typeof fetch
48
+
49
+ const result = await submitFormJson(
50
+ 'https://example.com/hook',
51
+ { a: 1 },
52
+ { fetch: fetchMock },
53
+ )
54
+ expect(result.ok).toBe(true)
55
+ expect(fetchMock).toHaveBeenCalledWith(
56
+ 'https://example.com/hook',
57
+ expect.objectContaining({
58
+ method: 'POST',
59
+ body: JSON.stringify({ a: 1 }),
60
+ }),
61
+ )
62
+ })
63
+ })