@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
package/dist/catalog.js CHANGED
@@ -3,8 +3,8 @@ import BarChartWidget from './components/BarChartWidget.svelte';
3
3
  import LineChartWidget from './components/LineChartWidget.svelte';
4
4
  import MetricWidget from './components/MetricWidget.svelte';
5
5
  import ResourceTableWidget from './components/ResourceTableWidget.svelte';
6
- import { barChartPropsSchema, lineChartPropsSchema, metricPropsSchema, resourceTablePropsSchema, } from './builtin-schemas.js';
7
- export const DEFAULT_SURFACE_CATALOG_VERSION = 'svadmin/v1';
6
+ import { defaultSurfaceDefinitions, styledSurfaceDefinitions } from './builtin-definitions.js';
7
+ export { DEFAULT_SURFACE_CATALOG_VERSION, STYLED_SURFACE_CATALOG_VERSION } from './builtin-definitions.js';
8
8
  export function defineSurfaceCatalog(catalog) {
9
9
  if (catalog.version.length === 0)
10
10
  throw new Error('Surface catalog version must not be empty');
@@ -13,49 +13,25 @@ export function defineSurfaceCatalog(catalog) {
13
13
  if (widgetTypes.has(widget.type))
14
14
  throw new Error(`Duplicate surface widget type "${widget.type}"`);
15
15
  widgetTypes.add(widget.type);
16
+ if ((widget.examples?.length ?? 0) > 3)
17
+ throw new Error('Catalog examples are limited to three per widget');
18
+ for (const example of widget.examples ?? []) {
19
+ if (!Value.Check(widget.propsSchema, example))
20
+ throw new Error('Invalid catalog example for ' + widget.type);
21
+ }
16
22
  }
17
23
  return catalog;
18
24
  }
19
- function tableFields(props) {
20
- return Value.Decode(resourceTablePropsSchema, props).columns.map((column) => column.field);
25
+ const components = {
26
+ metric: MetricWidget, 'resource-table': ResourceTableWidget, 'bar-chart': BarChartWidget, 'line-chart': LineChartWidget,
27
+ };
28
+ function rendered(definitions) {
29
+ return defineSurfaceCatalog({ ...definitions, widgets: definitions.widgets.map((widget) => {
30
+ const component = components[widget.type];
31
+ if (!component)
32
+ throw new Error(`Missing built-in component: ${widget.type}`);
33
+ return { ...widget, component };
34
+ }) });
21
35
  }
22
- function barChartFields(props) {
23
- const chartProps = Value.Decode(barChartPropsSchema, props);
24
- return [chartProps.labelField, chartProps.valueField];
25
- }
26
- function lineChartFields(props) {
27
- const chartProps = Value.Decode(lineChartPropsSchema, props);
28
- return [chartProps.labelField, chartProps.valueField];
29
- }
30
- export const defaultSurfaceCatalog = defineSurfaceCatalog({
31
- version: DEFAULT_SURFACE_CATALOG_VERSION,
32
- widgets: [
33
- {
34
- type: 'metric',
35
- dataKind: 'scalar',
36
- propsSchema: metricPropsSchema,
37
- component: MetricWidget,
38
- },
39
- {
40
- type: 'resource-table',
41
- dataKind: 'items',
42
- propsSchema: resourceTablePropsSchema,
43
- getReferencedFields: tableFields,
44
- component: ResourceTableWidget,
45
- },
46
- {
47
- type: 'bar-chart',
48
- dataKind: 'items',
49
- propsSchema: barChartPropsSchema,
50
- getReferencedFields: barChartFields,
51
- component: BarChartWidget,
52
- },
53
- {
54
- type: 'line-chart',
55
- dataKind: 'items',
56
- propsSchema: lineChartPropsSchema,
57
- getReferencedFields: lineChartFields,
58
- component: LineChartWidget,
59
- },
60
- ],
61
- });
36
+ export const defaultSurfaceCatalog = rendered(defaultSurfaceDefinitions);
37
+ export const styledSurfaceCatalog = rendered(styledSurfaceDefinitions);
@@ -0,0 +1,34 @@
1
+ <script lang="ts">
2
+ import { Value } from '@sinclair/typebox/value';
3
+ import ActivityFeed from '@svadmin/ui/components/ActivityFeed.svelte';
4
+ import { activityFeedPropsSchema } from '../business-definitions.js';
5
+ import { asSurfaceActivities } from '../business-data.js';
6
+ import type { SurfaceWidgetRendererProps } from '../catalog.js';
7
+ import { surfaceMetric } from '../recipes.js';
8
+ import { formatSurfaceMessage, resolveSurfaceMessages } from '../localization.js';
9
+
10
+ let { props, data, locale = 'en-US', messages }: SurfaceWidgetRendererProps = $props();
11
+ const baseProps = $derived(Object.fromEntries(Object.entries(props).filter(([key]) => key !== 'appearance')));
12
+ const p = $derived(Value.Decode(activityFeedPropsSchema, baseProps));
13
+ const appearance = $derived(typeof props['appearance'] === 'object' && props['appearance'] !== null ? props['appearance'] as { tone?: typeof p.tone; density?: typeof p.density } : undefined);
14
+ const tone = $derived(appearance?.tone ?? p.tone ?? 'neutral');
15
+ const density = $derived(appearance?.density ?? p.density ?? 'comfortable');
16
+ const classes = $derived(surfaceMetric({ tone, density }));
17
+ const text = $derived(resolveSurfaceMessages(locale, messages));
18
+ const activities = $derived(data.status === 'ready' ? asSurfaceActivities(data.value, p, text.activityUnknownActor) : null);
19
+ </script>
20
+
21
+ <section class={classes.root + ' ' + classes.card} aria-label={p.title} data-surface-business="activity-feed" data-surface-density={density}>
22
+ {#if data.status === 'loading'}
23
+ <p role="status">{text.activityLoading}</p>
24
+ {:else if data.status === 'empty'}
25
+ <p role="status">{text.activityEmpty}</p>
26
+ {:else if data.status === 'error'}
27
+ <p role="alert">{text.activityUnavailable}</p>
28
+ {:else if activities?.ok}
29
+ <ActivityFeed activities={activities.value} allowComment={false} title={p.title} emptyLabel={text.activityEmpty}
30
+ countLabel={formatSurfaceMessage(text.activityCount, { count: activities.value.length })} />
31
+ {:else}
32
+ <p role="alert">{text.activityInvalidData}</p>
33
+ {/if}
34
+ </section>
@@ -0,0 +1,4 @@
1
+ import type { SurfaceWidgetRendererProps } from '../catalog.js';
2
+ declare const ActivityFeedWidget: import("svelte").Component<SurfaceWidgetRendererProps, {}, "">;
3
+ type ActivityFeedWidget = ReturnType<typeof ActivityFeedWidget>;
4
+ export default ActivityFeedWidget;
@@ -1,7 +1,9 @@
1
1
  <script lang="ts">
2
2
  import { Value } from '@sinclair/typebox/value';
3
+ import { surfaceMetric } from '../recipes.js';
4
+ import '../styles.css';
3
5
  import StatsCard from '@svadmin/ui/components/StatsCard.svelte';
4
- import { metricPropsSchema } from '../builtin-schemas.js';
6
+ import { styledMetricPropsSchema as metricPropsSchema } from '../builtin-schemas.js';
5
7
  import type { SurfaceWidgetRendererProps } from '../catalog.js';
6
8
  import { resolveSurfaceMessages } from '../localization.js';
7
9
 
@@ -9,6 +11,8 @@
9
11
  const activeMessages = $derived(resolveSurfaceMessages(locale, messages));
10
12
 
11
13
  const metricProps = $derived(Value.Decode(metricPropsSchema, props));
14
+ const metricClasses = $derived(metricProps.tone !== undefined || metricProps.density !== undefined
15
+ ? surfaceMetric({ tone: metricProps.tone, density: metricProps.density }) : undefined);
12
16
  const formattedValue = $derived.by(() => {
13
17
  if (data.status !== 'ready') return '—';
14
18
  const value = data.value;
@@ -26,17 +30,17 @@
26
30
  });
27
31
  </script>
28
32
 
29
- <article aria-labelledby="{widgetId}-label" aria-busy={data.status === 'loading'}>
33
+ <article class={metricClasses?.root} data-surface-tone={metricProps.tone} data-surface-density={metricProps.density} aria-labelledby="{widgetId}-label" aria-busy={data.status === 'loading'}>
30
34
  <span id="{widgetId}-label" class="visually-hidden">{metricProps.label}</span>
31
35
  {#if data.status === 'loading'}
32
- <StatsCard label={metricProps.label} value="" loading />
36
+ <StatsCard class={metricClasses?.card ?? ''} label={metricProps.label} value="" loading />
33
37
  {:else if data.status === 'ready' && typeof data.value === 'number'}
34
- <StatsCard label={metricProps.label} value={formattedValue} />
38
+ <StatsCard class={metricClasses?.card ?? ''} label={metricProps.label} value={formattedValue} />
35
39
  {#if metricProps.description}
36
- <p class="metric-description">{metricProps.description}</p>
40
+ <p class={"metric-description " + (metricClasses?.description ?? "")}>{metricProps.description}</p>
37
41
  {/if}
38
42
  {:else}
39
- <div class="metric-state" role={data.status === 'error' ? 'alert' : 'status'}>
43
+ <div class={"metric-state " + (metricClasses?.state ?? "")} role={data.status === 'error' ? 'alert' : 'status'}>
40
44
  <strong>{metricProps.label}</strong>
41
45
  <span>{data.status === 'error' ? data.error.message : data.status === 'empty' ? activeMessages.metricNoData : activeMessages.metricInvalidData}</span>
42
46
  </div>
@@ -65,9 +69,10 @@
65
69
  .metric-state {
66
70
  display: grid;
67
71
  gap: 0.35rem;
68
- min-height: 6rem;
69
- padding: 1.25rem;
72
+ min-height: var(--svadmin-metric-state-height, 6rem);
73
+ padding: var(--svadmin-metric-state-padding, 1.25rem);
70
74
  border: 1px solid var(--border);
75
+ border-inline-start: var(--svadmin-metric-state-border-width, 1px) solid var(--svadmin-metric-state-accent, var(--border));
71
76
  border-radius: 0.75rem;
72
77
  background: var(--card);
73
78
  color: var(--muted-foreground);
@@ -1,3 +1,4 @@
1
+ import '../styles.css';
1
2
  import type { SurfaceWidgetRendererProps } from '../catalog.js';
2
3
  declare const MetricWidget: import("svelte").Component<SurfaceWidgetRendererProps, {}, "">;
3
4
  type MetricWidget = ReturnType<typeof MetricWidget>;
@@ -0,0 +1,56 @@
1
+ <script lang="ts">
2
+ import { Value } from '@sinclair/typebox/value';
3
+ import Card from '@svadmin/ui/components/ui/card/card.svelte';
4
+ import CardHeader from '@svadmin/ui/components/ui/card/card-header.svelte';
5
+ import CardTitle from '@svadmin/ui/components/ui/card/card-title.svelte';
6
+ import CardContent from '@svadmin/ui/components/ui/card/card-content.svelte';
7
+ import { resourceDetailPropsSchema } from '../business-definitions.js';
8
+ import type { SurfaceWidgetRendererProps } from '../catalog.js';
9
+ import type { JsonObject } from '../types.js';
10
+ import { surfaceMetric } from '../recipes.js';
11
+ import { resolveSurfaceMessages } from '../localization.js';
12
+ import { displayTableValue } from '../widget-data.js';
13
+
14
+ let { props, data, locale = 'en-US', messages }: SurfaceWidgetRendererProps = $props();
15
+ const baseProps = $derived(Object.fromEntries(Object.entries(props).filter(([key]) => key !== 'appearance')));
16
+ const p = $derived(Value.Decode(resourceDetailPropsSchema, baseProps));
17
+ const appearance = $derived(typeof props['appearance'] === 'object' && props['appearance'] !== null ? props['appearance'] as { tone?: typeof p.tone; density?: typeof p.density } : undefined);
18
+ const tone = $derived(appearance?.tone ?? p.tone ?? 'neutral');
19
+ const density = $derived(appearance?.density ?? p.density ?? 'comfortable');
20
+ const classes = $derived(surfaceMetric({ tone, density }));
21
+ const text = $derived(resolveSurfaceMessages(locale, messages));
22
+ const record = $derived(data.status === 'ready' && data.value !== null && typeof data.value === 'object' && !Array.isArray(data.value)
23
+ ? data.value as JsonObject : null);
24
+ </script>
25
+
26
+ <Card class={classes.root + ' ' + classes.card} data-surface-business="resource-detail" data-surface-density={density}>
27
+ <CardHeader><CardTitle>{p.title}</CardTitle></CardHeader>
28
+ <CardContent>
29
+ {#if data.status === 'loading'}
30
+ <p role="status">{text.detailLoading}</p>
31
+ {:else if data.status === 'empty'}
32
+ <p role="status">{text.detailEmpty}</p>
33
+ {:else if data.status === 'error'}
34
+ <p role="alert">{text.detailUnavailable}</p>
35
+ {:else if record}
36
+ <dl aria-label={p.title} data-density={density}>
37
+ {#each p.fields as item, index (`${index}:${item.field}`)}
38
+ <div>
39
+ <dt>{item.label}</dt>
40
+ <dd>{displayTableValue(record[item.field], { format: item.format, locale, messages: text })}</dd>
41
+ </div>
42
+ {/each}
43
+ </dl>
44
+ {:else}
45
+ <p role="alert">{text.detailInvalidData}</p>
46
+ {/if}
47
+ </CardContent>
48
+ </Card>
49
+
50
+ <style>
51
+ dl { display: grid; gap: 1rem; margin: 0; grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr)); }
52
+ dl > div { min-width: 0; }
53
+ dt { color: var(--muted-foreground); font-size: .8125rem; }
54
+ dd { margin: .25rem 0 0; color: var(--foreground); overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
55
+ dl[data-density='compact'] { gap: .5rem; }
56
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { SurfaceWidgetRendererProps } from '../catalog.js';
2
+ declare const ResourceDetailWidget: import("svelte").Component<SurfaceWidgetRendererProps, {}, "">;
3
+ type ResourceDetailWidget = ReturnType<typeof ResourceDetailWidget>;
4
+ export default ResourceDetailWidget;
@@ -0,0 +1,97 @@
1
+ <script lang="ts">
2
+ import { onDestroy, untrack } from 'svelte';
3
+ import { Value } from '@sinclair/typebox/value';
4
+ import JsonSchemaForm from '@svadmin/ui/components/JsonSchemaForm.svelte';
5
+ import { createSurfaceFormController, type SurfaceWorkflowClientState } from '../workflows/client.js';
6
+ import type { SurfaceWorkflowHost } from '../workflows/context.js';
7
+ import type { SurfaceActionDescriptor } from '../workflows/types.js';
8
+ import type { JsonObject } from '../types.js';
9
+ import { editorClasses, editorButtonClasses } from '../styles/editor.js';
10
+ import '../styles/editor.css';
11
+ let { widgetId, host, action, title, locale }: {
12
+ widgetId: string; host: SurfaceWorkflowHost; action: SurfaceActionDescriptor; title: string; locale: string;
13
+ } = $props();
14
+ const instanceId = $props.id();
15
+ let value = $state<Record<string, unknown>>({});
16
+ let formState = $state<SurfaceWorkflowClientState>({ busy: false });
17
+ let errors = $state<string[]>([]);
18
+ const schema = $derived(JSON.parse(JSON.stringify(action.inputSchema)));
19
+ const zh = $derived(locale.startsWith('zh'));
20
+ const labels = $derived(zh ? {
21
+ submit: '提交操作提案', confirm: '确认这些参数', execute: '执行已批准操作', refresh: '查询状态', reject: '取消提案', reset: '开始新操作',
22
+ disabled: '预览模式:业务操作尚未启用', pending: '等待确认或独立审批', approved: '已批准,尚未执行',
23
+ executing: '正在执行,请勿重复提交', succeeded: '操作已完成', indeterminate: '结果不明确,需要服务端核对,禁止直接重试', rejected: '提案已取消',
24
+ error: '操作未完成。请先查询状态,避免重复执行。', args: '待确认参数',
25
+ } : {
26
+ submit: 'Propose action', confirm: 'Confirm these arguments', execute: 'Execute approved action', refresh: 'Check status', reject: 'Cancel proposal', reset: 'Start another action',
27
+ disabled: 'Preview mode: business actions are disabled', pending: 'Waiting for confirmation or independent approval', approved: 'Approved; not yet executed',
28
+ executing: 'Executing; do not submit again', succeeded: 'Action completed', indeterminate: 'Outcome unknown; server reconciliation is required before retry', rejected: 'Proposal cancelled',
29
+ error: 'Operation did not complete. Check status before retrying.', args: 'Arguments to confirm',
30
+ });
31
+ const scope = $derived(host.getScope());
32
+ const controller = untrack(() => createSurfaceFormController({ action, getScope: host.getScope,
33
+ onState: (next) => { formState = next; }, onCommitted: host.committed }));
34
+ let identity: readonly unknown[] = [];
35
+ $effect.pre(() => {
36
+ const next = [scope.scopeKey, scope.surfaceId, scope.revision, scope.transport, scope.enabled];
37
+ untrack(() => {
38
+ if (identity.length && next.some((part, index) => part !== identity[index])) {
39
+ controller.reset();
40
+ value = {};
41
+ errors = [];
42
+ }
43
+ identity = next;
44
+ });
45
+ });
46
+ onDestroy(() => controller.dispose());
47
+ async function submit(input: Record<string, unknown>) {
48
+ // Optional cleared fields may be undefined in a bound form; omission is the
49
+ // wire representation. The server repeats JSON and schema validation.
50
+ const args: unknown = JSON.parse(JSON.stringify(input));
51
+ if (!Value.Check(action.inputSchema, args)) {
52
+ errors = [...Value.Errors(action.inputSchema, args)].slice(0, 12).map((error) => `${error.path}: ${error.message}`);
53
+ return;
54
+ }
55
+ errors = [];
56
+ await controller.submit(args as JsonObject);
57
+ }
58
+ </script>
59
+
60
+ <section class="svadmin-surface-editor {editorClasses.comfortable}" aria-labelledby="{instanceId}-form-title" aria-busy={formState.busy}>
61
+ <h3 id="{instanceId}-form-title">{title}</h3>
62
+ {#if !scope.enabled}<p role="status">{labels.disabled}</p>{/if}
63
+ {#if errors.length}
64
+ <div role="alert"><ul>{#each errors as error, index (`${index}:${error}`)}<li>{error}</li>{/each}</ul></div>
65
+ {/if}
66
+ <fieldset disabled={!scope.enabled || formState.busy || !!formState.proposal} class="form-fields">
67
+ <JsonSchemaForm {schema} {locale} bind:value onsubmit={submit} submitText={labels.submit} idPrefix={`surface-${widgetId}-${instanceId}`} />
68
+ </fieldset>
69
+ {#if formState.error}<p role="alert">{labels.error}</p>{/if}
70
+ {#if formState.proposal}
71
+ {@const p = formState.proposal}
72
+ <section aria-label={p.actionLabel}>
73
+ <p role="status" data-workflow-status={p.status}>{labels[p.status]}</p>
74
+ <strong>{p.actionLabel}</strong>
75
+ <details><summary>{labels.args}</summary><pre>{JSON.stringify(p.args, null, 2)}</pre></details>
76
+ <div data-part="actions">
77
+ {#if p.status === 'pending' && p.approval === 'confirm'}
78
+ <button class={editorButtonClasses.secondary} disabled={formState.busy || !scope.enabled} onclick={() => controller.confirm()}>{labels.confirm}</button>
79
+ {/if}
80
+ {#if p.status === 'approved'}
81
+ <button class={editorButtonClasses.primary} disabled={formState.busy || !scope.enabled} onclick={() => controller.execute()}>{labels.execute}</button>
82
+ {/if}
83
+ <button class={editorButtonClasses.secondary} disabled={formState.busy || !scope.enabled} onclick={() => controller.refresh()}>{labels.refresh}</button>
84
+ {#if p.status === 'pending' || p.status === 'approved'}
85
+ <button class={editorButtonClasses.secondary} disabled={formState.busy || !scope.enabled} onclick={() => controller.reject()}>{labels.reject}</button>
86
+ {:else if p.status === 'succeeded' || p.status === 'rejected'}
87
+ <button class={editorButtonClasses.secondary} disabled={formState.busy} onclick={() => controller.reset()}>{labels.reset}</button>
88
+ {/if}
89
+ </div>
90
+ </section>
91
+ {/if}
92
+ </section>
93
+
94
+ <style>
95
+ .form-fields { border: 0; padding: 0; margin: 0; min-width: 0; }
96
+ pre { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 16rem; overflow: auto; }
97
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { SurfaceWorkflowHost } from '../workflows/context.js';
2
+ import type { SurfaceActionDescriptor } from '../workflows/types.js';
3
+ import '../styles/editor.css';
4
+ type $$ComponentProps = {
5
+ widgetId: string;
6
+ host: SurfaceWorkflowHost;
7
+ action: SurfaceActionDescriptor;
8
+ title: string;
9
+ locale: string;
10
+ };
11
+ declare const ResourceFormBody: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type ResourceFormBody = ReturnType<typeof ResourceFormBody>;
13
+ export default ResourceFormBody;
@@ -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,7 @@
1
1
  <script lang="ts">
2
2
  import { Value } from '@sinclair/typebox/value';
3
+ import { surfaceTable } from '../recipes.js';
4
+ import '../styles.css';
3
5
  import CardContent from '@svadmin/ui/components/ui/card/card-content.svelte';
4
6
  import CardHeader from '@svadmin/ui/components/ui/card/card-header.svelte';
5
7
  import CardTitle from '@svadmin/ui/components/ui/card/card-title.svelte';
@@ -10,7 +12,7 @@
10
12
  import TableHeader from '@svadmin/ui/components/ui/table/table-header.svelte';
11
13
  import TableRow from '@svadmin/ui/components/ui/table/table-row.svelte';
12
14
  import Table from '@svadmin/ui/components/ui/table/table.svelte';
13
- import { resourceTablePropsSchema } from '../builtin-schemas.js';
15
+ import { styledResourceTablePropsSchema as resourceTablePropsSchema } from '../builtin-schemas.js';
14
16
  import type { SurfaceWidgetRendererProps } from '../catalog.js';
15
17
  import { resolveSurfaceMessages } from '../localization.js';
16
18
  import { asRecordArray, displayTableValue } from '../widget-data.js';
@@ -19,27 +21,28 @@
19
21
  const activeMessages = $derived(resolveSurfaceMessages(locale, messages));
20
22
 
21
23
  const tableProps = $derived(Value.Decode(resourceTablePropsSchema, props));
24
+ const tableClasses = $derived(tableProps.density === undefined ? undefined : surfaceTable({ density: tableProps.density }));
22
25
  const records = $derived(data.status === 'ready' ? asRecordArray(data.value) : null);
23
26
  </script>
24
27
 
25
- <Card>
26
- <CardHeader>
28
+ <Card class={tableClasses?.root} data-surface-density={tableProps.density}>
29
+ <CardHeader class={tableClasses?.header}>
27
30
  <CardTitle>{tableProps.title}</CardTitle>
28
31
  </CardHeader>
29
- <CardContent>
32
+ <CardContent class={tableClasses?.content}>
30
33
  {#if data.status === 'loading'}
31
- <div class="table-state table-loading" role="status">{activeMessages.tableLoading}</div>
34
+ <div class={"table-state table-loading " + (tableClasses?.state ?? "")} role="status">{activeMessages.tableLoading}</div>
32
35
  {:else if data.status === 'empty'}
33
- <p class="table-state" role="status">{tableProps.emptyLabel ?? activeMessages.tableNoRecords}</p>
36
+ <p class={"table-state " + (tableClasses?.state ?? "")} role="status">{tableProps.emptyLabel ?? activeMessages.tableNoRecords}</p>
34
37
  {:else if data.status === 'error'}
35
- <p class="table-state" role="alert">{data.error.message}</p>
38
+ <p class={"table-state " + (tableClasses?.state ?? "")} role="alert">{data.error.message}</p>
36
39
  {:else if data.status === 'ready' && records?.ok}
37
40
  <div class="surface-table">
38
- <Table aria-label={tableProps.title}>
41
+ <Table density={tableProps.density ?? "comfortable"} aria-label={tableProps.title}>
39
42
  <TableHeader>
40
43
  <TableRow>
41
44
  {#each tableProps.columns as column (column.field)}
42
- <TableHead scope="col">{column.label}</TableHead>
45
+ <TableHead class={tableClasses?.head} scope="col">{column.label}</TableHead>
43
46
  {/each}
44
47
  </TableRow>
45
48
  </TableHeader>
@@ -47,7 +50,7 @@
47
50
  {#each records.value as record, index (index)}
48
51
  <TableRow>
49
52
  {#each tableProps.columns as column (column.field)}
50
- <TableCell>{displayTableValue(record[column.field], {
53
+ <TableCell class={tableClasses?.cell}>{displayTableValue(record[column.field], {
51
54
  format: column.format,
52
55
  locale,
53
56
  messages: activeMessages,
@@ -59,7 +62,7 @@
59
62
  </Table>
60
63
  </div>
61
64
  {:else}
62
- <p class="table-state" role="alert">{records && !records.ok ? activeMessages.tableInvalidData : activeMessages.tableUnavailable}</p>
65
+ <p class={"table-state " + (tableClasses?.state ?? "")} role="alert">{records && !records.ok ? activeMessages.tableInvalidData : activeMessages.tableUnavailable}</p>
63
66
  {/if}
64
67
  </CardContent>
65
68
  </Card>
@@ -67,7 +70,7 @@
67
70
  <style>
68
71
  .table-state {
69
72
  display: grid;
70
- min-height: 8rem;
73
+ min-height: var(--svadmin-table-state-height, 8rem);
71
74
  margin: 0;
72
75
  place-items: center;
73
76
  color: var(--muted-foreground);
@@ -1,3 +1,4 @@
1
+ import '../styles.css';
1
2
  import type { SurfaceWidgetRendererProps } from '../catalog.js';
2
3
  declare const ResourceTableWidget: import("svelte").Component<SurfaceWidgetRendererProps, {}, "">;
3
4
  type ResourceTableWidget = ReturnType<typeof ResourceTableWidget>;
@@ -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.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;