@svadmin/ui 0.72.0 → 0.73.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 (252) hide show
  1. package/README.md +40 -25
  2. package/THIRD_PARTY_NOTICES.md +52 -0
  3. package/dist/app.css +7694 -5117
  4. package/dist/app.theme.css +7645 -5119
  5. package/dist/class-properties.generated.d.ts +1 -0
  6. package/dist/class-properties.generated.js +2 -0
  7. package/dist/classnames.d.ts +8 -0
  8. package/dist/classnames.js +26 -0
  9. package/dist/components/AutoForm.svelte +20 -6
  10. package/dist/components/AutoForm.svelte.d.ts +2 -0
  11. package/dist/components/AutoTable.svelte +249 -9
  12. package/dist/components/AutoTable.svelte.d.ts +5 -0
  13. package/dist/components/BoundRecordDetailDrawer.svelte +8 -2
  14. package/dist/components/BoundRecordDetailDrawer.svelte.d.ts +2 -0
  15. package/dist/components/ComboboxField.svelte +31 -9
  16. package/dist/components/ComboboxField.svelte.d.ts +3 -2
  17. package/dist/components/CreatePage.svelte +4 -0
  18. package/dist/components/CreatePage.svelte.d.ts +2 -0
  19. package/dist/components/DateRangeInput.svelte +75 -0
  20. package/dist/components/DateRangeInput.svelte.d.ts +25 -0
  21. package/dist/components/DateTimeInput.svelte +65 -0
  22. package/dist/components/DateTimeInput.svelte.d.ts +19 -0
  23. package/dist/components/DevTools.svelte +2 -2
  24. package/dist/components/EditPage.svelte +4 -0
  25. package/dist/components/EditPage.svelte.d.ts +2 -0
  26. package/dist/components/FieldRenderer.svelte +90 -18
  27. package/dist/components/FileUpload.svelte +250 -0
  28. package/dist/components/FileUpload.svelte.d.ts +32 -0
  29. package/dist/components/FilterBuilder.svelte +236 -279
  30. package/dist/components/FilterBuilder.svelte.d.ts +3 -0
  31. package/dist/components/ImportWizard.svelte +181 -94
  32. package/dist/components/JsonSchemaForm.svelte +255 -99
  33. package/dist/components/JsonSchemaForm.svelte.d.ts +12 -1
  34. package/dist/components/Layout.svelte +1 -2
  35. package/dist/components/LazyTaskQueueDrawer.svelte.d.ts +1 -1
  36. package/dist/components/ListPage.svelte +4 -0
  37. package/dist/components/ListPage.svelte.d.ts +2 -0
  38. package/dist/components/LoginPage.svelte +1 -1
  39. package/dist/components/MoneyInput.svelte +25 -0
  40. package/dist/components/MoneyInput.svelte.d.ts +20 -0
  41. package/dist/components/NumberInput.svelte +25 -0
  42. package/dist/components/NumberInput.svelte.d.ts +19 -0
  43. package/dist/components/NumericInput.svelte +110 -0
  44. package/dist/components/NumericInput.svelte.d.ts +23 -0
  45. package/dist/components/PercentInput.svelte +25 -0
  46. package/dist/components/PercentInput.svelte.d.ts +20 -0
  47. package/dist/components/QuickEditDrawer.svelte +4 -1
  48. package/dist/components/QuickEditDrawer.svelte.d.ts +2 -0
  49. package/dist/components/RecordDetailDrawer.svelte +4 -2
  50. package/dist/components/RecordDetailDrawer.svelte.d.ts +2 -0
  51. package/dist/components/ResourceOperationsPage.svelte +25 -19
  52. package/dist/components/ResourceOperationsPage.svelte.d.ts +5 -1
  53. package/dist/components/ShowInferencer.svelte +4 -2
  54. package/dist/components/ShowInferencer.svelte.d.ts +2 -0
  55. package/dist/components/ShowPage.svelte +7 -3
  56. package/dist/components/ShowPage.svelte.d.ts +2 -0
  57. package/dist/components/Sidebar.svelte +5 -5
  58. package/dist/components/SpreadsheetView.svelte +81 -286
  59. package/dist/components/StepsForm.svelte +5 -5
  60. package/dist/components/SvarAutoTable.svelte +39 -0
  61. package/dist/components/SvarAutoTable.svelte.d.ts +13 -0
  62. package/dist/components/SvarDataGrid.svelte +362 -0
  63. package/dist/components/SvarDataGrid.svelte.d.ts +57 -0
  64. package/dist/components/SvarGridCell.svelte +10 -0
  65. package/dist/components/SvarGridCell.svelte.d.ts +7 -0
  66. package/dist/components/SvarResourceRead.svelte +50 -0
  67. package/dist/components/SvarResourceRead.svelte.d.ts +23 -0
  68. package/dist/components/SvarResourceTable.svelte +63 -0
  69. package/dist/components/SvarResourceTable.svelte.d.ts +42 -0
  70. package/dist/components/SvarResourceTableView.svelte +364 -0
  71. package/dist/components/SvarResourceTableView.svelte.d.ts +4 -0
  72. package/dist/components/TaskQueueDrawer.svelte +72 -16
  73. package/dist/components/TreeSelect.svelte +208 -28
  74. package/dist/components/TreeSelect.svelte.d.ts +3 -1
  75. package/dist/components/auto-table-grid.d.ts +26 -0
  76. package/dist/components/auto-table-grid.js +1 -0
  77. package/dist/components/content/ContentPageHeader.svelte +12 -9
  78. package/dist/components/content/ContentPageShell.svelte +6 -9
  79. package/dist/components/content/FileBrowser.svelte +35 -0
  80. package/dist/components/content/FileBrowser.svelte.d.ts +14 -0
  81. package/dist/components/content/MetricBlock.svelte +10 -13
  82. package/dist/components/content/NetworkTable.svelte +3 -3
  83. package/dist/components/content/NetworkTable.svelte.d.ts +1 -0
  84. package/dist/components/content/PageToolbar.svelte +6 -10
  85. package/dist/components/content/SectionHeader.svelte +8 -13
  86. package/dist/components/content/SettingsFieldRow.svelte +9 -23
  87. package/dist/components/content/SettingsGroup.svelte +11 -27
  88. package/dist/components/content/StatusBadge.svelte +3 -8
  89. package/dist/components/content/WorkspaceLayout.svelte +8 -12
  90. package/dist/components/enterprise/filter-tree.d.ts +48 -0
  91. package/dist/components/enterprise/filter-tree.js +224 -0
  92. package/dist/components/enterprise/json-schema-form.d.ts +39 -0
  93. package/dist/components/enterprise/json-schema-form.js +195 -0
  94. package/dist/components/enterprise/recursive-schema-form.d.ts +30 -0
  95. package/dist/components/enterprise/recursive-schema-form.js +196 -0
  96. package/dist/components/enterprise/spreadsheet-formula.d.ts +12 -0
  97. package/dist/components/enterprise/spreadsheet-formula.js +245 -0
  98. package/dist/components/fieldComponentMap.js +5 -0
  99. package/dist/components/fields/DateField.svelte +4 -3
  100. package/dist/components/fields/date-field-value.d.ts +2 -0
  101. package/dist/components/fields/date-field-value.js +13 -0
  102. package/dist/components/json-schema-form-state.d.ts +37 -0
  103. package/dist/components/json-schema-form-state.js +168 -0
  104. package/dist/components/record-detail.svelte.d.ts +3 -0
  105. package/dist/components/record-detail.svelte.js +10 -4
  106. package/dist/components/svar-grid-cells.d.ts +14 -0
  107. package/dist/components/svar-grid-cells.js +2 -0
  108. package/dist/components/svar-grid-contract.d.ts +80 -0
  109. package/dist/components/svar-grid-contract.js +159 -0
  110. package/dist/components/svar-grid-interactions.d.ts +54 -0
  111. package/dist/components/svar-grid-interactions.js +174 -0
  112. package/dist/components/svar-grid-loading.d.ts +40 -0
  113. package/dist/components/svar-grid-loading.js +180 -0
  114. package/dist/components/svar-grid-operations.d.ts +37 -0
  115. package/dist/components/svar-grid-operations.js +186 -0
  116. package/dist/components/svar-grid-panes.d.ts +34 -0
  117. package/dist/components/svar-grid-panes.js +66 -0
  118. package/dist/components/svar-grid-resource-loading.d.ts +18 -0
  119. package/dist/components/svar-grid-resource-loading.js +68 -0
  120. package/dist/components/svar-grid-resource-session.svelte.d.ts +29 -0
  121. package/dist/components/svar-grid-resource-session.svelte.js +61 -0
  122. package/dist/components/ui/badge/badge-variants.js +3 -1
  123. package/dist/components/ui/button/button-variants.js +5 -2
  124. package/dist/components/ui/input/input.svelte +1 -4
  125. package/dist/components/ui/textarea/textarea.svelte +2 -1
  126. package/dist/components.css +3079 -0
  127. package/dist/design-contract.d.ts +13 -0
  128. package/dist/design-contract.js +11 -0
  129. package/dist/design-system.d.ts +15 -0
  130. package/dist/design-system.js +7 -0
  131. package/dist/index.d.ts +14 -0
  132. package/dist/index.js +9 -0
  133. package/dist/page-chrome.css +12 -49
  134. package/dist/recipes.d.ts +4 -0
  135. package/dist/recipes.js +4 -0
  136. package/dist/rendering/index.d.ts +87 -0
  137. package/dist/rendering/index.js +117 -0
  138. package/dist/styled-system/css/conditions.js +36 -0
  139. package/dist/styled-system/css/css.d.ts +22 -0
  140. package/dist/styled-system/css/css.js +45 -0
  141. package/dist/styled-system/css/cva.d.ts +6 -0
  142. package/dist/styled-system/css/cva.js +87 -0
  143. package/dist/styled-system/css/cx.d.ts +5 -0
  144. package/dist/styled-system/css/cx.js +15 -0
  145. package/dist/styled-system/css/index.d.ts +5 -0
  146. package/dist/styled-system/css/index.js +4 -0
  147. package/dist/styled-system/css/sva.d.ts +4 -0
  148. package/dist/styled-system/css/sva.js +46 -0
  149. package/dist/styled-system/helpers.js +316 -0
  150. package/dist/styled-system/patterns/aspect-ratio.d.ts +20 -0
  151. package/dist/styled-system/patterns/aspect-ratio.js +38 -0
  152. package/dist/styled-system/patterns/bleed.d.ts +21 -0
  153. package/dist/styled-system/patterns/bleed.js +24 -0
  154. package/dist/styled-system/patterns/box.d.ts +20 -0
  155. package/dist/styled-system/patterns/box.js +15 -0
  156. package/dist/styled-system/patterns/center.d.ts +20 -0
  157. package/dist/styled-system/patterns/center.js +21 -0
  158. package/dist/styled-system/patterns/circle.d.ts +20 -0
  159. package/dist/styled-system/patterns/circle.js +25 -0
  160. package/dist/styled-system/patterns/container.d.ts +20 -0
  161. package/dist/styled-system/patterns/container.js +21 -0
  162. package/dist/styled-system/patterns/cq.d.ts +21 -0
  163. package/dist/styled-system/patterns/cq.js +21 -0
  164. package/dist/styled-system/patterns/divider.d.ts +22 -0
  165. package/dist/styled-system/patterns/divider.js +25 -0
  166. package/dist/styled-system/patterns/flex.d.ts +26 -0
  167. package/dist/styled-system/patterns/flex.js +26 -0
  168. package/dist/styled-system/patterns/float.d.ts +23 -0
  169. package/dist/styled-system/patterns/float.js +52 -0
  170. package/dist/styled-system/patterns/grid-item.d.ts +25 -0
  171. package/dist/styled-system/patterns/grid-item.js +25 -0
  172. package/dist/styled-system/patterns/grid.d.ts +24 -0
  173. package/dist/styled-system/patterns/grid.js +27 -0
  174. package/dist/styled-system/patterns/hstack.d.ts +21 -0
  175. package/dist/styled-system/patterns/hstack.js +24 -0
  176. package/dist/styled-system/patterns/index.d.ts +21 -0
  177. package/dist/styled-system/patterns/index.js +20 -0
  178. package/dist/styled-system/patterns/link-overlay.d.ts +20 -0
  179. package/dist/styled-system/patterns/link-overlay.js +24 -0
  180. package/dist/styled-system/patterns/spacer.d.ts +20 -0
  181. package/dist/styled-system/patterns/spacer.js +25 -0
  182. package/dist/styled-system/patterns/square.d.ts +20 -0
  183. package/dist/styled-system/patterns/square.js +24 -0
  184. package/dist/styled-system/patterns/stack.d.ts +23 -0
  185. package/dist/styled-system/patterns/stack.js +24 -0
  186. package/dist/styled-system/patterns/visually-hidden.d.ts +20 -0
  187. package/dist/styled-system/patterns/visually-hidden.js +18 -0
  188. package/dist/styled-system/patterns/vstack.d.ts +21 -0
  189. package/dist/styled-system/patterns/vstack.js +24 -0
  190. package/dist/styled-system/patterns/wrap.d.ts +24 -0
  191. package/dist/styled-system/patterns/wrap.js +25 -0
  192. package/dist/styled-system/recipes/content-header.d.ts +31 -0
  193. package/dist/styled-system/recipes/content-header.js +65 -0
  194. package/dist/styled-system/recipes/content-page.d.ts +34 -0
  195. package/dist/styled-system/recipes/content-page.js +43 -0
  196. package/dist/styled-system/recipes/create-recipe.js +82 -0
  197. package/dist/styled-system/recipes/index.d.ts +17 -0
  198. package/dist/styled-system/recipes/index.js +16 -0
  199. package/dist/styled-system/recipes/metric-block.d.ts +34 -0
  200. package/dist/styled-system/recipes/metric-block.js +76 -0
  201. package/dist/styled-system/recipes/product-list.d.ts +31 -0
  202. package/dist/styled-system/recipes/product-list.js +73 -0
  203. package/dist/styled-system/recipes/product-section.d.ts +31 -0
  204. package/dist/styled-system/recipes/product-section.js +49 -0
  205. package/dist/styled-system/recipes/product-settings-row.d.ts +34 -0
  206. package/dist/styled-system/recipes/product-settings-row.js +58 -0
  207. package/dist/styled-system/recipes/product-settings.d.ts +31 -0
  208. package/dist/styled-system/recipes/product-settings.js +57 -0
  209. package/dist/styled-system/recipes/product-status.d.ts +34 -0
  210. package/dist/styled-system/recipes/product-status.js +45 -0
  211. package/dist/styled-system/recipes/product-toolbar.d.ts +31 -0
  212. package/dist/styled-system/recipes/product-toolbar.js +41 -0
  213. package/dist/styled-system/recipes/product-workspace.d.ts +34 -0
  214. package/dist/styled-system/recipes/product-workspace.js +58 -0
  215. package/dist/styled-system/recipes/surface-metric.d.ts +38 -0
  216. package/dist/styled-system/recipes/surface-metric.js +63 -0
  217. package/dist/styled-system/recipes/surface-table.d.ts +34 -0
  218. package/dist/styled-system/recipes/surface-table.js +62 -0
  219. package/dist/styled-system/recipes/ui-badge.d.ts +31 -0
  220. package/dist/styled-system/recipes/ui-badge.js +38 -0
  221. package/dist/styled-system/recipes/ui-button.d.ts +32 -0
  222. package/dist/styled-system/recipes/ui-button.js +43 -0
  223. package/dist/styled-system/recipes/ui-input.d.ts +31 -0
  224. package/dist/styled-system/recipes/ui-input.js +49 -0
  225. package/dist/styled-system/recipes/ui-textarea.d.ts +31 -0
  226. package/dist/styled-system/recipes/ui-textarea.js +24 -0
  227. package/dist/styled-system/styles.css +1257 -0
  228. package/dist/styled-system/tokens/index.d.ts +9 -0
  229. package/dist/styled-system/tokens/index.js +268 -0
  230. package/dist/styled-system/tokens/tokens.d.ts +36 -0
  231. package/dist/styled-system/types/composition.d.ts +227 -0
  232. package/dist/styled-system/types/conditions.d.ts +236 -0
  233. package/dist/styled-system/types/csstype.d.ts +22570 -0
  234. package/dist/styled-system/types/index.d.ts +6 -0
  235. package/dist/styled-system/types/parts.d.ts +8 -0
  236. package/dist/styled-system/types/pattern.d.ts +78 -0
  237. package/dist/styled-system/types/prop-type.d.ts +223 -0
  238. package/dist/styled-system/types/recipe.d.ts +181 -0
  239. package/dist/styled-system/types/selectors.d.ts +59 -0
  240. package/dist/styled-system/types/static-css.d.ts +56 -0
  241. package/dist/styled-system/types/style-props.d.ts +8088 -0
  242. package/dist/styled-system/types/system-types.d.ts +151 -0
  243. package/dist/styles/aliases.css +49 -0
  244. package/dist/styles/compatibility.epilogue.css +384 -0
  245. package/dist/styles/compatibility.prelude.css +5513 -0
  246. package/dist/styles/conditional-states.css +28 -0
  247. package/dist/styles/conditional-utilities.css +22 -0
  248. package/dist/styles/recipes.css +1262 -0
  249. package/dist/utils.d.ts +1 -1
  250. package/dist/utils.js +1 -1
  251. package/package.json +31 -13
  252. package/styles-compatibility.json +12 -0
@@ -1,16 +1,32 @@
1
1
  <script lang="ts">
2
- import { definedOptions } from '@svadmin/core/options';
3
2
  import { useTranslation } from '@svadmin/core/i18n';
4
-
5
3
  import { Button } from './ui/button/index.js';
6
- import { Loader2 } from '@lucide/svelte';
4
+ import { Loader2, Plus, Trash2 } from '@lucide/svelte';
7
5
  import { cn } from '../utils.js';
6
+ import { onDestroy, untrack } from 'svelte';
7
+ import {
8
+ initializeSchemaForm, schemaFormArrayItem, schemaFormEnumIndex,
9
+ schemaFormEnumValue, schemaFormSnapshot, writeSchemaFormPath,
10
+ type JsonSchemaFormSchema,
11
+ } from './json-schema-form-state.js';
12
+
13
+ import { readRecursiveSchemaForm, validateRecursiveSchemaForm, schemaFormPointer } from './enterprise/recursive-schema-form.js';
14
+ import { parseSchemaNumber, type SchemaFormIssue } from './enterprise/json-schema-form.js';
15
+ type JsonSchema = JsonSchemaFormSchema;
8
16
 
9
17
  interface Props {
10
- schema: Record<string, unknown>;
18
+ schema: JsonSchema;
11
19
  value?: Record<string, unknown>;
12
20
  onsubmit?: (data: Record<string, unknown>) => void | Promise<void>;
13
21
  submitText?: string;
22
+ disabled?: boolean;
23
+ readonly?: boolean;
24
+ /** 只决定此实例文案;不修改全局语言。 */
25
+ locale?: string;
26
+ onerror?: (error: unknown) => void;
27
+ onvalidationerror?: (issues: SchemaFormIssue[]) => void;
28
+ /** Use a unique stable prefix when several schema forms share a page. */
29
+ idPrefix?: string;
14
30
  class?: string;
15
31
  }
16
32
 
@@ -19,136 +35,276 @@
19
35
  value = $bindable({}),
20
36
  onsubmit,
21
37
  submitText = 'Submit Form',
38
+ disabled = false,
39
+ readonly = false,
40
+ locale,
41
+ onerror,
42
+ onvalidationerror,
43
+ idPrefix,
22
44
  class: className = '',
23
45
  }: Props = $props();
24
46
 
25
47
  const i18n = useTranslation();
26
-
48
+ const instanceId = $props.id();
49
+ const prefix = $derived(idPrefix ?? `json-form-${instanceId}`);
50
+ const isZh = $derived((locale ?? i18n.locale).startsWith('zh'));
51
+ const labels = $derived(isZh ? {
52
+ add: '添加项目', remove: '删除', empty: '暂无项目',
53
+ failed: '提交未完成,请检查状态后重试。', invalid: '请检查表单输入。',
54
+ } : {
55
+ add: 'Add item', remove: 'Remove item', empty: 'No items',
56
+ failed: 'Submission did not complete. Check its status before retrying.', invalid: 'Check the form input.',
57
+ });
58
+ const model = $derived(readRecursiveSchemaForm(schema));
59
+ let validationIssues = $state<SchemaFormIssue[]>([]);
60
+ let parseIssues = $state<SchemaFormIssue[]>([]);
61
+ let internalValue: Record<string, unknown> | undefined;
27
62
  let isSubmitting = $state(false);
28
-
29
- interface FieldMeta {
30
- key: string;
31
- title: string;
32
- description?: string;
33
- type: string;
34
- enum?: Array<string | number>;
35
- required?: boolean;
36
- default?: unknown;
37
- }
38
-
39
- const properties = $derived.by<FieldMeta[]>(() => {
40
- const propsObj = (schema['properties'] ?? {}) as Record<string, Record<string, unknown>>;
41
- const requiredKeys = Array.isArray(schema['required']) ? (schema['required'] as string[]) : [];
42
-
43
- return Object.entries(propsObj).map(([key, propDef]) => (definedOptions({
44
- key,
45
- title: String(propDef['title'] ?? key),
46
- description: propDef['description'] ? String(propDef['description']) : undefined,
47
- type: String(propDef['type'] ?? 'string'),
48
- enum: Array.isArray(propDef['enum']) ? (propDef['enum'] as Array<string | number>) : undefined,
49
- required: requiredKeys.includes(key),
50
- default: propDef['default'],
51
- })));
63
+ let failure = $state<'invalid' | 'failed' | null>(null);
64
+ let formElement: HTMLFormElement | undefined;
65
+ let destroyed = false;
66
+ let authorityRevision = 0;
67
+ let identity: readonly unknown[] = [];
68
+ const prepared = $derived.by(() => {
69
+ try {
70
+ if (model.issues.length) return { ok: false as const };
71
+ return { ok: true as const, value: initializeSchemaForm(schema, value) };
72
+ }
73
+ catch { return { ok: false as const }; }
74
+ });
75
+ const locked = $derived(disabled || readonly || isSubmitting || !prepared.ok);
76
+ // 按键是否存在初始化,不能每次输入都用 ?? 把清空的默认值补回来。
77
+ $effect.pre(() => {
78
+ if (prepared.ok && prepared.value !== value) value = prepared.value;
52
79
  });
80
+ $effect.pre(() => {
81
+ const next = [schema, value, disabled, readonly, onsubmit];
82
+ untrack(() => {
83
+ if (next.some((part, index) => part !== identity[index])) {
84
+ authorityRevision += 1;
85
+ failure = null;
86
+ validationIssues = [];
87
+ if (schema !== identity[0] || value !== internalValue) parseIssues = [];
88
+ internalValue = undefined;
89
+ }
90
+ identity = next;
91
+ });
92
+ });
93
+ onDestroy(() => { destroyed = true; });
53
94
 
54
- async function handleSubmit(e: SubmitEvent) {
55
- e.preventDefault();
95
+ function editable(): boolean {
96
+ return !destroyed && !disabled && !readonly && !isSubmitting && prepared.ok
97
+ && !formElement?.closest('fieldset[disabled]');
98
+ }
99
+ function nodeType(node: JsonSchema): string | undefined {
100
+ return Array.isArray(node.type) ? node.type.find(type => type !== 'null') : node.type;
101
+ }
102
+ function choices(node: JsonSchema): JsonSchema['enum'] {
103
+ return node.enum ?? (Object.hasOwn(node, 'const') ? [node.const ?? null] : undefined);
104
+ }
105
+ function pathReadonly(path: readonly string[]): boolean {
106
+ let node: JsonSchema | undefined = schema;
107
+ if (node.readOnly) return true;
108
+ for (const key of path) {
109
+ node = node && (nodeType(node) === 'array' ? node.items : node.properties?.[key]);
110
+ if (node?.readOnly) return true;
111
+ }
112
+ return false;
113
+ }
114
+ function invalid(issues: SchemaFormIssue[]): void {
115
+ failure = 'invalid';
116
+ validationIssues = issues;
117
+ try { onvalidationerror?.(issues); } catch { /* Observers do not authorize submission. */ }
118
+ }
119
+ function fieldInvalid(path: readonly string[]): boolean {
120
+ return [...validationIssues, ...parseIssues].some(issue => issue.path === schemaFormPointer(path));
121
+ }
122
+ function writeNumber(path: string[], input: HTMLInputElement): void {
123
+ if (!editable() || pathReadonly(path)) return;
124
+ const pointer = schemaFormPointer(path);
125
+ parseIssues = parseIssues.filter(issue => issue.path !== pointer);
126
+ try {
127
+ if (input.validity.badInput) throw new Error('Invalid numeric input');
128
+ writePath(path, parseSchemaNumber(input.value));
129
+ } catch {
130
+ parseIssues = [...parseIssues, { path: pointer, code: 'invalid-value' }];
131
+ invalid(parseIssues);
132
+ }
133
+ }
134
+ function readPath(path: string[]): unknown {
135
+ return path.reduce<unknown>((current, key) => (
136
+ current && typeof current === 'object' && Object.hasOwn(current, key)
137
+ ? (current as Record<string, unknown>)[key] : undefined
138
+ ), prepared.ok ? prepared.value : {});
139
+ }
140
+ function writePath(path: string[], nextValue: unknown): boolean {
141
+ if (!editable() || !prepared.ok || pathReadonly(path)) return false;
142
+ try {
143
+ value = writeSchemaFormPath(prepared.value, path, nextValue);
144
+ // Capture the actual bindable value after Svelte wraps it, not its raw source.
145
+ internalValue = value;
146
+ failure = null;
147
+ validationIssues = [];
148
+ return true;
149
+ }
150
+ catch { failure = 'invalid'; return false; }
151
+ }
152
+ function choose(path: string[], raw: string, choices: JsonSchema['enum']): void {
153
+ if (!editable()) return;
154
+ try { writePath(path, schemaFormEnumValue(choices ?? [], raw)); }
155
+ catch { failure = 'invalid'; }
156
+ }
157
+ function addArrayItem(path: string[], itemSchema: JsonSchema): void {
158
+ if (!editable()) return;
159
+ const current = readPath(path);
160
+ try { writePath(path, [...(Array.isArray(current) ? current : []), schemaFormArrayItem(itemSchema)]); }
161
+ catch { failure = 'invalid'; }
162
+ }
163
+ function removeArrayItem(path: string[], index: number): void {
164
+ if (!editable()) return;
165
+ const current = readPath(path);
166
+ if (!Array.isArray(current) || !Number.isSafeInteger(index) || index < 0 || index >= current.length) return;
167
+ const prefix = schemaFormPointer(path) + '/';
168
+ const nextIssues = parseIssues.flatMap(issue => {
169
+ if (!issue.path.startsWith(prefix)) return [issue];
170
+ const tail = issue.path.slice(prefix.length);
171
+ const separator = tail.indexOf('/');
172
+ const segment = separator < 0 ? tail : tail.slice(0, separator);
173
+ if (!/^(0|[1-9][0-9]*)$/u.test(segment)) return [issue];
174
+ const itemIndex = Number(segment);
175
+ if (itemIndex === index) return [];
176
+ if (itemIndex < index) return [issue];
177
+ return [{ ...issue, path: prefix + String(itemIndex - 1) + (separator < 0 ? '' : tail.slice(separator)) }];
178
+ });
179
+ // Retire only the removed item's error and shift surviving descendants with their
180
+ // array item. A rejected readonly/disabled write must not clear any error.
181
+ if (writePath(path, current.filter((_, itemIndex) => itemIndex !== index))) parseIssues = nextIssues;
182
+ }
183
+ async function handleSubmit(event: SubmitEvent): Promise<void> {
184
+ event.preventDefault();
185
+ // 同步置锁,防止同一帧的双击/重入 submit;遵循宿主 fieldset 的预览禁用状态;权限仍需服务端校验。
186
+ if (!editable() || !prepared.ok || !onsubmit) return;
187
+ const problems = [...parseIssues, ...validateRecursiveSchemaForm(model, prepared.value)];
188
+ if (problems.length) { invalid(problems); return; }
189
+ if (formElement && !formElement.checkValidity()) { invalid([{ path: '', code: 'invalid-value' }]); return; }
190
+ let payload: Record<string, unknown>;
191
+ try { payload = schemaFormSnapshot(prepared.value); }
192
+ catch { invalid([{ path: '', code: 'invalid-value' }]); return; }
193
+ const currentValue = value, currentSchema = schema, handler = onsubmit, revision = authorityRevision;
194
+ const fingerprint = JSON.stringify(payload);
195
+ failure = null;
56
196
  isSubmitting = true;
57
197
  try {
58
- await onsubmit?.(value);
198
+ await handler(payload);
199
+ } catch (error) {
200
+ // 回调只能拿到副本;被替换/卸载实例的失败不能污染新的表单上下文。
201
+ if (!destroyed && revision === authorityRevision && !disabled && !readonly && currentValue === value && currentSchema === schema && handler === onsubmit) {
202
+ try {
203
+ if (JSON.stringify(schemaFormSnapshot(prepared.ok ? prepared.value : value)) === fingerprint) {
204
+ failure = 'failed';
205
+ try { onerror?.(error); } catch { /* 错误观察者不改变提交状态。 */ }
206
+ }
207
+ } catch { /* 新上下文不再是可比较的 JSON 草稿,不发布旧错误。 */ }
208
+ }
59
209
  } finally {
60
- isSubmitting = false;
210
+ if (!destroyed) isSubmitting = false;
61
211
  }
62
212
  }
63
213
 
64
- function updateValue(key: string, val: unknown) {
65
- value = {
66
- ...value,
67
- [key]: val,
68
- };
69
- }
70
214
  </script>
71
215
 
72
216
  <form
217
+ novalidate
218
+ data-testid="json-schema-form"
219
+ bind:this={formElement}
220
+ aria-busy={isSubmitting}
73
221
  onsubmit={handleSubmit}
74
222
  class={cn('svadmin-u-3e7ce58d64fa svadmin-u-a217b4eaa918 svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-cd0ad9a56558 svadmin-u-0478c89a150f svadmin-u-cef5b893cf23 svadmin-u-359090c2d529', className)}
75
223
  >
76
- {#if schema['title']}
224
+ {#if schema.title}
77
225
  <div class="svadmin-u-f4cc511ff0c1 svadmin-u-65fdbade2025 svadmin-u-05faf5c801ff">
78
- <h3 class="svadmin-u-fc7473ca09eb svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">{schema['title']}</h3>
79
- {#if schema['description']}
80
- <p class="svadmin-u-359090c2d529 svadmin-u-bfa603190748 svadmin-u-15e1b1f444fe">{schema['description']}</p>
81
- {/if}
226
+ <h3 class="svadmin-u-fc7473ca09eb svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">{schema.title}</h3>
227
+ {#if schema.description}<p class="svadmin-u-359090c2d529 svadmin-u-bfa603190748 svadmin-u-15e1b1f444fe">{schema.description}</p>{/if}
82
228
  </div>
83
229
  {/if}
84
230
 
85
- <div class="svadmin-u-9c6cdfa2ba3d">
86
- {#each properties as field (field.key)}
231
+ {#if !prepared.ok || failure}
232
+ <p role="alert" data-testid="schema-form-errors">{labels[!prepared.ok ? 'invalid' : failure ?? 'invalid']}</p>
233
+ {/if}
234
+
235
+ {#snippet renderField(node: JsonSchema, path: string[], title: string, required = false)}
236
+ {@const current = readPath(path)}
237
+ {@const options = choices(node)}
238
+ {@const type = nodeType(node)}
239
+ {@const fieldLocked = locked || pathReadonly(path)}
240
+ {@const id = `${prefix}_${path.map(encodeURIComponent).join('/')}`}
241
+ {#if type === 'object' || node.properties}
242
+ <fieldset class="svadmin-u-da7c36cd8867 svadmin-u-421ac2be5045">
243
+ <legend class="svadmin-u-0214b4b355d1 svadmin-u-2689f3958069">{title}</legend>
244
+ {#if node.description}<p class="svadmin-u-d058ca6de60f svadmin-u-bfa603190748">{node.description}</p>{/if}
245
+ <div class="svadmin-u-9c6cdfa2ba3d">
246
+ {#each Object.entries(node.properties ?? {}) as [key, child] (key)}
247
+ {@render renderField(child, [...path, key], child.title ?? key, node.required?.includes(key) ?? false)}
248
+ {/each}
249
+ </div>
250
+ </fieldset>
251
+ {:else if type === 'array'}
252
+ {@const items = Array.isArray(current) ? current : []}
253
+ <fieldset class="svadmin-u-da7c36cd8867 svadmin-u-421ac2be5045">
254
+ <legend class="svadmin-u-0214b4b355d1 svadmin-u-2689f3958069">{title}</legend>
255
+ {#if node.description}<p class="svadmin-u-d058ca6de60f svadmin-u-bfa603190748">{node.description}</p>{/if}
256
+ {#each items as _, index (index)}
257
+ {@render renderField(node.items ?? { type: 'string' }, [...path, String(index)], `${title} ${index + 1}`, true)}
258
+ <Button type="button" disabled={fieldLocked} variant="ghost" size="sm" onclick={() => removeArrayItem(path, index)}>
259
+ <Trash2 class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" /> {labels.remove}
260
+ </Button>
261
+ {:else}
262
+ <span class="svadmin-u-bfa603190748">{labels.empty}</span>
263
+ {/each}
264
+ <Button type="button" disabled={fieldLocked} variant="outline" size="sm" onclick={() => addArrayItem(path, node.items ?? { type: 'string' })}>
265
+ <Plus class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" /> {labels.add}
266
+ </Button>
267
+ </fieldset>
268
+ {:else}
87
269
  <div class="svadmin-u-da7c36cd8867">
88
- <label for="json_field_{field.key}" class="svadmin-u-0214b4b355d1 svadmin-u-2689f3958069 svadmin-u-d4108abe6359">
89
- {field.title}
90
- {#if field.required}
91
- <span class="svadmin-u-811148b13d1e">*</span>
92
- {/if}
270
+ <label for={id} class="svadmin-u-0214b4b355d1 svadmin-u-2689f3958069 svadmin-u-d4108abe6359">
271
+ {title}{#if required}<span class="svadmin-u-811148b13d1e">*</span>{/if}
93
272
  </label>
94
-
95
- {#if field.description}
96
- <p class="svadmin-u-d058ca6de60f svadmin-u-bfa603190748">{field.description}</p>
97
- {/if}
98
-
99
- {#if field.enum}
100
- <select
101
- id="json_field_{field.key}"
102
- required={field.required}
103
- value={String(value[field.key] ?? field.default ?? '')}
104
- onchange={(e) => updateValue(field.key, e.currentTarget.value)}
105
- class="svadmin-u-ed8a5df7b2fb svadmin-u-6da6a3c3f741 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-d5eab218aa34 svadmin-u-359090c2d529 svadmin-u-f10f771f87e9 svadmin-u-3e94a98e1466 svadmin-u-9c1295a6914a"
106
- >
273
+ {#if node.description}<p class="svadmin-u-d058ca6de60f svadmin-u-bfa603190748">{node.description}</p>{/if}
274
+ {#if options}
275
+ <select id={id} name={path.join('.')} aria-invalid={fieldInvalid(path)} disabled={fieldLocked} required={required} value={schemaFormEnumIndex(options, current)} onchange={(event) => choose(path, event.currentTarget.value, options)} class="svadmin-u-ed8a5df7b2fb svadmin-u-6da6a3c3f741 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-d5eab218aa34 svadmin-u-359090c2d529">
107
276
  <option value="">{i18n.t('common.selectOption')}</option>
108
- {#each field.enum as option (option)}
109
- <option value={String(option)}>{option}</option>
110
- {/each}
277
+ {#each options as option, optionIndex (optionIndex)}<option value={String(optionIndex)}>{String(option)}</option>{/each}
111
278
  </select>
112
- {:else if field.type === 'boolean'}
113
- <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4 svadmin-u-6b7d6e21ccbd">
114
- <input
115
- id="json_field_{field.key}"
116
- type="checkbox"
117
- checked={Boolean(value[field.key] ?? field.default)}
118
- onchange={(e) => updateValue(field.key, e.currentTarget.checked)}
119
- class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3 svadmin-u-07389a777c1f svadmin-u-18049387f0af svadmin-u-20aaf08a7ed1 svadmin-u-4df2b13689a4 svadmin-u-34516836730d"
120
- />
121
- <span class="svadmin-u-359090c2d529 svadmin-u-bfa603190748">{field.title}</span>
122
- </div>
123
- {:else if field.type === 'number' || field.type === 'integer'}
124
- <input
125
- id="json_field_{field.key}"
126
- type="number"
127
- required={field.required}
128
- value={Number(value[field.key] ?? field.default ?? 0)}
129
- oninput={(e) => updateValue(field.key, Number(e.currentTarget.value))}
130
- class="svadmin-u-ed8a5df7b2fb svadmin-u-6da6a3c3f741 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-d5eab218aa34 svadmin-u-359090c2d529 svadmin-u-f10f771f87e9 svadmin-u-3e94a98e1466 svadmin-u-9c1295a6914a"
131
- />
279
+ {:else if type === 'boolean'}
280
+ <input id={id} name={path.join('.')} aria-invalid={fieldInvalid(path)} disabled={fieldLocked} type="checkbox" checked={Boolean(current)} onchange={(event) => writePath(path, event.currentTarget.checked)} />
281
+ {:else if type === 'number' || type === 'integer'}
282
+ <input id={id} name={path.join('.')} aria-invalid={fieldInvalid(path)} disabled={fieldLocked} type="number" step={type === 'integer' ? 1 : 'any'} required={required} value={current === undefined ? '' : String(current)} oninput={(event) => writeNumber(path, event.currentTarget)} class="svadmin-u-ed8a5df7b2fb svadmin-u-6da6a3c3f741 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-d5eab218aa34 svadmin-u-359090c2d529" />
132
283
  {:else}
133
- <input
134
- id="json_field_{field.key}"
135
- type="text"
136
- required={field.required}
137
- value={String(value[field.key] ?? field.default ?? '')}
138
- oninput={(e) => updateValue(field.key, e.currentTarget.value)}
139
- class="svadmin-u-ed8a5df7b2fb svadmin-u-6da6a3c3f741 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-d5eab218aa34 svadmin-u-359090c2d529 svadmin-u-f10f771f87e9 svadmin-u-3e94a98e1466 svadmin-u-9c1295a6914a"
140
- />
284
+ <input id={id} name={path.join('.')} aria-invalid={fieldInvalid(path)} disabled={fieldLocked} type="text" required={required} value={String(current ?? '')} oninput={(event) => writePath(path, event.currentTarget.value)} class="svadmin-u-ed8a5df7b2fb svadmin-u-6da6a3c3f741 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-d5eab218aa34 svadmin-u-359090c2d529" />
141
285
  {/if}
142
286
  </div>
287
+ {/if}
288
+ {/snippet}
289
+
290
+ <fieldset class="schema-form-fields svadmin-u-3e7ce58d64fa" disabled={locked}>
291
+ {#if prepared.ok}
292
+ <div class="svadmin-u-9c6cdfa2ba3d">
293
+ {#each Object.entries(schema.properties ?? {}) as [key, node] (key)}
294
+ {@render renderField(node, [key], node.title ?? key, schema.required?.includes(key) ?? false)}
143
295
  {/each}
144
296
  </div>
145
297
 
298
+ {/if}
146
299
  <div class="svadmin-u-173fa8f06789 svadmin-u-b950dda299d3 svadmin-u-05faf5c801ff svadmin-u-60fbb7713999 svadmin-u-77c08e015d14">
147
- <Button type="submit" size="sm" disabled={isSubmitting} class="svadmin-u-44ee8ba0a421 svadmin-u-25effcb585ab">
148
- {#if isSubmitting}
149
- <Loader2 class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c svadmin-u-afbdd13a380e" />
150
- {/if}
300
+ <Button type="submit" size="sm" disabled={locked} class="svadmin-u-44ee8ba0a421 svadmin-u-25effcb585ab">
301
+ {#if isSubmitting}<Loader2 class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c svadmin-u-afbdd13a380e" />{/if}
151
302
  {submitText}
152
303
  </Button>
153
304
  </div>
305
+ </fieldset>
154
306
  </form>
307
+
308
+ <style>
309
+ .schema-form-fields { min-width: 0; margin: 0; padding: 0; border: 0; }
310
+ </style>
@@ -1,8 +1,19 @@
1
+ import { type JsonSchemaFormSchema } from './json-schema-form-state.js';
2
+ import { type SchemaFormIssue } from './enterprise/json-schema-form.js';
3
+ type JsonSchema = JsonSchemaFormSchema;
1
4
  interface Props {
2
- schema: Record<string, unknown>;
5
+ schema: JsonSchema;
3
6
  value?: Record<string, unknown>;
4
7
  onsubmit?: (data: Record<string, unknown>) => void | Promise<void>;
5
8
  submitText?: string;
9
+ disabled?: boolean;
10
+ readonly?: boolean;
11
+ /** 只决定此实例文案;不修改全局语言。 */
12
+ locale?: string;
13
+ onerror?: (error: unknown) => void;
14
+ onvalidationerror?: (issues: SchemaFormIssue[]) => void;
15
+ /** Use a unique stable prefix when several schema forms share a page. */
16
+ idPrefix?: string;
6
17
  class?: string;
7
18
  }
8
19
  declare const JsonSchemaForm: import("svelte").Component<Props, {}, "value">;
@@ -203,9 +203,8 @@
203
203
 
204
204
  <div
205
205
  class="svadmin-u-36e579c0b41c svadmin-u-60fbb7713999 svadmin-u-8dddea0773ed svadmin-u-2cd02d11d1af svadmin-u-0fe7d7d814d0 svadmin-u-7890552ecd63"
206
- class:md:ml-[252px]={!collapsed}
206
+ data-svadmin-content-frame
207
207
  class:sidebar-content-expanded={!collapsed}
208
- class:md:ml-[70px]={collapsed}
209
208
  class:sidebar-content-collapsed={collapsed}
210
209
  >
211
210
  <!-- Header with mobile hamburger -->
@@ -2,7 +2,7 @@ declare const LazyTaskQueueDrawer: import("svelte").Component<{
2
2
  open?: boolean;
3
3
  taskProvider?: import("@svadmin/core").TaskProvider<import("@svadmin/core").TaskRecord>;
4
4
  title?: string;
5
- initialTab?: "dlq" | "tasks";
5
+ initialTab?: "tasks" | "dlq";
6
6
  }, {}, "open">;
7
7
  type LazyTaskQueueDrawer = ReturnType<typeof LazyTaskQueueDrawer>;
8
8
  export default LazyTaskQueueDrawer;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import type { ResourceRendering } from '../rendering/index.js';
2
3
  import { definedOptions } from '@svadmin/core/options';
3
4
 
4
5
  import { getResource } from '@svadmin/core';
@@ -11,6 +12,7 @@
11
12
 
12
13
  interface Props {
13
14
  resourceName: string;
15
+ rendering?: ResourceRendering | undefined;
14
16
  title?: string;
15
17
  canCreate?: boolean;
16
18
  density?: 'compact' | 'comfortable';
@@ -37,6 +39,7 @@
37
39
 
38
40
  let {
39
41
  resourceName,
42
+ rendering,
40
43
  title,
41
44
  canCreate,
42
45
  density = 'comfortable',
@@ -81,6 +84,7 @@
81
84
  {/if}
82
85
 
83
86
  <AutoTable
87
+ {rendering}
84
88
  {resourceName}
85
89
  showHeader={false}
86
90
  {density}
@@ -1,7 +1,9 @@
1
+ import type { ResourceRendering } from '../rendering/index.js';
1
2
  import type { Snippet } from 'svelte';
2
3
  import type { FieldDefinition } from '@svadmin/core';
3
4
  interface Props {
4
5
  resourceName: string;
6
+ rendering?: ResourceRendering | undefined;
5
7
  title?: string;
6
8
  canCreate?: boolean;
7
9
  density?: 'compact' | 'comfortable';
@@ -155,7 +155,7 @@
155
155
  </span>
156
156
  </div>
157
157
 
158
- <div class="svadmin-u-f3c543ad5fe9 svadmin-u-1004c0c3954c" class:grid-cols-2={socialProviders.length >= 2}>
158
+ <div class="svadmin-u-f3c543ad5fe9 svadmin-u-1004c0c3954c" class:svadmin-u-8e75e3db482b={socialProviders.length >= 2}>
159
159
  {#each socialProviders as provider (provider.name)}
160
160
  <Button
161
161
  variant="outline"
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import NumericInput from './NumericInput.svelte';
3
+
4
+ interface Props {
5
+ value?: number | null;
6
+ currency?: string;
7
+ min?: number;
8
+ max?: number;
9
+ step?: number;
10
+ precision?: number;
11
+ id?: string;
12
+ name?: string;
13
+ placeholder?: string;
14
+ disabled?: boolean;
15
+ required?: boolean;
16
+ invalid?: boolean;
17
+ describedby?: string;
18
+ class?: string;
19
+ onchange?: (value: number | null) => void;
20
+ }
21
+
22
+ let props: Props = $props();
23
+ </script>
24
+
25
+ <NumericInput {...props} mode="currency" />
@@ -0,0 +1,20 @@
1
+ interface Props {
2
+ value?: number | null;
3
+ currency?: string;
4
+ min?: number;
5
+ max?: number;
6
+ step?: number;
7
+ precision?: number;
8
+ id?: string;
9
+ name?: string;
10
+ placeholder?: string;
11
+ disabled?: boolean;
12
+ required?: boolean;
13
+ invalid?: boolean;
14
+ describedby?: string;
15
+ class?: string;
16
+ onchange?: (value: number | null) => void;
17
+ }
18
+ declare const MoneyInput: import("svelte").Component<Props, {}, "">;
19
+ type MoneyInput = ReturnType<typeof MoneyInput>;
20
+ export default MoneyInput;
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import NumericInput from './NumericInput.svelte';
3
+ import type { NumericInputMode } from './NumericInput.svelte';
4
+
5
+ interface Props {
6
+ value?: number | null;
7
+ min?: number;
8
+ max?: number;
9
+ step?: number;
10
+ precision?: number;
11
+ id?: string;
12
+ name?: string;
13
+ placeholder?: string;
14
+ disabled?: boolean;
15
+ required?: boolean;
16
+ invalid?: boolean;
17
+ describedby?: string;
18
+ class?: string;
19
+ onchange?: (value: number | null) => void;
20
+ }
21
+
22
+ let props: Props = $props();
23
+ </script>
24
+
25
+ <NumericInput {...props} mode={'number' satisfies NumericInputMode} />
@@ -0,0 +1,19 @@
1
+ interface Props {
2
+ value?: number | null;
3
+ min?: number;
4
+ max?: number;
5
+ step?: number;
6
+ precision?: number;
7
+ id?: string;
8
+ name?: string;
9
+ placeholder?: string;
10
+ disabled?: boolean;
11
+ required?: boolean;
12
+ invalid?: boolean;
13
+ describedby?: string;
14
+ class?: string;
15
+ onchange?: (value: number | null) => void;
16
+ }
17
+ declare const NumberInput: import("svelte").Component<Props, {}, "">;
18
+ type NumberInput = ReturnType<typeof NumberInput>;
19
+ export default NumberInput;