@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,2 @@
1
+ export declare function buildCurlStatement(payload: unknown, callbackUrl?: string | null): string;
2
+ //# sourceMappingURL=buildCurlStatement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildCurlStatement.d.ts","sourceRoot":"","sources":["../../src/submit/buildCurlStatement.ts"],"names":[],"mappings":"AAIA,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAQxF"}
@@ -0,0 +1,17 @@
1
+ import type { FormBlock, FormSchema } from '../types/index';
2
+ /** payload 单个字段的元数据约定(见 docs/ask/ payload.json) */
3
+ export interface AskUserPayloadField {
4
+ 用途描述?: string;
5
+ 字段类型?: string;
6
+ 字段格式?: string;
7
+ 字段枚举?: string;
8
+ [key: string]: unknown;
9
+ }
10
+ export type AskUserPayload = Record<string, AskUserPayloadField | unknown>;
11
+ export interface PayloadFormIntersection {
12
+ matchedBlocks: FormBlock[];
13
+ remainderPayload: AskUserPayload;
14
+ }
15
+ export declare function getPayloadKeys(payload: unknown): string[];
16
+ export declare function intersectPayloadWithForm(payload: unknown, formDefinition: FormSchema | undefined): PayloadFormIntersection;
17
+ //# sourceMappingURL=intersectPayloadWithForm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intersectPayloadWithForm.d.ts","sourceRoot":"","sources":["../../src/submit/intersectPayloadWithForm.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAG3D,mDAAmD;AACnD,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,CAAA;AAE1E,MAAM,WAAW,uBAAuB;IACtC,aAAa,EAAE,SAAS,EAAE,CAAA;IAC1B,gBAAgB,EAAE,cAAc,CAAA;CACjC;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,CAGzD;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,UAAU,GAAG,SAAS,GACrC,uBAAuB,CA0BzB"}
@@ -0,0 +1,12 @@
1
+ export interface SubmitFormJsonOptions {
2
+ fetch?: typeof fetch;
3
+ headers?: Record<string, string>;
4
+ }
5
+ export interface SubmitFormJsonResult {
6
+ ok: boolean;
7
+ status: number;
8
+ body?: unknown;
9
+ }
10
+ export declare function submitFormJson(callbackUrl: string, payload: unknown, options?: SubmitFormJsonOptions): Promise<SubmitFormJsonResult>;
11
+ export declare function copyTextToClipboard(text: string): Promise<void>;
12
+ //# sourceMappingURL=submitFormJson.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submitFormJson.d.ts","sourceRoot":"","sources":["../../src/submit/submitFormJson.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACpC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,wBAAsB,cAAc,CAClC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,oBAAoB,CAAC,CA8B/B;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAErE"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * A2UI v0.9 协议子集类型(对齐 @a2ui/react basicCatalog + skopon 自定义 catalog)。
3
+ */
4
+ export type A2uiBinding<T> = T | {
5
+ path: string;
6
+ };
7
+ export type A2uiComponentName = 'Column' | 'Row' | 'Text' | 'Image' | 'Video' | 'AudioPlayer' | 'TextField' | 'CheckBox' | 'ChoicePicker' | 'DateTimeInput' | 'FileUpload' | 'SkoponMedia' | 'SkoponSelect';
8
+ export interface A2uiComponentNode {
9
+ id: string;
10
+ component: A2uiComponentName | string;
11
+ children?: string[];
12
+ [prop: string]: unknown;
13
+ }
14
+ export interface A2uiSurfaceDoc {
15
+ root: string;
16
+ components: A2uiComponentNode[];
17
+ dataModel?: Record<string, unknown>;
18
+ surfaceProperties?: Record<string, unknown> & {
19
+ styleId?: string | null;
20
+ };
21
+ }
22
+ export declare const A2UI_PROTOCOL_VERSION: "v0.9";
23
+ export type FormJsonSchema = Record<string, unknown>;
24
+ export type FormBlockType = 'heading' | 'paragraph' | 'text' | 'textarea' | 'email' | 'number' | 'select' | 'multiselect' | 'radio' | 'checkbox' | 'toggle' | 'tel' | 'url' | 'datetime' | 'time' | 'file' | 'image' | 'video' | 'audio';
25
+ export type FormMediaSize = 'huge' | 'large' | 'medium' | 'small' | 'icon';
26
+ export type FormFilePlaceholderIcon = 'video' | 'audio' | 'image' | 'file' | 'spreadsheet' | 'document';
27
+ export declare const FORM_MEDIA_SIZES: FormMediaSize[];
28
+ export interface FormBlockOption {
29
+ value: string;
30
+ label: string;
31
+ }
32
+ export interface FormBlock {
33
+ id: string;
34
+ type: FormBlockType;
35
+ name?: string;
36
+ label?: string;
37
+ placeholder?: string;
38
+ help?: string;
39
+ required?: boolean;
40
+ options?: FormBlockOption[];
41
+ mediaUrl?: string;
42
+ mediaUrls?: string[];
43
+ mediaSize?: FormMediaSize;
44
+ fileAcceptTypes?: string[];
45
+ fileAcceptExtensions?: string[];
46
+ filePlaceholderIcon?: FormFilePlaceholderIcon;
47
+ fileMinCount?: number;
48
+ fileMaxCount?: number;
49
+ defaultValue?: string | string[] | boolean;
50
+ }
51
+ export interface FormSchema {
52
+ title?: string;
53
+ description?: string;
54
+ blocks: FormBlock[];
55
+ jsonSchema: FormJsonSchema;
56
+ }
57
+ export declare function isMediaBlockType(type: FormBlockType): boolean;
58
+ export declare function isLayoutBlockType(type: FormBlockType): boolean;
59
+ export declare function isInputBlockType(type: FormBlockType): boolean;
60
+ export interface FormDefinitionPayload {
61
+ title?: string;
62
+ description?: string;
63
+ blocks?: FormBlock[];
64
+ a2ui?: A2uiSurfaceDoc;
65
+ json_schema?: FormJsonSchema;
66
+ jsonSchema?: FormJsonSchema;
67
+ }
68
+ export interface FormDetailResult {
69
+ formUniqueId: string;
70
+ formId?: number;
71
+ disabled: boolean;
72
+ formDefinition?: FormSchema;
73
+ a2ui?: A2uiSurfaceDoc;
74
+ }
75
+ export type SubmitMode = 'curl' | 'post';
76
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@skopon-cool/form-sdk",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "version": "0.1.0",
7
+ "description": "Skopon form rendering SDK (A2UI + form_definition) with submit helpers",
8
+ "type": "module",
9
+ "main": "./dist/index.js",
10
+ "module": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ },
17
+ "./styles.css": "./dist/form-sdk.css"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "src"
22
+ ],
23
+ "scripts": {
24
+ "build": "vite build && tsc -p tsconfig.build.json",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest"
27
+ },
28
+ "peerDependencies": {
29
+ "@ant-design/icons": "^6.0.0",
30
+ "@a2ui/react": "^0.10.1",
31
+ "@a2ui/web_core": "^0.10.2",
32
+ "antd": "^6.4.3",
33
+ "dayjs": "^1.11.0",
34
+ "react": "^18.0.0 || ^19.0.0",
35
+ "react-dom": "^18.0.0 || ^19.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@ant-design/icons": "^6.2.3",
39
+ "@a2ui/react": "^0.10.1",
40
+ "@a2ui/web_core": "^0.10.2",
41
+ "@types/react": "^19.1.2",
42
+ "@types/react-dom": "^19.1.2",
43
+ "@vitejs/plugin-react": "^4.5.2",
44
+ "antd": "^6.4.3",
45
+ "dayjs": "^1.11.13",
46
+ "react": "^19.1.0",
47
+ "react-dom": "^19.1.0",
48
+ "typescript": "~5.8.3",
49
+ "vite": "^6.3.5",
50
+ "vite-plugin-dts": "^4.5.4",
51
+ "vitest": "^3.2.4"
52
+ }
53
+ }
@@ -0,0 +1,150 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import type { FormSchema } from '../types/index'
3
+ import { a2uiToBlocks, blocksToA2ui, isA2uiSurfaceEmpty, surfaceDocToMessages } from './a2uiAdapter'
4
+
5
+ function makeDefinition(): Pick<FormSchema, 'title' | 'description' | 'blocks'> {
6
+ return {
7
+ title: '报名表',
8
+ description: '请填写信息',
9
+ blocks: [
10
+ { id: 'b-name', type: 'text', name: 'name', label: '姓名' },
11
+ { id: 'b-email', type: 'email', name: 'email', label: '邮箱' },
12
+ { id: 'b-bio', type: 'textarea', name: 'bio', label: '简介' },
13
+ { id: 'b-age', type: 'number', name: 'age', label: '年龄' },
14
+ {
15
+ id: 'b-gender',
16
+ type: 'radio',
17
+ name: 'gender',
18
+ label: '性别',
19
+ options: [
20
+ { label: '男', value: 'm' },
21
+ { label: '女', value: 'f' },
22
+ ],
23
+ },
24
+ {
25
+ id: 'b-hobby',
26
+ type: 'checkbox',
27
+ name: 'hobby',
28
+ label: '爱好',
29
+ options: [
30
+ { label: '阅读', value: 'read' },
31
+ { label: '运动', value: 'sport' },
32
+ ],
33
+ },
34
+ { id: 'b-sub', type: 'toggle', name: 'subscribe', label: '订阅' },
35
+ { id: 'b-when', type: 'datetime', name: 'when', label: '时间' },
36
+ { id: 'b-file', type: 'file', name: 'attach', label: '附件' },
37
+ { id: 'b-img', type: 'image', label: '示例图', mediaUrls: ['https://x/y.png'], mediaSize: 'large' },
38
+ ],
39
+ }
40
+ }
41
+
42
+ describe('a2uiAdapter blocks <-> surface', () => {
43
+ it('blocksToA2ui builds a Column root referencing all components', () => {
44
+ const doc = blocksToA2ui(makeDefinition(), { styleId: 'theme-a' })
45
+ const root = doc.components.find((c) => c.id === 'root')
46
+ expect(root?.component).toBe('Column')
47
+ expect(Array.isArray(root?.children)).toBe(true)
48
+ // title + desc + 10 blocks
49
+ expect((root?.children as string[]).length).toBe(12)
50
+ expect(doc.surfaceProperties?.styleId).toBe('theme-a')
51
+ // every child id exists in components
52
+ const ids = new Set(doc.components.map((c) => c.id))
53
+ for (const childId of root?.children as string[]) {
54
+ expect(ids.has(childId)).toBe(true)
55
+ }
56
+ })
57
+
58
+ it('round-trips block types through a2uiToBlocks', () => {
59
+ const doc = blocksToA2ui(makeDefinition())
60
+ const back = a2uiToBlocks(doc)
61
+ expect(back.title).toBe('报名表')
62
+ expect(back.description).toBe('请填写信息')
63
+ const byName = (n: string) => back.blocks.find((b) => b.name === n)
64
+ expect(byName('name')?.type).toBe('text')
65
+ expect(byName('email')?.type).toBe('email')
66
+ expect(byName('bio')?.type).toBe('textarea')
67
+ expect(byName('age')?.type).toBe('number')
68
+ expect(byName('gender')?.type).toBe('radio')
69
+ expect(byName('gender')?.options?.length).toBe(2)
70
+ expect(byName('hobby')?.type).toBe('checkbox')
71
+ expect(byName('subscribe')?.type).toBe('toggle')
72
+ expect(byName('when')?.type).toBe('datetime')
73
+ expect(byName('attach')?.type).toBe('file')
74
+ const img = back.blocks.find((b) => b.type === 'image')
75
+ expect(img?.mediaUrls?.[0]).toBe('https://x/y.png')
76
+ expect(img?.mediaSize).toBe('large')
77
+ })
78
+
79
+ it('blocksToA2ui emits SkoponMedia for image blocks', () => {
80
+ const doc = blocksToA2ui(makeDefinition(), { includeHeader: false })
81
+ const imgNode = doc.components.find((c) => c.id === 'b-img')
82
+ expect(imgNode?.component).toBe('SkoponMedia')
83
+ expect(imgNode?.mediaType).toBe('image')
84
+ expect(imgNode?.mediaSize).toBe('large')
85
+ })
86
+
87
+ it('blocksToA2ui preserves select vs multiselect picker types', () => {
88
+ const doc = blocksToA2ui(
89
+ {
90
+ title: '',
91
+ description: '',
92
+ blocks: [
93
+ {
94
+ id: 'b-city',
95
+ type: 'select',
96
+ name: 'city',
97
+ label: '城市',
98
+ options: [{ label: '北京', value: 'bj' }],
99
+ },
100
+ {
101
+ id: 'b-tags',
102
+ type: 'multiselect',
103
+ name: 'tags',
104
+ label: '标签',
105
+ options: [{ label: 'A', value: 'a' }],
106
+ },
107
+ ],
108
+ },
109
+ { includeHeader: false },
110
+ )
111
+ const city = doc.components.find((c) => c.id === 'b-city')
112
+ const tags = doc.components.find((c) => c.id === 'b-tags')
113
+ expect(city?.component).toBe('SkoponSelect')
114
+ expect(city?.mode).toBe('single')
115
+ expect(tags?.component).toBe('SkoponSelect')
116
+ expect(tags?.mode).toBe('multiple')
117
+
118
+ const back = a2uiToBlocks(doc)
119
+ expect(back.blocks.find((b) => b.name === 'city')?.type).toBe('select')
120
+ expect(back.blocks.find((b) => b.name === 'tags')?.type).toBe('multiselect')
121
+ })
122
+
123
+ it('surfaceDocToMessages emits createSurface + updateComponents + updateDataModel', () => {
124
+ const doc = blocksToA2ui(makeDefinition())
125
+ const messages = surfaceDocToMessages(doc, { surfaceId: 's1', catalogId: 'c1' })
126
+ expect(messages[0]).toHaveProperty('createSurface')
127
+ expect(messages[1]).toHaveProperty('updateComponents')
128
+ expect(messages.some((m) => 'updateDataModel' in m)).toBe(true)
129
+ })
130
+
131
+ it('isA2uiSurfaceEmpty detects empty root Column', () => {
132
+ const empty = blocksToA2ui({ title: '', description: '', blocks: [] })
133
+ expect(isA2uiSurfaceEmpty(empty)).toBe(true)
134
+ expect(isA2uiSurfaceEmpty(null)).toBe(true)
135
+ const full = blocksToA2ui(makeDefinition())
136
+ const root = full.components.find((c) => c.id === 'root')
137
+ expect(root?.children?.length).toBeGreaterThan(0)
138
+ expect(isA2uiSurfaceEmpty(full)).toBe(false)
139
+ })
140
+
141
+ it('blocksToA2ui includeHeader:false skips title and description Text nodes', () => {
142
+ const doc = blocksToA2ui(makeDefinition(), { includeHeader: false })
143
+ const root = doc.components.find((c) => c.id === 'root')
144
+ expect(root?.children).not.toContain('__title__')
145
+ expect(root?.children).not.toContain('__desc__')
146
+ expect((root?.children as string[]).length).toBe(10)
147
+ const withHeader = blocksToA2ui(makeDefinition())
148
+ expect((withHeader.components.find((c) => c.id === 'root')?.children as string[]).length).toBe(12)
149
+ })
150
+ })