@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.
- package/README.md +70 -38
- package/STYLING.md +31 -0
- package/compatibility.json +23 -2
- package/dist/agent-contract.d.ts +37 -0
- package/dist/agent-contract.js +203 -0
- package/dist/agent.d.ts +38 -7
- package/dist/agent.js +98 -59
- package/dist/binding.d.ts +2 -0
- package/dist/binding.js +7 -1
- package/dist/builtin-definitions.d.ts +6 -0
- package/dist/builtin-definitions.js +40 -0
- package/dist/builtin-schemas.d.ts +25 -0
- package/dist/builtin-schemas.js +13 -0
- package/dist/catalog.d.ts +4 -61
- package/dist/catalog.js +20 -44
- package/dist/components/MetricWidget.svelte +12 -8
- package/dist/components/ResourceFormBody.svelte +97 -0
- package/dist/components/ResourceFormBody.svelte.d.ts +13 -0
- package/dist/components/ResourceFormWidget.svelte +19 -0
- package/dist/components/ResourceFormWidget.svelte.d.ts +4 -0
- package/dist/components/ResourceTableWidget.svelte +14 -12
- package/dist/components/SurfaceEditPreview.svelte +107 -0
- package/dist/components/SurfaceEditPreview.svelte.d.ts +20 -0
- package/dist/components/SurfaceRenderer.svelte +120 -163
- package/dist/components/SurfaceRenderer.svelte.d.ts +4 -0
- package/dist/components/SurfaceWorkflowProvider.svelte +22 -0
- package/dist/components/SurfaceWorkflowProvider.svelte.d.ts +17 -0
- package/dist/components/SvarGridWidget.svelte +47 -0
- package/dist/components/SvarGridWidget.svelte.d.ts +4 -0
- package/dist/components/SvarSurfaceProvider.svelte +12 -0
- package/dist/components/SvarSurfaceProvider.svelte.d.ts +11 -0
- package/dist/edits.d.ts +57 -0
- package/dist/edits.js +166 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +5 -1
- package/dist/interactive.d.ts +6 -0
- package/dist/interactive.js +15 -0
- package/dist/openui.d.ts +64 -0
- package/dist/openui.js +168 -0
- package/dist/schema.d.ts +108 -0
- package/dist/schema.js +73 -0
- package/dist/server-sqlite.d.ts +1 -0
- package/dist/server-sqlite.js +2 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +4 -0
- package/dist/source-cache.d.ts +30 -0
- package/dist/source-cache.js +135 -0
- package/dist/styled-system/css/conditions.js +36 -0
- package/dist/styled-system/css/css.d.ts +22 -0
- package/dist/styled-system/css/css.js +45 -0
- package/dist/styled-system/css/cva.d.ts +6 -0
- package/dist/styled-system/css/cva.js +87 -0
- package/dist/styled-system/css/cx.d.ts +5 -0
- package/dist/styled-system/css/cx.js +15 -0
- package/dist/styled-system/css/index.d.ts +5 -0
- package/dist/styled-system/css/index.js +4 -0
- package/dist/styled-system/css/sva.d.ts +4 -0
- package/dist/styled-system/css/sva.js +46 -0
- package/dist/styled-system/design-contract.d.ts +13 -0
- package/dist/styled-system/design-contract.js +11 -0
- package/dist/styled-system/helpers.js +316 -0
- package/dist/styled-system/patterns/aspect-ratio.d.ts +20 -0
- package/dist/styled-system/patterns/aspect-ratio.js +38 -0
- package/dist/styled-system/patterns/bleed.d.ts +21 -0
- package/dist/styled-system/patterns/bleed.js +24 -0
- package/dist/styled-system/patterns/box.d.ts +20 -0
- package/dist/styled-system/patterns/box.js +15 -0
- package/dist/styled-system/patterns/center.d.ts +20 -0
- package/dist/styled-system/patterns/center.js +21 -0
- package/dist/styled-system/patterns/circle.d.ts +20 -0
- package/dist/styled-system/patterns/circle.js +25 -0
- package/dist/styled-system/patterns/container.d.ts +20 -0
- package/dist/styled-system/patterns/container.js +21 -0
- package/dist/styled-system/patterns/cq.d.ts +21 -0
- package/dist/styled-system/patterns/cq.js +21 -0
- package/dist/styled-system/patterns/divider.d.ts +22 -0
- package/dist/styled-system/patterns/divider.js +25 -0
- package/dist/styled-system/patterns/flex.d.ts +26 -0
- package/dist/styled-system/patterns/flex.js +26 -0
- package/dist/styled-system/patterns/float.d.ts +23 -0
- package/dist/styled-system/patterns/float.js +52 -0
- package/dist/styled-system/patterns/grid-item.d.ts +25 -0
- package/dist/styled-system/patterns/grid-item.js +25 -0
- package/dist/styled-system/patterns/grid.d.ts +24 -0
- package/dist/styled-system/patterns/grid.js +27 -0
- package/dist/styled-system/patterns/hstack.d.ts +21 -0
- package/dist/styled-system/patterns/hstack.js +24 -0
- package/dist/styled-system/patterns/index.d.ts +21 -0
- package/dist/styled-system/patterns/index.js +20 -0
- package/dist/styled-system/patterns/link-overlay.d.ts +20 -0
- package/dist/styled-system/patterns/link-overlay.js +24 -0
- package/dist/styled-system/patterns/spacer.d.ts +20 -0
- package/dist/styled-system/patterns/spacer.js +25 -0
- package/dist/styled-system/patterns/square.d.ts +20 -0
- package/dist/styled-system/patterns/square.js +24 -0
- package/dist/styled-system/patterns/stack.d.ts +23 -0
- package/dist/styled-system/patterns/stack.js +24 -0
- package/dist/styled-system/patterns/visually-hidden.d.ts +20 -0
- package/dist/styled-system/patterns/visually-hidden.js +18 -0
- package/dist/styled-system/patterns/vstack.d.ts +21 -0
- package/dist/styled-system/patterns/vstack.js +24 -0
- package/dist/styled-system/patterns/wrap.d.ts +24 -0
- package/dist/styled-system/patterns/wrap.js +25 -0
- package/dist/styled-system/recipes/content-header.d.ts +31 -0
- package/dist/styled-system/recipes/content-header.js +65 -0
- package/dist/styled-system/recipes/content-page.d.ts +34 -0
- package/dist/styled-system/recipes/content-page.js +43 -0
- package/dist/styled-system/recipes/create-recipe.js +82 -0
- package/dist/styled-system/recipes/index.d.ts +17 -0
- package/dist/styled-system/recipes/index.js +16 -0
- package/dist/styled-system/recipes/metric-block.d.ts +34 -0
- package/dist/styled-system/recipes/metric-block.js +76 -0
- package/dist/styled-system/recipes/product-list.d.ts +31 -0
- package/dist/styled-system/recipes/product-list.js +73 -0
- package/dist/styled-system/recipes/product-section.d.ts +31 -0
- package/dist/styled-system/recipes/product-section.js +49 -0
- package/dist/styled-system/recipes/product-settings-row.d.ts +34 -0
- package/dist/styled-system/recipes/product-settings-row.js +58 -0
- package/dist/styled-system/recipes/product-settings.d.ts +31 -0
- package/dist/styled-system/recipes/product-settings.js +57 -0
- package/dist/styled-system/recipes/product-status.d.ts +34 -0
- package/dist/styled-system/recipes/product-status.js +45 -0
- package/dist/styled-system/recipes/product-toolbar.d.ts +31 -0
- package/dist/styled-system/recipes/product-toolbar.js +41 -0
- package/dist/styled-system/recipes/product-workspace.d.ts +34 -0
- package/dist/styled-system/recipes/product-workspace.js +58 -0
- package/dist/styled-system/recipes/surface-metric.d.ts +38 -0
- package/dist/styled-system/recipes/surface-metric.js +63 -0
- package/dist/styled-system/recipes/surface-table.d.ts +34 -0
- package/dist/styled-system/recipes/surface-table.js +62 -0
- package/dist/styled-system/recipes/ui-badge.d.ts +31 -0
- package/dist/styled-system/recipes/ui-badge.js +38 -0
- package/dist/styled-system/recipes/ui-button.d.ts +32 -0
- package/dist/styled-system/recipes/ui-button.js +43 -0
- package/dist/styled-system/recipes/ui-input.d.ts +31 -0
- package/dist/styled-system/recipes/ui-input.js +49 -0
- package/dist/styled-system/recipes/ui-textarea.d.ts +31 -0
- package/dist/styled-system/recipes/ui-textarea.js +24 -0
- package/dist/styled-system/styles.css +1257 -0
- package/dist/styled-system/tokens/index.d.ts +9 -0
- package/dist/styled-system/tokens/index.js +268 -0
- package/dist/styled-system/tokens/tokens.d.ts +36 -0
- package/dist/styled-system/types/composition.d.ts +227 -0
- package/dist/styled-system/types/conditions.d.ts +236 -0
- package/dist/styled-system/types/csstype.d.ts +22570 -0
- package/dist/styled-system/types/index.d.ts +6 -0
- package/dist/styled-system/types/parts.d.ts +8 -0
- package/dist/styled-system/types/pattern.d.ts +78 -0
- package/dist/styled-system/types/prop-type.d.ts +223 -0
- package/dist/styled-system/types/recipe.d.ts +181 -0
- package/dist/styled-system/types/selectors.d.ts +59 -0
- package/dist/styled-system/types/static-css.d.ts +56 -0
- package/dist/styled-system/types/style-props.d.ts +8088 -0
- package/dist/styled-system/types/system-types.d.ts +151 -0
- package/dist/styles/editor.css +124 -0
- package/dist/styles/editor.generated.d.ts +8 -0
- package/dist/styles/editor.generated.js +9 -0
- package/dist/styles.css +1262 -0
- package/dist/svar-catalog.d.ts +3 -0
- package/dist/svar-catalog.js +7 -0
- package/dist/svar-context.d.ts +9 -0
- package/dist/svar-context.js +1 -0
- package/dist/svar-schema.d.ts +21 -0
- package/dist/svar-schema.js +36 -0
- package/dist/svar.d.ts +4 -0
- package/dist/svar.js +3 -0
- package/dist/svelte.d.ts +4 -2
- package/dist/svelte.js +2 -1
- package/dist/types.d.ts +4 -0
- package/dist/validation.js +2 -102
- package/dist/wire.d.ts +13 -0
- package/dist/wire.js +30 -0
- package/dist/workflows/action-contracts.d.ts +7 -0
- package/dist/workflows/action-contracts.js +54 -0
- package/dist/workflows/catalog.d.ts +20 -0
- package/dist/workflows/catalog.js +50 -0
- package/dist/workflows/client.d.ts +45 -0
- package/dist/workflows/client.js +125 -0
- package/dist/workflows/context.d.ts +9 -0
- package/dist/workflows/context.js +5 -0
- package/dist/workflows/openui-guard.d.ts +19 -0
- package/dist/workflows/openui-guard.js +209 -0
- package/dist/workflows/service.d.ts +58 -0
- package/dist/workflows/service.js +296 -0
- package/dist/workflows/sqlite-store.d.ts +20 -0
- package/dist/workflows/sqlite-store.js +91 -0
- package/dist/workflows/types.d.ts +103 -0
- package/dist/workflows/types.js +8 -0
- package/dist/workflows.d.ts +5 -0
- package/dist/workflows.js +4 -0
- package/package.json +52 -8
package/dist/agent.js
CHANGED
|
@@ -1,78 +1,117 @@
|
|
|
1
1
|
import { Type } from '@sinclair/typebox';
|
|
2
2
|
import { TypeCompiler } from '@sinclair/typebox/compiler';
|
|
3
|
-
import {
|
|
3
|
+
import { SURFACE_AGENT_LIMITS, SURFACE_AGENT_RESPONSE_SCHEMA_VERSION, SURFACE_AGENT_SCHEMA_VERSION, createSurfaceAgentResponseSchema, createSurfaceCatalogManifest, createSurfaceGenerationSpecSchema, surfaceCannotFulfillSchema, surfaceSchemaToJson, } from './agent-contract.js';
|
|
4
4
|
import { validateSurfaceSpec } from './validation.js';
|
|
5
|
-
|
|
6
|
-
export
|
|
7
|
-
const
|
|
5
|
+
import { parseSurfaceJson, surfaceWireError } from './wire.js';
|
|
6
|
+
export { SURFACE_AGENT_SCHEMA_VERSION } from './agent-contract.js';
|
|
7
|
+
const proposalFields = {
|
|
8
8
|
schemaVersion: Type.Literal(SURFACE_AGENT_SCHEMA_VERSION),
|
|
9
9
|
action: Type.Literal('propose'),
|
|
10
10
|
summary: Type.Optional(Type.String({ minLength: 1, maxLength: 240 })),
|
|
11
11
|
spec: Type.Unknown(),
|
|
12
|
+
};
|
|
13
|
+
const proposalSchema = Type.Object(proposalFields, { additionalProperties: false });
|
|
14
|
+
const responseProposalSchema = Type.Object({
|
|
15
|
+
...proposalFields,
|
|
16
|
+
schemaVersion: Type.Literal(SURFACE_AGENT_RESPONSE_SCHEMA_VERSION),
|
|
12
17
|
}, { additionalProperties: false });
|
|
13
18
|
const compiledProposalSchema = TypeCompiler.Compile(proposalSchema);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return JSON.parse(source);
|
|
19
|
+
const compiledResponseSchema = TypeCompiler.Compile(Type.Union([
|
|
20
|
+
proposalSchema, responseProposalSchema, surfaceCannotFulfillSchema,
|
|
21
|
+
]));
|
|
22
|
+
function schemaIssues(errors) {
|
|
23
|
+
return [...errors].map((issue) => ({ code: 'invalid_json', path: issue.path || '', message: issue.message }));
|
|
20
24
|
}
|
|
21
|
-
|
|
22
|
-
return {
|
|
23
|
-
code: 'invalid_json',
|
|
24
|
-
path: '/',
|
|
25
|
-
message,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
function proposalSchemaIssues(errors) {
|
|
29
|
-
return [...errors].map((issue) => ({
|
|
30
|
-
code: 'invalid_json',
|
|
31
|
-
path: issue.path || jsonPointer([]),
|
|
32
|
-
message: issue.message,
|
|
33
|
-
}));
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Parse and fully validate an AI-generated proposal before a host previews it.
|
|
37
|
-
* This function has no side effects and never queries a provider.
|
|
38
|
-
*/
|
|
25
|
+
/** 保留 v1 的函数签名、返回类型以及带说明文字的 fenced JSON 兼容行为。 */
|
|
39
26
|
export function parseSurfaceAgentProposal(input, catalog, policy) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return { ok: false, issues: [proposalIssue('Agent proposal must be valid JSON')] };
|
|
27
|
+
const parsed = parseSurfaceJson(input, true);
|
|
28
|
+
if (!parsed.ok)
|
|
29
|
+
return parsed;
|
|
30
|
+
if (!compiledProposalSchema.Check(parsed.value)) {
|
|
31
|
+
return { ok: false, issues: schemaIssues(compiledProposalSchema.Errors(parsed.value)) };
|
|
46
32
|
}
|
|
47
|
-
|
|
48
|
-
|
|
33
|
+
const surface = validateSurfaceSpec(parsed.value.spec, catalog, policy);
|
|
34
|
+
if (!surface.ok)
|
|
35
|
+
return surface;
|
|
36
|
+
return { ok: true, value: { ...parsed.value, spec: surface.value } };
|
|
37
|
+
}
|
|
38
|
+
/** 结构化“无法完成”也是有效响应,但永远不是可渲染/可执行的提案。 */
|
|
39
|
+
export function parseSurfaceAgentResponse(input, catalog, policy) {
|
|
40
|
+
const parsed = parseSurfaceJson(input);
|
|
41
|
+
if (!parsed.ok)
|
|
42
|
+
return parsed;
|
|
43
|
+
if (!compiledResponseSchema.Check(parsed.value)) {
|
|
44
|
+
return { ok: false, issues: schemaIssues(compiledResponseSchema.Errors(parsed.value)) };
|
|
49
45
|
}
|
|
50
|
-
const
|
|
51
|
-
|
|
46
|
+
const candidate = parsed.value;
|
|
47
|
+
if (candidate.action === 'cannot-fulfill')
|
|
48
|
+
return { ok: true, value: candidate };
|
|
49
|
+
const surface = validateSurfaceSpec(candidate.spec, catalog, policy);
|
|
52
50
|
if (!surface.ok)
|
|
53
51
|
return surface;
|
|
52
|
+
return { ok: true, value: { ...candidate, spec: surface.value } };
|
|
53
|
+
}
|
|
54
|
+
const safetyInstructions = 'Never generate or execute Svelte, HTML, CSS, JavaScript, SQL, URLs, event handlers, or mutations. Use only catalog widgets and policy-authorized resources and fields. Widget IDs and source IDs must be unique across the document. Bind item widgets to /items, and scalar widgets to /total or an explicitly readable getOne field. JSON Schema does not replace runtime policy validation or server authorization.';
|
|
55
|
+
function validateRequest(request) {
|
|
56
|
+
if (typeof request !== 'string' || request.trim().length === 0 || request.length > SURFACE_AGENT_LIMITS.maxRequestCharacters) {
|
|
57
|
+
throw new Error('Surface request must be non-empty and within the request character limit');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function contractContext(catalog, policy) {
|
|
61
|
+
const manifest = createSurfaceCatalogManifest(catalog);
|
|
62
|
+
const resources = Object.entries(policy.resources).map(([resource, rule]) => (`${resource}(read=${rule.readFields.join(',') || '(none)'};filter=${rule.filterFields?.join(',') || '(none)'};sort=${rule.sortFields?.join(',') || '(none)'};getOne=${rule.allowGetOne === true};maxPageSize=${rule.maxPageSize ?? 'default'})`)).join(' | ') || '(none)';
|
|
63
|
+
return `Catalog: ${JSON.stringify(manifest.catalogVersion)}\nComponent contracts: ${JSON.stringify(manifest)}\nResource policy: ${resources}\nPolicy JSON: ${JSON.stringify(policy)}`;
|
|
64
|
+
}
|
|
65
|
+
function boundedPrompt(prompt) {
|
|
66
|
+
if (prompt.length > SURFACE_AGENT_LIMITS.maxContractCharacters)
|
|
67
|
+
throw new Error('Surface prompt exceeds the contract character limit; select a smaller catalog');
|
|
68
|
+
return prompt;
|
|
69
|
+
}
|
|
70
|
+
/** 兼容旧 v1 输出,同时补齐从组件契约派生的完整参数/结构 schema。 */
|
|
71
|
+
export function buildSurfaceAgentPrompt(request, catalog, policy) {
|
|
72
|
+
validateRequest(request);
|
|
73
|
+
const schema = surfaceSchemaToJson(Type.Object({
|
|
74
|
+
...proposalFields,
|
|
75
|
+
spec: createSurfaceGenerationSpecSchema(catalog, policy),
|
|
76
|
+
}, { additionalProperties: false }));
|
|
77
|
+
return boundedPrompt(`${request}\n\n[svadmin surface agent protocol]\nReturn only a human-reviewable fenced JSON proposal. ${safetyInstructions}\n${contractContext(catalog, policy)}\nOutput JSON Schema: ${JSON.stringify(schema)}\nIf the request cannot be represented safely, explain the limitation without inventing fields or capabilities. For machine-readable cannot-fulfill responses use the surface-agent/v2 message API.`);
|
|
78
|
+
}
|
|
79
|
+
/** 系统契约与用户需求分开传递;不绑定某个模型厂商或 CSS 编译器。 */
|
|
80
|
+
export function buildSurfaceAgentMessages(request, catalog, policy) {
|
|
81
|
+
validateRequest(request);
|
|
82
|
+
const content = boundedPrompt(`[svadmin surface agent protocol]\nReturn exactly one JSON object matching the output schema, without markdown or prose. ${safetyInstructions}\n${contractContext(catalog, policy)}\nOutput JSON Schema: ${JSON.stringify(createSurfaceAgentResponseSchema(catalog, policy))}\nIf the request cannot be represented, return action "cannot-fulfill" with a reason and a short message. Never invent capabilities or return an incomplete spec.`);
|
|
83
|
+
return [{ role: 'system', content }, { role: 'user', content: request }];
|
|
84
|
+
}
|
|
85
|
+
/** 有界传输缓冲,不冒充 OpenUI 增量渲染;半成品不进入运行时。 */
|
|
86
|
+
export function createSurfaceAgentStream(catalog, policy) {
|
|
87
|
+
let chunks = [];
|
|
88
|
+
let characters = 0;
|
|
89
|
+
let result;
|
|
54
90
|
return {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
91
|
+
push(chunk) {
|
|
92
|
+
if (result !== undefined)
|
|
93
|
+
return surfaceWireError('Surface stream is already closed');
|
|
94
|
+
if (typeof chunk !== 'string') {
|
|
95
|
+
result = surfaceWireError('Surface stream chunks must be text');
|
|
96
|
+
}
|
|
97
|
+
else if (characters + chunk.length > SURFACE_AGENT_LIMITS.maxInputCharacters) {
|
|
98
|
+
result = surfaceWireError('Surface stream is too large', 'limit_exceeded');
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
characters += chunk.length;
|
|
102
|
+
if (chunk.length > 0)
|
|
103
|
+
chunks.push(chunk);
|
|
104
|
+
return { ok: true, characters };
|
|
105
|
+
}
|
|
106
|
+
chunks = [];
|
|
107
|
+
return result;
|
|
108
|
+
},
|
|
109
|
+
finish() {
|
|
110
|
+
if (result === undefined) {
|
|
111
|
+
result = parseSurfaceAgentResponse(chunks.join(''), catalog, policy);
|
|
112
|
+
chunks = [];
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
61
115
|
},
|
|
62
116
|
};
|
|
63
117
|
}
|
|
64
|
-
/** Build a model instruction that keeps generation inside the Surface contract. */
|
|
65
|
-
export function buildSurfaceAgentPrompt(request, catalog, policy) {
|
|
66
|
-
const widgetTypes = catalog.widgets.map((widget) => widget.type).join(', ') || '(none)';
|
|
67
|
-
const resources = Object.entries(policy.resources).map(([resource, resourcePolicy]) => {
|
|
68
|
-
const permissions = [
|
|
69
|
-
`read=${resourcePolicy.readFields.join(',') || '(none)'}`,
|
|
70
|
-
`filter=${resourcePolicy.filterFields?.join(',') || '(none)'}`,
|
|
71
|
-
`sort=${resourcePolicy.sortFields?.join(',') || '(none)'}`,
|
|
72
|
-
`getOne=${resourcePolicy.allowGetOne === true}`,
|
|
73
|
-
`maxPageSize=${resourcePolicy.maxPageSize ?? 'default'}`,
|
|
74
|
-
];
|
|
75
|
-
return `${resource}(${permissions.join(';')})`;
|
|
76
|
-
}).join(' | ') || '(none)';
|
|
77
|
-
return `${request}\n\n[svadmin surface agent protocol]\nReturn only a human-reviewable fenced JSON proposal. Never generate or execute Svelte, HTML, CSS, JavaScript, SQL, URLs, event handlers, or mutations. The envelope must be {"schemaVersion":"${SURFACE_AGENT_SCHEMA_VERSION}","action":"propose","summary":"...","spec":{...}}. The spec must use schemaVersion "surface/v1" and catalogVersion "${catalog.version}". Allowed widget types: ${widgetTypes}. Resource policy: ${resources}. Use only catalog widgets and policy-authorized resources and fields. If the request cannot be represented safely, explain the limitation without inventing fields or capabilities.`;
|
|
78
|
-
}
|
package/dist/binding.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import type { SurfaceSourceDataState, SurfaceWidget, SurfaceWidgetDataState } from './types.js';
|
|
2
2
|
export declare function resolveSurfaceWidgetData(widget: SurfaceWidget, sourceStates: Readonly<Record<string, SurfaceSourceDataState>>): SurfaceWidgetDataState;
|
|
3
|
+
/** Resolve one binding without subscribing the widget to unrelated source states. */
|
|
4
|
+
export declare function resolveSurfaceSourceData(widget: SurfaceWidget, sourceState: SurfaceSourceDataState | undefined): SurfaceWidgetDataState;
|
package/dist/binding.js
CHANGED
|
@@ -37,10 +37,16 @@ function missingPointerState(sourceId) {
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
export function resolveSurfaceWidgetData(widget, sourceStates) {
|
|
40
|
+
const sourceId = widget.binding?.sourceId;
|
|
41
|
+
return resolveSurfaceSourceData(widget, sourceId !== undefined && Object.hasOwn(sourceStates, sourceId)
|
|
42
|
+
? sourceStates[sourceId]
|
|
43
|
+
: undefined);
|
|
44
|
+
}
|
|
45
|
+
/** Resolve one binding without subscribing the widget to unrelated source states. */
|
|
46
|
+
export function resolveSurfaceSourceData(widget, sourceState) {
|
|
40
47
|
if (!widget.binding)
|
|
41
48
|
return { status: 'unbound' };
|
|
42
49
|
const sourceId = widget.binding.sourceId;
|
|
43
|
-
const sourceState = Object.hasOwn(sourceStates, sourceId) ? sourceStates[sourceId] : undefined;
|
|
44
50
|
if (!sourceState)
|
|
45
51
|
return { status: 'loading', sourceId };
|
|
46
52
|
if (sourceState.status !== 'ready')
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SurfaceCatalog } from './types.js';
|
|
2
|
+
export declare const DEFAULT_SURFACE_CATALOG_VERSION: "svadmin/v1";
|
|
3
|
+
export declare const STYLED_SURFACE_CATALOG_VERSION: "svadmin/styled-v1";
|
|
4
|
+
/** The same definitions are consumed by Node validation and Svelte rendering. */
|
|
5
|
+
export declare const defaultSurfaceDefinitions: SurfaceCatalog;
|
|
6
|
+
export declare const styledSurfaceDefinitions: SurfaceCatalog;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Value } from '@sinclair/typebox/value';
|
|
2
|
+
import { styledMetricPropsSchema, styledResourceTablePropsSchema, barChartPropsSchema, lineChartPropsSchema, metricPropsSchema, resourceTablePropsSchema, } from './builtin-schemas.js';
|
|
3
|
+
export const DEFAULT_SURFACE_CATALOG_VERSION = 'svadmin/v1';
|
|
4
|
+
export const STYLED_SURFACE_CATALOG_VERSION = 'svadmin/styled-v1';
|
|
5
|
+
/** The same definitions are consumed by Node validation and Svelte rendering. */
|
|
6
|
+
export const defaultSurfaceDefinitions = {
|
|
7
|
+
version: DEFAULT_SURFACE_CATALOG_VERSION,
|
|
8
|
+
widgets: [
|
|
9
|
+
{ type: 'metric', dataKind: 'scalar',
|
|
10
|
+
description: 'Display a count or readable scalar. Bind list counts to /total; currency metrics require an ISO currency code.',
|
|
11
|
+
examples: [{ label: 'Total', format: 'number' }], propsSchema: metricPropsSchema },
|
|
12
|
+
{ type: 'resource-table', dataKind: 'items',
|
|
13
|
+
description: 'Display up to eight explicitly readable resource fields. Bind to /items and select fields from the active resource policy.',
|
|
14
|
+
propsSchema: resourceTablePropsSchema,
|
|
15
|
+
getReferencedFields: (props) => Value.Decode(resourceTablePropsSchema, props).columns.map((column) => column.field) },
|
|
16
|
+
{ type: 'bar-chart', dataKind: 'items',
|
|
17
|
+
description: 'Compare a readable numeric field by a readable label field. Bind to /items; aggregated data must come from an authorized backend resource.',
|
|
18
|
+
propsSchema: barChartPropsSchema,
|
|
19
|
+
getReferencedFields: (props) => { const p = Value.Decode(barChartPropsSchema, props); return [p.labelField, p.valueField]; } },
|
|
20
|
+
{ type: 'line-chart', dataKind: 'items',
|
|
21
|
+
description: 'Show a trend using readable label and numeric fields. Bind to /items; request only policy-authorized sorting.',
|
|
22
|
+
propsSchema: lineChartPropsSchema,
|
|
23
|
+
getReferencedFields: (props) => { const p = Value.Decode(lineChartPropsSchema, props); return [p.labelField, p.valueField]; } },
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
export const styledSurfaceDefinitions = {
|
|
27
|
+
version: STYLED_SURFACE_CATALOG_VERSION,
|
|
28
|
+
widgets: defaultSurfaceDefinitions.widgets.map((widget) => {
|
|
29
|
+
if (widget.type === 'metric')
|
|
30
|
+
return { ...widget, propsSchema: styledMetricPropsSchema,
|
|
31
|
+
description: 'Numeric KPI; tone and density select prebuilt semantic styles, never CSS.',
|
|
32
|
+
examples: [{ label: 'Pending orders', format: 'number', tone: 'warning', density: 'compact' }] };
|
|
33
|
+
if (widget.type === 'resource-table')
|
|
34
|
+
return { ...widget, propsSchema: styledResourceTablePropsSchema,
|
|
35
|
+
getReferencedFields: (props) => Value.Decode(styledResourceTablePropsSchema, props).columns.map((column) => column.field),
|
|
36
|
+
description: 'Read-only table; density is a prebuilt semantic variant.',
|
|
37
|
+
examples: [{ title: 'Inventory', columns: [{ field: 'name', label: 'Name' }], density: 'compact' }] };
|
|
38
|
+
return widget;
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
@@ -36,3 +36,28 @@ export type MetricProps = Static<typeof metricPropsSchema>;
|
|
|
36
36
|
export type ResourceTableProps = Static<typeof resourceTablePropsSchema>;
|
|
37
37
|
export type BarChartProps = Static<typeof barChartPropsSchema>;
|
|
38
38
|
export type LineChartProps = Static<typeof lineChartPropsSchema>;
|
|
39
|
+
export declare const styledMetricPropsSchema: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
|
|
40
|
+
tone: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"info" | "warning" | "success" | "neutral" | "danger">[]>>;
|
|
41
|
+
density: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"compact" | "comfortable">[]>>;
|
|
42
|
+
label: import("@sinclair/typebox").TString;
|
|
43
|
+
format: import("@sinclair/typebox").TLiteral<"currency">;
|
|
44
|
+
currency: import("@sinclair/typebox").TString;
|
|
45
|
+
description: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
46
|
+
}>, import("@sinclair/typebox").TObject<{
|
|
47
|
+
tone: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"info" | "warning" | "success" | "neutral" | "danger">[]>>;
|
|
48
|
+
density: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"compact" | "comfortable">[]>>;
|
|
49
|
+
label: import("@sinclair/typebox").TString;
|
|
50
|
+
format: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"number">, import("@sinclair/typebox").TLiteral<"percent">]>;
|
|
51
|
+
currency: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNever>;
|
|
52
|
+
description: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
53
|
+
}>]>;
|
|
54
|
+
export declare const styledResourceTablePropsSchema: import("@sinclair/typebox").TObject<{
|
|
55
|
+
density: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"compact" | "comfortable">[]>>;
|
|
56
|
+
title: import("@sinclair/typebox").TString;
|
|
57
|
+
emptyLabel: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
58
|
+
columns: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
59
|
+
field: import("@sinclair/typebox").TString;
|
|
60
|
+
label: import("@sinclair/typebox").TString;
|
|
61
|
+
format: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"text">, import("@sinclair/typebox").TLiteral<"number">, import("@sinclair/typebox").TLiteral<"date">, import("@sinclair/typebox").TLiteral<"boolean">]>>;
|
|
62
|
+
}>>;
|
|
63
|
+
}>;
|
package/dist/builtin-schemas.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { surfaceDesignContract } from './styled-system/design-contract.js';
|
|
1
2
|
import { Type } from "@sinclair/typebox";
|
|
2
3
|
const catalogFieldSchema = Type.String({
|
|
3
4
|
minLength: 1,
|
|
@@ -44,3 +45,15 @@ export const lineChartPropsSchema = Type.Object({
|
|
|
44
45
|
showDots: Type.Optional(Type.Boolean()),
|
|
45
46
|
fill: Type.Optional(Type.Boolean()),
|
|
46
47
|
}, { additionalProperties: false });
|
|
48
|
+
const styledMetricProperties = {
|
|
49
|
+
tone: Type.Optional(Type.Union(surfaceDesignContract.metric.tone.map((value) => Type.Literal(value)))),
|
|
50
|
+
density: Type.Optional(Type.Union(surfaceDesignContract.metric.density.map((value) => Type.Literal(value)))),
|
|
51
|
+
};
|
|
52
|
+
export const styledMetricPropsSchema = Type.Union([
|
|
53
|
+
Type.Object({ ...currencyMetric.properties, ...styledMetricProperties }, { additionalProperties: false }),
|
|
54
|
+
Type.Object({ ...otherMetric.properties, ...styledMetricProperties }, { additionalProperties: false }),
|
|
55
|
+
]);
|
|
56
|
+
export const styledResourceTablePropsSchema = Type.Object({
|
|
57
|
+
...resourceTablePropsSchema.properties,
|
|
58
|
+
density: Type.Optional(Type.Union(surfaceDesignContract.table.density.map((value) => Type.Literal(value)))),
|
|
59
|
+
}, { additionalProperties: false });
|
package/dist/catalog.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Component } from 'svelte';
|
|
2
2
|
import type { JsonObject, SurfaceCatalog, SurfaceWidgetDataState, SurfaceWidgetDefinition } from './types.js';
|
|
3
3
|
import type { SurfaceMessages } from './localization.js';
|
|
4
|
-
export
|
|
4
|
+
export { DEFAULT_SURFACE_CATALOG_VERSION, STYLED_SURFACE_CATALOG_VERSION } from './builtin-definitions.js';
|
|
5
5
|
export interface SurfaceWidgetRendererProps {
|
|
6
6
|
readonly widgetId: string;
|
|
7
7
|
readonly props: JsonObject;
|
|
@@ -10,69 +10,12 @@ export interface SurfaceWidgetRendererProps {
|
|
|
10
10
|
readonly messages?: SurfaceMessages;
|
|
11
11
|
}
|
|
12
12
|
export interface SurfaceWidgetRegistration extends SurfaceWidgetDefinition {
|
|
13
|
+
readonly presentation?: 'surface-appearance/v1';
|
|
13
14
|
readonly component: Component<SurfaceWidgetRendererProps>;
|
|
14
15
|
}
|
|
15
16
|
export interface SurfaceRenderCatalog extends SurfaceCatalog {
|
|
16
17
|
readonly widgets: readonly SurfaceWidgetRegistration[];
|
|
17
18
|
}
|
|
18
19
|
export declare function defineSurfaceCatalog<const TCatalog extends SurfaceRenderCatalog>(catalog: TCatalog): TCatalog;
|
|
19
|
-
declare
|
|
20
|
-
declare
|
|
21
|
-
declare function lineChartFields(props: JsonObject): readonly string[];
|
|
22
|
-
export declare const defaultSurfaceCatalog: {
|
|
23
|
-
readonly version: "svadmin/v1";
|
|
24
|
-
readonly widgets: readonly [{
|
|
25
|
-
readonly type: "metric";
|
|
26
|
-
readonly dataKind: "scalar";
|
|
27
|
-
readonly propsSchema: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
|
|
28
|
-
label: import("@sinclair/typebox").TString;
|
|
29
|
-
format: import("@sinclair/typebox").TLiteral<"currency">;
|
|
30
|
-
currency: import("@sinclair/typebox").TString;
|
|
31
|
-
description: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
32
|
-
}>, import("@sinclair/typebox").TObject<{
|
|
33
|
-
label: import("@sinclair/typebox").TString;
|
|
34
|
-
format: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"number">, import("@sinclair/typebox").TLiteral<"percent">]>;
|
|
35
|
-
currency: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNever>;
|
|
36
|
-
description: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
37
|
-
}>]>;
|
|
38
|
-
readonly component: Component<SurfaceWidgetRendererProps, {}, "">;
|
|
39
|
-
}, {
|
|
40
|
-
readonly type: "resource-table";
|
|
41
|
-
readonly dataKind: "items";
|
|
42
|
-
readonly propsSchema: import("@sinclair/typebox").TObject<{
|
|
43
|
-
title: import("@sinclair/typebox").TString;
|
|
44
|
-
emptyLabel: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
45
|
-
columns: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
46
|
-
field: import("@sinclair/typebox").TString;
|
|
47
|
-
label: import("@sinclair/typebox").TString;
|
|
48
|
-
format: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"text">, import("@sinclair/typebox").TLiteral<"number">, import("@sinclair/typebox").TLiteral<"date">, import("@sinclair/typebox").TLiteral<"boolean">]>>;
|
|
49
|
-
}>>;
|
|
50
|
-
}>;
|
|
51
|
-
readonly getReferencedFields: typeof tableFields;
|
|
52
|
-
readonly component: Component<SurfaceWidgetRendererProps, {}, "">;
|
|
53
|
-
}, {
|
|
54
|
-
readonly type: "bar-chart";
|
|
55
|
-
readonly dataKind: "items";
|
|
56
|
-
readonly propsSchema: import("@sinclair/typebox").TObject<{
|
|
57
|
-
title: import("@sinclair/typebox").TString;
|
|
58
|
-
labelField: import("@sinclair/typebox").TString;
|
|
59
|
-
valueField: import("@sinclair/typebox").TString;
|
|
60
|
-
showValues: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
61
|
-
}>;
|
|
62
|
-
readonly getReferencedFields: typeof barChartFields;
|
|
63
|
-
readonly component: Component<SurfaceWidgetRendererProps, {}, "">;
|
|
64
|
-
}, {
|
|
65
|
-
readonly type: "line-chart";
|
|
66
|
-
readonly dataKind: "items";
|
|
67
|
-
readonly propsSchema: import("@sinclair/typebox").TObject<{
|
|
68
|
-
title: import("@sinclair/typebox").TString;
|
|
69
|
-
labelField: import("@sinclair/typebox").TString;
|
|
70
|
-
valueField: import("@sinclair/typebox").TString;
|
|
71
|
-
showDots: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
72
|
-
fill: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
73
|
-
}>;
|
|
74
|
-
readonly getReferencedFields: typeof lineChartFields;
|
|
75
|
-
readonly component: Component<SurfaceWidgetRendererProps, {}, "">;
|
|
76
|
-
}];
|
|
77
|
-
};
|
|
78
|
-
export {};
|
|
20
|
+
export declare const defaultSurfaceCatalog: SurfaceRenderCatalog;
|
|
21
|
+
export declare const styledSurfaceCatalog: SurfaceRenderCatalog;
|
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 {
|
|
7
|
-
export
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Value } from '@sinclair/typebox/value';
|
|
3
|
+
import { surfaceMetric } from '../styled-system/recipes/index.js';
|
|
3
4
|
import StatsCard from '@svadmin/ui/components/StatsCard.svelte';
|
|
4
|
-
import { metricPropsSchema } from '../builtin-schemas.js';
|
|
5
|
+
import { styledMetricPropsSchema as metricPropsSchema } from '../builtin-schemas.js';
|
|
5
6
|
import type { SurfaceWidgetRendererProps } from '../catalog.js';
|
|
6
7
|
import { resolveSurfaceMessages } from '../localization.js';
|
|
7
8
|
|
|
@@ -9,6 +10,8 @@
|
|
|
9
10
|
const activeMessages = $derived(resolveSurfaceMessages(locale, messages));
|
|
10
11
|
|
|
11
12
|
const metricProps = $derived(Value.Decode(metricPropsSchema, props));
|
|
13
|
+
const metricClasses = $derived(metricProps.tone !== undefined || metricProps.density !== undefined
|
|
14
|
+
? surfaceMetric({ tone: metricProps.tone, density: metricProps.density }) : undefined);
|
|
12
15
|
const formattedValue = $derived.by(() => {
|
|
13
16
|
if (data.status !== 'ready') return '—';
|
|
14
17
|
const value = data.value;
|
|
@@ -26,17 +29,17 @@
|
|
|
26
29
|
});
|
|
27
30
|
</script>
|
|
28
31
|
|
|
29
|
-
<article aria-labelledby="{widgetId}-label" aria-busy={data.status === 'loading'}>
|
|
32
|
+
<article class={metricClasses?.root} data-surface-tone={metricProps.tone} data-surface-density={metricProps.density} aria-labelledby="{widgetId}-label" aria-busy={data.status === 'loading'}>
|
|
30
33
|
<span id="{widgetId}-label" class="visually-hidden">{metricProps.label}</span>
|
|
31
34
|
{#if data.status === 'loading'}
|
|
32
|
-
<StatsCard label={metricProps.label} value="" loading />
|
|
35
|
+
<StatsCard class={metricClasses?.card ?? ''} label={metricProps.label} value="" loading />
|
|
33
36
|
{:else if data.status === 'ready' && typeof data.value === 'number'}
|
|
34
|
-
<StatsCard label={metricProps.label} value={formattedValue} />
|
|
37
|
+
<StatsCard class={metricClasses?.card ?? ''} label={metricProps.label} value={formattedValue} />
|
|
35
38
|
{#if metricProps.description}
|
|
36
|
-
<p class="metric-description">{metricProps.description}</p>
|
|
39
|
+
<p class={"metric-description " + (metricClasses?.description ?? "")}>{metricProps.description}</p>
|
|
37
40
|
{/if}
|
|
38
41
|
{:else}
|
|
39
|
-
<div class="metric-state" role={data.status === 'error' ? 'alert' : 'status'}>
|
|
42
|
+
<div class={"metric-state " + (metricClasses?.state ?? "")} role={data.status === 'error' ? 'alert' : 'status'}>
|
|
40
43
|
<strong>{metricProps.label}</strong>
|
|
41
44
|
<span>{data.status === 'error' ? data.error.message : data.status === 'empty' ? activeMessages.metricNoData : activeMessages.metricInvalidData}</span>
|
|
42
45
|
</div>
|
|
@@ -65,9 +68,10 @@
|
|
|
65
68
|
.metric-state {
|
|
66
69
|
display: grid;
|
|
67
70
|
gap: 0.35rem;
|
|
68
|
-
min-height: 6rem;
|
|
69
|
-
padding: 1.25rem;
|
|
71
|
+
min-height: var(--svadmin-metric-state-height, 6rem);
|
|
72
|
+
padding: var(--svadmin-metric-state-padding, 1.25rem);
|
|
70
73
|
border: 1px solid var(--border);
|
|
74
|
+
border-inline-start: var(--svadmin-metric-state-border-width, 1px) solid var(--svadmin-metric-state-accent, var(--border));
|
|
71
75
|
border-radius: 0.75rem;
|
|
72
76
|
background: var(--card);
|
|
73
77
|
color: var(--muted-foreground);
|
|
@@ -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.generated.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;
|