@vojtaholik/static-kit-core 1.0.7 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/package.json +14 -49
  2. package/src/block-registry.ts +196 -0
  3. package/src/config.ts +32 -0
  4. package/src/html-renderer.ts +288 -0
  5. package/src/index.ts +70 -0
  6. package/src/layout.ts +27 -0
  7. package/src/schema-address.ts +63 -0
  8. package/src/schema.ts +164 -0
  9. package/src/template-compiler.ts +584 -0
  10. package/dist/index.d.ts +0 -8
  11. package/dist/index.d.ts.map +0 -1
  12. package/dist/index.js +0 -8
  13. package/dist/plugins/build-plugins.d.ts +0 -9
  14. package/dist/plugins/build-plugins.d.ts.map +0 -1
  15. package/dist/plugins/build-plugins.js +0 -271
  16. package/dist/plugins/index.d.ts +0 -7
  17. package/dist/plugins/index.d.ts.map +0 -1
  18. package/dist/plugins/index.js +0 -3
  19. package/dist/plugins/pages-preview.d.ts +0 -4
  20. package/dist/plugins/pages-preview.d.ts.map +0 -1
  21. package/dist/plugins/pages-preview.js +0 -292
  22. package/dist/plugins/svg-sprite.d.ts +0 -8
  23. package/dist/plugins/svg-sprite.d.ts.map +0 -1
  24. package/dist/plugins/svg-sprite.js +0 -98
  25. package/dist/types.d.ts +0 -27
  26. package/dist/types.d.ts.map +0 -1
  27. package/dist/types.js +0 -1
  28. package/dist/utils/config.d.ts +0 -5
  29. package/dist/utils/config.d.ts.map +0 -1
  30. package/dist/utils/config.js +0 -34
  31. package/dist/utils/file-scanner.d.ts +0 -9
  32. package/dist/utils/file-scanner.d.ts.map +0 -1
  33. package/dist/utils/file-scanner.js +0 -43
  34. package/dist/utils/html-imports.d.ts +0 -7
  35. package/dist/utils/html-imports.d.ts.map +0 -1
  36. package/dist/utils/html-imports.js +0 -100
  37. package/dist/utils/typescript-compiler.d.ts +0 -13
  38. package/dist/utils/typescript-compiler.d.ts.map +0 -1
  39. package/dist/utils/typescript-compiler.js +0 -61
  40. package/dist/vite-config.d.ts +0 -5
  41. package/dist/vite-config.d.ts.map +0 -1
  42. package/dist/vite-config.js +0 -101
package/package.json CHANGED
@@ -1,62 +1,27 @@
1
1
  {
2
2
  "name": "@vojtaholik/static-kit-core",
3
- "version": "1.0.7",
4
- "description": "Core library for Static Kit - simple static site framework",
3
+ "version": "2.0.0",
5
4
  "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
5
  "exports": {
9
6
  ".": {
10
- "types": "./dist/index.d.ts",
11
- "import": "./dist/index.js"
12
- },
13
- "./vite": {
14
- "types": "./dist/vite-config.d.ts",
15
- "import": "./dist/vite-config.js"
16
- },
17
- "./plugins": {
18
- "types": "./dist/plugins/index.d.ts",
19
- "import": "./dist/plugins/index.js"
7
+ "import": "./src/index.ts",
8
+ "types": "./src/index.ts"
20
9
  }
21
10
  },
22
- "files": [
23
- "dist/",
24
- "templates/"
25
- ],
26
- "scripts": {
27
- "build": "tsc",
28
- "dev": "tsc --watch",
29
- "clean": "rm -rf dist"
11
+ "files": ["src"],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/vojtaholik/module-kit",
15
+ "directory": "packages/core"
30
16
  },
31
- "peerDependencies": {
32
- "vite": "^7.0.0"
17
+ "publishConfig": {
18
+ "access": "public"
33
19
  },
34
20
  "dependencies": {
35
- "fast-glob": "^3.3.3",
36
- "svgo": "^4.0.0"
37
- },
38
- "devDependencies": {
39
- "@types/node": "^24.2.1",
40
- "typescript": "~5.9.2",
41
- "vite": "^7.1.1"
21
+ "parse5": "^8.0.0",
22
+ "zod": "^4.1.13"
42
23
  },
43
- "keywords": [
44
- "static-site",
45
- "vite",
46
- "html",
47
- "scss",
48
- "typescript",
49
- "components"
50
- ],
51
- "author": "Vojta Holik <vojta@holik.dev>",
52
- "license": "MIT",
53
- "repository": {
54
- "type": "git",
55
- "url": "https://github.com/vojtaholik/static-kit.git",
56
- "directory": "packages/static-kit-core"
57
- },
58
- "homepage": "https://github.com/vojtaholik/static-kit#readme",
59
- "bugs": {
60
- "url": "https://github.com/vojtaholik/static-kit/issues"
24
+ "peerDependencies": {
25
+ "typescript": "^5"
61
26
  }
62
27
  }
@@ -0,0 +1,196 @@
1
+ import { z } from "zod/v4";
2
+ import type { LayoutProps } from "./layout.ts";
3
+ import type { SchemaAddress } from "./schema-address.ts";
4
+
5
+ /**
6
+ * Context passed to block render functions
7
+ */
8
+ export interface RenderContext {
9
+ /** Current page ID */
10
+ pageId: string;
11
+ /** Base URL for assets */
12
+ assetBase: string;
13
+ /** Whether we're in dev mode */
14
+ isDev: boolean;
15
+ /** Layout props for styling context */
16
+ layout: LayoutProps;
17
+ }
18
+
19
+ /**
20
+ * Input to block render function
21
+ */
22
+ export interface RenderBlockInput<T extends z.ZodType = z.ZodType> {
23
+ /** Validated block props */
24
+ props: z.infer<T>;
25
+ /** Render context */
26
+ ctx: RenderContext;
27
+ /** Schema address for CMS editing */
28
+ addr: SchemaAddress;
29
+ }
30
+
31
+ /**
32
+ * Block definition - ties together type, schema, and render function
33
+ */
34
+ export interface BlockDefinition<T extends z.ZodType = z.ZodType> {
35
+ type: string;
36
+ propsSchema: T;
37
+ renderHtml: (input: RenderBlockInput<T>) => string;
38
+ /** Source file path (for dev inspector) - pass import.meta.url */
39
+ sourceFile?: string;
40
+ }
41
+
42
+ /**
43
+ * Define a block with type-safe props
44
+ * Pass `sourceFile: import.meta.url` to enable click-to-open in dev inspector
45
+ */
46
+ export function defineBlock<T extends z.ZodType>(config: {
47
+ type: string;
48
+ propsSchema: T;
49
+ renderHtml: (input: RenderBlockInput<T>) => string;
50
+ /** Source file path - pass import.meta.url */
51
+ sourceFile?: string;
52
+ }): BlockDefinition<T> {
53
+ return {
54
+ type: config.type,
55
+ propsSchema: config.propsSchema,
56
+ renderHtml: config.renderHtml,
57
+ sourceFile: config.sourceFile,
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Registry of all block definitions
63
+ */
64
+ export class BlockRegistry {
65
+ private blocks = new Map<string, BlockDefinition>();
66
+
67
+ register<T extends z.ZodType>(definition: BlockDefinition<T>): void {
68
+ if (this.blocks.has(definition.type)) {
69
+ throw new Error(`Block type "${definition.type}" is already registered`);
70
+ }
71
+ this.blocks.set(definition.type, definition as BlockDefinition);
72
+ }
73
+
74
+ get(type: string): BlockDefinition | undefined {
75
+ return this.blocks.get(type);
76
+ }
77
+
78
+ getOrThrow(type: string): BlockDefinition {
79
+ const block = this.blocks.get(type);
80
+ if (!block) {
81
+ throw new Error(`Unknown block type: "${type}"`);
82
+ }
83
+ return block;
84
+ }
85
+
86
+ has(type: string): boolean {
87
+ return this.blocks.has(type);
88
+ }
89
+
90
+ all(): BlockDefinition[] {
91
+ return Array.from(this.blocks.values());
92
+ }
93
+
94
+ types(): string[] {
95
+ return Array.from(this.blocks.keys());
96
+ }
97
+
98
+ clear(): void {
99
+ this.blocks.clear();
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Global block registry instance
105
+ */
106
+ export const blockRegistry = new BlockRegistry();
107
+
108
+ /**
109
+ * HTML escape helper for templates
110
+ */
111
+ export function escapeHtml(str: unknown): string {
112
+ if (str === null || str === undefined) return "";
113
+ return String(str)
114
+ .replace(/&/g, "&amp;")
115
+ .replace(/</g, "&lt;")
116
+ .replace(/>/g, "&gt;")
117
+ .replace(/"/g, "&quot;")
118
+ .replace(/'/g, "&#39;");
119
+ }
120
+
121
+ /**
122
+ * Escape for use in HTML attributes
123
+ */
124
+ export function escapeAttr(str: unknown): string {
125
+ return escapeHtml(str);
126
+ }
127
+
128
+ /**
129
+ * Format a slot error for dev overlay display
130
+ */
131
+ function formatSlotError(
132
+ type: "not-found" | "validation",
133
+ blockType: string,
134
+ addr: SchemaAddress,
135
+ details?: string
136
+ ): string {
137
+ const errorData = JSON.stringify({
138
+ type,
139
+ blockType,
140
+ addr,
141
+ details,
142
+ });
143
+ // Hidden element that dev overlay will pick up
144
+ return `<script type="application/json" data-slot-error>${escapeHtml(
145
+ errorData
146
+ )}</script>`;
147
+ }
148
+
149
+ /**
150
+ * Render a slot with optional block delegation
151
+ *
152
+ * If blockType is provided and valid, renders that block with slotProps.
153
+ * Otherwise, renders the fallback content.
154
+ *
155
+ * @param blockType - Block type string to delegate rendering to (optional)
156
+ * @param slotProps - Props to pass to the delegated block
157
+ * @param ctx - Render context
158
+ * @param addr - Schema address for CMS editing
159
+ * @param fallback - Function that returns fallback HTML if no block specified
160
+ */
161
+ export function renderSlot(
162
+ blockType: string | undefined,
163
+ slotProps: Record<string, unknown>,
164
+ ctx: RenderContext,
165
+ addr: SchemaAddress,
166
+ fallback: () => string
167
+ ): string {
168
+ if (!blockType) return fallback();
169
+
170
+ const block = blockRegistry.get(blockType);
171
+ if (!block) {
172
+ const msg = `renderSlot: Unknown block type "${blockType}", using fallback`;
173
+ console.warn(msg);
174
+ const errorHtml = ctx.isDev
175
+ ? formatSlotError("not-found", blockType, addr)
176
+ : "";
177
+ return errorHtml + fallback();
178
+ }
179
+
180
+ const result = block.propsSchema.safeParse(slotProps);
181
+ if (!result.success) {
182
+ const issues = result.error.issues
183
+ .map((i) => `${i.path.join(".")}: ${i.message}`)
184
+ .join("; ");
185
+ console.warn(
186
+ `renderSlot: Props validation failed for "${blockType}", using fallback:`,
187
+ issues
188
+ );
189
+ const errorHtml = ctx.isDev
190
+ ? formatSlotError("validation", blockType, addr, issues)
191
+ : "";
192
+ return errorHtml + fallback();
193
+ }
194
+
195
+ return block.renderHtml({ props: result.data, ctx, addr });
196
+ }
package/src/config.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { z } from "zod/v4";
2
+
3
+ /**
4
+ * Static Kit configuration schema
5
+ */
6
+ export const configSchema = z.object({
7
+ /** Directory containing block definitions and templates */
8
+ blocksDir: z.string().default("blocks"),
9
+ /** Directory containing page configs and templates */
10
+ pagesDir: z.string().default("site/pages"),
11
+ /** Source directory for public assets (css, js, images) - structure mirrors output */
12
+ publicDir: z.string().default("public"),
13
+ /** Output directory for built site */
14
+ outDir: z.string().default("dist"),
15
+ /** URL path prefix where public assets are served */
16
+ publicPath: z.string().default("/public"),
17
+ /** Dev server port */
18
+ devPort: z.number().default(3000),
19
+ /** Path to cms-blocks file (relative to project root) */
20
+ cmsBlocksFile: z.string().optional(),
21
+ });
22
+
23
+ export type StaticKitConfig = z.infer<typeof configSchema>;
24
+
25
+ /**
26
+ * Define a Static Kit configuration with type-safe defaults
27
+ */
28
+ export function defineConfig(
29
+ config: z.input<typeof configSchema> = {}
30
+ ): StaticKitConfig {
31
+ return configSchema.parse(config);
32
+ }
@@ -0,0 +1,288 @@
1
+ import * as parse5 from "parse5";
2
+ import type { LayoutProps } from "./layout.ts";
3
+ import { layoutPropsSchema } from "./layout.ts";
4
+ import { blockRegistry, type RenderContext } from "./block-registry.ts";
5
+ import type { SchemaAddress } from "./schema-address.ts";
6
+
7
+ /**
8
+ * Block instance in a page config
9
+ */
10
+ export interface BlockInstance {
11
+ id: string;
12
+ type: string;
13
+ props: Record<string, unknown>;
14
+ layout?: Partial<LayoutProps>;
15
+ }
16
+
17
+ /**
18
+ * Region config - blocks in a named region
19
+ */
20
+ export interface RegionConfig {
21
+ blocks: BlockInstance[];
22
+ }
23
+
24
+ /**
25
+ * Page configuration
26
+ */
27
+ export interface PageConfig {
28
+ id: string;
29
+ path: string;
30
+ title: string;
31
+ template: string;
32
+ density?: "compact" | "comfortable" | "relaxed";
33
+ regions: Record<string, RegionConfig>;
34
+ meta?: Record<string, string>;
35
+ }
36
+
37
+ interface Element {
38
+ nodeName: string;
39
+ tagName?: string;
40
+ attrs?: Array<{ name: string; value: string }>;
41
+ childNodes?: Node[];
42
+ value?: string;
43
+ data?: string;
44
+ parentNode?: Node;
45
+ }
46
+
47
+ type Node = Element;
48
+
49
+ /**
50
+ * Options for rendering a page
51
+ */
52
+ export interface RenderPageOptions {
53
+ /** Directory containing page templates */
54
+ templateDir: string;
55
+ /** Whether we're in dev mode (injects dev overlay) */
56
+ isDev?: boolean;
57
+ /** Base URL for assets */
58
+ assetBase?: string;
59
+ /** Function to read template file content */
60
+ readFile?: (path: string) => Promise<string>;
61
+ }
62
+
63
+ /**
64
+ * Render a page from its config
65
+ */
66
+ export async function renderPage(
67
+ page: PageConfig,
68
+ options: RenderPageOptions
69
+ ): Promise<string> {
70
+ const templatePath = `${options.templateDir}/${page.template}`;
71
+
72
+ // Use provided readFile or default to Bun.file
73
+ const readFile =
74
+ options.readFile ?? (async (path: string) => await Bun.file(path).text());
75
+ const templateHtml = await readFile(templatePath);
76
+
77
+ // Parse the template
78
+ const document = parse5.parse(templateHtml) as Element;
79
+
80
+ // Track regions and their rendered content
81
+ const regionContent: Record<string, string> = {};
82
+
83
+ // Find and update elements
84
+ walkTree(document, (node) => {
85
+ // Update <title>
86
+ if (node.nodeName === "title") {
87
+ const textNode = node.childNodes?.[0];
88
+ if (textNode && textNode.nodeName === "#text") {
89
+ textNode.value = page.title;
90
+ }
91
+ }
92
+
93
+ // Update <html> attributes
94
+ if (node.nodeName === "html") {
95
+ setAttr(node, "data-page-id", page.id);
96
+ // if (page.density) {
97
+ // setAttr(node, "data-density", page.density);
98
+ // }
99
+ }
100
+
101
+ // Process regions - inject a marker that we'll replace after serialization
102
+ const regionName = getAttr(node, "data-region");
103
+ if (regionName) {
104
+ const regionConfig = page.regions[regionName];
105
+ if (regionConfig) {
106
+ // Render blocks for this region
107
+ regionContent[regionName] = renderRegionBlocks(regionConfig, {
108
+ pageId: page.id,
109
+ region: regionName,
110
+ isDev: options.isDev ?? false,
111
+ assetBase: options.assetBase ?? "/",
112
+ });
113
+
114
+ // Insert a marker that won't get escaped
115
+ node.childNodes = [
116
+ {
117
+ nodeName: "#comment",
118
+ data: `__REGION_CONTENT_${regionName}__`,
119
+ } as Node,
120
+ ];
121
+ }
122
+ }
123
+ });
124
+
125
+ // Serialize to HTML
126
+ let html = parse5.serialize(
127
+ document as unknown as parse5.DefaultTreeAdapterMap["parentNode"]
128
+ );
129
+
130
+ // Replace markers with actual region content
131
+ for (const [regionName, content] of Object.entries(regionContent)) {
132
+ const marker = `<!--__REGION_CONTENT_${regionName}__-->`;
133
+ html = html.replace(marker, content);
134
+ }
135
+
136
+ // Inject dev overlay if in dev mode
137
+ if (options.isDev) {
138
+ html = injectDevOverlay(html);
139
+ }
140
+
141
+ return html;
142
+ }
143
+
144
+ /**
145
+ * Render all blocks in a region
146
+ */
147
+ function renderRegionBlocks(
148
+ region: RegionConfig,
149
+ context: {
150
+ pageId: string;
151
+ region: string;
152
+ isDev: boolean;
153
+ assetBase: string;
154
+ }
155
+ ): string {
156
+ let html = "";
157
+
158
+ for (const block of region.blocks) {
159
+ const definition = blockRegistry.get(block.type);
160
+ if (!definition) {
161
+ console.warn(`Unknown block type: ${block.type}`);
162
+ continue;
163
+ }
164
+
165
+ // Validate and parse props
166
+ const propsResult = definition.propsSchema.safeParse(block.props);
167
+ if (!propsResult.success) {
168
+ const issues = propsResult.error.issues
169
+ .map((i) => `${i.path.join(".")}: ${i.message}`)
170
+ .join("; ");
171
+ console.warn(`Invalid props for block ${block.id}: ${issues}`);
172
+ continue;
173
+ }
174
+
175
+ // Build layout props
176
+ const layout = layoutPropsSchema.parse(block.layout ?? {});
177
+
178
+ // Build render context
179
+ const ctx: RenderContext = {
180
+ pageId: context.pageId,
181
+ assetBase: context.assetBase,
182
+ isDev: context.isDev,
183
+ layout,
184
+ };
185
+
186
+ // Build schema address
187
+ const addr: SchemaAddress = {
188
+ pageId: context.pageId,
189
+ region: context.region,
190
+ blockId: block.id,
191
+ };
192
+
193
+ // Render the block
194
+ html += definition.renderHtml({
195
+ props: propsResult.data,
196
+ ctx,
197
+ addr,
198
+ });
199
+ }
200
+
201
+ return html;
202
+ }
203
+
204
+ /**
205
+ * Walk the parse5 tree
206
+ */
207
+ function walkTree(node: Node, callback: (node: Node) => void): void {
208
+ callback(node);
209
+ if (node.childNodes) {
210
+ for (const child of node.childNodes) {
211
+ walkTree(child, callback);
212
+ }
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Get attribute value
218
+ */
219
+ function getAttr(node: Node, name: string): string | undefined {
220
+ const element = node as Element;
221
+ const attr = element.attrs?.find((a) => a.name === name);
222
+ return attr?.value;
223
+ }
224
+
225
+ /**
226
+ * Set attribute value
227
+ */
228
+ function setAttr(node: Node, name: string, value: string): void {
229
+ const element = node as Element;
230
+ if (!element.attrs) {
231
+ element.attrs = [];
232
+ }
233
+ const existing = element.attrs.find((a) => a.name === name);
234
+ if (existing) {
235
+ existing.value = value;
236
+ } else {
237
+ element.attrs.push({ name, value });
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Inject dev overlay script
243
+ */
244
+ function injectDevOverlay(html: string): string {
245
+ const script = `<script src="/__dev-overlay.js"></script>`;
246
+ return html.replace("</body>", `${script}</body>`);
247
+ }
248
+
249
+ /**
250
+ * Render a standalone block (for API/preview)
251
+ */
252
+ export function renderBlock(
253
+ block: BlockInstance,
254
+ context: {
255
+ pageId: string;
256
+ region: string;
257
+ isDev: boolean;
258
+ assetBase: string;
259
+ }
260
+ ): string {
261
+ const definition = blockRegistry.getOrThrow(block.type);
262
+
263
+ const propsResult = definition.propsSchema.safeParse(block.props);
264
+ if (!propsResult.success) {
265
+ throw new Error(`Invalid props: ${propsResult.error.message}`);
266
+ }
267
+
268
+ const layout = layoutPropsSchema.parse(block.layout ?? {});
269
+
270
+ const ctx: RenderContext = {
271
+ pageId: context.pageId,
272
+ assetBase: context.assetBase,
273
+ isDev: context.isDev,
274
+ layout,
275
+ };
276
+
277
+ const addr: SchemaAddress = {
278
+ pageId: context.pageId,
279
+ region: context.region,
280
+ blockId: block.id,
281
+ };
282
+
283
+ return definition.renderHtml({
284
+ props: propsResult.data,
285
+ ctx,
286
+ addr,
287
+ });
288
+ }
package/src/index.ts ADDED
@@ -0,0 +1,70 @@
1
+ // Layout schemas and types
2
+ export {
3
+ toneEnum,
4
+ contentAlignEnum,
5
+ densityEnum,
6
+ contentWidthEnum,
7
+ layoutPropsSchema,
8
+ type Tone,
9
+ type ContentAlign,
10
+ type Density,
11
+ type ContentWidth,
12
+ type LayoutProps,
13
+ } from "./layout.ts";
14
+
15
+ // Schema address utilities
16
+ export {
17
+ schemaAddressSchema,
18
+ encodeSchemaAddress,
19
+ decodeSchemaAddress,
20
+ withPropPath,
21
+ isSameBlock,
22
+ type SchemaAddress,
23
+ } from "./schema-address.ts";
24
+
25
+ // CMS schema utilities
26
+ export {
27
+ cmsFieldTypeEnum,
28
+ cmsFieldSchema,
29
+ cmsBlockSchemaMapSchema,
30
+ createSchemaFromCmsFields,
31
+ createSchemaFromCmsBlocks,
32
+ type CmsFieldType,
33
+ type CmsField,
34
+ type CmsBlockSchema,
35
+ type CmsBlockSchemaMap,
36
+ } from "./schema.ts";
37
+
38
+ // Block registry
39
+ export {
40
+ defineBlock,
41
+ BlockRegistry,
42
+ blockRegistry,
43
+ escapeHtml,
44
+ escapeAttr,
45
+ renderSlot,
46
+ type RenderContext,
47
+ type RenderBlockInput,
48
+ type BlockDefinition,
49
+ } from "./block-registry.ts";
50
+
51
+ // HTML renderer
52
+ export {
53
+ renderPage,
54
+ renderBlock,
55
+ type BlockInstance,
56
+ type RegionConfig,
57
+ type PageConfig,
58
+ type RenderPageOptions,
59
+ } from "./html-renderer.ts";
60
+
61
+ // Template compiler
62
+ export {
63
+ compileBlockTemplates,
64
+ compileTemplateFile,
65
+ compileTemplate,
66
+ type CompileOptions,
67
+ } from "./template-compiler.ts";
68
+
69
+ // Configuration
70
+ export { configSchema, defineConfig, type StaticKitConfig } from "./config.ts";
package/src/layout.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { z } from "zod/v4";
2
+
3
+ // Layout enums - design system tokens for blocks
4
+ export const toneEnum = z.enum(["surface", "raised", "accent", "inverted"]);
5
+ export const contentAlignEnum = z.enum([
6
+ "left",
7
+ "center",
8
+ "right",
9
+ "split-start",
10
+ "split-end",
11
+ ]);
12
+ export const densityEnum = z.enum(["compact", "comfortable", "relaxed"]);
13
+ export const contentWidthEnum = z.enum(["narrow", "default", "wide"]);
14
+
15
+ // Combined layout props schema for block rendering context
16
+ export const layoutPropsSchema = z.object({
17
+ tone: toneEnum.default("surface"),
18
+ contentAlign: contentAlignEnum.default("left"),
19
+ density: densityEnum.default("comfortable"),
20
+ contentWidth: contentWidthEnum.default("default"),
21
+ });
22
+
23
+ export type Tone = z.infer<typeof toneEnum>;
24
+ export type ContentAlign = z.infer<typeof contentAlignEnum>;
25
+ export type Density = z.infer<typeof densityEnum>;
26
+ export type ContentWidth = z.infer<typeof contentWidthEnum>;
27
+ export type LayoutProps = z.infer<typeof layoutPropsSchema>;