@svadmin/surface 0.1.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.
@@ -0,0 +1,143 @@
1
+ import type { DataProvider } from '@svadmin/core';
2
+ import type { ZodType } from 'zod';
3
+ export declare const SURFACE_SCHEMA_VERSION: "surface/v1";
4
+ export declare const SURFACE_LIMITS: {
5
+ readonly maxDataSources: 8;
6
+ readonly maxWidgets: 24;
7
+ readonly maxPageSize: 100;
8
+ readonly maxFilters: 8;
9
+ readonly maxSorters: 3;
10
+ readonly maxIdLength: 64;
11
+ readonly maxTitleLength: 120;
12
+ readonly maxJsonDepth: 64;
13
+ readonly maxJsonNodes: 10000;
14
+ };
15
+ export type JsonPrimitive = string | number | boolean | null;
16
+ export type JsonValue = JsonPrimitive | JsonObject | readonly JsonValue[];
17
+ export interface JsonObject {
18
+ readonly [key: string]: JsonValue;
19
+ }
20
+ export type SurfaceGridSpan = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
21
+ export interface SurfaceGridLayout {
22
+ readonly type: 'grid';
23
+ readonly columns: 12;
24
+ readonly gap?: 'sm' | 'md' | 'lg';
25
+ }
26
+ export interface SurfaceSort {
27
+ readonly field: string;
28
+ readonly order: 'asc' | 'desc';
29
+ }
30
+ export type SurfaceFilter = {
31
+ readonly field: string;
32
+ readonly operator: 'eq' | 'ne' | 'lt' | 'lte' | 'gt' | 'gte' | 'contains' | 'startswith' | 'endswith';
33
+ readonly value: JsonPrimitive;
34
+ } | {
35
+ readonly field: string;
36
+ readonly operator: 'in' | 'nin';
37
+ readonly value: readonly JsonPrimitive[];
38
+ } | {
39
+ readonly field: string;
40
+ readonly operator: 'null' | 'nnull';
41
+ };
42
+ export interface ResourceListSource {
43
+ readonly id: string;
44
+ readonly type: 'resource-list';
45
+ readonly resource: string;
46
+ readonly pageSize?: number;
47
+ readonly sorters?: readonly SurfaceSort[];
48
+ readonly filters?: readonly SurfaceFilter[];
49
+ }
50
+ export interface ResourceOneSource {
51
+ readonly id: string;
52
+ readonly type: 'resource-one';
53
+ readonly resource: string;
54
+ readonly recordId: string | number;
55
+ }
56
+ export type ResourceListDataSource = ResourceListSource;
57
+ export type ResourceOneDataSource = ResourceOneSource;
58
+ export type SurfaceDataSource = ResourceListSource | ResourceOneSource;
59
+ export interface SurfaceBinding {
60
+ readonly sourceId: string;
61
+ readonly pointer?: string;
62
+ }
63
+ export interface SurfaceWidget {
64
+ readonly id: string;
65
+ readonly type: string;
66
+ readonly props: JsonObject;
67
+ readonly binding?: SurfaceBinding;
68
+ readonly placement?: {
69
+ readonly columnSpan?: SurfaceGridSpan;
70
+ };
71
+ }
72
+ export interface SurfaceSpec {
73
+ readonly schemaVersion: typeof SURFACE_SCHEMA_VERSION;
74
+ readonly catalogVersion: string;
75
+ readonly surfaceId: string;
76
+ readonly title: string;
77
+ readonly layout: SurfaceGridLayout;
78
+ readonly dataSources: readonly SurfaceDataSource[];
79
+ readonly widgets: readonly SurfaceWidget[];
80
+ }
81
+ export interface SurfaceResourcePolicy {
82
+ readonly readFields: readonly string[];
83
+ readonly filterFields?: readonly string[];
84
+ readonly sortFields?: readonly string[];
85
+ readonly allowGetOne?: boolean;
86
+ readonly maxPageSize?: number;
87
+ }
88
+ export interface SurfacePolicy {
89
+ readonly resources: Readonly<Record<string, SurfaceResourcePolicy>>;
90
+ }
91
+ export type SurfaceCatalogDataKind = 'none' | 'scalar' | 'items';
92
+ export interface SurfaceWidgetDefinition {
93
+ readonly type: string;
94
+ readonly dataKind: SurfaceCatalogDataKind;
95
+ readonly propsSchema: ZodType<unknown>;
96
+ /** 组件从绑定记录中读取的字段,由可信 Catalog 提供。 */
97
+ readonly getReferencedFields?: (props: JsonObject) => readonly string[];
98
+ }
99
+ export interface SurfaceCatalog {
100
+ readonly version: string;
101
+ readonly widgets: readonly SurfaceWidgetDefinition[];
102
+ }
103
+ export type SurfaceValidationCode = 'invalid_json' | 'unsupported_schema_version' | 'catalog_version_mismatch' | 'duplicate_id' | 'unknown_widget_type' | 'unknown_data_source' | 'invalid_binding_pointer' | 'invalid_widget_props' | 'resource_denied' | 'field_denied' | 'limit_exceeded';
104
+ export interface SurfaceValidationIssue {
105
+ readonly code: SurfaceValidationCode;
106
+ readonly path: string;
107
+ readonly message: string;
108
+ readonly widgetId?: string;
109
+ readonly sourceId?: string;
110
+ }
111
+ export type SurfaceValidationResult = {
112
+ readonly ok: true;
113
+ readonly value: SurfaceSpec;
114
+ } | {
115
+ readonly ok: false;
116
+ readonly issues: readonly SurfaceValidationIssue[];
117
+ };
118
+ export type SurfaceDataProvider = Pick<DataProvider, 'getList' | 'getOne'>;
119
+ export interface SurfaceDataError {
120
+ readonly code: 'access_denied' | 'access_check_failed' | 'provider_failed' | 'provider_result_not_json' | 'binding_pointer_not_found';
121
+ readonly sourceId: string;
122
+ readonly message: string;
123
+ }
124
+ export type SurfaceWidgetDataState = {
125
+ readonly status: 'unbound';
126
+ } | {
127
+ readonly status: 'loading';
128
+ readonly sourceId: string;
129
+ } | {
130
+ readonly status: 'empty';
131
+ readonly sourceId: string;
132
+ } | {
133
+ readonly status: 'ready';
134
+ readonly sourceId: string;
135
+ readonly value: JsonValue;
136
+ } | {
137
+ readonly status: 'error';
138
+ readonly sourceId: string;
139
+ readonly error: SurfaceDataError;
140
+ };
141
+ export type SurfaceSourceDataState = Extract<SurfaceWidgetDataState, {
142
+ status: 'loading' | 'ready' | 'error';
143
+ }>;
package/dist/types.js ADDED
@@ -0,0 +1,12 @@
1
+ export const SURFACE_SCHEMA_VERSION = 'surface/v1';
2
+ export const SURFACE_LIMITS = {
3
+ maxDataSources: 8,
4
+ maxWidgets: 24,
5
+ maxPageSize: 100,
6
+ maxFilters: 8,
7
+ maxSorters: 3,
8
+ maxIdLength: 64,
9
+ maxTitleLength: 120,
10
+ maxJsonDepth: 64,
11
+ maxJsonNodes: 10_000,
12
+ };
@@ -0,0 +1,2 @@
1
+ import { type SurfaceCatalog, type SurfacePolicy, type SurfaceValidationResult } from './types.js';
2
+ export declare function validateSurfaceSpec(input: unknown, catalog: SurfaceCatalog, policy: SurfacePolicy): SurfaceValidationResult;
@@ -0,0 +1,307 @@
1
+ import { z } from 'zod';
2
+ import { decodedJsonPointerToken, jsonPointer, jsonValueIssue } from './json.js';
3
+ import { SURFACE_LIMITS, SURFACE_SCHEMA_VERSION, } from './types.js';
4
+ const idSchema = z.string().min(1).max(SURFACE_LIMITS.maxIdLength).regex(/^[A-Za-z][A-Za-z0-9_-]*$/u);
5
+ const fieldSchema = z.string().min(1).max(SURFACE_LIMITS.maxIdLength);
6
+ const jsonPrimitiveSchema = z.union([z.string(), z.number().finite(), z.boolean(), z.null()]);
7
+ const surfaceFilterSchema = z.discriminatedUnion('operator', [
8
+ z.object({
9
+ field: fieldSchema,
10
+ operator: z.enum(['eq', 'ne', 'lt', 'lte', 'gt', 'gte', 'contains', 'startswith', 'endswith']),
11
+ value: jsonPrimitiveSchema,
12
+ }).strict(),
13
+ z.object({
14
+ field: fieldSchema,
15
+ operator: z.enum(['in', 'nin']),
16
+ value: z.array(jsonPrimitiveSchema),
17
+ }).strict(),
18
+ z.object({
19
+ field: fieldSchema,
20
+ operator: z.enum(['null', 'nnull']),
21
+ }).strict(),
22
+ ]);
23
+ const resourceListSchema = z.object({
24
+ id: idSchema,
25
+ type: z.literal('resource-list'),
26
+ resource: idSchema,
27
+ pageSize: z.number().int().positive().optional(),
28
+ sorters: z.array(z.object({ field: fieldSchema, order: z.enum(['asc', 'desc']) }).strict()).optional(),
29
+ filters: z.array(surfaceFilterSchema).optional(),
30
+ }).strict();
31
+ const resourceOneSchema = z.object({
32
+ id: idSchema,
33
+ type: z.literal('resource-one'),
34
+ resource: idSchema,
35
+ recordId: z.union([z.string(), z.number().finite()]),
36
+ }).strict();
37
+ const surfaceSpecSchema = z.object({
38
+ schemaVersion: z.string(),
39
+ catalogVersion: z.string().min(1),
40
+ surfaceId: idSchema,
41
+ title: z.string().min(1).max(SURFACE_LIMITS.maxTitleLength),
42
+ layout: z.object({
43
+ type: z.literal('grid'),
44
+ columns: z.literal(12),
45
+ gap: z.enum(['sm', 'md', 'lg']).optional(),
46
+ }).strict(),
47
+ dataSources: z.array(z.discriminatedUnion('type', [resourceListSchema, resourceOneSchema])),
48
+ widgets: z.array(z.object({
49
+ id: idSchema,
50
+ type: idSchema,
51
+ props: z.record(z.string(), z.unknown()),
52
+ binding: z.object({ sourceId: idSchema, pointer: z.string().optional() }).strict().optional(),
53
+ placement: z.object({ columnSpan: z.number().int().min(1).max(12).optional() }).strict().optional(),
54
+ }).strict()),
55
+ }).strict();
56
+ const forbiddenPropertyNames = new Set([
57
+ 'class',
58
+ 'className',
59
+ 'color',
60
+ 'href',
61
+ 'html',
62
+ 'innerHTML',
63
+ 'src',
64
+ 'style',
65
+ 'url',
66
+ ]);
67
+ const strictSchemaProbeKey = '__surface_unknown_property_probe__';
68
+ function invalidJsonIssue(pathSegments, message) {
69
+ return { code: 'invalid_json', path: jsonPointer(pathSegments), message };
70
+ }
71
+ function schemaIssues(error) {
72
+ return error.issues.map((issue) => invalidJsonIssue(issue.path.map((segment) => typeof segment === 'symbol' ? String(segment) : segment), issue.message));
73
+ }
74
+ function catalogDefinitions(catalog) {
75
+ return new Map(catalog.widgets.map((widgetDefinition) => [widgetDefinition.type, widgetDefinition]));
76
+ }
77
+ function resourcePolicyFor(policy, resource) {
78
+ return Object.hasOwn(policy.resources, resource) ? policy.resources[resource] : undefined;
79
+ }
80
+ function sourcePolicyIssue(source, policy, index) {
81
+ const resourcePolicy = resourcePolicyFor(policy, source.resource);
82
+ if (!resourcePolicy) {
83
+ return {
84
+ code: 'resource_denied',
85
+ path: `/dataSources/${index}/resource`,
86
+ message: `Resource "${source.resource}" is not allowed`,
87
+ sourceId: source.id,
88
+ };
89
+ }
90
+ if (source.type === 'resource-one' && resourcePolicy.allowGetOne !== true) {
91
+ return {
92
+ code: 'resource_denied',
93
+ path: `/dataSources/${index}`,
94
+ message: `Resource "${source.resource}" does not allow getOne`,
95
+ sourceId: source.id,
96
+ };
97
+ }
98
+ if (source.type === 'resource-list') {
99
+ const pageSizeLimit = Math.min(resourcePolicy.maxPageSize ?? SURFACE_LIMITS.maxPageSize, SURFACE_LIMITS.maxPageSize);
100
+ if ((source.pageSize ?? 10) > pageSizeLimit) {
101
+ return {
102
+ code: 'limit_exceeded',
103
+ path: `/dataSources/${index}/pageSize`,
104
+ message: `Page size exceeds ${pageSizeLimit}`,
105
+ sourceId: source.id,
106
+ };
107
+ }
108
+ const disallowedSorter = source.sorters?.find((sorter) => !resourcePolicy.sortFields?.includes(sorter.field));
109
+ if (disallowedSorter) {
110
+ return {
111
+ code: 'field_denied',
112
+ path: `/dataSources/${index}/sorters`,
113
+ message: `Sorting by "${disallowedSorter.field}" is not allowed`,
114
+ sourceId: source.id,
115
+ };
116
+ }
117
+ const disallowedFilter = source.filters?.find((filter) => !resourcePolicy.filterFields?.includes(filter.field));
118
+ if (disallowedFilter) {
119
+ return {
120
+ code: 'field_denied',
121
+ path: `/dataSources/${index}/filters`,
122
+ message: `Filtering by "${disallowedFilter.field}" is not allowed`,
123
+ sourceId: source.id,
124
+ };
125
+ }
126
+ }
127
+ return null;
128
+ }
129
+ function propsContainForbiddenKey(props) {
130
+ const pending = [props];
131
+ while (pending.length > 0) {
132
+ const current = pending.pop();
133
+ if (current === undefined || current === null || typeof current !== 'object')
134
+ continue;
135
+ if (Array.isArray(current)) {
136
+ pending.push(...current);
137
+ continue;
138
+ }
139
+ for (const [key, entry] of Object.entries(current)) {
140
+ if (forbiddenPropertyNames.has(key) || key.startsWith('on'))
141
+ return key;
142
+ pending.push(entry);
143
+ }
144
+ }
145
+ return null;
146
+ }
147
+ function referencedFieldIssue(widget, widgetIndex, definition, source, policy) {
148
+ if (!definition.getReferencedFields)
149
+ return null;
150
+ let referencedFields;
151
+ try {
152
+ referencedFields = definition.getReferencedFields(widget.props);
153
+ }
154
+ catch {
155
+ return {
156
+ code: 'invalid_widget_props',
157
+ path: `/widgets/${widgetIndex}/props`,
158
+ message: 'Widget field references could not be resolved',
159
+ widgetId: widget.id,
160
+ };
161
+ }
162
+ const readableFields = resourcePolicyFor(policy, source.resource)?.readFields ?? [];
163
+ const deniedField = referencedFields.find((field) => (typeof field !== 'string' || !readableFields.includes(field)));
164
+ return deniedField === undefined
165
+ ? null
166
+ : {
167
+ code: 'field_denied',
168
+ path: `/widgets/${widgetIndex}/props`,
169
+ message: `Field "${String(deniedField)}" is not readable`,
170
+ widgetId: widget.id,
171
+ sourceId: source.id,
172
+ };
173
+ }
174
+ function resourceOnePointerField(pointer) {
175
+ if (!pointer.startsWith('/') || pointer.slice(1).includes('/'))
176
+ return null;
177
+ const field = decodedJsonPointerToken(pointer.slice(1));
178
+ return field && field.length <= SURFACE_LIMITS.maxIdLength ? field : null;
179
+ }
180
+ function widgetIssue(widget, widgetIndex, definition, sources, policy) {
181
+ if (!definition) {
182
+ return {
183
+ code: 'unknown_widget_type',
184
+ path: `/widgets/${widgetIndex}/type`,
185
+ message: `Widget type "${widget.type}" is not registered`,
186
+ widgetId: widget.id,
187
+ };
188
+ }
189
+ const forbiddenKey = propsContainForbiddenKey(widget.props);
190
+ const propsValidation = definition.propsSchema.safeParse(widget.props);
191
+ const strictSchemaProbe = definition.propsSchema.safeParse({
192
+ ...widget.props,
193
+ [strictSchemaProbeKey]: null,
194
+ });
195
+ if (forbiddenKey || !propsValidation.success || strictSchemaProbe.success) {
196
+ return {
197
+ code: 'invalid_widget_props',
198
+ path: `/widgets/${widgetIndex}/props`,
199
+ message: forbiddenKey
200
+ ? `Property "${forbiddenKey}" is not allowed`
201
+ : strictSchemaProbe.success
202
+ ? 'Catalog props schema must reject unknown properties'
203
+ : 'Widget props do not match the catalog schema',
204
+ widgetId: widget.id,
205
+ };
206
+ }
207
+ if (definition.dataKind === 'none') {
208
+ return widget.binding
209
+ ? {
210
+ code: 'invalid_binding_pointer',
211
+ path: `/widgets/${widgetIndex}/binding`,
212
+ message: 'This widget does not accept a data binding',
213
+ widgetId: widget.id,
214
+ }
215
+ : null;
216
+ }
217
+ const source = widget.binding ? sources.get(widget.binding.sourceId) : undefined;
218
+ if (!source) {
219
+ return {
220
+ code: 'unknown_data_source',
221
+ path: `/widgets/${widgetIndex}/binding/sourceId`,
222
+ message: 'Widget binding must reference an existing data source',
223
+ widgetId: widget.id,
224
+ };
225
+ }
226
+ const pointer = widget.binding?.pointer ?? '';
227
+ const resourceOneField = source.type === 'resource-one' ? resourceOnePointerField(pointer) : null;
228
+ const pointerIsValid = definition.dataKind === 'items'
229
+ ? source.type === 'resource-list' && pointer === '/items'
230
+ : (source.type === 'resource-list' && pointer === '/total')
231
+ || resourceOneField !== null;
232
+ if (!pointerIsValid) {
233
+ return {
234
+ code: 'invalid_binding_pointer',
235
+ path: `/widgets/${widgetIndex}/binding/pointer`,
236
+ message: 'Binding pointer is not valid for this widget and data source',
237
+ widgetId: widget.id,
238
+ sourceId: source.id,
239
+ };
240
+ }
241
+ const deniedReferencedField = referencedFieldIssue(widget, widgetIndex, definition, source, policy);
242
+ if (deniedReferencedField)
243
+ return deniedReferencedField;
244
+ if (source.type === 'resource-one' && resourceOneField !== null) {
245
+ if (!resourcePolicyFor(policy, source.resource)?.readFields.includes(resourceOneField)) {
246
+ return {
247
+ code: 'field_denied',
248
+ path: `/widgets/${widgetIndex}/binding/pointer`,
249
+ message: `Field "${resourceOneField}" is not readable`,
250
+ widgetId: widget.id,
251
+ sourceId: source.id,
252
+ };
253
+ }
254
+ }
255
+ return null;
256
+ }
257
+ function duplicateIdIssue(spec) {
258
+ const ids = new Set();
259
+ const entries = [
260
+ ...spec.dataSources.map((source, index) => ({ id: source.id, path: `/dataSources/${index}/id` })),
261
+ ...spec.widgets.map((widget, index) => ({ id: widget.id, path: `/widgets/${index}/id` })),
262
+ ];
263
+ for (const entry of entries) {
264
+ if (ids.has(entry.id))
265
+ return { code: 'duplicate_id', path: entry.path, message: `Duplicate id "${entry.id}"` };
266
+ ids.add(entry.id);
267
+ }
268
+ return null;
269
+ }
270
+ export function validateSurfaceSpec(input, catalog, policy) {
271
+ const unsafeJson = jsonValueIssue(input);
272
+ if (unsafeJson)
273
+ return { ok: false, issues: [invalidJsonIssue(unsafeJson.path, unsafeJson.message)] };
274
+ const parsedSpec = surfaceSpecSchema.safeParse(input);
275
+ if (!parsedSpec.success)
276
+ return { ok: false, issues: schemaIssues(parsedSpec.error) };
277
+ const spec = parsedSpec.data;
278
+ if (spec.schemaVersion !== SURFACE_SCHEMA_VERSION) {
279
+ return { ok: false, issues: [{ code: 'unsupported_schema_version', path: '/schemaVersion', message: `Expected ${SURFACE_SCHEMA_VERSION}` }] };
280
+ }
281
+ if (spec.catalogVersion !== catalog.version) {
282
+ return { ok: false, issues: [{ code: 'catalog_version_mismatch', path: '/catalogVersion', message: `Expected ${catalog.version}` }] };
283
+ }
284
+ if (spec.dataSources.length > SURFACE_LIMITS.maxDataSources || spec.widgets.length > SURFACE_LIMITS.maxWidgets) {
285
+ return { ok: false, issues: [{ code: 'limit_exceeded', path: '', message: 'Surface exceeds the source or widget limit' }] };
286
+ }
287
+ const duplicateIssue = duplicateIdIssue(spec);
288
+ if (duplicateIssue)
289
+ return { ok: false, issues: [duplicateIssue] };
290
+ for (const [sourceIndex, source] of spec.dataSources.entries()) {
291
+ const issue = sourcePolicyIssue(source, policy, sourceIndex);
292
+ if (issue)
293
+ return { ok: false, issues: [issue] };
294
+ if ((source.type === 'resource-list' && (source.filters?.length ?? 0) > SURFACE_LIMITS.maxFilters)
295
+ || (source.type === 'resource-list' && (source.sorters?.length ?? 0) > SURFACE_LIMITS.maxSorters)) {
296
+ return { ok: false, issues: [{ code: 'limit_exceeded', path: `/dataSources/${sourceIndex}`, message: 'Source exceeds the filter or sorter limit', sourceId: source.id }] };
297
+ }
298
+ }
299
+ const sources = new Map(spec.dataSources.map((source) => [source.id, source]));
300
+ const definitions = catalogDefinitions(catalog);
301
+ for (const [widgetIndex, widget] of spec.widgets.entries()) {
302
+ const issue = widgetIssue(widget, widgetIndex, definitions.get(widget.type), sources, policy);
303
+ if (issue)
304
+ return { ok: false, issues: [issue] };
305
+ }
306
+ return { ok: true, value: spec };
307
+ }
@@ -0,0 +1,16 @@
1
+ import type { JsonObject, JsonValue } from './types.js';
2
+ export interface SurfaceChartPoint {
3
+ readonly label: string;
4
+ readonly value: number;
5
+ }
6
+ export type WidgetValueResult<T> = {
7
+ readonly ok: true;
8
+ readonly value: T;
9
+ } | {
10
+ readonly ok: false;
11
+ readonly message: string;
12
+ };
13
+ export declare function compactChartLabel(label: string, maxLength: number): string;
14
+ export declare function asRecordArray(value: JsonValue): WidgetValueResult<readonly JsonObject[]>;
15
+ export declare function asChartPoints(value: JsonValue, labelField: string, valueField: string): WidgetValueResult<readonly SurfaceChartPoint[]>;
16
+ export declare function displayTableValue(value: JsonValue | undefined, format: string | undefined): string;
@@ -0,0 +1,41 @@
1
+ export function compactChartLabel(label, maxLength) {
2
+ const isoDate = /^(?:\d{4})-(\d{2})-(\d{2})$/u.exec(label);
3
+ if (isoDate)
4
+ return `${Number(isoDate[1])}/${Number(isoDate[2])}`;
5
+ const characters = [...label];
6
+ return characters.length <= maxLength
7
+ ? label
8
+ : `${characters.slice(0, Math.max(1, maxLength - 1)).join('')}…`;
9
+ }
10
+ export function asRecordArray(value) {
11
+ if (!Array.isArray(value))
12
+ return { ok: false, message: 'Expected a record collection' };
13
+ const records = value.filter((entry) => (entry !== null && typeof entry === 'object' && !Array.isArray(entry)));
14
+ return records.length === value.length
15
+ ? { ok: true, value: records }
16
+ : { ok: false, message: 'Every collection item must be a record' };
17
+ }
18
+ export function asChartPoints(value, labelField, valueField) {
19
+ const records = asRecordArray(value);
20
+ if (!records.ok)
21
+ return records;
22
+ const points = [];
23
+ for (const record of records.value) {
24
+ const label = record[labelField];
25
+ const numericValue = record[valueField];
26
+ if ((typeof label !== 'string' && typeof label !== 'number') || typeof numericValue !== 'number') {
27
+ return { ok: false, message: `Chart fields "${labelField}" and "${valueField}" have incompatible values` };
28
+ }
29
+ points.push({ label: String(label), value: numericValue });
30
+ }
31
+ return { ok: true, value: points };
32
+ }
33
+ export function displayTableValue(value, format) {
34
+ if (value === undefined || value === null)
35
+ return '—';
36
+ if (format === 'boolean')
37
+ return value === true ? 'Yes' : value === false ? 'No' : '—';
38
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
39
+ return String(value);
40
+ return JSON.stringify(value);
41
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@svadmin/surface",
3
+ "version": "0.1.0",
4
+ "description": "Validated declarative surfaces for svadmin dashboards",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "files": [
8
+ "dist",
9
+ "README.md",
10
+ "compatibility.json",
11
+ "!dist/**/*.test.*",
12
+ "!dist/**/*.spec.*"
13
+ ],
14
+ "main": "dist/index.js",
15
+ "types": "dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "./svelte": {
22
+ "types": "./dist/svelte.d.ts",
23
+ "svelte": "./dist/svelte.js",
24
+ "default": "./dist/svelte.js"
25
+ }
26
+ },
27
+ "peerDependencies": {
28
+ "@svadmin/core": ">=0.34.2 <0.35.0",
29
+ "@svadmin/ui": ">=0.40.6 <0.41.0",
30
+ "svelte": "^5.56.8"
31
+ },
32
+ "dependencies": {
33
+ "zod": "^4.4.3"
34
+ },
35
+ "devDependencies": {
36
+ "@svadmin/core": "workspace:*",
37
+ "@svadmin/ui": "workspace:*",
38
+ "@sveltejs/vite-plugin-svelte": "^7.3.0",
39
+ "@testing-library/dom": "^10.4.1",
40
+ "@testing-library/svelte": "^5.4.2",
41
+ "@testing-library/user-event": "^14.6.3",
42
+ "happy-dom": "^20.11.2",
43
+ "vitest": "^4.1.10"
44
+ },
45
+ "scripts": {
46
+ "build": "node ../../node_modules/@sveltejs/package/svelte-package.js -i src -o dist",
47
+ "check": "svelte-check --tsgo-experimental-api --tsconfig ./tsconfig.json --threshold error",
48
+ "test": "vitest run --config ./vitest.config.ts"
49
+ },
50
+ "license": "MIT",
51
+ "publishConfig": {
52
+ "access": "public",
53
+ "registry": "https://registry.npmjs.org/"
54
+ },
55
+ "author": "zuohuadong",
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "https://github.com/zuohuadong/svadmin.git",
59
+ "directory": "packages/surface"
60
+ },
61
+ "homepage": "https://github.com/zuohuadong/svadmin#readme",
62
+ "bugs": {
63
+ "url": "https://github.com/zuohuadong/svadmin/issues"
64
+ }
65
+ }