@svadmin/ui 0.88.1 → 0.92.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 (172) hide show
  1. package/dist/app.css +4 -0
  2. package/dist/app.theme.css +4 -0
  3. package/dist/component-registry.svelte.d.ts +2 -2
  4. package/dist/components/AboutSettings.svelte +23 -4
  5. package/dist/components/AdminApp.svelte +6 -4
  6. package/dist/components/AdminApp.svelte.d.ts +3 -3
  7. package/dist/components/ApiSettings.svelte +43 -27
  8. package/dist/components/ApprovalActionCard.svelte +8 -9
  9. package/dist/components/ApprovalCenter.svelte +24 -5
  10. package/dist/components/AuditLogViewer.svelte +2 -2
  11. package/dist/components/AutoComplete.svelte +610 -0
  12. package/dist/components/AutoComplete.svelte.d.ts +25 -0
  13. package/dist/components/AutoForm.svelte +265 -18
  14. package/dist/components/AutoForm.svelte.d.ts +19 -2
  15. package/dist/components/AutoTable.svelte +141 -36
  16. package/dist/components/AutoTable.svelte.d.ts +8 -0
  17. package/dist/components/BatchActionBar.svelte +42 -14
  18. package/dist/components/BatchActionBar.svelte.d.ts +5 -0
  19. package/dist/components/BoundRecordDetailDrawer.svelte +5 -3
  20. package/dist/components/CancelTaskButton.svelte +14 -1
  21. package/dist/components/Cascader.svelte +412 -53
  22. package/dist/components/Cascader.svelte.d.ts +7 -1
  23. package/dist/components/ColorPicker.svelte +1 -1
  24. package/dist/components/ColorPicker.svelte.d.ts +1 -1
  25. package/dist/components/ComboboxField.svelte +270 -34
  26. package/dist/components/ComboboxField.svelte.d.ts +4 -0
  27. package/dist/components/CommandPalette.svelte +4 -4
  28. package/dist/components/DashboardView.svelte +73 -7
  29. package/dist/components/DashboardView.svelte.d.ts +3 -1
  30. package/dist/components/DateRangeInput.svelte +8 -1
  31. package/dist/components/DetailWorkspaceAsyncSection.svelte +107 -0
  32. package/dist/components/DetailWorkspaceAsyncSection.svelte.d.ts +36 -0
  33. package/dist/components/DetailWorkspaceSections.svelte +233 -0
  34. package/dist/components/DetailWorkspaceSections.svelte.d.ts +9 -0
  35. package/dist/components/DrawerForm.svelte +78 -9
  36. package/dist/components/DrawerForm.svelte.d.ts +3 -3
  37. package/dist/components/ErrorBoundary.svelte +1 -1
  38. package/dist/components/ErrorComponent.svelte +19 -6
  39. package/dist/components/ErrorComponent.svelte.d.ts +1 -0
  40. package/dist/components/ErrorSummary.svelte +9 -4
  41. package/dist/components/FieldRenderer.svelte +203 -90
  42. package/dist/components/FieldRenderer.svelte.d.ts +16 -1
  43. package/dist/components/FileUpload.svelte +116 -51
  44. package/dist/components/FileUpload.svelte.d.ts +7 -27
  45. package/dist/components/FileUploadField.svelte +170 -0
  46. package/dist/components/FileUploadField.svelte.d.ts +21 -0
  47. package/dist/components/FilterBuilder.svelte +13 -7
  48. package/dist/components/FilterCollectionInput.svelte +9 -7
  49. package/dist/components/FilterCollectionInput.svelte.d.ts +3 -0
  50. package/dist/components/ForgotPasswordPage.svelte +1 -1
  51. package/dist/components/Header.svelte +1 -1
  52. package/dist/components/ImportWizard.svelte +197 -28
  53. package/dist/components/ImportWizard.svelte.d.ts +5 -1
  54. package/dist/components/InferencerPanel.svelte +4 -2
  55. package/dist/components/InfiniteList.svelte +52 -9
  56. package/dist/components/JsonSchemaForm.svelte +237 -42
  57. package/dist/components/JsonSchemaForm.svelte.d.ts +10 -3
  58. package/dist/components/KanbanBoard.svelte +43 -7
  59. package/dist/components/KanbanBoard.svelte.d.ts +2 -2
  60. package/dist/components/Layout.svelte +1 -1
  61. package/dist/components/LoginPage.svelte +1 -1
  62. package/dist/components/MetricStrip.svelte +8 -3
  63. package/dist/components/MetricStrip.svelte.d.ts +1 -0
  64. package/dist/components/ModalForm.svelte +85 -9
  65. package/dist/components/ModalForm.svelte.d.ts +3 -3
  66. package/dist/components/MoneyInput.svelte +5 -3
  67. package/dist/components/MoneyInput.svelte.d.ts +4 -2
  68. package/dist/components/MultiTabKeepAlive.svelte +502 -43
  69. package/dist/components/MultiTabKeepAlive.svelte.d.ts +40 -6
  70. package/dist/components/NotificationsSettings.svelte +10 -10
  71. package/dist/components/NumberInput.svelte +5 -3
  72. package/dist/components/NumberInput.svelte.d.ts +4 -2
  73. package/dist/components/NumericInput.svelte +277 -25
  74. package/dist/components/NumericInput.svelte.d.ts +3 -1
  75. package/dist/components/PercentInput.svelte +5 -3
  76. package/dist/components/PercentInput.svelte.d.ts +4 -2
  77. package/dist/components/PermissionMatrix.svelte +44 -8
  78. package/dist/components/PermissionMatrix.svelte.d.ts +1 -0
  79. package/dist/components/ProfilePage.svelte +38 -13
  80. package/dist/components/RadioGroup.svelte +203 -4
  81. package/dist/components/RadioGroup.svelte.d.ts +1 -1
  82. package/dist/components/Rate.svelte +3 -3
  83. package/dist/components/Rate.svelte.d.ts +2 -2
  84. package/dist/components/RecordDetailDrawer.svelte +1 -1
  85. package/dist/components/RecordDetailDrawer.svelte.d.ts +1 -1
  86. package/dist/components/RegisterPage.svelte +1 -1
  87. package/dist/components/RelationPicker.svelte +39 -11
  88. package/dist/components/ResourceAccessGuard.svelte +7 -2
  89. package/dist/components/RetryTaskButton.svelte +14 -1
  90. package/dist/components/RolesSettings.svelte +74 -28
  91. package/dist/components/SecuritySettings.svelte +66 -51
  92. package/dist/components/SegmentedControl.svelte +4 -2
  93. package/dist/components/SegmentedControl.svelte.d.ts +1 -1
  94. package/dist/components/Sidebar.svelte +1 -1
  95. package/dist/components/StepsForm.svelte +62 -10
  96. package/dist/components/StepsForm.svelte.d.ts +4 -2
  97. package/dist/components/SvadminLogo.svelte +1 -0
  98. package/dist/components/SvarDataGrid.svelte +42 -24
  99. package/dist/components/SvarDataGrid.svelte.d.ts +10 -10
  100. package/dist/components/SvarResourceRead.svelte +2 -0
  101. package/dist/components/SvarResourceTableView.svelte +4 -2
  102. package/dist/components/TagsInput.svelte +3 -3
  103. package/dist/components/TagsInput.svelte.d.ts +2 -2
  104. package/dist/components/TaskDetails.svelte +22 -16
  105. package/dist/components/TaskDetails.svelte.d.ts +1 -1
  106. package/dist/components/TaskList.svelte +57 -15
  107. package/dist/components/TaskQueueDrawer.svelte +54 -34
  108. package/dist/components/Toast.svelte +13 -2
  109. package/dist/components/Transfer.svelte +67 -5
  110. package/dist/components/Transfer.svelte.d.ts +4 -0
  111. package/dist/components/TreeSelect.svelte +337 -77
  112. package/dist/components/TreeSelect.svelte.d.ts +4 -0
  113. package/dist/components/TreeTable.svelte +6 -1
  114. package/dist/components/TwoFactorAuthPage.svelte +8 -4
  115. package/dist/components/UpdatePasswordPage.svelte +1 -1
  116. package/dist/components/VersionDiffViewer.svelte +9 -7
  117. package/dist/components/account/CompanyProfilePage.svelte +13 -12
  118. package/dist/components/account/MemberDirectory.svelte +14 -10
  119. package/dist/components/account/SettingsEnterprisePage.svelte +18 -18
  120. package/dist/components/content/AuditTimeline.svelte +8 -6
  121. package/dist/components/content/DataState.svelte +1 -1
  122. package/dist/components/content/FileList.svelte +44 -13
  123. package/dist/components/content/FileList.svelte.d.ts +4 -2
  124. package/dist/components/content/FileListRow.svelte +87 -0
  125. package/dist/components/content/FileListRow.svelte.d.ts +11 -0
  126. package/dist/components/content/ImportDropzone.svelte +109 -12
  127. package/dist/components/content/ImportDropzone.svelte.d.ts +1 -0
  128. package/dist/components/content/IntegrationCard.svelte +3 -3
  129. package/dist/components/content/IntegrationCard.svelte.d.ts +2 -0
  130. package/dist/components/content/MediaThumbnail.svelte +6 -2
  131. package/dist/components/detail-workspace.d.ts +60 -0
  132. package/dist/components/detail-workspace.js +3 -0
  133. package/dist/components/enterprise/json-schema-form.d.ts +1 -1
  134. package/dist/components/enterprise/recursive-schema-form.d.ts +15 -1
  135. package/dist/components/enterprise/recursive-schema-form.js +288 -14
  136. package/dist/components/enterprise/schema-form-references.d.ts +14 -0
  137. package/dist/components/enterprise/schema-form-references.js +316 -0
  138. package/dist/components/fields/ArrayField.svelte +178 -14
  139. package/dist/components/fields/ArrayField.svelte.d.ts +17 -3
  140. package/dist/components/fields/array-field-metadata.d.ts +4 -0
  141. package/dist/components/fields/array-field-metadata.js +36 -0
  142. package/dist/components/file-upload-reference.d.ts +11 -0
  143. package/dist/components/file-upload-reference.js +50 -0
  144. package/dist/components/file-upload-types.d.ts +32 -0
  145. package/dist/components/file-upload-types.js +1 -0
  146. package/dist/components/form-contract.upload-type-fixture.svelte +108 -0
  147. package/dist/components/form-contract.upload-type-fixture.svelte.d.ts +18 -0
  148. package/dist/components/json-schema-form-state.d.ts +19 -0
  149. package/dist/components/list-deletion.svelte.js +6 -1
  150. package/dist/components/list-state.svelte.d.ts +12 -1
  151. package/dist/components/list-state.svelte.js +56 -16
  152. package/dist/components/numeric-format.d.ts +44 -0
  153. package/dist/components/numeric-format.js +198 -0
  154. package/dist/components/record-detail.svelte.js +2 -0
  155. package/dist/components/saved-list-views.d.ts +9 -2
  156. package/dist/components/saved-list-views.js +42 -11
  157. package/dist/components/saved-list-views.svelte.d.ts +3 -0
  158. package/dist/components/saved-list-views.svelte.js +56 -7
  159. package/dist/components/task-action.svelte.d.ts +1 -0
  160. package/dist/components/task-action.svelte.js +55 -3
  161. package/dist/components/task-utils.d.ts +1 -0
  162. package/dist/components/task-utils.js +18 -0
  163. package/dist/components/ui/input/input.svelte +1 -1
  164. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  165. package/dist/components/ui/sheet/sheet.svelte +4 -1
  166. package/dist/components/version-history.d.ts +14 -0
  167. package/dist/components/version-history.js +32 -0
  168. package/dist/components/workspace-tab-state.d.ts +78 -0
  169. package/dist/components/workspace-tab-state.js +587 -0
  170. package/dist/index.d.ts +13 -1
  171. package/dist/index.js +4 -0
  172. package/package.json +5 -5
@@ -0,0 +1,50 @@
1
+ function text(value) {
2
+ return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
3
+ }
4
+ export function uploadReference(value) {
5
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
6
+ return undefined;
7
+ const descriptor = (key) => Object.getOwnPropertyDescriptor(value, key);
8
+ const read = (key) => {
9
+ const entry = descriptor(key);
10
+ return entry && 'value' in entry ? text(entry.value) : undefined;
11
+ };
12
+ const url = read('url');
13
+ const uploadId = read('uploadId');
14
+ if (url === undefined && uploadId === undefined)
15
+ return undefined;
16
+ return {
17
+ ...(url === undefined ? {} : { url }),
18
+ ...(uploadId === undefined ? {} : { uploadId }),
19
+ };
20
+ }
21
+ export function uploadReferenceFor(item) {
22
+ return item.status === 'success' ? uploadReference(item) : undefined;
23
+ }
24
+ /** 兼容历史字符串基线;只读取 plain object 自身数据属性。 */
25
+ export function normalizeUploadReferences(value) {
26
+ const entries = Array.isArray(value) ? value : value === null || value === undefined ? [] : [value];
27
+ return entries.flatMap(entry => {
28
+ if (typeof entry === 'string' && entry.length > 0)
29
+ return [{ url: entry }];
30
+ const reference = uploadReference(entry);
31
+ return reference === undefined ? [] : [reference];
32
+ });
33
+ }
34
+ export function uploadReferenceKey(reference) {
35
+ return JSON.stringify([reference.url ?? null, reference.uploadId ?? null]);
36
+ }
37
+ /** 纯文本回显:优先 URL,其次 uploadId,避免把引用渲染成可点击链接。 */
38
+ export function uploadReferenceLabel(reference) {
39
+ return reference.url ?? reference.uploadId ?? '';
40
+ }
41
+ export function mergeUploadReferences(baseline, uploaded) {
42
+ const seen = new Set();
43
+ return [...baseline, ...uploaded].filter(reference => {
44
+ const key = uploadReferenceKey(reference);
45
+ if (seen.has(key))
46
+ return false;
47
+ seen.add(key);
48
+ return true;
49
+ });
50
+ }
@@ -0,0 +1,32 @@
1
+ /** FileUpload 的共享队列与回调契约;单独文件便于非 Svelte 单元静态检查。 */
2
+ export type UploadItemStatus = 'queued' | 'uploading' | 'success' | 'error' | 'cancelled';
3
+ export interface UploadItem {
4
+ id: string;
5
+ file: File;
6
+ status: UploadItemStatus;
7
+ progress: number;
8
+ url?: string;
9
+ error?: string;
10
+ uploadId?: string;
11
+ cleanupStatus?: 'pending' | 'success' | 'error';
12
+ }
13
+ export interface UploadSession {
14
+ signal: AbortSignal;
15
+ idempotencyKey: string;
16
+ setUploadId: (uploadId: string) => void;
17
+ onProgress: (progress: number) => void;
18
+ }
19
+ export interface UploadCancellation {
20
+ uploadId: string;
21
+ idempotencyKey: string;
22
+ reason: 'cancel' | 'remove' | 'replace' | 'scope-change' | 'unmount';
23
+ }
24
+ export interface UploadHandler {
25
+ (file: File, session: UploadSession): Promise<{
26
+ url?: string;
27
+ uploadId?: string;
28
+ } | undefined> | Promise<void>;
29
+ }
30
+ export interface UploadCancelHandler {
31
+ (file: File, cancellation: UploadCancellation): Promise<void>;
32
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,108 @@
1
+ <script lang="ts">
2
+ import type { FieldDefinition } from '@svadmin/core';
3
+ import AutoForm from './AutoForm.svelte';
4
+ import FieldRenderer from './FieldRenderer.svelte';
5
+ import ArrayField from './fields/ArrayField.svelte';
6
+ import FileUploadField from './FileUploadField.svelte';
7
+ import type {
8
+ UploadCancellation, UploadCancelHandler, UploadHandler, UploadSession,
9
+ } from './file-upload-types.js';
10
+
11
+ type Assignable<Value extends Expected, Expected> = Value;
12
+ type UploadReceipt = { url?: string; uploadId?: string } | undefined;
13
+ type ReceiptContract = Assignable<Exclude<Awaited<ReturnType<UploadHandler>>, void>, UploadReceipt>;
14
+
15
+ const upload: UploadHandler = async (file: File, session: UploadSession) => {
16
+ const signal: AbortSignal = session.signal;
17
+ const idempotencyKey: string = session.idempotencyKey;
18
+ session.setUploadId(`upload-${file.name}`);
19
+ session.onProgress(0.5);
20
+ void signal;
21
+ void idempotencyKey;
22
+ return { url: `/uploads/${file.name}`, uploadId: `upload-${file.name}` };
23
+ };
24
+ const emptyUpload: UploadHandler = async () => {};
25
+ const cancelUpload: UploadCancelHandler = async (file: File, cancellation: UploadCancellation) => {
26
+ const uploadId: string = cancellation.uploadId;
27
+ const idempotencyKey: string = cancellation.idempotencyKey;
28
+ const reason: UploadCancellation['reason'] = cancellation.reason;
29
+ void file;
30
+ void uploadId;
31
+ void idempotencyKey;
32
+ void reason;
33
+ };
34
+
35
+ // @ts-expect-error Upload receipts expose only the checked reference identifiers.
36
+ const invalidReceipt: UploadHandler = async () => ({ localPath: '/tmp/private' });
37
+ // @ts-expect-error Cancellation reasons come from the checked lifecycle set.
38
+ const invalidReason: UploadCancellation['reason'] = 'done';
39
+
40
+ const field: FieldDefinition = { key: 'attachment', label: 'Attachment', type: 'file' };
41
+ const nestedArray: FieldDefinition = {
42
+ key: 'rows',
43
+ label: 'Rows',
44
+ type: 'array',
45
+ subFields: [
46
+ { key: 'attachment', label: 'Attachment', type: 'file' },
47
+ { key: 'children', label: 'Children', type: 'array',
48
+ subFields: [{ key: 'attachment', label: 'Attachment', type: 'file' }] },
49
+ ],
50
+ };
51
+ const value: unknown = null;
52
+ const onchange = (_next: unknown): void => {};
53
+ const onUploadStateChange = (state: { busy: boolean; unresolved: boolean }): void => {
54
+ const busy: boolean = state.busy;
55
+ const unresolved: boolean = state.unresolved;
56
+ void busy;
57
+ void unresolved;
58
+ };
59
+
60
+ const receiptContract: ReceiptContract | undefined = undefined;
61
+ void emptyUpload;
62
+ void invalidReceipt;
63
+ void invalidReason;
64
+ void receiptContract;
65
+ </script>
66
+
67
+ {#snippet uploadActions({ isLoading, isDisabled, onSubmit }: {
68
+ isLoading: boolean; isDisabled: boolean; onSubmit: () => void;
69
+ })}
70
+ {@const typedLoading: boolean = isLoading}
71
+ {@const typedDisabled: boolean = isDisabled}
72
+ <button type="button" disabled={typedDisabled} onclick={onSubmit} data-loading={typedLoading}>
73
+ Save
74
+ </button>
75
+ {/snippet}
76
+
77
+ <AutoForm
78
+ resourceName="records"
79
+ upload={upload}
80
+ cancelUpload={cancelUpload}
81
+ formActions={uploadActions}
82
+ />
83
+ <FileUploadField
84
+ {field}
85
+ {value}
86
+ {onchange}
87
+ upload={upload}
88
+ cancelUpload={cancelUpload}
89
+ {onUploadStateChange}
90
+ />
91
+ <FieldRenderer
92
+ {field}
93
+ {value}
94
+ {onchange}
95
+ upload={upload}
96
+ cancelUpload={cancelUpload}
97
+ fileReferenceMode
98
+ {onUploadStateChange}
99
+ />
100
+ <ArrayField
101
+ field={nestedArray}
102
+ {value}
103
+ {onchange}
104
+ upload={upload}
105
+ cancelUpload={cancelUpload}
106
+ fileReferenceMode
107
+ {onUploadStateChange}
108
+ />
@@ -0,0 +1,18 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const FormContract: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type FormContract = InstanceType<typeof FormContract>;
18
+ export default FormContract;
@@ -1,8 +1,16 @@
1
1
  /** 嵌套表单的草稿与传输语义;不是替代服务端的 JSON Schema 校验器。 */
2
2
  export interface JsonSchemaFormSchema {
3
+ $schema?: string;
4
+ $id?: string;
5
+ $comment?: string;
6
+ $ref?: string;
7
+ $defs?: Record<string, JsonSchemaFormSchema>;
3
8
  type?: string | string[];
4
9
  const?: string | number | boolean | null;
5
10
  readOnly?: boolean;
11
+ writeOnly?: boolean;
12
+ deprecated?: boolean;
13
+ examples?: unknown[];
6
14
  minimum?: number;
7
15
  maximum?: number;
8
16
  exclusiveMinimum?: number;
@@ -13,13 +21,24 @@ export interface JsonSchemaFormSchema {
13
21
  minItems?: number;
14
22
  maxItems?: number;
15
23
  additionalProperties?: boolean;
24
+ pattern?: string;
16
25
  title?: string;
17
26
  description?: string;
18
27
  properties?: Record<string, JsonSchemaFormSchema>;
19
28
  items?: JsonSchemaFormSchema;
29
+ oneOf?: JsonSchemaFormSchema[];
30
+ discriminator?: {
31
+ propertyName: string;
32
+ };
20
33
  enum?: Array<string | number | boolean | null>;
21
34
  default?: unknown;
22
35
  required?: string[];
36
+ visibleWhen?: {
37
+ path: readonly string[];
38
+ equals?: string | number | boolean | null;
39
+ notEquals?: string | number | boolean | null;
40
+ exists?: boolean;
41
+ };
23
42
  }
24
43
  export declare const SCHEMA_FORM_LIMITS: Readonly<{
25
44
  depth: 32;
@@ -6,6 +6,7 @@ export function createListDeletion(options) {
6
6
  let message = $state('');
7
7
  let pending = $state(false);
8
8
  let activeDelete;
9
+ let criteriaVersion = 0;
9
10
  $effect(() => {
10
11
  if (request && !options.permission().isLoading && !options.permission().allowed) {
11
12
  open = false;
@@ -38,6 +39,7 @@ export function createListDeletion(options) {
38
39
  request = null;
39
40
  }
40
41
  function cancelBatchOnCriteriaChange() {
42
+ criteriaVersion += 1;
41
43
  if (request?.batch && !pending)
42
44
  cancel();
43
45
  }
@@ -58,6 +60,7 @@ export function createListDeletion(options) {
58
60
  return;
59
61
  }
60
62
  const scopeIsCurrent = options.captureScope();
63
+ const confirmedCriteriaVersion = criteriaVersion;
61
64
  const token = {};
62
65
  activeDelete = token;
63
66
  pending = true;
@@ -66,7 +69,9 @@ export function createListDeletion(options) {
66
69
  const selectionBeforeDelete = clearBeforeMutation ? selection.captureExplicit() : undefined;
67
70
  if (clearBeforeMutation)
68
71
  options.clearSelection();
69
- const current = () => activeDelete === token && scopeIsCurrent();
72
+ const current = () => activeDelete === token
73
+ && scopeIsCurrent()
74
+ && criteriaVersion === confirmedCriteriaVersion;
70
75
  try {
71
76
  await options.mutate([...confirmedRequest.ids]);
72
77
  if (!current())
@@ -1,7 +1,13 @@
1
1
  import type { FieldDefinition, Filter, Pagination, Sort, readURLState, writeURLState } from '@svadmin/core';
2
2
  import type { SavedListViewState } from './saved-list-views.js';
3
3
  type URLState = ReturnType<typeof readURLState>;
4
- type ListStateSnapshot = Pick<SavedListViewState, 'search' | 'filters' | 'sorters' | 'pagination'>;
4
+ /** AutoTable 可恢复的列表查询快照;不包含列可见性、草稿或远程视图元数据。 */
5
+ export interface ListStateSnapshot {
6
+ search: string;
7
+ filters: Filter[];
8
+ sorters: Sort[];
9
+ pagination: Pagination;
10
+ }
5
11
  interface ListStateOptions {
6
12
  fields: () => FieldDefinition[];
7
13
  defaultPageSize: () => number;
@@ -9,12 +15,17 @@ interface ListStateOptions {
9
15
  externalPagination: () => Pagination | undefined;
10
16
  externalSorters: () => Sort[] | undefined;
11
17
  initialURLState: URLState;
18
+ initialListState?: ListStateSnapshot | undefined;
12
19
  initialViewState: SavedListViewState | undefined;
13
20
  syncWithLocation: () => boolean;
14
21
  writeURLState: (state: Parameters<typeof writeURLState>[0]) => void;
15
22
  onDirty: () => void;
23
+ onStateChange?: ((state: ListStateSnapshot) => void) | undefined;
16
24
  }
17
25
  export declare function cloneListFilter(filter: Filter): Filter;
26
+ export declare function cloneListStateSnapshot(state: ListStateSnapshot): ListStateSnapshot;
27
+ /** 将可恢复的查询快照收敛为持久化视图契约;分页可选字段在写入前补齐。 */
28
+ export declare function captureSavedListViewState(snapshot: ListStateSnapshot, columns: Pick<SavedListViewState, 'columnVisibility' | 'columnOrder'>, fallbackPageSize: number): SavedListViewState;
18
29
  export declare function createListState(options: ListStateOptions): {
19
30
  pagination: Pagination;
20
31
  sorters: Sort[];
@@ -11,6 +11,28 @@ export function cloneListFilter(filter) {
11
11
  }
12
12
  return { operator: filter.operator, value: filter.value.map(cloneListFilter) };
13
13
  }
14
+ export function cloneListStateSnapshot(state) {
15
+ return {
16
+ search: state.search,
17
+ filters: state.filters.map(cloneListFilter),
18
+ sorters: state.sorters.map(sorter => ({ ...sorter })),
19
+ pagination: { ...state.pagination },
20
+ };
21
+ }
22
+ /** 将可恢复的查询快照收敛为持久化视图契约;分页可选字段在写入前补齐。 */
23
+ export function captureSavedListViewState(snapshot, columns, fallbackPageSize) {
24
+ return {
25
+ search: snapshot.search,
26
+ filters: snapshot.filters.map(cloneListFilter),
27
+ sorters: snapshot.sorters.map(sorter => ({ ...sorter })),
28
+ pagination: {
29
+ current: snapshot.pagination.current ?? 1,
30
+ pageSize: snapshot.pagination.pageSize ?? fallbackPageSize,
31
+ },
32
+ columnVisibility: { ...columns.columnVisibility },
33
+ columnOrder: [...columns.columnOrder],
34
+ };
35
+ }
14
36
  function editableValues(filters, fields) {
15
37
  const keys = new Set(fields.filter(field => field.filterable).map(field => field.key));
16
38
  const counts = new Map();
@@ -28,23 +50,29 @@ function editableValues(filters, fields) {
28
50
  return values;
29
51
  }
30
52
  export function createListState(options) {
31
- const url = options.initialURLState;
53
+ const url = untrack(() => options.syncWithLocation()) ? options.initialURLState : {};
54
+ const initialListState = options.initialListState
55
+ ? cloneListStateSnapshot(options.initialListState)
56
+ : undefined;
32
57
  const initialView = options.initialViewState;
33
- let pagination = $state(untrack(() => options.externalPagination() ?? {
34
- current: url.page ?? initialView?.pagination.current ?? 1,
35
- pageSize: url.pageSize ?? initialView?.pagination.pageSize ?? options.defaultPageSize(),
36
- }));
58
+ const initialPagination = initialListState?.pagination ?? initialView?.pagination;
59
+ let pagination = $state(untrack(() => options.externalPagination() ?? definedOptions({
60
+ ...initialPagination,
61
+ current: url.page ?? initialPagination?.current ?? 1,
62
+ pageSize: url.pageSize ?? initialPagination?.pageSize ?? options.defaultPageSize(),
63
+ })));
37
64
  let sorters = $state(untrack(() => {
38
65
  const defaultSort = options.defaultSort();
39
66
  return options.externalSorters() ?? (url.sortField
40
67
  ? [{ field: url.sortField, order: url.sortOrder ?? 'asc' }]
41
- : initialView?.sorters ?? (defaultSort ? [defaultSort] : []));
68
+ : initialListState?.sorters ?? initialView?.sorters ?? (defaultSort ? [defaultSort] : []));
42
69
  }));
43
- let filters = $state(url.filters ?? initialView?.filters ?? []);
70
+ let filters = $state(url.filters ?? initialListState?.filters ?? initialView?.filters ?? []);
44
71
  let filterValues = $state(untrack(() => editableValues(filters, options.fields())));
45
- let searchText = $state(url.search ?? initialView?.search ?? '');
72
+ let searchText = $state(url.search ?? initialListState?.search ?? initialView?.search ?? '');
46
73
  let appliedSearchText = $state(untrack(() => searchText));
47
74
  let searchTimer;
75
+ let lastNotifiedState = untrack(() => JSON.stringify(captureState()));
48
76
  const searchableFields = $derived(options.fields().filter(field => field.searchable));
49
77
  const filterableFields = $derived(options.fields().filter(field => field.filterable));
50
78
  const activeFilterItems = $derived.by(() => {
@@ -101,6 +129,14 @@ export function createListState(options) {
101
129
  filters,
102
130
  });
103
131
  });
132
+ $effect(() => {
133
+ const snapshot = captureState();
134
+ const serialized = JSON.stringify(snapshot);
135
+ if (serialized === lastNotifiedState)
136
+ return;
137
+ lastNotifiedState = serialized;
138
+ untrack(() => options.onStateChange?.(snapshot));
139
+ });
104
140
  $effect(() => cancelSearch);
105
141
  function cancelSearch() {
106
142
  if (searchTimer !== undefined)
@@ -175,20 +211,24 @@ export function createListState(options) {
175
211
  search: appliedSearchText,
176
212
  filters: filters.map(cloneListFilter),
177
213
  sorters: sorters.map(sorter => ({ ...sorter })),
178
- pagination: {
214
+ pagination: definedOptions({
179
215
  current: pagination.current ?? 1,
180
216
  pageSize: pagination.pageSize ?? options.defaultPageSize(),
181
- },
217
+ mode: pagination.mode,
218
+ }),
182
219
  };
183
220
  }
184
221
  function applyState(state) {
185
222
  cancelSearch();
186
- searchText = state.search;
187
- appliedSearchText = state.search;
188
- filters = state.filters;
189
- filterValues = editableValues(state.filters, options.fields());
190
- pagination = { ...pagination, ...state.pagination };
191
- sorters = state.sorters;
223
+ const snapshot = cloneListStateSnapshot(state);
224
+ const controlledPagination = options.externalPagination();
225
+ const controlledSorters = options.externalSorters();
226
+ searchText = snapshot.search;
227
+ appliedSearchText = snapshot.search;
228
+ filters = snapshot.filters;
229
+ filterValues = editableValues(snapshot.filters, options.fields());
230
+ pagination = controlledPagination ?? { ...pagination, ...snapshot.pagination };
231
+ sorters = controlledSorters ?? snapshot.sorters;
192
232
  }
193
233
  function resetToDefaults() {
194
234
  cancelSearch();
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Minimal, explicit locale contract for the numeric input family.
3
+ *
4
+ * Only the locales below are accepted by the public `locale` prop. The default
5
+ * (no `locale`) keeps the legacy `type="number"` behaviour untouched. Adding a
6
+ * locale is a product decision and must come with its own parsing/screen-reader
7
+ * matrix instead of silently widening the contract.
8
+ */
9
+ export declare const NUMERIC_INPUT_LOCALES: readonly ["en-US", "de-DE", "zh-CN"];
10
+ export type NumericInputLocale = (typeof NUMERIC_INPUT_LOCALES)[number];
11
+ export type NumericParseResult = {
12
+ ok: true;
13
+ value: number | null;
14
+ } | {
15
+ ok: false;
16
+ };
17
+ export declare function isNumericInputLocale(value: string | null | undefined): value is NumericInputLocale;
18
+ export declare function clampNumericPrecision(precision: number | null | undefined): number | undefined;
19
+ /**
20
+ * Formats a stored value for display. `precision` only rounds the display; the
21
+ * caller keeps the original stored number. Intl's default rounding is used.
22
+ */
23
+ export declare function formatNumericValue(locale: NumericInputLocale, value: number | null, precision?: number | null): string;
24
+ /**
25
+ * Editable text mirrors formatting but omits grouping so that inserting a digit
26
+ * does not make an otherwise valid number look like a mixed-separator error.
27
+ */
28
+ export declare function formatNumericEditingText(locale: NumericInputLocale, value: number | null, precision?: number | null): string;
29
+ /**
30
+ * Parses locale-formatted text into a stored value. Group separators are
31
+ * display-only and never become part of the number. Invalid or mixed input
32
+ * returns `{ ok: false }` without echoing the raw text or an error message.
33
+ */
34
+ export declare function parseNumericInput(locale: NumericInputLocale, text: string): NumericParseResult;
35
+ /** Rounds a stepped value to avoid floating-point drift when precision is set. */
36
+ export declare function roundToNumericPrecision(value: number, precision: number | null | undefined): number;
37
+ /**
38
+ * Adds one step without rounding to the display precision. Display precision
39
+ * must not erase an explicit smaller step; decimal scaling only protects the
40
+ * arithmetic itself. Scaling is used while its integers stay inside
41
+ * Number.MAX_SAFE_INTEGER; beyond that, direct Number addition is the only
42
+ * truthful result. Non-finite results keep the original value.
43
+ */
44
+ export declare function addNumericStep(base: number, step: number, direction?: 1 | -1): number;