@svadmin/surface 0.4.6 → 0.6.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 CHANGED
@@ -7,7 +7,7 @@ The MVP supports Svelte 5 + Vite, a fixed 12-column grid, four built-in widgets,
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- bun add @svadmin/surface @svadmin/core @svadmin/ui @tanstack/svelte-query svelte zod
10
+ bun add @svadmin/surface @svadmin/core @svadmin/ui @tanstack/svelte-query svelte @sinclair/typebox
11
11
  ```
12
12
 
13
13
  Import protocol types and validation from the DOM-free root entry. Import rendering code from the Svelte subpath.
@@ -89,7 +89,7 @@ When rendered inside `AdminApp`, the renderer resolves the configured provider f
89
89
 
90
90
  The catalog version is `svadmin/v1`. `catalogVersion` must match exactly.
91
91
 
92
- Custom catalogs are trusted runtime configuration. Every registration must use a strict Zod v4 object schema and a trusted Svelte component. Item widgets that read record fields must expose those fields through `getReferencedFields`; validation then checks them against `SurfacePolicy.readFields`.
92
+ Custom catalogs are trusted runtime configuration. Every registration must use a strict TypeBox object schema and a trusted Svelte component. Item widgets that read record fields must expose those fields through `getReferencedFields`; validation then checks them against `SurfacePolicy.readFields`.
93
93
 
94
94
  ```ts
95
95
  const catalog = defineSurfaceCatalog({
@@ -1,5 +1,5 @@
1
1
  {
2
- "surface": "0.4.x",
2
+ "surface": "0.6.x",
3
3
  "minimumSupported": {
4
4
  "@svadmin/core": "0.34.2",
5
5
  "@svadmin/ui": "0.40.6",
@@ -1,42 +1,172 @@
1
- import { z } from 'zod';
2
- export declare const metricPropsSchema: z.ZodObject<{
3
- label: z.ZodString;
4
- format: z.ZodEnum<{
5
- number: "number";
6
- currency: "currency";
7
- percent: "percent";
8
- }>;
9
- currency: z.ZodOptional<z.ZodString>;
10
- description: z.ZodOptional<z.ZodString>;
11
- }, z.core.$strict>;
12
- export declare const resourceTablePropsSchema: z.ZodObject<{
13
- title: z.ZodString;
14
- emptyLabel: z.ZodOptional<z.ZodString>;
15
- columns: z.ZodArray<z.ZodObject<{
16
- field: z.ZodString;
17
- label: z.ZodString;
18
- format: z.ZodOptional<z.ZodEnum<{
19
- number: "number";
20
- boolean: "boolean";
21
- text: "text";
22
- date: "date";
23
- }>>;
24
- }, z.core.$strict>>;
25
- }, z.core.$strict>;
26
- export declare const barChartPropsSchema: z.ZodObject<{
27
- title: z.ZodString;
28
- labelField: z.ZodString;
29
- valueField: z.ZodString;
30
- showValues: z.ZodOptional<z.ZodBoolean>;
31
- }, z.core.$strict>;
32
- export declare const lineChartPropsSchema: z.ZodObject<{
33
- title: z.ZodString;
34
- labelField: z.ZodString;
35
- valueField: z.ZodString;
36
- showDots: z.ZodOptional<z.ZodBoolean>;
37
- fill: z.ZodOptional<z.ZodBoolean>;
38
- }, z.core.$strict>;
39
- export type MetricProps = z.infer<typeof metricPropsSchema>;
40
- export type ResourceTableProps = z.infer<typeof resourceTablePropsSchema>;
41
- export type BarChartProps = z.infer<typeof barChartPropsSchema>;
42
- export type LineChartProps = z.infer<typeof lineChartPropsSchema>;
1
+ import { type Static, type TSchema } from "@sinclair/typebox";
2
+ export declare function createParsedSchema<T extends TSchema>(schema: T): T & {
3
+ parse: (value: unknown) => Static<T>;
4
+ safeParse: (value: unknown) => {
5
+ success: true;
6
+ data: Static<T>;
7
+ } | {
8
+ success: false;
9
+ error: {
10
+ issues: Array<{
11
+ path: string[];
12
+ message: string;
13
+ }>;
14
+ };
15
+ };
16
+ Check: (value: unknown) => boolean;
17
+ };
18
+ export declare const metricPropsSchema: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
19
+ label: import("@sinclair/typebox").TString;
20
+ format: import("@sinclair/typebox").TLiteral<"currency">;
21
+ currency: import("@sinclair/typebox").TString;
22
+ description: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
23
+ }>, import("@sinclair/typebox").TObject<{
24
+ label: import("@sinclair/typebox").TString;
25
+ format: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"number">, import("@sinclair/typebox").TLiteral<"percent">]>;
26
+ currency: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNever>;
27
+ description: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
28
+ }>]> & {
29
+ parse: (value: unknown) => {
30
+ description?: string | undefined;
31
+ currency: string;
32
+ label: string;
33
+ format: "currency";
34
+ } | {
35
+ description?: string | undefined;
36
+ currency?: undefined;
37
+ label: string;
38
+ format: "number" | "percent";
39
+ };
40
+ safeParse: (value: unknown) => {
41
+ success: false;
42
+ error: {
43
+ issues: Array<{
44
+ path: string[];
45
+ message: string;
46
+ }>;
47
+ };
48
+ } | {
49
+ success: true;
50
+ data: {
51
+ description?: string | undefined;
52
+ currency: string;
53
+ label: string;
54
+ format: "currency";
55
+ } | {
56
+ description?: string | undefined;
57
+ currency?: undefined;
58
+ label: string;
59
+ format: "number" | "percent";
60
+ };
61
+ };
62
+ Check: (value: unknown) => boolean;
63
+ };
64
+ export declare const resourceTablePropsSchema: import("@sinclair/typebox").TObject<{
65
+ title: import("@sinclair/typebox").TString;
66
+ emptyLabel: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
67
+ columns: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
68
+ field: import("@sinclair/typebox").TString;
69
+ label: import("@sinclair/typebox").TString;
70
+ 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">]>>;
71
+ }>>;
72
+ }> & {
73
+ parse: (value: unknown) => {
74
+ emptyLabel?: string | undefined;
75
+ title: string;
76
+ columns: {
77
+ format?: "number" | "boolean" | "text" | "date" | undefined;
78
+ field: string;
79
+ label: string;
80
+ }[];
81
+ };
82
+ safeParse: (value: unknown) => {
83
+ success: false;
84
+ error: {
85
+ issues: Array<{
86
+ path: string[];
87
+ message: string;
88
+ }>;
89
+ };
90
+ } | {
91
+ success: true;
92
+ data: {
93
+ emptyLabel?: string | undefined;
94
+ title: string;
95
+ columns: {
96
+ format?: "number" | "boolean" | "text" | "date" | undefined;
97
+ field: string;
98
+ label: string;
99
+ }[];
100
+ };
101
+ };
102
+ Check: (value: unknown) => boolean;
103
+ };
104
+ export declare const barChartPropsSchema: import("@sinclair/typebox").TObject<{
105
+ title: import("@sinclair/typebox").TString;
106
+ labelField: import("@sinclair/typebox").TString;
107
+ valueField: import("@sinclair/typebox").TString;
108
+ showValues: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
109
+ }> & {
110
+ parse: (value: unknown) => {
111
+ showValues?: boolean | undefined;
112
+ title: string;
113
+ labelField: string;
114
+ valueField: string;
115
+ };
116
+ safeParse: (value: unknown) => {
117
+ success: false;
118
+ error: {
119
+ issues: Array<{
120
+ path: string[];
121
+ message: string;
122
+ }>;
123
+ };
124
+ } | {
125
+ success: true;
126
+ data: {
127
+ showValues?: boolean | undefined;
128
+ title: string;
129
+ labelField: string;
130
+ valueField: string;
131
+ };
132
+ };
133
+ Check: (value: unknown) => boolean;
134
+ };
135
+ export declare const lineChartPropsSchema: import("@sinclair/typebox").TObject<{
136
+ title: import("@sinclair/typebox").TString;
137
+ labelField: import("@sinclair/typebox").TString;
138
+ valueField: import("@sinclair/typebox").TString;
139
+ showDots: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
140
+ fill: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
141
+ }> & {
142
+ parse: (value: unknown) => {
143
+ fill?: boolean | undefined;
144
+ showDots?: boolean | undefined;
145
+ title: string;
146
+ labelField: string;
147
+ valueField: string;
148
+ };
149
+ safeParse: (value: unknown) => {
150
+ success: false;
151
+ error: {
152
+ issues: Array<{
153
+ path: string[];
154
+ message: string;
155
+ }>;
156
+ };
157
+ } | {
158
+ success: true;
159
+ data: {
160
+ fill?: boolean | undefined;
161
+ showDots?: boolean | undefined;
162
+ title: string;
163
+ labelField: string;
164
+ valueField: string;
165
+ };
166
+ };
167
+ Check: (value: unknown) => boolean;
168
+ };
169
+ export type MetricProps = Static<typeof metricPropsSchema>;
170
+ export type ResourceTableProps = Static<typeof resourceTablePropsSchema>;
171
+ export type BarChartProps = Static<typeof barChartPropsSchema>;
172
+ export type LineChartProps = Static<typeof lineChartPropsSchema>;
@@ -1,37 +1,82 @@
1
- import { z } from 'zod';
2
- const catalogFieldSchema = z.string().min(1).max(64).regex(/^[A-Za-z][A-Za-z0-9_-]*$/u);
3
- export const metricPropsSchema = z.object({
4
- label: z.string().min(1).max(80),
5
- format: z.enum(['number', 'currency', 'percent']),
6
- currency: z.string().regex(/^[A-Z]{3}$/u).optional(),
7
- description: z.string().min(1).max(160).optional(),
8
- }).strict().superRefine((props, context) => {
9
- if (props.format === 'currency' && !props.currency) {
10
- context.addIssue({ code: 'custom', path: ['currency'], message: 'Currency format requires an ISO currency code' });
11
- }
12
- if (props.format !== 'currency' && props.currency) {
13
- context.addIssue({ code: 'custom', path: ['currency'], message: 'Currency is only valid with currency format' });
14
- }
1
+ import { Type } from "@sinclair/typebox";
2
+ import { TypeCompiler } from "@sinclair/typebox/compiler";
3
+ export function createParsedSchema(schema) {
4
+ const compiled = TypeCompiler.Compile(schema);
5
+ const parse = (value) => {
6
+ if (compiled.Check(value))
7
+ return value;
8
+ const errors = [...compiled.Errors(value)];
9
+ const message = errors.map((e) => (e.path ? e.path + ": " : "") + e.message).join(", ") || "Validation error";
10
+ const err = new Error(message);
11
+ err.issues = errors.map((e) => ({
12
+ path: e.path ? (e.path.startsWith("/") ? e.path.slice(1) : e.path).split("/") : [],
13
+ message: e.message,
14
+ }));
15
+ throw err;
16
+ };
17
+ const safeParse = (value) => {
18
+ if (compiled.Check(value)) {
19
+ return { success: true, data: value };
20
+ }
21
+ const errors = [...compiled.Errors(value)];
22
+ return {
23
+ success: false,
24
+ error: {
25
+ issues: errors.map((e) => ({
26
+ path: e.path ? (e.path.startsWith("/") ? e.path.slice(1) : e.path).split("/") : [],
27
+ message: e.message,
28
+ })),
29
+ },
30
+ };
31
+ };
32
+ return Object.assign(schema, {
33
+ parse,
34
+ safeParse,
35
+ Check: (val) => compiled.Check(val),
36
+ });
37
+ }
38
+ const catalogFieldSchema = Type.String({
39
+ minLength: 1,
40
+ maxLength: 64,
41
+ pattern: "^[A-Za-z][A-Za-z0-9_-]*$",
15
42
  });
16
- export const resourceTablePropsSchema = z.object({
17
- title: z.string().min(1).max(80),
18
- emptyLabel: z.string().min(1).max(80).optional(),
19
- columns: z.array(z.object({
43
+ const currencyMetric = Type.Object({
44
+ label: Type.String({ minLength: 1, maxLength: 80 }),
45
+ format: Type.Literal("currency"),
46
+ currency: Type.String({ pattern: "^[A-Z]{3}$" }),
47
+ description: Type.Optional(Type.String({ minLength: 1, maxLength: 160 })),
48
+ }, { additionalProperties: false });
49
+ const otherMetric = Type.Object({
50
+ label: Type.String({ minLength: 1, maxLength: 80 }),
51
+ format: Type.Union([Type.Literal("number"), Type.Literal("percent")]),
52
+ currency: Type.Optional(Type.Never()),
53
+ description: Type.Optional(Type.String({ minLength: 1, maxLength: 160 })),
54
+ }, { additionalProperties: false });
55
+ export const metricPropsSchema = createParsedSchema(Type.Union([currencyMetric, otherMetric]));
56
+ export const resourceTablePropsSchema = createParsedSchema(Type.Object({
57
+ title: Type.String({ minLength: 1, maxLength: 80 }),
58
+ emptyLabel: Type.Optional(Type.String({ minLength: 1, maxLength: 80 })),
59
+ columns: Type.Array(Type.Object({
20
60
  field: catalogFieldSchema,
21
- label: z.string().min(1).max(60),
22
- format: z.enum(['text', 'number', 'date', 'boolean']).optional(),
23
- }).strict()).min(1).max(8),
24
- }).strict();
25
- export const barChartPropsSchema = z.object({
26
- title: z.string().min(1).max(80),
61
+ label: Type.String({ minLength: 1, maxLength: 60 }),
62
+ format: Type.Optional(Type.Union([
63
+ Type.Literal("text"),
64
+ Type.Literal("number"),
65
+ Type.Literal("date"),
66
+ Type.Literal("boolean"),
67
+ ])),
68
+ }, { additionalProperties: false }), { minItems: 1, maxItems: 8 }),
69
+ }, { additionalProperties: false }));
70
+ export const barChartPropsSchema = createParsedSchema(Type.Object({
71
+ title: Type.String({ minLength: 1, maxLength: 80 }),
27
72
  labelField: catalogFieldSchema,
28
73
  valueField: catalogFieldSchema,
29
- showValues: z.boolean().optional(),
30
- }).strict();
31
- export const lineChartPropsSchema = z.object({
32
- title: z.string().min(1).max(80),
74
+ showValues: Type.Optional(Type.Boolean()),
75
+ }, { additionalProperties: false }));
76
+ export const lineChartPropsSchema = createParsedSchema(Type.Object({
77
+ title: Type.String({ minLength: 1, maxLength: 80 }),
33
78
  labelField: catalogFieldSchema,
34
79
  valueField: catalogFieldSchema,
35
- showDots: z.boolean().optional(),
36
- fill: z.boolean().optional(),
37
- }).strict();
80
+ showDots: Type.Optional(Type.Boolean()),
81
+ fill: Type.Optional(Type.Boolean()),
82
+ }, { additionalProperties: false }));
package/dist/catalog.d.ts CHANGED
@@ -21,57 +21,171 @@ export declare const defaultSurfaceCatalog: {
21
21
  readonly widgets: readonly [{
22
22
  readonly type: "metric";
23
23
  readonly dataKind: "scalar";
24
- readonly propsSchema: import("zod").ZodObject<{
25
- label: import("zod").ZodString;
26
- format: import("zod").ZodEnum<{
27
- number: "number";
28
- currency: "currency";
29
- percent: "percent";
30
- }>;
31
- currency: import("zod").ZodOptional<import("zod").ZodString>;
32
- description: import("zod").ZodOptional<import("zod").ZodString>;
33
- }, import("zod/v4/core").$strict>;
24
+ readonly propsSchema: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
25
+ label: import("@sinclair/typebox").TString;
26
+ format: import("@sinclair/typebox").TLiteral<"currency">;
27
+ currency: import("@sinclair/typebox").TString;
28
+ description: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
29
+ }>, import("@sinclair/typebox").TObject<{
30
+ label: import("@sinclair/typebox").TString;
31
+ format: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"number">, import("@sinclair/typebox").TLiteral<"percent">]>;
32
+ currency: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNever>;
33
+ description: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
34
+ }>]> & {
35
+ parse: (value: unknown) => {
36
+ description?: string | undefined;
37
+ currency: string;
38
+ label: string;
39
+ format: "currency";
40
+ } | {
41
+ description?: string | undefined;
42
+ currency?: undefined;
43
+ label: string;
44
+ format: "number" | "percent";
45
+ };
46
+ safeParse: (value: unknown) => {
47
+ success: false;
48
+ error: {
49
+ issues: Array<{
50
+ path: string[];
51
+ message: string;
52
+ }>;
53
+ };
54
+ } | {
55
+ success: true;
56
+ data: {
57
+ description?: string | undefined;
58
+ currency: string;
59
+ label: string;
60
+ format: "currency";
61
+ } | {
62
+ description?: string | undefined;
63
+ currency?: undefined;
64
+ label: string;
65
+ format: "number" | "percent";
66
+ };
67
+ };
68
+ Check: (value: unknown) => boolean;
69
+ };
34
70
  readonly component: Component<SurfaceWidgetRendererProps, {}, "">;
35
71
  }, {
36
72
  readonly type: "resource-table";
37
73
  readonly dataKind: "items";
38
- readonly propsSchema: import("zod").ZodObject<{
39
- title: import("zod").ZodString;
40
- emptyLabel: import("zod").ZodOptional<import("zod").ZodString>;
41
- columns: import("zod").ZodArray<import("zod").ZodObject<{
42
- field: import("zod").ZodString;
43
- label: import("zod").ZodString;
44
- format: import("zod").ZodOptional<import("zod").ZodEnum<{
45
- number: "number";
46
- boolean: "boolean";
47
- text: "text";
48
- date: "date";
49
- }>>;
50
- }, import("zod/v4/core").$strict>>;
51
- }, import("zod/v4/core").$strict>;
74
+ readonly propsSchema: import("@sinclair/typebox").TObject<{
75
+ title: import("@sinclair/typebox").TString;
76
+ emptyLabel: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
77
+ columns: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
78
+ field: import("@sinclair/typebox").TString;
79
+ label: import("@sinclair/typebox").TString;
80
+ 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">]>>;
81
+ }>>;
82
+ }> & {
83
+ parse: (value: unknown) => {
84
+ emptyLabel?: string | undefined;
85
+ title: string;
86
+ columns: {
87
+ format?: "number" | "boolean" | "text" | "date" | undefined;
88
+ field: string;
89
+ label: string;
90
+ }[];
91
+ };
92
+ safeParse: (value: unknown) => {
93
+ success: false;
94
+ error: {
95
+ issues: Array<{
96
+ path: string[];
97
+ message: string;
98
+ }>;
99
+ };
100
+ } | {
101
+ success: true;
102
+ data: {
103
+ emptyLabel?: string | undefined;
104
+ title: string;
105
+ columns: {
106
+ format?: "number" | "boolean" | "text" | "date" | undefined;
107
+ field: string;
108
+ label: string;
109
+ }[];
110
+ };
111
+ };
112
+ Check: (value: unknown) => boolean;
113
+ };
52
114
  readonly getReferencedFields: typeof tableFields;
53
115
  readonly component: Component<SurfaceWidgetRendererProps, {}, "">;
54
116
  }, {
55
117
  readonly type: "bar-chart";
56
118
  readonly dataKind: "items";
57
- readonly propsSchema: import("zod").ZodObject<{
58
- title: import("zod").ZodString;
59
- labelField: import("zod").ZodString;
60
- valueField: import("zod").ZodString;
61
- showValues: import("zod").ZodOptional<import("zod").ZodBoolean>;
62
- }, import("zod/v4/core").$strict>;
119
+ readonly propsSchema: import("@sinclair/typebox").TObject<{
120
+ title: import("@sinclair/typebox").TString;
121
+ labelField: import("@sinclair/typebox").TString;
122
+ valueField: import("@sinclair/typebox").TString;
123
+ showValues: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
124
+ }> & {
125
+ parse: (value: unknown) => {
126
+ showValues?: boolean | undefined;
127
+ title: string;
128
+ labelField: string;
129
+ valueField: string;
130
+ };
131
+ safeParse: (value: unknown) => {
132
+ success: false;
133
+ error: {
134
+ issues: Array<{
135
+ path: string[];
136
+ message: string;
137
+ }>;
138
+ };
139
+ } | {
140
+ success: true;
141
+ data: {
142
+ showValues?: boolean | undefined;
143
+ title: string;
144
+ labelField: string;
145
+ valueField: string;
146
+ };
147
+ };
148
+ Check: (value: unknown) => boolean;
149
+ };
63
150
  readonly getReferencedFields: typeof barChartFields;
64
151
  readonly component: Component<SurfaceWidgetRendererProps, {}, "">;
65
152
  }, {
66
153
  readonly type: "line-chart";
67
154
  readonly dataKind: "items";
68
- readonly propsSchema: import("zod").ZodObject<{
69
- title: import("zod").ZodString;
70
- labelField: import("zod").ZodString;
71
- valueField: import("zod").ZodString;
72
- showDots: import("zod").ZodOptional<import("zod").ZodBoolean>;
73
- fill: import("zod").ZodOptional<import("zod").ZodBoolean>;
74
- }, import("zod/v4/core").$strict>;
155
+ readonly propsSchema: import("@sinclair/typebox").TObject<{
156
+ title: import("@sinclair/typebox").TString;
157
+ labelField: import("@sinclair/typebox").TString;
158
+ valueField: import("@sinclair/typebox").TString;
159
+ showDots: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
160
+ fill: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
161
+ }> & {
162
+ parse: (value: unknown) => {
163
+ fill?: boolean | undefined;
164
+ showDots?: boolean | undefined;
165
+ title: string;
166
+ labelField: string;
167
+ valueField: string;
168
+ };
169
+ safeParse: (value: unknown) => {
170
+ success: false;
171
+ error: {
172
+ issues: Array<{
173
+ path: string[];
174
+ message: string;
175
+ }>;
176
+ };
177
+ } | {
178
+ success: true;
179
+ data: {
180
+ fill?: boolean | undefined;
181
+ showDots?: boolean | undefined;
182
+ title: string;
183
+ labelField: string;
184
+ valueField: string;
185
+ };
186
+ };
187
+ Check: (value: unknown) => boolean;
188
+ };
75
189
  readonly getReferencedFields: typeof lineChartFields;
76
190
  readonly component: Component<SurfaceWidgetRendererProps, {}, "">;
77
191
  }];
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { DataProvider } from '@svadmin/core';
2
- import type { ZodType } from 'zod';
2
+ import type { TSchema } from '@sinclair/typebox';
3
3
  export declare const SURFACE_SCHEMA_VERSION: "surface/v1";
4
4
  export declare const SURFACE_LIMITS: {
5
5
  readonly maxDataSources: 8;
@@ -92,7 +92,12 @@ export type SurfaceCatalogDataKind = 'none' | 'scalar' | 'items';
92
92
  export interface SurfaceWidgetDefinition {
93
93
  readonly type: string;
94
94
  readonly dataKind: SurfaceCatalogDataKind;
95
- readonly propsSchema: ZodType<unknown>;
95
+ readonly propsSchema: TSchema | {
96
+ Check?: (data: unknown) => boolean;
97
+ safeParse?: (data: unknown) => {
98
+ success: boolean;
99
+ };
100
+ };
96
101
  /** 组件从绑定记录中读取的字段,由可信 Catalog 提供。 */
97
102
  readonly getReferencedFields?: (props: JsonObject) => readonly string[];
98
103
  }
@@ -1,2 +1,2 @@
1
- import { type SurfaceCatalog, type SurfacePolicy, type SurfaceValidationResult } from './types.js';
1
+ import { type SurfaceCatalog, type SurfacePolicy, type SurfaceValidationResult } from "./types.js";
2
2
  export declare function validateSurfaceSpec(input: unknown, catalog: SurfaceCatalog, policy: SurfacePolicy): SurfaceValidationResult;
@@ -1,75 +1,126 @@
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({
1
+ import { Type } from "@sinclair/typebox";
2
+ import { TypeCompiler } from "@sinclair/typebox/compiler";
3
+ import { Value } from "@sinclair/typebox/value";
4
+ import { decodedJsonPointerToken, jsonPointer, jsonValueIssue } from "./json.js";
5
+ import { SURFACE_LIMITS, SURFACE_SCHEMA_VERSION, } from "./types.js";
6
+ const idSchema = Type.String({
7
+ minLength: 1,
8
+ maxLength: SURFACE_LIMITS.maxIdLength,
9
+ pattern: "^[A-Za-z][A-Za-z0-9_-]*$",
10
+ });
11
+ const fieldSchema = Type.String({
12
+ minLength: 1,
13
+ maxLength: SURFACE_LIMITS.maxIdLength,
14
+ });
15
+ const jsonPrimitiveSchema = Type.Union([
16
+ Type.String(),
17
+ Type.Number(),
18
+ Type.Boolean(),
19
+ Type.Null(),
20
+ ]);
21
+ const surfaceFilterSchema = Type.Union([
22
+ Type.Object({
9
23
  field: fieldSchema,
10
- operator: z.enum(['eq', 'ne', 'lt', 'lte', 'gt', 'gte', 'contains', 'startswith', 'endswith']),
24
+ operator: Type.Union([
25
+ Type.Literal("eq"),
26
+ Type.Literal("ne"),
27
+ Type.Literal("lt"),
28
+ Type.Literal("lte"),
29
+ Type.Literal("gt"),
30
+ Type.Literal("gte"),
31
+ Type.Literal("contains"),
32
+ Type.Literal("startswith"),
33
+ Type.Literal("endswith"),
34
+ ]),
11
35
  value: jsonPrimitiveSchema,
12
- }).strict(),
13
- z.object({
36
+ }, { additionalProperties: false }),
37
+ Type.Object({
14
38
  field: fieldSchema,
15
- operator: z.enum(['in', 'nin']),
16
- value: z.array(jsonPrimitiveSchema),
17
- }).strict(),
18
- z.object({
39
+ operator: Type.Union([
40
+ Type.Literal("in"),
41
+ Type.Literal("nin"),
42
+ ]),
43
+ value: Type.Array(jsonPrimitiveSchema),
44
+ }, { additionalProperties: false }),
45
+ Type.Object({
19
46
  field: fieldSchema,
20
- operator: z.enum(['null', 'nnull']),
21
- }).strict(),
47
+ operator: Type.Union([
48
+ Type.Literal("null"),
49
+ Type.Literal("nnull"),
50
+ ]),
51
+ }, { additionalProperties: false }),
22
52
  ]);
23
- const resourceListSchema = z.object({
53
+ const resourceListSchema = Type.Object({
24
54
  id: idSchema,
25
- type: z.literal('resource-list'),
55
+ type: Type.Literal("resource-list"),
26
56
  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({
57
+ pageSize: Type.Optional(Type.Integer({ minimum: 1 })),
58
+ sorters: Type.Optional(Type.Array(Type.Object({
59
+ field: fieldSchema,
60
+ order: Type.Union([Type.Literal("asc"), Type.Literal("desc")]),
61
+ }, { additionalProperties: false }))),
62
+ filters: Type.Optional(Type.Array(surfaceFilterSchema)),
63
+ }, { additionalProperties: false });
64
+ const resourceOneSchema = Type.Object({
32
65
  id: idSchema,
33
- type: z.literal('resource-one'),
66
+ type: Type.Literal("resource-one"),
34
67
  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),
68
+ recordId: Type.Union([Type.String(), Type.Number()]),
69
+ }, { additionalProperties: false });
70
+ const surfaceSpecSchema = Type.Object({
71
+ schemaVersion: Type.String(),
72
+ catalogVersion: Type.String({ minLength: 1 }),
40
73
  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({
74
+ title: Type.String({ minLength: 1, maxLength: SURFACE_LIMITS.maxTitleLength }),
75
+ layout: Type.Object({
76
+ type: Type.Literal("grid"),
77
+ columns: Type.Literal(12),
78
+ gap: Type.Optional(Type.Union([
79
+ Type.Literal("sm"),
80
+ Type.Literal("md"),
81
+ Type.Literal("lg"),
82
+ ])),
83
+ }, { additionalProperties: false }),
84
+ dataSources: Type.Array(Type.Union([resourceListSchema, resourceOneSchema])),
85
+ widgets: Type.Array(Type.Object({
49
86
  id: idSchema,
50
87
  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();
88
+ props: Type.Record(Type.String(), Type.Unknown()),
89
+ binding: Type.Optional(Type.Object({
90
+ sourceId: idSchema,
91
+ pointer: Type.Optional(Type.String()),
92
+ }, { additionalProperties: false })),
93
+ placement: Type.Optional(Type.Object({
94
+ columnSpan: Type.Optional(Type.Integer({ minimum: 1, maximum: 12 })),
95
+ }, { additionalProperties: false })),
96
+ }, { additionalProperties: false })),
97
+ }, { additionalProperties: false });
98
+ const compiledSurfaceSpec = TypeCompiler.Compile(surfaceSpecSchema);
56
99
  const forbiddenPropertyNames = new Set([
57
- 'class',
58
- 'className',
59
- 'color',
60
- 'href',
61
- 'html',
62
- 'innerHTML',
63
- 'src',
64
- 'style',
65
- 'url',
100
+ "class",
101
+ "className",
102
+ "color",
103
+ "href",
104
+ "html",
105
+ "innerHTML",
106
+ "src",
107
+ "style",
108
+ "url",
66
109
  ]);
67
- const strictSchemaProbeKey = '__surface_unknown_property_probe__';
110
+ const strictSchemaProbeKey = "__surface_unknown_property_probe__";
68
111
  function invalidJsonIssue(pathSegments, message) {
69
- return { code: 'invalid_json', path: jsonPointer(pathSegments), message };
112
+ return { code: "invalid_json", path: jsonPointer(pathSegments), message };
70
113
  }
71
- function schemaIssues(error) {
72
- return error.issues.map((issue) => invalidJsonIssue(issue.path.map((segment) => typeof segment === 'symbol' ? String(segment) : segment), issue.message));
114
+ function schemaErrors(errors) {
115
+ const issues = [];
116
+ for (const err of errors) {
117
+ issues.push({
118
+ code: "invalid_json",
119
+ path: err.path || "",
120
+ message: err.message,
121
+ });
122
+ }
123
+ return issues;
73
124
  }
74
125
  function catalogDefinitions(catalog) {
75
126
  return new Map(catalog.widgets.map((widgetDefinition) => [widgetDefinition.type, widgetDefinition]));
@@ -81,25 +132,25 @@ function sourcePolicyIssue(source, policy, index) {
81
132
  const resourcePolicy = resourcePolicyFor(policy, source.resource);
82
133
  if (!resourcePolicy) {
83
134
  return {
84
- code: 'resource_denied',
135
+ code: "resource_denied",
85
136
  path: `/dataSources/${index}/resource`,
86
137
  message: `Resource "${source.resource}" is not allowed`,
87
138
  sourceId: source.id,
88
139
  };
89
140
  }
90
- if (source.type === 'resource-one' && resourcePolicy.allowGetOne !== true) {
141
+ if (source.type === "resource-one" && resourcePolicy.allowGetOne !== true) {
91
142
  return {
92
- code: 'resource_denied',
143
+ code: "resource_denied",
93
144
  path: `/dataSources/${index}`,
94
145
  message: `Resource "${source.resource}" does not allow getOne`,
95
146
  sourceId: source.id,
96
147
  };
97
148
  }
98
- if (source.type === 'resource-list') {
149
+ if (source.type === "resource-list") {
99
150
  const pageSizeLimit = Math.min(resourcePolicy.maxPageSize ?? SURFACE_LIMITS.maxPageSize, SURFACE_LIMITS.maxPageSize);
100
151
  if ((source.pageSize ?? 10) > pageSizeLimit) {
101
152
  return {
102
- code: 'limit_exceeded',
153
+ code: "limit_exceeded",
103
154
  path: `/dataSources/${index}/pageSize`,
104
155
  message: `Page size exceeds ${pageSizeLimit}`,
105
156
  sourceId: source.id,
@@ -108,7 +159,7 @@ function sourcePolicyIssue(source, policy, index) {
108
159
  const disallowedSorter = source.sorters?.find((sorter) => !resourcePolicy.sortFields?.includes(sorter.field));
109
160
  if (disallowedSorter) {
110
161
  return {
111
- code: 'field_denied',
162
+ code: "field_denied",
112
163
  path: `/dataSources/${index}/sorters`,
113
164
  message: `Sorting by "${disallowedSorter.field}" is not allowed`,
114
165
  sourceId: source.id,
@@ -117,7 +168,7 @@ function sourcePolicyIssue(source, policy, index) {
117
168
  const disallowedFilter = source.filters?.find((filter) => !resourcePolicy.filterFields?.includes(filter.field));
118
169
  if (disallowedFilter) {
119
170
  return {
120
- code: 'field_denied',
171
+ code: "field_denied",
121
172
  path: `/dataSources/${index}/filters`,
122
173
  message: `Filtering by "${disallowedFilter.field}" is not allowed`,
123
174
  sourceId: source.id,
@@ -130,14 +181,14 @@ function propsContainForbiddenKey(props) {
130
181
  const pending = [props];
131
182
  while (pending.length > 0) {
132
183
  const current = pending.pop();
133
- if (current === undefined || current === null || typeof current !== 'object')
184
+ if (current === undefined || current === null || typeof current !== "object")
134
185
  continue;
135
186
  if (Array.isArray(current)) {
136
187
  pending.push(...current);
137
188
  continue;
138
189
  }
139
190
  for (const [key, entry] of Object.entries(current)) {
140
- if (forbiddenPropertyNames.has(key) || key.startsWith('on'))
191
+ if (forbiddenPropertyNames.has(key) || key.startsWith("on"))
141
192
  return key;
142
193
  pending.push(entry);
143
194
  }
@@ -153,18 +204,18 @@ function referencedFieldIssue(widget, widgetIndex, definition, source, policy) {
153
204
  }
154
205
  catch {
155
206
  return {
156
- code: 'invalid_widget_props',
207
+ code: "invalid_widget_props",
157
208
  path: `/widgets/${widgetIndex}/props`,
158
- message: 'Widget field references could not be resolved',
209
+ message: "Widget field references could not be resolved",
159
210
  widgetId: widget.id,
160
211
  };
161
212
  }
162
213
  const readableFields = resourcePolicyFor(policy, source.resource)?.readFields ?? [];
163
- const deniedField = referencedFields.find((field) => (typeof field !== 'string' || !readableFields.includes(field)));
214
+ const deniedField = referencedFields.find((field) => (typeof field !== "string" || !readableFields.includes(field)));
164
215
  return deniedField === undefined
165
216
  ? null
166
217
  : {
167
- code: 'field_denied',
218
+ code: "field_denied",
168
219
  path: `/widgets/${widgetIndex}/props`,
169
220
  message: `Field "${String(deniedField)}" is not readable`,
170
221
  widgetId: widget.id,
@@ -172,44 +223,55 @@ function referencedFieldIssue(widget, widgetIndex, definition, source, policy) {
172
223
  };
173
224
  }
174
225
  function resourceOnePointerField(pointer) {
175
- if (!pointer.startsWith('/') || pointer.slice(1).includes('/'))
226
+ if (!pointer.startsWith("/") || pointer.slice(1).includes("/"))
176
227
  return null;
177
228
  const field = decodedJsonPointerToken(pointer.slice(1));
178
229
  return field && field.length <= SURFACE_LIMITS.maxIdLength ? field : null;
179
230
  }
231
+ function checkPropsSchema(schema, data) {
232
+ if (!schema || typeof schema !== "object")
233
+ return false;
234
+ if ("Check" in schema && typeof schema.Check === "function") {
235
+ return schema.Check(data);
236
+ }
237
+ if ("safeParse" in schema && typeof schema.safeParse === "function") {
238
+ return schema.safeParse(data).success;
239
+ }
240
+ return Value.Check(schema, data);
241
+ }
180
242
  function widgetIssue(widget, widgetIndex, definition, sources, policy) {
181
243
  if (!definition) {
182
244
  return {
183
- code: 'unknown_widget_type',
245
+ code: "unknown_widget_type",
184
246
  path: `/widgets/${widgetIndex}/type`,
185
247
  message: `Widget type "${widget.type}" is not registered`,
186
248
  widgetId: widget.id,
187
249
  };
188
250
  }
189
251
  const forbiddenKey = propsContainForbiddenKey(widget.props);
190
- const propsValidation = definition.propsSchema.safeParse(widget.props);
191
- const strictSchemaProbe = definition.propsSchema.safeParse({
252
+ const propsValid = checkPropsSchema(definition.propsSchema, widget.props);
253
+ const strictSchemaProbe = checkPropsSchema(definition.propsSchema, {
192
254
  ...widget.props,
193
255
  [strictSchemaProbeKey]: null,
194
256
  });
195
- if (forbiddenKey || !propsValidation.success || strictSchemaProbe.success) {
257
+ if (forbiddenKey || !propsValid || strictSchemaProbe) {
196
258
  return {
197
- code: 'invalid_widget_props',
259
+ code: "invalid_widget_props",
198
260
  path: `/widgets/${widgetIndex}/props`,
199
261
  message: forbiddenKey
200
262
  ? `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',
263
+ : strictSchemaProbe
264
+ ? "Catalog props schema must reject unknown properties"
265
+ : "Widget props do not match the catalog schema",
204
266
  widgetId: widget.id,
205
267
  };
206
268
  }
207
- if (definition.dataKind === 'none') {
269
+ if (definition.dataKind === "none") {
208
270
  return widget.binding
209
271
  ? {
210
- code: 'invalid_binding_pointer',
272
+ code: "invalid_binding_pointer",
211
273
  path: `/widgets/${widgetIndex}/binding`,
212
- message: 'This widget does not accept a data binding',
274
+ message: "This widget does not accept a data binding",
213
275
  widgetId: widget.id,
214
276
  }
215
277
  : null;
@@ -217,23 +279,23 @@ function widgetIssue(widget, widgetIndex, definition, sources, policy) {
217
279
  const source = widget.binding ? sources.get(widget.binding.sourceId) : undefined;
218
280
  if (!source) {
219
281
  return {
220
- code: 'unknown_data_source',
282
+ code: "unknown_data_source",
221
283
  path: `/widgets/${widgetIndex}/binding/sourceId`,
222
- message: 'Widget binding must reference an existing data source',
284
+ message: "Widget binding must reference an existing data source",
223
285
  widgetId: widget.id,
224
286
  };
225
287
  }
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')
288
+ const pointer = widget.binding?.pointer ?? "";
289
+ const resourceOneField = source.type === "resource-one" ? resourceOnePointerField(pointer) : null;
290
+ const pointerIsValid = definition.dataKind === "items"
291
+ ? source.type === "resource-list" && pointer === "/items"
292
+ : (source.type === "resource-list" && pointer === "/total")
231
293
  || resourceOneField !== null;
232
294
  if (!pointerIsValid) {
233
295
  return {
234
- code: 'invalid_binding_pointer',
296
+ code: "invalid_binding_pointer",
235
297
  path: `/widgets/${widgetIndex}/binding/pointer`,
236
- message: 'Binding pointer is not valid for this widget and data source',
298
+ message: "Binding pointer is not valid for this widget and data source",
237
299
  widgetId: widget.id,
238
300
  sourceId: source.id,
239
301
  };
@@ -241,10 +303,10 @@ function widgetIssue(widget, widgetIndex, definition, sources, policy) {
241
303
  const deniedReferencedField = referencedFieldIssue(widget, widgetIndex, definition, source, policy);
242
304
  if (deniedReferencedField)
243
305
  return deniedReferencedField;
244
- if (source.type === 'resource-one' && resourceOneField !== null) {
306
+ if (source.type === "resource-one" && resourceOneField !== null) {
245
307
  if (!resourcePolicyFor(policy, source.resource)?.readFields.includes(resourceOneField)) {
246
308
  return {
247
- code: 'field_denied',
309
+ code: "field_denied",
248
310
  path: `/widgets/${widgetIndex}/binding/pointer`,
249
311
  message: `Field "${resourceOneField}" is not readable`,
250
312
  widgetId: widget.id,
@@ -262,7 +324,7 @@ function duplicateIdIssue(spec) {
262
324
  ];
263
325
  for (const entry of entries) {
264
326
  if (ids.has(entry.id))
265
- return { code: 'duplicate_id', path: entry.path, message: `Duplicate id "${entry.id}"` };
327
+ return { code: "duplicate_id", path: entry.path, message: `Duplicate id "${entry.id}"` };
266
328
  ids.add(entry.id);
267
329
  }
268
330
  return null;
@@ -271,18 +333,18 @@ export function validateSurfaceSpec(input, catalog, policy) {
271
333
  const unsafeJson = jsonValueIssue(input);
272
334
  if (unsafeJson)
273
335
  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;
336
+ if (!compiledSurfaceSpec.Check(input)) {
337
+ return { ok: false, issues: schemaErrors(compiledSurfaceSpec.Errors(input)) };
338
+ }
339
+ const spec = input;
278
340
  if (spec.schemaVersion !== SURFACE_SCHEMA_VERSION) {
279
- return { ok: false, issues: [{ code: 'unsupported_schema_version', path: '/schemaVersion', message: `Expected ${SURFACE_SCHEMA_VERSION}` }] };
341
+ return { ok: false, issues: [{ code: "unsupported_schema_version", path: "/schemaVersion", message: `Expected ${SURFACE_SCHEMA_VERSION}` }] };
280
342
  }
281
343
  if (spec.catalogVersion !== catalog.version) {
282
- return { ok: false, issues: [{ code: 'catalog_version_mismatch', path: '/catalogVersion', message: `Expected ${catalog.version}` }] };
344
+ return { ok: false, issues: [{ code: "catalog_version_mismatch", path: "/catalogVersion", message: `Expected ${catalog.version}` }] };
283
345
  }
284
346
  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' }] };
347
+ return { ok: false, issues: [{ code: "limit_exceeded", path: "", message: "Surface exceeds the source or widget limit" }] };
286
348
  }
287
349
  const duplicateIssue = duplicateIdIssue(spec);
288
350
  if (duplicateIssue)
@@ -291,9 +353,9 @@ export function validateSurfaceSpec(input, catalog, policy) {
291
353
  const issue = sourcePolicyIssue(source, policy, sourceIndex);
292
354
  if (issue)
293
355
  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 }] };
356
+ if ((source.type === "resource-list" && (source.filters?.length ?? 0) > SURFACE_LIMITS.maxFilters)
357
+ || (source.type === "resource-list" && (source.sorters?.length ?? 0) > SURFACE_LIMITS.maxSorters)) {
358
+ return { ok: false, issues: [{ code: "limit_exceeded", path: `/dataSources/${sourceIndex}`, message: "Source exceeds the filter or sorter limit", sourceId: source.id }] };
297
359
  }
298
360
  }
299
361
  const sources = new Map(spec.dataSources.map((source) => [source.id, source]));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/surface",
3
- "version": "0.4.6",
3
+ "version": "0.6.0",
4
4
  "description": "Validated declarative surfaces for svadmin dashboards",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -25,20 +25,20 @@
25
25
  }
26
26
  },
27
27
  "peerDependencies": {
28
- "@svadmin/core": ">=0.34.2 <0.43.0",
29
- "@svadmin/ui": ">=0.40.6 <0.61.0",
28
+ "@svadmin/core": ">=0.34.2 <0.46.0",
29
+ "@svadmin/ui": ">=0.40.6 <0.64.0",
30
30
  "svelte": "^5.56.8"
31
31
  },
32
32
  "dependencies": {
33
- "zod": "^4.4.3"
33
+ "@sinclair/typebox": "^0.34.52"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
37
37
  "@testing-library/dom": "^10.4.1",
38
38
  "@testing-library/svelte": "^5.4.2",
39
- "@testing-library/user-event": "^14.6.4",
40
- "happy-dom": "^20.11.2",
41
- "vitest": "^4.1.10"
39
+ "@testing-library/user-event": "^14.6.6",
40
+ "happy-dom": "^20.11.12",
41
+ "vitest": "^4.1.11"
42
42
  },
43
43
  "scripts": {
44
44
  "build": "node ../../node_modules/@sveltejs/package/svelte-package.js -i src -o dist",