@svadmin/surface 0.8.19 → 0.9.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 (191) hide show
  1. package/README.md +70 -38
  2. package/STYLING.md +31 -0
  3. package/compatibility.json +23 -2
  4. package/dist/agent-contract.d.ts +37 -0
  5. package/dist/agent-contract.js +203 -0
  6. package/dist/agent.d.ts +38 -7
  7. package/dist/agent.js +98 -59
  8. package/dist/binding.d.ts +2 -0
  9. package/dist/binding.js +7 -1
  10. package/dist/builtin-definitions.d.ts +6 -0
  11. package/dist/builtin-definitions.js +40 -0
  12. package/dist/builtin-schemas.d.ts +25 -0
  13. package/dist/builtin-schemas.js +13 -0
  14. package/dist/catalog.d.ts +4 -61
  15. package/dist/catalog.js +20 -44
  16. package/dist/components/MetricWidget.svelte +12 -8
  17. package/dist/components/ResourceFormBody.svelte +97 -0
  18. package/dist/components/ResourceFormBody.svelte.d.ts +13 -0
  19. package/dist/components/ResourceFormWidget.svelte +19 -0
  20. package/dist/components/ResourceFormWidget.svelte.d.ts +4 -0
  21. package/dist/components/ResourceTableWidget.svelte +14 -12
  22. package/dist/components/SurfaceEditPreview.svelte +107 -0
  23. package/dist/components/SurfaceEditPreview.svelte.d.ts +20 -0
  24. package/dist/components/SurfaceRenderer.svelte +120 -163
  25. package/dist/components/SurfaceRenderer.svelte.d.ts +4 -0
  26. package/dist/components/SurfaceWorkflowProvider.svelte +22 -0
  27. package/dist/components/SurfaceWorkflowProvider.svelte.d.ts +17 -0
  28. package/dist/components/SvarGridWidget.svelte +47 -0
  29. package/dist/components/SvarGridWidget.svelte.d.ts +4 -0
  30. package/dist/components/SvarSurfaceProvider.svelte +12 -0
  31. package/dist/components/SvarSurfaceProvider.svelte.d.ts +11 -0
  32. package/dist/edits.d.ts +57 -0
  33. package/dist/edits.js +166 -0
  34. package/dist/index.d.ts +8 -2
  35. package/dist/index.js +5 -1
  36. package/dist/interactive.d.ts +6 -0
  37. package/dist/interactive.js +15 -0
  38. package/dist/openui.d.ts +64 -0
  39. package/dist/openui.js +168 -0
  40. package/dist/schema.d.ts +108 -0
  41. package/dist/schema.js +73 -0
  42. package/dist/server-sqlite.d.ts +1 -0
  43. package/dist/server-sqlite.js +2 -0
  44. package/dist/server.d.ts +4 -0
  45. package/dist/server.js +4 -0
  46. package/dist/source-cache.d.ts +30 -0
  47. package/dist/source-cache.js +135 -0
  48. package/dist/styled-system/css/conditions.js +36 -0
  49. package/dist/styled-system/css/css.d.ts +22 -0
  50. package/dist/styled-system/css/css.js +45 -0
  51. package/dist/styled-system/css/cva.d.ts +6 -0
  52. package/dist/styled-system/css/cva.js +87 -0
  53. package/dist/styled-system/css/cx.d.ts +5 -0
  54. package/dist/styled-system/css/cx.js +15 -0
  55. package/dist/styled-system/css/index.d.ts +5 -0
  56. package/dist/styled-system/css/index.js +4 -0
  57. package/dist/styled-system/css/sva.d.ts +4 -0
  58. package/dist/styled-system/css/sva.js +46 -0
  59. package/dist/styled-system/design-contract.d.ts +13 -0
  60. package/dist/styled-system/design-contract.js +11 -0
  61. package/dist/styled-system/helpers.js +316 -0
  62. package/dist/styled-system/patterns/aspect-ratio.d.ts +20 -0
  63. package/dist/styled-system/patterns/aspect-ratio.js +38 -0
  64. package/dist/styled-system/patterns/bleed.d.ts +21 -0
  65. package/dist/styled-system/patterns/bleed.js +24 -0
  66. package/dist/styled-system/patterns/box.d.ts +20 -0
  67. package/dist/styled-system/patterns/box.js +15 -0
  68. package/dist/styled-system/patterns/center.d.ts +20 -0
  69. package/dist/styled-system/patterns/center.js +21 -0
  70. package/dist/styled-system/patterns/circle.d.ts +20 -0
  71. package/dist/styled-system/patterns/circle.js +25 -0
  72. package/dist/styled-system/patterns/container.d.ts +20 -0
  73. package/dist/styled-system/patterns/container.js +21 -0
  74. package/dist/styled-system/patterns/cq.d.ts +21 -0
  75. package/dist/styled-system/patterns/cq.js +21 -0
  76. package/dist/styled-system/patterns/divider.d.ts +22 -0
  77. package/dist/styled-system/patterns/divider.js +25 -0
  78. package/dist/styled-system/patterns/flex.d.ts +26 -0
  79. package/dist/styled-system/patterns/flex.js +26 -0
  80. package/dist/styled-system/patterns/float.d.ts +23 -0
  81. package/dist/styled-system/patterns/float.js +52 -0
  82. package/dist/styled-system/patterns/grid-item.d.ts +25 -0
  83. package/dist/styled-system/patterns/grid-item.js +25 -0
  84. package/dist/styled-system/patterns/grid.d.ts +24 -0
  85. package/dist/styled-system/patterns/grid.js +27 -0
  86. package/dist/styled-system/patterns/hstack.d.ts +21 -0
  87. package/dist/styled-system/patterns/hstack.js +24 -0
  88. package/dist/styled-system/patterns/index.d.ts +21 -0
  89. package/dist/styled-system/patterns/index.js +20 -0
  90. package/dist/styled-system/patterns/link-overlay.d.ts +20 -0
  91. package/dist/styled-system/patterns/link-overlay.js +24 -0
  92. package/dist/styled-system/patterns/spacer.d.ts +20 -0
  93. package/dist/styled-system/patterns/spacer.js +25 -0
  94. package/dist/styled-system/patterns/square.d.ts +20 -0
  95. package/dist/styled-system/patterns/square.js +24 -0
  96. package/dist/styled-system/patterns/stack.d.ts +23 -0
  97. package/dist/styled-system/patterns/stack.js +24 -0
  98. package/dist/styled-system/patterns/visually-hidden.d.ts +20 -0
  99. package/dist/styled-system/patterns/visually-hidden.js +18 -0
  100. package/dist/styled-system/patterns/vstack.d.ts +21 -0
  101. package/dist/styled-system/patterns/vstack.js +24 -0
  102. package/dist/styled-system/patterns/wrap.d.ts +24 -0
  103. package/dist/styled-system/patterns/wrap.js +25 -0
  104. package/dist/styled-system/recipes/content-header.d.ts +31 -0
  105. package/dist/styled-system/recipes/content-header.js +65 -0
  106. package/dist/styled-system/recipes/content-page.d.ts +34 -0
  107. package/dist/styled-system/recipes/content-page.js +43 -0
  108. package/dist/styled-system/recipes/create-recipe.js +82 -0
  109. package/dist/styled-system/recipes/index.d.ts +17 -0
  110. package/dist/styled-system/recipes/index.js +16 -0
  111. package/dist/styled-system/recipes/metric-block.d.ts +34 -0
  112. package/dist/styled-system/recipes/metric-block.js +76 -0
  113. package/dist/styled-system/recipes/product-list.d.ts +31 -0
  114. package/dist/styled-system/recipes/product-list.js +73 -0
  115. package/dist/styled-system/recipes/product-section.d.ts +31 -0
  116. package/dist/styled-system/recipes/product-section.js +49 -0
  117. package/dist/styled-system/recipes/product-settings-row.d.ts +34 -0
  118. package/dist/styled-system/recipes/product-settings-row.js +58 -0
  119. package/dist/styled-system/recipes/product-settings.d.ts +31 -0
  120. package/dist/styled-system/recipes/product-settings.js +57 -0
  121. package/dist/styled-system/recipes/product-status.d.ts +34 -0
  122. package/dist/styled-system/recipes/product-status.js +45 -0
  123. package/dist/styled-system/recipes/product-toolbar.d.ts +31 -0
  124. package/dist/styled-system/recipes/product-toolbar.js +41 -0
  125. package/dist/styled-system/recipes/product-workspace.d.ts +34 -0
  126. package/dist/styled-system/recipes/product-workspace.js +58 -0
  127. package/dist/styled-system/recipes/surface-metric.d.ts +38 -0
  128. package/dist/styled-system/recipes/surface-metric.js +63 -0
  129. package/dist/styled-system/recipes/surface-table.d.ts +34 -0
  130. package/dist/styled-system/recipes/surface-table.js +62 -0
  131. package/dist/styled-system/recipes/ui-badge.d.ts +31 -0
  132. package/dist/styled-system/recipes/ui-badge.js +38 -0
  133. package/dist/styled-system/recipes/ui-button.d.ts +32 -0
  134. package/dist/styled-system/recipes/ui-button.js +43 -0
  135. package/dist/styled-system/recipes/ui-input.d.ts +31 -0
  136. package/dist/styled-system/recipes/ui-input.js +49 -0
  137. package/dist/styled-system/recipes/ui-textarea.d.ts +31 -0
  138. package/dist/styled-system/recipes/ui-textarea.js +24 -0
  139. package/dist/styled-system/styles.css +1257 -0
  140. package/dist/styled-system/tokens/index.d.ts +9 -0
  141. package/dist/styled-system/tokens/index.js +268 -0
  142. package/dist/styled-system/tokens/tokens.d.ts +36 -0
  143. package/dist/styled-system/types/composition.d.ts +227 -0
  144. package/dist/styled-system/types/conditions.d.ts +236 -0
  145. package/dist/styled-system/types/csstype.d.ts +22570 -0
  146. package/dist/styled-system/types/index.d.ts +6 -0
  147. package/dist/styled-system/types/parts.d.ts +8 -0
  148. package/dist/styled-system/types/pattern.d.ts +78 -0
  149. package/dist/styled-system/types/prop-type.d.ts +223 -0
  150. package/dist/styled-system/types/recipe.d.ts +181 -0
  151. package/dist/styled-system/types/selectors.d.ts +59 -0
  152. package/dist/styled-system/types/static-css.d.ts +56 -0
  153. package/dist/styled-system/types/style-props.d.ts +8088 -0
  154. package/dist/styled-system/types/system-types.d.ts +151 -0
  155. package/dist/styles/editor.css +124 -0
  156. package/dist/styles/editor.generated.d.ts +8 -0
  157. package/dist/styles/editor.generated.js +9 -0
  158. package/dist/styles.css +1262 -0
  159. package/dist/svar-catalog.d.ts +3 -0
  160. package/dist/svar-catalog.js +7 -0
  161. package/dist/svar-context.d.ts +9 -0
  162. package/dist/svar-context.js +1 -0
  163. package/dist/svar-schema.d.ts +21 -0
  164. package/dist/svar-schema.js +36 -0
  165. package/dist/svar.d.ts +4 -0
  166. package/dist/svar.js +3 -0
  167. package/dist/svelte.d.ts +4 -2
  168. package/dist/svelte.js +2 -1
  169. package/dist/types.d.ts +4 -0
  170. package/dist/validation.js +2 -102
  171. package/dist/wire.d.ts +13 -0
  172. package/dist/wire.js +30 -0
  173. package/dist/workflows/action-contracts.d.ts +7 -0
  174. package/dist/workflows/action-contracts.js +54 -0
  175. package/dist/workflows/catalog.d.ts +20 -0
  176. package/dist/workflows/catalog.js +50 -0
  177. package/dist/workflows/client.d.ts +45 -0
  178. package/dist/workflows/client.js +125 -0
  179. package/dist/workflows/context.d.ts +9 -0
  180. package/dist/workflows/context.js +5 -0
  181. package/dist/workflows/openui-guard.d.ts +19 -0
  182. package/dist/workflows/openui-guard.js +209 -0
  183. package/dist/workflows/service.d.ts +58 -0
  184. package/dist/workflows/service.js +296 -0
  185. package/dist/workflows/sqlite-store.d.ts +20 -0
  186. package/dist/workflows/sqlite-store.js +91 -0
  187. package/dist/workflows/types.d.ts +103 -0
  188. package/dist/workflows/types.js +8 -0
  189. package/dist/workflows.d.ts +5 -0
  190. package/dist/workflows.js +4 -0
  191. package/package.json +52 -8
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ import type { SurfaceWidgetRendererProps } from '../catalog.js';
3
+ import { getSurfaceWorkflowHost } from '../workflows/context.js';
4
+ import ResourceFormBody from './ResourceFormBody.svelte';
5
+ let { widgetId, props, locale = 'en-US' }: SurfaceWidgetRendererProps = $props();
6
+ const host = getSurfaceWorkflowHost();
7
+ const action = $derived(host?.getAction(String(props['actionId'])));
8
+ const zh = $derived(locale.startsWith('zh'));
9
+ </script>
10
+
11
+ {#if host && action}
12
+ {#key `${action.id}:${action.version}`}
13
+ <ResourceFormBody {widgetId} {host} {action} title={typeof props['title'] === 'string' ? props['title'] : action.label} {locale} />
14
+ {/key}
15
+ {:else}
16
+ <section role="status" data-surface-form-unavailable>
17
+ {zh ? '该表单尚未连接到已授权的业务操作。' : 'This form is not connected to an authorized business action.'}
18
+ </section>
19
+ {/if}
@@ -0,0 +1,4 @@
1
+ import type { SurfaceWidgetRendererProps } from '../catalog.js';
2
+ declare const ResourceFormWidget: import("svelte").Component<SurfaceWidgetRendererProps, {}, "">;
3
+ type ResourceFormWidget = ReturnType<typeof ResourceFormWidget>;
4
+ export default ResourceFormWidget;
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { Value } from '@sinclair/typebox/value';
3
+ import { surfaceTable } from '../styled-system/recipes/index.js';
3
4
  import CardContent from '@svadmin/ui/components/ui/card/card-content.svelte';
4
5
  import CardHeader from '@svadmin/ui/components/ui/card/card-header.svelte';
5
6
  import CardTitle from '@svadmin/ui/components/ui/card/card-title.svelte';
@@ -10,7 +11,7 @@
10
11
  import TableHeader from '@svadmin/ui/components/ui/table/table-header.svelte';
11
12
  import TableRow from '@svadmin/ui/components/ui/table/table-row.svelte';
12
13
  import Table from '@svadmin/ui/components/ui/table/table.svelte';
13
- import { resourceTablePropsSchema } from '../builtin-schemas.js';
14
+ import { styledResourceTablePropsSchema as resourceTablePropsSchema } from '../builtin-schemas.js';
14
15
  import type { SurfaceWidgetRendererProps } from '../catalog.js';
15
16
  import { resolveSurfaceMessages } from '../localization.js';
16
17
  import { asRecordArray, displayTableValue } from '../widget-data.js';
@@ -19,27 +20,28 @@
19
20
  const activeMessages = $derived(resolveSurfaceMessages(locale, messages));
20
21
 
21
22
  const tableProps = $derived(Value.Decode(resourceTablePropsSchema, props));
23
+ const tableClasses = $derived(tableProps.density === undefined ? undefined : surfaceTable({ density: tableProps.density }));
22
24
  const records = $derived(data.status === 'ready' ? asRecordArray(data.value) : null);
23
25
  </script>
24
26
 
25
- <Card>
26
- <CardHeader>
27
+ <Card class={tableClasses?.root} data-surface-density={tableProps.density}>
28
+ <CardHeader class={tableClasses?.header}>
27
29
  <CardTitle>{tableProps.title}</CardTitle>
28
30
  </CardHeader>
29
- <CardContent>
31
+ <CardContent class={tableClasses?.content}>
30
32
  {#if data.status === 'loading'}
31
- <div class="table-state table-loading" role="status">{activeMessages.tableLoading}</div>
33
+ <div class={"table-state table-loading " + (tableClasses?.state ?? "")} role="status">{activeMessages.tableLoading}</div>
32
34
  {:else if data.status === 'empty'}
33
- <p class="table-state" role="status">{tableProps.emptyLabel ?? activeMessages.tableNoRecords}</p>
35
+ <p class={"table-state " + (tableClasses?.state ?? "")} role="status">{tableProps.emptyLabel ?? activeMessages.tableNoRecords}</p>
34
36
  {:else if data.status === 'error'}
35
- <p class="table-state" role="alert">{data.error.message}</p>
37
+ <p class={"table-state " + (tableClasses?.state ?? "")} role="alert">{data.error.message}</p>
36
38
  {:else if data.status === 'ready' && records?.ok}
37
39
  <div class="surface-table">
38
- <Table aria-label={tableProps.title}>
40
+ <Table density={tableProps.density ?? "comfortable"} aria-label={tableProps.title}>
39
41
  <TableHeader>
40
42
  <TableRow>
41
43
  {#each tableProps.columns as column (column.field)}
42
- <TableHead scope="col">{column.label}</TableHead>
44
+ <TableHead class={tableClasses?.head} scope="col">{column.label}</TableHead>
43
45
  {/each}
44
46
  </TableRow>
45
47
  </TableHeader>
@@ -47,7 +49,7 @@
47
49
  {#each records.value as record, index (index)}
48
50
  <TableRow>
49
51
  {#each tableProps.columns as column (column.field)}
50
- <TableCell>{displayTableValue(record[column.field], {
52
+ <TableCell class={tableClasses?.cell}>{displayTableValue(record[column.field], {
51
53
  format: column.format,
52
54
  locale,
53
55
  messages: activeMessages,
@@ -59,7 +61,7 @@
59
61
  </Table>
60
62
  </div>
61
63
  {:else}
62
- <p class="table-state" role="alert">{records && !records.ok ? activeMessages.tableInvalidData : activeMessages.tableUnavailable}</p>
64
+ <p class={"table-state " + (tableClasses?.state ?? "")} role="alert">{records && !records.ok ? activeMessages.tableInvalidData : activeMessages.tableUnavailable}</p>
63
65
  {/if}
64
66
  </CardContent>
65
67
  </Card>
@@ -67,7 +69,7 @@
67
69
  <style>
68
70
  .table-state {
69
71
  display: grid;
70
- min-height: 8rem;
72
+ min-height: var(--svadmin-table-state-height, 8rem);
71
73
  margin: 0;
72
74
  place-items: center;
73
75
  color: var(--muted-foreground);
@@ -0,0 +1,107 @@
1
+ <script lang="ts">
2
+ import { useTranslation } from '@svadmin/core/i18n';
3
+ import { applySurfaceEditProposal } from '../edits.js';
4
+ import type { SurfaceRevision } from '../edits.js';
5
+ import { defaultSurfaceCatalog } from '../catalog.js';
6
+ import type { SurfaceRenderCatalog } from '../catalog.js';
7
+ import type { SurfaceDataProvider, SurfacePolicy } from '../types.js';
8
+ import SurfaceRenderer from './SurfaceRenderer.svelte';
9
+ import { editorClasses, editorButtonClasses } from '../styles/editor.generated.js';
10
+ import '../styles/editor.css';
11
+
12
+ export interface SurfaceEditPreviewProps {
13
+ readonly revision: SurfaceRevision;
14
+ readonly proposal?: unknown;
15
+ readonly streaming?: boolean;
16
+ readonly policy: SurfacePolicy;
17
+ readonly catalog?: SurfaceRenderCatalog;
18
+ readonly dataProvider?: SurfaceDataProvider;
19
+ readonly scopeKey?: string;
20
+ readonly locale?: string;
21
+ readonly density?: 'compact' | 'comfortable';
22
+ /** 用户确认后返回候选快照。宿主负责持久化 CAS 与服务端授权。 */
23
+ readonly onApply?: (candidate: SurfaceRevision) => void | Promise<void>;
24
+ }
25
+
26
+ let {
27
+ revision, proposal, streaming = false, policy, catalog = defaultSurfaceCatalog,
28
+ dataProvider, scopeKey = '', locale, density = 'comfortable', onApply,
29
+ }: SurfaceEditPreviewProps = $props();
30
+ const i18n = useTranslation();
31
+ const activeLocale = $derived(locale ?? i18n.locale);
32
+ const labels = $derived(activeLocale.startsWith('zh') ? {
33
+ title: '界面修改提案', current: '当前版本', preview: '预览修改', back: '返回当前',
34
+ apply: '确认应用', loading: '正在接收提案;当前界面保持不变', empty: '尚无修改提案',
35
+ invalid: '提案未通过校验,不能应用', ready: '提案已校验,尚未应用',
36
+ previewing: '正在预览,尚未应用', applying: '正在应用', failed: '应用失败,请重新确认后重试',
37
+ readonly: '当前宿主未配置应用操作',
38
+ } : {
39
+ title: 'Surface edit proposal', current: 'Current revision', preview: 'Preview changes', back: 'Back to current',
40
+ apply: 'Apply changes', loading: 'Receiving proposal; current surface is unchanged', empty: 'No edit proposal',
41
+ invalid: 'Proposal is invalid and cannot be applied', ready: 'Validated proposal; not applied',
42
+ previewing: 'Preview only; not applied', applying: 'Applying changes', failed: 'Application failed; confirm again to retry',
43
+ readonly: 'No apply handler is configured',
44
+ });
45
+ const candidate = $derived(streaming || proposal === undefined || proposal === null || proposal === ''
46
+ ? null : applySurfaceEditProposal(revision, proposal, catalog, policy));
47
+ const candidateKey = $derived(candidate?.ok ? JSON.stringify([scopeKey, candidate.value]) : '');
48
+ let selectedPreview = $state('');
49
+ let applying = $state(false);
50
+ let failureKey = $state('');
51
+ const previewing = $derived(candidateKey !== '' && selectedPreview === candidateKey && !streaming);
52
+ const visibleSpec = $derived(previewing && candidate?.ok ? candidate.value.spec : revision.spec);
53
+ const status = $derived(streaming ? labels.loading : applying ? labels.applying : candidate === null
54
+ ? labels.empty : !candidate.ok ? labels.invalid : previewing ? labels.previewing : labels.ready);
55
+
56
+ async function apply(): Promise<void> {
57
+ if (streaming || applying || !onApply) return;
58
+ // 点击时使用最新 revision/policy 重新校验,不复用旧预览作为权限凭据。
59
+ const latest = applySurfaceEditProposal(revision, proposal, catalog, policy);
60
+ if (!latest.ok) return;
61
+ const submittedKey = candidateKey;
62
+ applying = true;
63
+ failureKey = '';
64
+ try {
65
+ await onApply(latest.value);
66
+ selectedPreview = '';
67
+ } catch {
68
+ failureKey = submittedKey;
69
+ } finally {
70
+ applying = false;
71
+ }
72
+ }
73
+ </script>
74
+
75
+ <section class="svadmin-surface-editor {editorClasses[density]}" aria-label={labels.title} data-density={density}>
76
+ <div data-part="toolbar">
77
+ <div>
78
+ <h3 data-part="title">{labels.title}</h3>
79
+ <p data-part="status" role="status" aria-live="polite">{status} · {labels.current} {revision.revision}</p>
80
+ </div>
81
+ <div data-part="actions">
82
+ <button type="button" class={editorButtonClasses.secondary}
83
+ disabled={streaming || applying || !candidate?.ok} aria-pressed={previewing}
84
+ onclick={() => { selectedPreview = previewing ? '' : candidateKey; }}
85
+ >{previewing ? labels.back : labels.preview}</button>
86
+ <button type="button" class={editorButtonClasses.primary}
87
+ disabled={streaming || applying || !candidate?.ok || !onApply}
88
+ title={onApply ? undefined : labels.readonly} onclick={apply}
89
+ >{applying ? labels.applying : labels.apply}</button>
90
+ </div>
91
+ </div>
92
+ {#if candidate && !candidate.ok}
93
+ <div data-part="error" role="alert">
94
+ {labels.invalid}
95
+ <ul>{#each candidate.issues as issue, index (`${issue.code}:${issue.path}:${index}`)}
96
+ <li><code>{issue.code}</code>: {issue.message}</li>
97
+ {/each}</ul>
98
+ </div>
99
+ {/if}
100
+ {#if failureKey !== '' && failureKey === candidateKey}
101
+ <div data-part="error" role="alert">{labels.failed}</div>
102
+ {/if}
103
+ <div data-part="viewport" data-preview={previewing}>
104
+ <SurfaceRenderer spec={visibleSpec} {policy} {catalog} {scopeKey} locale={activeLocale}
105
+ {...(dataProvider === undefined ? {} : { dataProvider })} />
106
+ </div>
107
+ </section>
@@ -0,0 +1,20 @@
1
+ import type { SurfaceRevision } from '../edits.js';
2
+ import type { SurfaceRenderCatalog } from '../catalog.js';
3
+ import type { SurfaceDataProvider, SurfacePolicy } from '../types.js';
4
+ import '../styles/editor.css';
5
+ export interface SurfaceEditPreviewProps {
6
+ readonly revision: SurfaceRevision;
7
+ readonly proposal?: unknown;
8
+ readonly streaming?: boolean;
9
+ readonly policy: SurfacePolicy;
10
+ readonly catalog?: SurfaceRenderCatalog;
11
+ readonly dataProvider?: SurfaceDataProvider;
12
+ readonly scopeKey?: string;
13
+ readonly locale?: string;
14
+ readonly density?: 'compact' | 'comfortable';
15
+ /** 用户确认后返回候选快照。宿主负责持久化 CAS 与服务端授权。 */
16
+ readonly onApply?: (candidate: SurfaceRevision) => void | Promise<void>;
17
+ }
18
+ declare const SurfaceEditPreview: import("svelte").Component<SurfaceEditPreviewProps, {}, "">;
19
+ type SurfaceEditPreview = ReturnType<typeof SurfaceEditPreview>;
20
+ export default SurfaceEditPreview;
@@ -1,22 +1,30 @@
1
1
  <script lang="ts">
2
- import { canAccessAsync, captureAdminContext } from '@svadmin/core';
2
+ import { captureAdminContext, getLogoutVersion } from '@svadmin/core';
3
3
  import { useTranslation } from '@svadmin/core/i18n';
4
- import { untrack } from 'svelte';
5
- import { resolveSurfaceWidgetData } from '../binding.js';
4
+ import { onDestroy, untrack } from 'svelte';
5
+ import { SvelteMap } from 'svelte/reactivity';
6
+ import { resolveSurfaceSourceData } from '../binding.js';
6
7
  import { defaultSurfaceCatalog } from '../catalog.js';
7
8
  import type { SurfaceRenderCatalog } from '../catalog.js';
8
9
  import { loadSurfaceSource } from '../runtime.js';
9
10
  import { resolveSurfaceMessages } from '../localization.js';
10
11
  import type { SurfaceMessages } from '../localization.js';
11
12
  import type {
12
- SurfaceDataError,
13
- SurfaceDataProvider,
14
- SurfaceDataSource,
15
- SurfacePolicy,
16
- SurfaceSourceDataState,
17
- SurfaceValidationIssue,
13
+ SurfaceDataError, SurfaceDataProvider, SurfacePolicy,
14
+ SurfaceSourceDataState, SurfaceValidationIssue, JsonObject,
18
15
  } from '../types.js';
19
16
  import { validateSurfaceSpec } from '../validation.js';
17
+ import { createSurfaceSourceCache, sameSourceIdentity, snapshotSurfaceSource } from '../source-cache.js';
18
+ import type { SurfaceSourceRequest } from '../source-cache.js';
19
+ import { surfaceMetric } from '../styled-system/recipes/index.js';
20
+ import { withoutSurfaceAppearance } from '../workflows/catalog.js';
21
+
22
+ function appearanceClasses(props: JsonObject): string {
23
+ const appearance = props['appearance'] as { tone?: 'neutral' | 'success' | 'warning' | 'danger' | 'info'; density?: 'compact' | 'comfortable' } | undefined;
24
+ if (!appearance) return '';
25
+ const classes = surfaceMetric(appearance);
26
+ return `surface-appearance ${classes.root} ${classes.card}`;
27
+ }
20
28
 
21
29
  export type SurfaceRendererError =
22
30
  | { readonly type: 'validation'; readonly issues: readonly SurfaceValidationIssue[] }
@@ -27,6 +35,10 @@
27
35
  readonly policy: SurfacePolicy;
28
36
  readonly catalog?: SurfaceRenderCatalog;
29
37
  readonly dataProvider?: SurfaceDataProvider;
38
+ /** 可信宿主的用户/租户/授权会话标识;禁止使用凭证或模型提供的值。 */
39
+ readonly scopeKey?: string;
40
+ /** 与独立增量加载接口保持兼容;任一作用域标识变化都会清空状态。 */
41
+ readonly dataScopeKey?: string | number;
30
42
  readonly locale?: string;
31
43
  readonly messages?: Partial<SurfaceMessages>;
32
44
  readonly class?: string;
@@ -34,14 +46,8 @@
34
46
  }
35
47
 
36
48
  let {
37
- spec,
38
- policy,
39
- catalog = defaultSurfaceCatalog,
40
- dataProvider,
41
- locale,
42
- messages,
43
- class: className = '',
44
- onError,
49
+ spec, policy, catalog = defaultSurfaceCatalog, dataProvider, scopeKey = '', dataScopeKey,
50
+ locale, messages, class: className = '', onError,
45
51
  }: SurfaceRendererProps = $props();
46
52
 
47
53
  const adminContext = captureAdminContext();
@@ -50,182 +56,133 @@
50
56
  const activeMessages = $derived(resolveSurfaceMessages(activeLocale, messages));
51
57
  const validation = $derived(validateSurfaceSpec(spec, catalog, policy));
52
58
  const widgetRegistrations = $derived(new Map(catalog.widgets.map((widget) => [widget.type, widget])));
53
- const currentSpec = $derived(validation.ok ? validation.value : null);
54
- const sourceGenerations: Record<string, number> = Object.create(null) as Record<string, number>;
55
- let sourceStates = $state.raw<Record<string, SurfaceSourceDataState>>({});
56
-
57
- function nextGeneration(sourceId: string): number {
58
- const generation = (sourceGenerations[sourceId] ?? 0) + 1;
59
- sourceGenerations[sourceId] = generation;
60
- return generation;
61
- }
62
-
63
- function setSourceState(sourceId: string, state: SurfaceSourceDataState): void {
64
- sourceStates = { ...sourceStates, [sourceId]: state };
65
- }
66
-
67
- function providerFor(resource: string): SurfaceDataProvider {
68
- return dataProvider ?? adminContext.getDataProviderForResource(resource);
69
- }
70
-
71
- async function authorize(resource: string, action: 'list' | 'show') {
72
- return canAccessAsync(resource, action);
73
- }
59
+ const sourceStates = new SvelteMap<string, SurfaceSourceDataState>();
60
+ let destroyed = false;
61
+ let previousScope: readonly unknown[] = [];
62
+ let widgetScopeRevision = $state(0);
63
+ const sessionIdentity = $derived([
64
+ validation.ok ? validation.value.surfaceId : undefined,
65
+ catalog.version, scopeKey, dataScopeKey, getLogoutVersion(),
66
+ adminContext.tenantCacheKey?.__svadminTenant,
67
+ adminContext.authProvider, adminContext.accessControlProvider,
68
+ ]);
69
+ // 复用 PR #428 的请求所有权与会话隔离实现,不维护第二套缓存。
70
+ const sourceCache = createSurfaceSourceCache({
71
+ set: (id, state) => { sourceStates.set(id, state); },
72
+ remove: (id) => { sourceStates.delete(id); },
73
+ onError: (state) => { onError?.({ type: 'data', error: state.error }); },
74
+ });
74
75
 
75
76
  function providerError(sourceId: string, failure: unknown): SurfaceSourceDataState {
76
77
  const message = failure instanceof Error ? failure.message : activeMessages.providerUnavailable;
77
- return {
78
- status: 'error',
79
- sourceId,
80
- error: { code: 'provider_failed', sourceId, message },
81
- };
78
+ return { status: 'error', sourceId, error: { code: 'provider_failed', sourceId, message } };
82
79
  }
83
80
 
84
- async function loadSource(source: SurfaceDataSource): Promise<void> {
85
- const generation = nextGeneration(source.id);
86
- setSourceState(source.id, { status: 'loading', sourceId: source.id });
87
-
88
- let result: SurfaceSourceDataState;
89
- try {
90
- const resourcePolicy = Object.hasOwn(policy.resources, source.resource)
91
- ? policy.resources[source.resource]
92
- : undefined;
81
+ const sourceRequests: SurfaceSourceRequest[] = $derived.by(() => {
82
+ if (!validation.ok) return [];
83
+ const accessControl = adminContext.accessControlProvider;
84
+ const scope = sessionIdentity;
85
+ return validation.value.dataSources.map((source): SurfaceSourceRequest => {
86
+ const resourcePolicy = Object.hasOwn(policy.resources, source.resource) ? policy.resources[source.resource] : undefined;
93
87
  if (!resourcePolicy) throw new Error(`Resource "${source.resource}" is not allowed`);
94
- result = await loadSurfaceSource({
95
- source,
96
- resourcePolicy,
97
- provider: providerFor(source.resource),
98
- authorize,
99
- });
100
- } catch (failure) {
101
- result = providerError(source.id, failure);
102
- }
103
-
104
- if (sourceGenerations[source.id] !== generation) return;
105
- setSourceState(source.id, result);
106
- if (result.status === 'error') onError?.({ type: 'data', error: result.error });
107
- }
108
-
109
- async function loadSources(sources: readonly SurfaceDataSource[]): Promise<void> {
110
- await Promise.all(sources.map(loadSource));
111
- }
112
-
113
- function invalidateCurrentSources(): void {
114
- for (const sourceId of Object.keys(sourceGenerations)) nextGeneration(sourceId);
115
- sourceStates = {};
116
- }
88
+ const snapshot = snapshotSurfaceSource(source, resourcePolicy);
89
+ let provider: SurfaceDataProvider | undefined;
90
+ let providerFailure: unknown;
91
+ try {
92
+ provider = dataProvider ?? adminContext.getDataProviderForResource(source.resource);
93
+ } catch (failure) { providerFailure = failure; }
94
+ const identity = [snapshot.key, provider, provider?.getList, provider?.getOne, ...scope];
95
+ const isCurrent = () => !destroyed && sourceRequests.some((request) =>
96
+ request.id === snapshot.source.id && sameSourceIdentity(request.identity, identity));
97
+ return {
98
+ id: snapshot.source.id, identity, isCurrent,
99
+ async load(isRequestCurrent) {
100
+ if (!provider) return providerError(snapshot.source.id, providerFailure);
101
+ try {
102
+ return await loadSurfaceSource({
103
+ source: snapshot.source, resourcePolicy: snapshot.resourcePolicy, provider,
104
+ async authorize(resource, action) {
105
+ if (!isRequestCurrent()) return { can: false };
106
+ // 授权检查与缓存身份必须使用同一个所属上下文,不能回退到无关全局授权。
107
+ const meta = adminContext.getProviderMeta(resource);
108
+ const decision = accessControl
109
+ ? await accessControl.can({ resource, action, ...(meta === undefined ? {} : { meta }) })
110
+ : { can: true };
111
+ return isRequestCurrent() ? decision : { can: false };
112
+ },
113
+ });
114
+ } catch (failure) { return providerError(snapshot.source.id, failure); }
115
+ },
116
+ };
117
+ });
118
+ });
117
119
 
118
120
  export async function refresh(sourceId?: string): Promise<void> {
119
- const activeSpec = currentSpec;
120
- if (!activeSpec) return;
121
- const sources = sourceId === undefined
122
- ? activeSpec.dataSources
123
- : activeSpec.dataSources.filter((source) => source.id === sourceId);
124
- await loadSources(sources);
121
+ if (!validation.ok) { sourceCache.clear(); return; }
122
+ await untrack(() => sourceCache.reconcile(sourceRequests, sourceId ?? true));
125
123
  }
126
124
 
127
- $effect(() => {
125
+ $effect.pre(() => {
128
126
  const validatedSpec = validation;
129
- void dataProvider;
130
- invalidateCurrentSources();
131
- if (!validatedSpec.ok) {
132
- onError?.({ type: 'validation', issues: validatedSpec.issues });
133
- return;
134
- }
135
-
127
+ const requests = sourceRequests;
128
+ const scope = sessionIdentity;
136
129
  untrack(() => {
137
- void loadSources(validatedSpec.value.dataSources);
130
+ if (!sameSourceIdentity(previousScope, scope)) {
131
+ previousScope = [...scope];
132
+ widgetScopeRevision += 1;
133
+ }
134
+ if (!validatedSpec.ok) {
135
+ sourceCache.clear();
136
+ onError?.({ type: 'validation', issues: validatedSpec.issues });
137
+ return;
138
+ }
139
+ void sourceCache.reconcile(requests);
138
140
  });
139
141
  });
142
+ onDestroy(() => { destroyed = true; sourceCache.dispose(); });
140
143
  </script>
141
144
 
142
145
  {#if validation.ok}
143
- <section
144
- class="surface {className}"
145
- aria-labelledby="surface-{validation.value.surfaceId}-title"
146
- data-surface-id={validation.value.surfaceId}
147
- >
148
- <header class="surface-header">
149
- <h2 id="surface-{validation.value.surfaceId}-title">{validation.value.title}</h2>
150
- </header>
146
+ <section class="surface {className}" aria-labelledby="surface-{validation.value.surfaceId}-title" data-surface-id={validation.value.surfaceId}>
147
+ <header class="surface-header"><h2 id="surface-{validation.value.surfaceId}-title">{validation.value.title}</h2></header>
151
148
  <div class="surface-grid surface-gap-{validation.value.layout.gap ?? 'md'}">
152
- {#each validation.value.widgets as widget (widget.id)}
153
- {@const registration = widgetRegistrations.get(widget.type)}
154
- {@const WidgetComponent = registration?.component}
155
- <div
156
- class="surface-widget surface-span-{widget.placement?.columnSpan ?? 12}"
157
- data-testid="surface-widget-{widget.id}"
158
- >
159
- {#if WidgetComponent}
160
- <WidgetComponent
161
- widgetId={widget.id}
162
- props={widget.props}
163
- data={resolveSurfaceWidgetData(widget, sourceStates)}
164
- locale={activeLocale}
165
- messages={activeMessages}
166
- />
167
- {/if}
168
- </div>
169
- {/each}
149
+ {#key widgetScopeRevision}
150
+ {#each validation.value.widgets as widget (widget.id)}
151
+ {@const registration = widgetRegistrations.get(widget.type)}
152
+ {@const WidgetComponent = registration?.component}
153
+ {@const semantic = registration?.presentation === 'surface-appearance/v1'}
154
+ <div class="surface-widget surface-span-{widget.placement?.columnSpan ?? 12} {semantic ? appearanceClasses(widget.props) : ''}" data-testid="surface-widget-{widget.id}">
155
+ {#if WidgetComponent}
156
+ <WidgetComponent widgetId={widget.id} props={semantic ? withoutSurfaceAppearance(widget.props) : widget.props}
157
+ data={resolveSurfaceSourceData(widget, widget.binding ? sourceStates.get(widget.binding.sourceId) : undefined)}
158
+ locale={activeLocale} messages={activeMessages} />
159
+ {/if}
160
+ </div>
161
+ {/each}
162
+ {/key}
170
163
  </div>
171
164
  </section>
172
165
  {:else}
173
166
  <section class="surface-error {className}" role="alert" data-surface-error>
174
167
  <h2>{activeMessages.renderErrorTitle}</h2>
175
- <ul>
176
- {#each validation.issues as issue, issueIndex (`${issue.code}:${issue.path}:${issueIndex}`)}
177
- <li><code>{issue.code}</code>: {issue.message}</li>
178
- {/each}
179
- </ul>
168
+ <ul>{#each validation.issues as issue, issueIndex (`${issue.code}:${issue.path}:${issueIndex}`)}
169
+ <li><code>{issue.code}</code>: {issue.message}</li>
170
+ {/each}</ul>
180
171
  </section>
181
172
  {/if}
182
173
 
183
174
  <style>
184
- .surface,
185
- .surface-error {
186
- width: 100%;
187
- min-width: 0;
188
- }
189
-
190
- .surface-header {
191
- margin-bottom: 1rem;
192
- }
193
-
194
- .surface-header h2,
195
- .surface-error h2 {
196
- margin: 0;
197
- color: var(--foreground);
198
- font-size: clamp(1.25rem, 2vw, 1.75rem);
199
- line-height: 1.2;
200
- }
201
-
202
- .surface-grid {
203
- display: grid;
204
- grid-template-columns: repeat(12, minmax(0, 1fr));
205
- }
206
-
175
+ .surface, .surface-error { width: 100%; min-width: 0; }
176
+ .surface-header { margin-bottom: 1rem; }
177
+ .surface-header h2, .surface-error h2 { margin: 0; color: var(--foreground); font-size: clamp(1.25rem, 2vw, 1.75rem); line-height: 1.2; }
178
+ .surface-grid { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); }
207
179
  .surface-gap-sm { gap: 0.5rem; }
208
180
  .surface-gap-md { gap: 1rem; }
209
181
  .surface-gap-lg { gap: 1.5rem; }
210
-
211
- .surface-widget {
212
- grid-column: 1 / -1;
213
- min-width: 0;
214
- }
215
-
216
- .surface-error {
217
- padding: 1rem;
218
- border: 1px solid var(--destructive);
219
- border-radius: 0.75rem;
220
- background: var(--card);
221
- color: var(--destructive);
222
- }
223
-
224
- .surface-error ul {
225
- margin: 0.75rem 0 0;
226
- padding-left: 1.25rem;
227
- }
228
-
182
+ .surface-widget { grid-column: 1 / -1; min-width: 0; }
183
+ .surface-appearance { border-block: 1px solid var(--border); border-inline-end: 1px solid var(--border); border-inline-start-style: solid; border-radius: 0.75rem; background: var(--card); }
184
+ .surface-error { padding: 1rem; border: 1px solid var(--destructive); border-radius: 0.75rem; background: var(--card); color: var(--destructive); }
185
+ .surface-error ul { margin: 0.75rem 0 0; padding-left: 1.25rem; }
229
186
  @media (min-width: 48rem) {
230
187
  .surface-span-1 { grid-column: span 1; }
231
188
  .surface-span-2 { grid-column: span 2; }
@@ -13,6 +13,10 @@ export interface SurfaceRendererProps {
13
13
  readonly policy: SurfacePolicy;
14
14
  readonly catalog?: SurfaceRenderCatalog;
15
15
  readonly dataProvider?: SurfaceDataProvider;
16
+ /** 可信宿主的用户/租户/授权会话标识;禁止使用凭证或模型提供的值。 */
17
+ readonly scopeKey?: string;
18
+ /** 与独立增量加载接口保持兼容;任一作用域标识变化都会清空状态。 */
19
+ readonly dataScopeKey?: string | number;
16
20
  readonly locale?: string;
17
21
  readonly messages?: Partial<SurfaceMessages>;
18
22
  readonly class?: string;
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ import { setContext, type Snippet } from 'svelte';
3
+ import { SURFACE_WORKFLOW_CONTEXT, type SurfaceWorkflowHost } from '../workflows/context.js';
4
+ import type { SurfaceActionDescriptor, SurfaceActionProposal } from '../workflows/types.js';
5
+ import type { SurfaceWorkflowTransport } from '../workflows/client.js';
6
+ let { actions, transport, scopeKey, surfaceId, revision, enabled = false, onCommitted, children }: {
7
+ actions: readonly SurfaceActionDescriptor[]; transport: SurfaceWorkflowTransport;
8
+ scopeKey: string; surfaceId: string; revision: number;
9
+ /** Opt-in only after a complete validated surface is explicitly accepted. */
10
+ enabled?: boolean; onCommitted?: (proposal: SurfaceActionProposal) => void; children: Snippet;
11
+ } = $props();
12
+ const host: SurfaceWorkflowHost = {
13
+ getScope: () => ({ transport, scopeKey, surfaceId, revision, enabled }),
14
+ getAction: (id) => actions.find((action) => action.id === id),
15
+ committed: (proposal) => onCommitted?.(proposal),
16
+ };
17
+ setContext(SURFACE_WORKFLOW_CONTEXT, host);
18
+ </script>
19
+
20
+ {#key scopeKey}
21
+ {@render children()}
22
+ {/key}
@@ -0,0 +1,17 @@
1
+ import { type Snippet } from 'svelte';
2
+ import type { SurfaceActionDescriptor, SurfaceActionProposal } from '../workflows/types.js';
3
+ import type { SurfaceWorkflowTransport } from '../workflows/client.js';
4
+ type $$ComponentProps = {
5
+ actions: readonly SurfaceActionDescriptor[];
6
+ transport: SurfaceWorkflowTransport;
7
+ scopeKey: string;
8
+ surfaceId: string;
9
+ revision: number;
10
+ /** Opt-in only after a complete validated surface is explicitly accepted. */
11
+ enabled?: boolean;
12
+ onCommitted?: (proposal: SurfaceActionProposal) => void;
13
+ children: Snippet;
14
+ };
15
+ declare const SurfaceWorkflowProvider: import("svelte").Component<$$ComponentProps, {}, "">;
16
+ type SurfaceWorkflowProvider = ReturnType<typeof SurfaceWorkflowProvider>;
17
+ export default SurfaceWorkflowProvider;