@svadmin/surface 0.8.19 → 0.10.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 (102) hide show
  1. package/README.md +76 -38
  2. package/STYLING.md +33 -0
  3. package/compatibility.json +40 -8
  4. package/dist/agent-contract.d.ts +37 -0
  5. package/dist/agent-contract.js +206 -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/business-data.d.ts +16 -0
  15. package/dist/business-data.js +42 -0
  16. package/dist/business-definitions.d.ts +28 -0
  17. package/dist/business-definitions.js +53 -0
  18. package/dist/business.d.ts +3 -0
  19. package/dist/business.js +17 -0
  20. package/dist/catalog.d.ts +4 -61
  21. package/dist/catalog.js +20 -44
  22. package/dist/components/ActivityFeedWidget.svelte +34 -0
  23. package/dist/components/ActivityFeedWidget.svelte.d.ts +4 -0
  24. package/dist/components/MetricWidget.svelte +13 -8
  25. package/dist/components/MetricWidget.svelte.d.ts +1 -0
  26. package/dist/components/ResourceDetailWidget.svelte +56 -0
  27. package/dist/components/ResourceDetailWidget.svelte.d.ts +4 -0
  28. package/dist/components/ResourceFormBody.svelte +97 -0
  29. package/dist/components/ResourceFormBody.svelte.d.ts +13 -0
  30. package/dist/components/ResourceFormWidget.svelte +19 -0
  31. package/dist/components/ResourceFormWidget.svelte.d.ts +4 -0
  32. package/dist/components/ResourceTableWidget.svelte +15 -12
  33. package/dist/components/ResourceTableWidget.svelte.d.ts +1 -0
  34. package/dist/components/SurfaceEditPreview.svelte +107 -0
  35. package/dist/components/SurfaceEditPreview.svelte.d.ts +20 -0
  36. package/dist/components/SurfaceRenderer.svelte +121 -163
  37. package/dist/components/SurfaceRenderer.svelte.d.ts +5 -0
  38. package/dist/components/SurfaceWorkflowProvider.svelte +22 -0
  39. package/dist/components/SurfaceWorkflowProvider.svelte.d.ts +17 -0
  40. package/dist/components/SvarGridWidget.svelte +47 -0
  41. package/dist/components/SvarGridWidget.svelte.d.ts +4 -0
  42. package/dist/components/SvarSurfaceProvider.svelte +12 -0
  43. package/dist/components/SvarSurfaceProvider.svelte.d.ts +11 -0
  44. package/dist/design-contract.d.ts +2 -0
  45. package/dist/design-contract.js +2 -0
  46. package/dist/edits.d.ts +57 -0
  47. package/dist/edits.js +166 -0
  48. package/dist/index.d.ts +8 -2
  49. package/dist/index.js +5 -1
  50. package/dist/interactive.d.ts +6 -0
  51. package/dist/interactive.js +15 -0
  52. package/dist/localization.d.ts +11 -1
  53. package/dist/localization.js +20 -0
  54. package/dist/openui.d.ts +64 -0
  55. package/dist/openui.js +168 -0
  56. package/dist/recipes.d.ts +20 -0
  57. package/dist/recipes.js +20 -0
  58. package/dist/schema.d.ts +108 -0
  59. package/dist/schema.js +73 -0
  60. package/dist/server-sqlite.d.ts +1 -0
  61. package/dist/server-sqlite.js +2 -0
  62. package/dist/server.d.ts +4 -0
  63. package/dist/server.js +4 -0
  64. package/dist/source-cache.d.ts +30 -0
  65. package/dist/source-cache.js +135 -0
  66. package/dist/styles/editor.css +124 -0
  67. package/dist/styles/editor.d.ts +8 -0
  68. package/dist/styles/editor.js +9 -0
  69. package/dist/styles.css +43 -0
  70. package/dist/svar-catalog.d.ts +3 -0
  71. package/dist/svar-catalog.js +7 -0
  72. package/dist/svar-context.d.ts +9 -0
  73. package/dist/svar-context.js +1 -0
  74. package/dist/svar-schema.d.ts +21 -0
  75. package/dist/svar-schema.js +36 -0
  76. package/dist/svar.d.ts +4 -0
  77. package/dist/svar.js +3 -0
  78. package/dist/svelte.d.ts +4 -2
  79. package/dist/svelte.js +2 -1
  80. package/dist/types.d.ts +5 -1
  81. package/dist/validation.js +14 -108
  82. package/dist/wire.d.ts +13 -0
  83. package/dist/wire.js +30 -0
  84. package/dist/workflows/action-contracts.d.ts +7 -0
  85. package/dist/workflows/action-contracts.js +54 -0
  86. package/dist/workflows/catalog.d.ts +20 -0
  87. package/dist/workflows/catalog.js +50 -0
  88. package/dist/workflows/client.d.ts +45 -0
  89. package/dist/workflows/client.js +125 -0
  90. package/dist/workflows/context.d.ts +9 -0
  91. package/dist/workflows/context.js +5 -0
  92. package/dist/workflows/openui-guard.d.ts +19 -0
  93. package/dist/workflows/openui-guard.js +209 -0
  94. package/dist/workflows/service.d.ts +58 -0
  95. package/dist/workflows/service.js +296 -0
  96. package/dist/workflows/sqlite-store.d.ts +20 -0
  97. package/dist/workflows/sqlite-store.js +91 -0
  98. package/dist/workflows/types.d.ts +103 -0
  99. package/dist/workflows/types.js +8 -0
  100. package/dist/workflows.d.ts +5 -0
  101. package/dist/workflows.js +4 -0
  102. package/package.json +59 -8
@@ -1,22 +1,31 @@
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 '../recipes.js';
20
+ import '../styles.css';
21
+ import { withoutSurfaceAppearance } from '../workflows/catalog.js';
22
+
23
+ function appearanceClasses(props: JsonObject): string {
24
+ const appearance = props['appearance'] as { tone?: 'neutral' | 'success' | 'warning' | 'danger' | 'info'; density?: 'compact' | 'comfortable' } | undefined;
25
+ if (!appearance) return '';
26
+ const classes = surfaceMetric(appearance);
27
+ return `surface-appearance ${classes.root} ${classes.card}`;
28
+ }
20
29
 
21
30
  export type SurfaceRendererError =
22
31
  | { readonly type: 'validation'; readonly issues: readonly SurfaceValidationIssue[] }
@@ -27,6 +36,10 @@
27
36
  readonly policy: SurfacePolicy;
28
37
  readonly catalog?: SurfaceRenderCatalog;
29
38
  readonly dataProvider?: SurfaceDataProvider;
39
+ /** 可信宿主的用户/租户/授权会话标识;禁止使用凭证或模型提供的值。 */
40
+ readonly scopeKey?: string;
41
+ /** 与独立增量加载接口保持兼容;任一作用域标识变化都会清空状态。 */
42
+ readonly dataScopeKey?: string | number;
30
43
  readonly locale?: string;
31
44
  readonly messages?: Partial<SurfaceMessages>;
32
45
  readonly class?: string;
@@ -34,14 +47,8 @@
34
47
  }
35
48
 
36
49
  let {
37
- spec,
38
- policy,
39
- catalog = defaultSurfaceCatalog,
40
- dataProvider,
41
- locale,
42
- messages,
43
- class: className = '',
44
- onError,
50
+ spec, policy, catalog = defaultSurfaceCatalog, dataProvider, scopeKey = '', dataScopeKey,
51
+ locale, messages, class: className = '', onError,
45
52
  }: SurfaceRendererProps = $props();
46
53
 
47
54
  const adminContext = captureAdminContext();
@@ -50,182 +57,133 @@
50
57
  const activeMessages = $derived(resolveSurfaceMessages(activeLocale, messages));
51
58
  const validation = $derived(validateSurfaceSpec(spec, catalog, policy));
52
59
  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
- }
60
+ const sourceStates = new SvelteMap<string, SurfaceSourceDataState>();
61
+ let destroyed = false;
62
+ let previousScope: readonly unknown[] = [];
63
+ let widgetScopeRevision = $state(0);
64
+ const sessionIdentity = $derived([
65
+ validation.ok ? validation.value.surfaceId : undefined,
66
+ catalog.version, scopeKey, dataScopeKey, getLogoutVersion(),
67
+ adminContext.tenantCacheKey?.__svadminTenant,
68
+ adminContext.authProvider, adminContext.accessControlProvider,
69
+ ]);
70
+ // 复用 PR #428 的请求所有权与会话隔离实现,不维护第二套缓存。
71
+ const sourceCache = createSurfaceSourceCache({
72
+ set: (id, state) => { sourceStates.set(id, state); },
73
+ remove: (id) => { sourceStates.delete(id); },
74
+ onError: (state) => { onError?.({ type: 'data', error: state.error }); },
75
+ });
74
76
 
75
77
  function providerError(sourceId: string, failure: unknown): SurfaceSourceDataState {
76
78
  const message = failure instanceof Error ? failure.message : activeMessages.providerUnavailable;
77
- return {
78
- status: 'error',
79
- sourceId,
80
- error: { code: 'provider_failed', sourceId, message },
81
- };
79
+ return { status: 'error', sourceId, error: { code: 'provider_failed', sourceId, message } };
82
80
  }
83
81
 
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;
82
+ const sourceRequests: SurfaceSourceRequest[] = $derived.by(() => {
83
+ if (!validation.ok) return [];
84
+ const accessControl = adminContext.accessControlProvider;
85
+ const scope = sessionIdentity;
86
+ return validation.value.dataSources.map((source): SurfaceSourceRequest => {
87
+ const resourcePolicy = Object.hasOwn(policy.resources, source.resource) ? policy.resources[source.resource] : undefined;
93
88
  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
- }
89
+ const snapshot = snapshotSurfaceSource(source, resourcePolicy);
90
+ let provider: SurfaceDataProvider | undefined;
91
+ let providerFailure: unknown;
92
+ try {
93
+ provider = dataProvider ?? adminContext.getDataProviderForResource(source.resource);
94
+ } catch (failure) { providerFailure = failure; }
95
+ const identity = [snapshot.key, provider, provider?.getList, provider?.getOne, ...scope];
96
+ const isCurrent = () => !destroyed && sourceRequests.some((request) =>
97
+ request.id === snapshot.source.id && sameSourceIdentity(request.identity, identity));
98
+ return {
99
+ id: snapshot.source.id, identity, isCurrent,
100
+ async load(isRequestCurrent) {
101
+ if (!provider) return providerError(snapshot.source.id, providerFailure);
102
+ try {
103
+ return await loadSurfaceSource({
104
+ source: snapshot.source, resourcePolicy: snapshot.resourcePolicy, provider,
105
+ async authorize(resource, action) {
106
+ if (!isRequestCurrent()) return { can: false };
107
+ // 授权检查与缓存身份必须使用同一个所属上下文,不能回退到无关全局授权。
108
+ const meta = adminContext.getProviderMeta(resource);
109
+ const decision = accessControl
110
+ ? await accessControl.can({ resource, action, ...(meta === undefined ? {} : { meta }) })
111
+ : { can: true };
112
+ return isRequestCurrent() ? decision : { can: false };
113
+ },
114
+ });
115
+ } catch (failure) { return providerError(snapshot.source.id, failure); }
116
+ },
117
+ };
118
+ });
119
+ });
117
120
 
118
121
  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);
122
+ if (!validation.ok) { sourceCache.clear(); return; }
123
+ await untrack(() => sourceCache.reconcile(sourceRequests, sourceId ?? true));
125
124
  }
126
125
 
127
- $effect(() => {
126
+ $effect.pre(() => {
128
127
  const validatedSpec = validation;
129
- void dataProvider;
130
- invalidateCurrentSources();
131
- if (!validatedSpec.ok) {
132
- onError?.({ type: 'validation', issues: validatedSpec.issues });
133
- return;
134
- }
135
-
128
+ const requests = sourceRequests;
129
+ const scope = sessionIdentity;
136
130
  untrack(() => {
137
- void loadSources(validatedSpec.value.dataSources);
131
+ if (!sameSourceIdentity(previousScope, scope)) {
132
+ previousScope = [...scope];
133
+ widgetScopeRevision += 1;
134
+ }
135
+ if (!validatedSpec.ok) {
136
+ sourceCache.clear();
137
+ onError?.({ type: 'validation', issues: validatedSpec.issues });
138
+ return;
139
+ }
140
+ void sourceCache.reconcile(requests);
138
141
  });
139
142
  });
143
+ onDestroy(() => { destroyed = true; sourceCache.dispose(); });
140
144
  </script>
141
145
 
142
146
  {#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>
147
+ <section class="surface {className}" aria-labelledby="surface-{validation.value.surfaceId}-title" data-surface-id={validation.value.surfaceId}>
148
+ <header class="surface-header"><h2 id="surface-{validation.value.surfaceId}-title">{validation.value.title}</h2></header>
151
149
  <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}
150
+ {#key widgetScopeRevision}
151
+ {#each validation.value.widgets as widget (widget.id)}
152
+ {@const registration = widgetRegistrations.get(widget.type)}
153
+ {@const WidgetComponent = registration?.component}
154
+ {@const semantic = registration?.presentation === 'surface-appearance/v1'}
155
+ <div class="surface-widget surface-span-{widget.placement?.columnSpan ?? 12} {semantic ? appearanceClasses(widget.props) : ''}" data-testid="surface-widget-{widget.id}">
156
+ {#if WidgetComponent}
157
+ <WidgetComponent widgetId={widget.id} props={semantic ? withoutSurfaceAppearance(widget.props) : widget.props}
158
+ data={resolveSurfaceSourceData(widget, widget.binding ? sourceStates.get(widget.binding.sourceId) : undefined)}
159
+ locale={activeLocale} messages={activeMessages} />
160
+ {/if}
161
+ </div>
162
+ {/each}
163
+ {/key}
170
164
  </div>
171
165
  </section>
172
166
  {:else}
173
167
  <section class="surface-error {className}" role="alert" data-surface-error>
174
168
  <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>
169
+ <ul>{#each validation.issues as issue, issueIndex (`${issue.code}:${issue.path}:${issueIndex}`)}
170
+ <li><code>{issue.code}</code>: {issue.message}</li>
171
+ {/each}</ul>
180
172
  </section>
181
173
  {/if}
182
174
 
183
175
  <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
-
176
+ .surface, .surface-error { width: 100%; min-width: 0; }
177
+ .surface-header { margin-bottom: 1rem; }
178
+ .surface-header h2, .surface-error h2 { margin: 0; color: var(--foreground); font-size: clamp(1.25rem, 2vw, 1.75rem); line-height: 1.2; }
179
+ .surface-grid { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); }
207
180
  .surface-gap-sm { gap: 0.5rem; }
208
181
  .surface-gap-md { gap: 1rem; }
209
182
  .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
-
183
+ .surface-widget { grid-column: 1 / -1; min-width: 0; }
184
+ .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); }
185
+ .surface-error { padding: 1rem; border: 1px solid var(--destructive); border-radius: 0.75rem; background: var(--card); color: var(--destructive); }
186
+ .surface-error ul { margin: 0.75rem 0 0; padding-left: 1.25rem; }
229
187
  @media (min-width: 48rem) {
230
188
  .surface-span-1 { grid-column: span 1; }
231
189
  .surface-span-2 { grid-column: span 2; }
@@ -1,6 +1,7 @@
1
1
  import type { SurfaceRenderCatalog } from '../catalog.js';
2
2
  import type { SurfaceMessages } from '../localization.js';
3
3
  import type { SurfaceDataError, SurfaceDataProvider, SurfacePolicy, SurfaceValidationIssue } from '../types.js';
4
+ import '../styles.css';
4
5
  export type SurfaceRendererError = {
5
6
  readonly type: 'validation';
6
7
  readonly issues: readonly SurfaceValidationIssue[];
@@ -13,6 +14,10 @@ export interface SurfaceRendererProps {
13
14
  readonly policy: SurfacePolicy;
14
15
  readonly catalog?: SurfaceRenderCatalog;
15
16
  readonly dataProvider?: SurfaceDataProvider;
17
+ /** 可信宿主的用户/租户/授权会话标识;禁止使用凭证或模型提供的值。 */
18
+ readonly scopeKey?: string;
19
+ /** 与独立增量加载接口保持兼容;任一作用域标识变化都会清空状态。 */
20
+ readonly dataScopeKey?: string | number;
16
21
  readonly locale?: string;
17
22
  readonly messages?: Partial<SurfaceMessages>;
18
23
  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;
@@ -0,0 +1,47 @@
1
+ <script lang="ts">
2
+ import { getContext } from 'svelte';
3
+ import SvarDataGrid from '@svadmin/ui/components/SvarDataGrid.svelte';
4
+ import type { SurfaceWidgetRendererProps } from '../catalog.js';
5
+ import { SVAR_SURFACE_CONTEXT, type SvarSurfaceContext } from '../svar-context.js';
6
+ import { decodeSvarGridProps } from '../svar-schema.js';
7
+ import { asRecordArray } from '../widget-data.js';
8
+ import { resolveSurfaceMessages } from '../localization.js';
9
+
10
+ let { widgetId, props, data, locale = 'en-US', messages }: SurfaceWidgetRendererProps = $props();
11
+ const host = getContext<SvarSurfaceContext | undefined>(SVAR_SURFACE_CONTEXT);
12
+ const labels = $derived(resolveSurfaceMessages(locale, messages));
13
+ const config = $derived(decodeSvarGridProps(props));
14
+ const records = $derived(data.status === 'ready' ? asRecordArray(data.value) : undefined);
15
+ const columns = $derived(config.columns.map(column => ({
16
+ key: column.field, label: column.label,
17
+ ...(column.width === undefined ? {} : { width: column.width }),
18
+ ...(column.format === undefined ? {} : { format: column.format }),
19
+ })));
20
+ const Grid = $derived(host?.Grid);
21
+ const Theme = $derived(host?.Theme);
22
+ </script>
23
+
24
+ <section aria-labelledby="svar-surface-{widgetId}" class="surface-data-grid">
25
+ <h3 id="svar-surface-{widgetId}">{config.title}</h3>
26
+ {#if !Grid || !Theme}
27
+ <p role="alert">{locale.startsWith('zh') ? '宿主尚未配置 SVAR 引擎' : 'The host has not configured the SVAR engine'}</p>
28
+ {:else if data.status === 'error'}
29
+ <p role="alert">{data.error.message}</p>
30
+ {:else if data.status === 'unbound' || (records && !records.ok)}
31
+ <p role="alert">{labels.tableInvalidData}</p>
32
+ {:else}
33
+ <SvarDataGrid {Grid} {Theme} {columns} items={records?.ok ? records.value : []}
34
+ primaryKey={config.rowKey ?? 'id'} height={config.height ?? 360} density={config.density ?? 'comfortable'}
35
+ freezeLeft={config.freezeLeft ?? 0} freezeRight={config.freezeRight ?? 0}
36
+ loading={data.status === 'loading'} label={config.title} {locale}
37
+ scopeKey={`${host?.scopeKey ?? 0}:${widgetId}:${'sourceId' in data ? data.sourceId : ''}`}
38
+ loadingLabel={labels.tableLoading} emptyLabel={config.emptyLabel ?? labels.tableNoRecords}
39
+ selectable={false} />
40
+ {/if}
41
+ </section>
42
+
43
+ <style>
44
+ .surface-data-grid { min-width: 0; padding: 1rem; background: var(--card, Canvas); color: var(--foreground, CanvasText); border: 1px solid var(--border, GrayText); border-radius: var(--radius, 8px); }
45
+ h3 { margin: 0 0 0.75rem; font-size: 1rem; }
46
+ [role='alert'] { color: var(--destructive, CanvasText); }
47
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { SurfaceWidgetRendererProps } from '../catalog.js';
2
+ declare const SvarGridWidget: import("svelte").Component<SurfaceWidgetRendererProps, {}, "">;
3
+ type SvarGridWidget = ReturnType<typeof SvarGridWidget>;
4
+ export default SvarGridWidget;
@@ -0,0 +1,12 @@
1
+ <script lang="ts">
2
+ import { setContext, type Snippet } from 'svelte';
3
+ import { SVAR_SURFACE_CONTEXT, type SvarSurfaceContext } from '../svar-context.js';
4
+ let { Grid, Theme, scopeKey = 0, children }: {
5
+ Grid: SvarSurfaceContext['Grid']; Theme: SvarSurfaceContext['Theme']; scopeKey?: string | number; children: Snippet;
6
+ } = $props();
7
+ setContext<SvarSurfaceContext>(SVAR_SURFACE_CONTEXT, {
8
+ get Grid() { return Grid; }, get Theme() { return Theme; }, get scopeKey() { return scopeKey; },
9
+ });
10
+ </script>
11
+
12
+ {@render children()}
@@ -0,0 +1,11 @@
1
+ import { type Snippet } from 'svelte';
2
+ import { type SvarSurfaceContext } from '../svar-context.js';
3
+ type $$ComponentProps = {
4
+ Grid: SvarSurfaceContext['Grid'];
5
+ Theme: SvarSurfaceContext['Theme'];
6
+ scopeKey?: string | number;
7
+ children: Snippet;
8
+ };
9
+ declare const SvarSurfaceProvider: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type SvarSurfaceProvider = ReturnType<typeof SvarSurfaceProvider>;
11
+ export default SvarSurfaceProvider;
@@ -0,0 +1,2 @@
1
+ export { surfaceDesignContract } from '@svadmin/ui/design-contract';
2
+ export type { SurfaceTone, SurfaceDensity } from '@svadmin/ui/design-contract';
@@ -0,0 +1,2 @@
1
+ // 唯一事实源是 UI 的纯数据入口,不加载 Svelte 或浏览器代码。
2
+ export { surfaceDesignContract } from '@svadmin/ui/design-contract';
@@ -0,0 +1,57 @@
1
+ import type { SurfaceAgentMessage } from './agent.js';
2
+ import type { JsonObject, SurfaceCatalog, SurfaceDataSource, SurfaceGridLayout, SurfacePolicy, SurfaceSpec, SurfaceValidationIssue, SurfaceWidget } from './types.js';
3
+ export declare const SURFACE_EDIT_SCHEMA_VERSION: "surface-edit/v1";
4
+ export declare const SURFACE_EDIT_LIMITS: {
5
+ readonly maxOperations: 64;
6
+ };
7
+ export type SurfaceEditOperation = {
8
+ readonly op: 'set-title';
9
+ readonly value: string;
10
+ } | {
11
+ readonly op: 'set-layout';
12
+ readonly value: SurfaceGridLayout;
13
+ } | {
14
+ readonly op: 'upsert-widget';
15
+ readonly widget: SurfaceWidget;
16
+ } | {
17
+ readonly op: 'remove-widget';
18
+ readonly id: string;
19
+ } | {
20
+ readonly op: 'upsert-source';
21
+ readonly source: SurfaceDataSource;
22
+ } | {
23
+ readonly op: 'remove-source';
24
+ readonly id: string;
25
+ } | {
26
+ readonly op: 'reorder-widgets';
27
+ readonly ids: readonly string[];
28
+ };
29
+ export interface SurfaceEditProposal {
30
+ readonly schemaVersion: typeof SURFACE_EDIT_SCHEMA_VERSION;
31
+ readonly catalogVersion: string;
32
+ readonly surfaceId: string;
33
+ readonly baseRevision: number;
34
+ readonly operations: readonly SurfaceEditOperation[];
35
+ }
36
+ export interface SurfaceRevision {
37
+ readonly revision: number;
38
+ readonly spec: SurfaceSpec;
39
+ }
40
+ export interface SurfaceEditIssue extends Omit<SurfaceValidationIssue, 'code'> {
41
+ readonly code: SurfaceValidationIssue['code'] | 'revision_conflict' | 'invalid_edit';
42
+ }
43
+ export type SurfaceRevisionResult = {
44
+ readonly ok: true;
45
+ readonly value: SurfaceRevision;
46
+ } | {
47
+ readonly ok: false;
48
+ readonly issues: readonly SurfaceEditIssue[];
49
+ };
50
+ export declare function createSurfaceRevision(spec: unknown, catalog: SurfaceCatalog, policy: SurfacePolicy, revision?: number): SurfaceRevisionResult;
51
+ /**
52
+ * 只生成下一版候选快照,不保存、不渲染、不访问 DataProvider。
53
+ * 服务端保存时仍必须对 revision 做原子 compare-and-swap,并重新授权。
54
+ */
55
+ export declare function applySurfaceEditProposal(current: SurfaceRevision, input: unknown, catalog: SurfaceCatalog, policy: SurfacePolicy): SurfaceRevisionResult;
56
+ export declare function createSurfaceEditSchema(): JsonObject;
57
+ export declare function buildSurfaceEditMessages(request: string, current: SurfaceRevision, catalog: SurfaceCatalog, policy: SurfacePolicy): readonly SurfaceAgentMessage[];