@kondeio/kdf 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.
Files changed (59) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/CONTRIBUTING.md +57 -0
  3. package/LICENSE +21 -0
  4. package/README.md +374 -0
  5. package/SECURITY.md +53 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +58 -0
  8. package/dist/css-generator.d.ts +17 -0
  9. package/dist/css-generator.js +59 -0
  10. package/dist/index.d.ts +48 -0
  11. package/dist/index.js +62 -0
  12. package/dist/plugin.d.ts +30 -0
  13. package/dist/plugin.js +40 -0
  14. package/dist/postinstall.d.ts +1 -0
  15. package/dist/postinstall.js +48 -0
  16. package/dist/resolver.d.ts +11 -0
  17. package/dist/resolver.js +184 -0
  18. package/dist/types.d.ts +44 -0
  19. package/dist/types.js +1 -0
  20. package/docs/kdf-doc.md +653 -0
  21. package/docs/kdf-skill.md +374 -0
  22. package/example/bootstrap/globals.css +12 -0
  23. package/example/bootstrap/hero-section.tsx +38 -0
  24. package/example/bootstrap/kdf/homepage.json +35 -0
  25. package/example/bootstrap/kdf/shared/button.json +10 -0
  26. package/example/bootstrap/kdf/shared/card.json +4 -0
  27. package/example/bootstrap/kdf/shared/color.json +17 -0
  28. package/example/bootstrap/kdf/shared/layout.json +7 -0
  29. package/example/bootstrap/kdf/shared/typography.json +8 -0
  30. package/example/bootstrap/konde-server.css +3 -0
  31. package/example/bootstrap/konde.css +3 -0
  32. package/example/preview.ts +428 -0
  33. package/example/pure-css/hero-section.tsx +40 -0
  34. package/example/pure-css/kdf/homepage.json +20 -0
  35. package/example/pure-css/kdf/shared/button.json +10 -0
  36. package/example/pure-css/kdf/shared/card.json +4 -0
  37. package/example/pure-css/kdf/shared/color.json +11 -0
  38. package/example/pure-css/kdf/shared/typography.json +8 -0
  39. package/example/pure-css/konde-server.css +3 -0
  40. package/example/pure-css/konde.css +3 -0
  41. package/example/pure-css/styles.css +34 -0
  42. package/example/sample-pages/about.json +30 -0
  43. package/example/sample-pages/dashboard.json +12 -0
  44. package/example/sample-pages/pricing.json +28 -0
  45. package/example/shadcn/globals.css +15 -0
  46. package/example/shadcn/hero-section.tsx +34 -0
  47. package/example/shadcn/konde-server.css +3 -0
  48. package/example/shadcn/konde.css +3 -0
  49. package/example/tailwind/globals.css +5 -0
  50. package/example/tailwind/hero-section.tsx +34 -0
  51. package/example/tailwind/konde-server.css +3 -0
  52. package/example/tailwind/konde.css +3 -0
  53. package/kdf/homepage.json +25 -0
  54. package/kdf/shared/button.json +10 -0
  55. package/kdf/shared/card.json +4 -0
  56. package/kdf/shared/color.json +17 -0
  57. package/kdf/shared/layout.json +7 -0
  58. package/kdf/shared/typography.json +8 -0
  59. package/package.json +77 -0
@@ -0,0 +1,48 @@
1
+ import { type ClassValue } from "clsx";
2
+ import type { DesignAccessor, GetDesignOptions } from "./types.js";
3
+ export type { DesignAccessor, DesignTokenFile, DesignTokenValue, GetDesignOptions, KdfCacheMode, } from "./types.js";
4
+ export { clearKdfCache } from "./resolver.js";
5
+ export type ClassMergeFunction = (className: string) => string;
6
+ export interface ClassComposerOptions {
7
+ /**
8
+ * Optional app-defined semantic merge step.
9
+ *
10
+ * KDF's default merge only removes exact duplicate classes. If an app wants
11
+ * semantic rules such as "keep the last class in this project-specific group",
12
+ * inject that logic here.
13
+ */
14
+ merge?: ClassMergeFunction;
15
+ }
16
+ /**
17
+ * Get design accessor for a page.
18
+ *
19
+ * Usage:
20
+ * const d = getDesign("homepage");
21
+ * <h1 data-kdf="hero.title" className={d("hero.title")}>{t("hero.headline")}</h1>
22
+ *
23
+ * Resolution order: kdf/<page>.json -> kdf/shared/
24
+ */
25
+ export declare function getDesign(page: string, options?: GetDesignOptions): DesignAccessor;
26
+ /**
27
+ * Remove exact duplicate class names while preserving first-seen order.
28
+ *
29
+ * This is intentionally semantic-free: it does not try to understand any CSS
30
+ * framework. It only turns "btn btn btn-primary" into "btn btn-primary".
31
+ */
32
+ export declare function dedupeClasses(className: string): string;
33
+ /**
34
+ * Create a UI-library agnostic class composer.
35
+ *
36
+ * Default behavior:
37
+ * - flatten strings, arrays, and objects through clsx
38
+ * - drop falsy values
39
+ * - normalize whitespace
40
+ * - remove exact duplicate class names
41
+ *
42
+ * Apps that need semantic class conflict handling can inject their own merge
43
+ * function. KDF does not ship CSS-framework-specific class rules.
44
+ */
45
+ export declare function createClassComposer(options?: ClassComposerOptions): (...inputs: ClassValue[]) => string;
46
+ export declare const composeClasses: (...inputs: ClassValue[]) => string;
47
+ export declare const cx: (...inputs: ClassValue[]) => string;
48
+ export declare const cn: (...inputs: ClassValue[]) => string;
package/dist/index.js ADDED
@@ -0,0 +1,62 @@
1
+ import { clsx } from "clsx";
2
+ import { loadFile, resolveClassName, resolveCSS } from "./resolver.js";
3
+ export { clearKdfCache } from "./resolver.js";
4
+ /**
5
+ * Get design accessor for a page.
6
+ *
7
+ * Usage:
8
+ * const d = getDesign("homepage");
9
+ * <h1 data-kdf="hero.title" className={d("hero.title")}>{t("hero.headline")}</h1>
10
+ *
11
+ * Resolution order: kdf/<page>.json -> kdf/shared/
12
+ */
13
+ export function getDesign(page, options) {
14
+ const accessor = ((path) => {
15
+ const pageTokens = loadFile(page, options);
16
+ return resolveClassName(path, pageTokens, options);
17
+ });
18
+ accessor.css = (path) => {
19
+ const pageTokens = loadFile(page, options);
20
+ return resolveCSS(path, pageTokens);
21
+ };
22
+ return accessor;
23
+ }
24
+ /**
25
+ * Remove exact duplicate class names while preserving first-seen order.
26
+ *
27
+ * This is intentionally semantic-free: it does not try to understand any CSS
28
+ * framework. It only turns "btn btn btn-primary" into "btn btn-primary".
29
+ */
30
+ export function dedupeClasses(className) {
31
+ const seen = new Set();
32
+ const output = [];
33
+ for (const part of className.trim().split(/\s+/)) {
34
+ if (!part || seen.has(part))
35
+ continue;
36
+ seen.add(part);
37
+ output.push(part);
38
+ }
39
+ return output.join(" ");
40
+ }
41
+ /**
42
+ * Create a UI-library agnostic class composer.
43
+ *
44
+ * Default behavior:
45
+ * - flatten strings, arrays, and objects through clsx
46
+ * - drop falsy values
47
+ * - normalize whitespace
48
+ * - remove exact duplicate class names
49
+ *
50
+ * Apps that need semantic class conflict handling can inject their own merge
51
+ * function. KDF does not ship CSS-framework-specific class rules.
52
+ */
53
+ export function createClassComposer(options = {}) {
54
+ const merge = options.merge ?? dedupeClasses;
55
+ return (...inputs) => {
56
+ const joined = clsx(...inputs);
57
+ return merge(joined);
58
+ };
59
+ }
60
+ export const composeClasses = createClassComposer();
61
+ export const cx = composeClasses;
62
+ export const cn = composeClasses;
@@ -0,0 +1,30 @@
1
+ /** Minimal Next.js config shape — avoids hard dependency on next */
2
+ interface NextConfig {
3
+ env?: Record<string, string>;
4
+ webpack?: (config: Record<string, unknown>, context: {
5
+ dev: boolean;
6
+ }) => Record<string, unknown>;
7
+ [key: string]: unknown;
8
+ }
9
+ export interface KDFPluginOptions {
10
+ /** KDF directory path (default: "./kdf"). Rename to anything: "./design", "./designs/lander" */
11
+ dir?: string;
12
+ }
13
+ /**
14
+ * Next.js plugin for @kondeio/kdf.
15
+ *
16
+ * Single config — everything follows from `dir`:
17
+ * - JSON tokens: <dir>/shared/, <dir>/homepage.json
18
+ * - konde-server.css: <dir>/konde-server.css (import in layout.tsx)
19
+ * - konde.css: <dir>/konde.css
20
+ *
21
+ * The plugin only exposes paths via env (KDF_DIR, KDF_SERVER_CSS,
22
+ * KDF_CLIENT_CSS). It does NOT inject CSS — wire the <link>/import in your app.
23
+ *
24
+ * Usage:
25
+ * export default withKDF()(nextConfig); // default: ./kdf
26
+ * export default withKDF({ dir: "./design" })(nextConfig); // rename folder
27
+ * export default withKDF({ dir: "./designs/lander" })(nextConfig); // multi-template
28
+ */
29
+ export default function withKDF(options?: KDFPluginOptions): (nextConfig?: NextConfig) => NextConfig;
30
+ export {};
package/dist/plugin.js ADDED
@@ -0,0 +1,40 @@
1
+ import { existsSync } from "fs";
2
+ import { join } from "path";
3
+ /**
4
+ * Next.js plugin for @kondeio/kdf.
5
+ *
6
+ * Single config — everything follows from `dir`:
7
+ * - JSON tokens: <dir>/shared/, <dir>/homepage.json
8
+ * - konde-server.css: <dir>/konde-server.css (import in layout.tsx)
9
+ * - konde.css: <dir>/konde.css
10
+ *
11
+ * The plugin only exposes paths via env (KDF_DIR, KDF_SERVER_CSS,
12
+ * KDF_CLIENT_CSS). It does NOT inject CSS — wire the <link>/import in your app.
13
+ *
14
+ * Usage:
15
+ * export default withKDF()(nextConfig); // default: ./kdf
16
+ * export default withKDF({ dir: "./design" })(nextConfig); // rename folder
17
+ * export default withKDF({ dir: "./designs/lander" })(nextConfig); // multi-template
18
+ */
19
+ export default function withKDF(options) {
20
+ return (nextConfig = {}) => {
21
+ const kdfDir = options?.dir || "./kdf";
22
+ const serverCssPath = join(process.cwd(), kdfDir, "konde-server.css");
23
+ const clientCssPath = join(process.cwd(), kdfDir, "konde.css");
24
+ return {
25
+ ...nextConfig,
26
+ env: {
27
+ ...nextConfig.env,
28
+ KDF_DIR: kdfDir,
29
+ ...(existsSync(serverCssPath) ? { KDF_SERVER_CSS: join(kdfDir, "konde-server.css") } : {}),
30
+ ...(existsSync(clientCssPath) ? { KDF_CLIENT_CSS: join(kdfDir, "konde.css") } : {}),
31
+ },
32
+ webpack(config, context) {
33
+ if (typeof nextConfig.webpack === "function") {
34
+ return nextConfig.webpack(config, context);
35
+ }
36
+ return config;
37
+ },
38
+ };
39
+ };
40
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ /**
2
+ * postinstall — runs automatically after installing @kondeio/kdf.
3
+ *
4
+ * Creates kdf/ folder + konde CSS files in user's project root.
5
+ * All writes are safe — never overwrites existing files.
6
+ * Uses INIT_CWD (project root), not package directory.
7
+ *
8
+ * Set KDF_SKIP_INIT=1 to install the package without scaffolding files.
9
+ */
10
+ import { mkdirSync, existsSync, copyFileSync, readdirSync } from "fs";
11
+ import { join, dirname } from "path";
12
+ import { fileURLToPath } from "url";
13
+ import { writeKondeCSS } from "./css-generator.js";
14
+ // INIT_CWD = project root (set by npm/bun during install)
15
+ // Fallback to cwd for direct execution
16
+ const projectRoot = process.env.INIT_CWD || process.cwd();
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const TEMPLATES_DIR = join(__dirname, "..", "kdf");
19
+ if (process.env.KDF_SKIP_INIT === "1" || process.env.KDF_SKIP_INIT === "true") {
20
+ process.exit(0);
21
+ }
22
+ const targetDir = join(projectRoot, "kdf");
23
+ // Skip if already initialized
24
+ if (existsSync(targetDir)) {
25
+ process.exit(0);
26
+ }
27
+ // Create kdf/ and kdf/shared/
28
+ mkdirSync(join(targetDir, "shared"), { recursive: true });
29
+ // Copy shared token files
30
+ const sharedDir = join(TEMPLATES_DIR, "shared");
31
+ if (existsSync(sharedDir)) {
32
+ for (const file of readdirSync(sharedDir).filter((f) => f.endsWith(".json"))) {
33
+ copyFileSync(join(sharedDir, file), join(targetDir, "shared", file));
34
+ }
35
+ }
36
+ // Copy homepage.json as starter
37
+ const homepageSrc = join(TEMPLATES_DIR, "homepage.json");
38
+ if (existsSync(homepageSrc)) {
39
+ copyFileSync(homepageSrc, join(targetDir, "homepage.json"));
40
+ }
41
+ // Generate empty CSS files inside kdf/ folder
42
+ writeKondeCSS(targetDir);
43
+ console.log("@kondeio/kdf initialized:");
44
+ console.log(" kdf/");
45
+ console.log(" shared/ ← design tokens");
46
+ console.log(" homepage.json");
47
+ console.log(" konde-server.css ← critical overrides (import in layout.tsx)");
48
+ console.log(" konde.css ← non-critical overrides (load after framework CSS)");
@@ -0,0 +1,11 @@
1
+ import type { DesignTokenFile, GetDesignOptions } from "./types.js";
2
+ /** Returns KDF root path — reads env at call time, not module load time */
3
+ declare function getKdfRoot(): string;
4
+ export declare function clearKdfCache(): void;
5
+ declare function loadFile(name: string, options?: GetDesignOptions): DesignTokenFile | null;
6
+ /** Resolve className for a token path.
7
+ * Looks in page JSON first. @references resolve from shared/ files. */
8
+ export declare function resolveClassName(path: string, pageTokens: DesignTokenFile | null, options?: GetDesignOptions): string;
9
+ /** Resolve CSS custom properties for a token path */
10
+ export declare function resolveCSS(path: string, pageTokens: DesignTokenFile | null): Record<string, string>;
11
+ export { loadFile, getKdfRoot };
@@ -0,0 +1,184 @@
1
+ import { readFileSync, statSync } from "fs";
2
+ import { isAbsolute, join } from "path";
3
+ /** Returns KDF root path — reads env at call time, not module load time */
4
+ function getKdfRoot() {
5
+ const dir = process.env.KDF_DIR || "kdf";
6
+ return isAbsolute(dir) ? dir : join(process.cwd(), dir);
7
+ }
8
+ const DEFAULT_DEV_CACHE_MAX_AGE_MS = 250;
9
+ /** Cache: path -> parsed JSON plus lightweight file identity */
10
+ const cache = new Map();
11
+ function getCacheMode(options) {
12
+ return options?.cache ?? "auto";
13
+ }
14
+ function getDevMaxAgeMs(options) {
15
+ if (typeof options?.maxAgeMs === "number" && options.maxAgeMs >= 0) {
16
+ return options.maxAgeMs;
17
+ }
18
+ const fromEnv = Number(process.env.KDF_CACHE_MAX_AGE_MS);
19
+ if (Number.isFinite(fromEnv) && fromEnv >= 0) {
20
+ return fromEnv;
21
+ }
22
+ return DEFAULT_DEV_CACHE_MAX_AGE_MS;
23
+ }
24
+ function readJsonFile(filePath) {
25
+ try {
26
+ return JSON.parse(readFileSync(filePath, "utf-8"));
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ function loadJsonFile(filePath, options) {
33
+ const mode = getCacheMode(options);
34
+ if (mode === "none") {
35
+ return readJsonFile(filePath);
36
+ }
37
+ const cached = cache.get(filePath);
38
+ if (mode === "always" || process.env.NODE_ENV === "production") {
39
+ if (cached)
40
+ return cached.value;
41
+ const value = readJsonFile(filePath);
42
+ cache.set(filePath, { value, mtimeMs: -1, size: -1, checkedAt: Date.now() });
43
+ return value;
44
+ }
45
+ const now = Date.now();
46
+ const maxAgeMs = getDevMaxAgeMs(options);
47
+ if (cached && now - cached.checkedAt < maxAgeMs) {
48
+ return cached.value;
49
+ }
50
+ try {
51
+ const stat = statSync(filePath);
52
+ if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
53
+ cached.checkedAt = now;
54
+ return cached.value;
55
+ }
56
+ const value = readJsonFile(filePath);
57
+ cache.set(filePath, {
58
+ value,
59
+ mtimeMs: stat.mtimeMs,
60
+ size: stat.size,
61
+ checkedAt: now,
62
+ });
63
+ return value;
64
+ }
65
+ catch {
66
+ cache.set(filePath, { value: null, mtimeMs: -1, size: -1, checkedAt: now });
67
+ return null;
68
+ }
69
+ }
70
+ export function clearKdfCache() {
71
+ cache.clear();
72
+ }
73
+ function loadFile(name, options) {
74
+ const filePath = join(getKdfRoot(), `${name}.json`);
75
+ return loadJsonFile(filePath, options);
76
+ }
77
+ /** Load JSON from absolute path (for root shared fallback) */
78
+ function loadFileAbsolute(filePath, options) {
79
+ return loadJsonFile(filePath, options);
80
+ }
81
+ /** Get value from nested object by dot-path */
82
+ function getByPath(obj, path) {
83
+ return path.split(".").reduce((acc, key) => {
84
+ if (acc && typeof acc === "object" && key in acc) {
85
+ return acc[key];
86
+ }
87
+ return undefined;
88
+ }, obj);
89
+ }
90
+ /**
91
+ * Resolve a single @reference: "@button.cta"
92
+ * @button.cta -> load shared/button.json -> key "cta"
93
+ */
94
+ function resolveSingleRef(ref, pageTokens, depth, options) {
95
+ if (depth > 5)
96
+ return "";
97
+ const refPart = ref.slice(1); // remove @
98
+ const dotIdx = refPart.indexOf(".");
99
+ const component = dotIdx > 0 ? refPart.slice(0, dotIdx) : refPart;
100
+ const key = dotIdx > 0 ? refPart.slice(dotIdx + 1) : "";
101
+ // Reject path-traversal in the component name: it becomes part of a file path
102
+ // (shared/<component>.json). A ref like "@../../secret.x" must never escape the
103
+ // shared/ folder. Allow only safe filename chars.
104
+ if (!/^[A-Za-z0-9_-]+$/.test(component)) {
105
+ if (process.env.NODE_ENV !== "production") {
106
+ console.warn(`[kdf] ignoring unsafe @ref component: "${component}"`);
107
+ }
108
+ return "";
109
+ }
110
+ // Load shared/<component>.json — cascade: template shared → root shared
111
+ let resolved = undefined;
112
+ // 1. Template-specific shared (e.g., designs/lander/shared/button.json)
113
+ const templateShared = loadFile(`shared/${component}`, options);
114
+ if (key && templateShared) {
115
+ resolved = getByPath(templateShared, key);
116
+ }
117
+ // 2. Root shared fallback (e.g., designs/shared/button.json)
118
+ if (resolved === undefined && key) {
119
+ const rootShared = loadFileAbsolute(join(getKdfRoot(), "..", "shared", `${component}.json`), options);
120
+ if (rootShared) {
121
+ resolved = getByPath(rootShared, key);
122
+ }
123
+ }
124
+ // 3. Fallback: check page-level tokens
125
+ if (resolved === undefined && pageTokens) {
126
+ resolved = getByPath(pageTokens, refPart);
127
+ }
128
+ if (typeof resolved === "string") {
129
+ // Resolved value might itself contain @refs
130
+ return resolveTokenString(resolved, pageTokens, depth + 1, options);
131
+ }
132
+ if (resolved && typeof resolved === "object" && "className" in resolved) {
133
+ const cn = resolved.className;
134
+ return resolveTokenString(cn, pageTokens, depth + 1, options);
135
+ }
136
+ return "";
137
+ }
138
+ /**
139
+ * Resolve a token string that may contain multiple @refs and plain classes.
140
+ * Examples:
141
+ * "@button.cta" -> resolves single ref
142
+ * "@button.cta shadow-xl" -> resolves ref + appends classes
143
+ * "@button.base @button.ghost @button.sm" -> resolves all three refs
144
+ */
145
+ function resolveTokenString(value, pageTokens, depth = 0, options) {
146
+ if (depth > 5)
147
+ return value;
148
+ const parts = value.split(/\s+/).filter(Boolean);
149
+ const resolved = parts.map((part) => {
150
+ if (part.startsWith("@")) {
151
+ return resolveSingleRef(part, pageTokens, depth, options);
152
+ }
153
+ return part;
154
+ });
155
+ return resolved.filter(Boolean).join(" ");
156
+ }
157
+ /** Resolve className for a token path.
158
+ * Looks in page JSON first. @references resolve from shared/ files. */
159
+ export function resolveClassName(path, pageTokens, options) {
160
+ const val = pageTokens
161
+ ? getByPath(pageTokens, path)
162
+ : undefined;
163
+ if (val === undefined)
164
+ return "";
165
+ if (typeof val === "string") {
166
+ return resolveTokenString(val, pageTokens, 0, options);
167
+ }
168
+ if (typeof val === "object" && val && "className" in val) {
169
+ const cn = val.className;
170
+ return resolveTokenString(cn, pageTokens, 0, options);
171
+ }
172
+ return "";
173
+ }
174
+ /** Resolve CSS custom properties for a token path */
175
+ export function resolveCSS(path, pageTokens) {
176
+ const val = pageTokens
177
+ ? getByPath(pageTokens, path)
178
+ : undefined;
179
+ if (val && typeof val === "object" && "css" in val) {
180
+ return val.css;
181
+ }
182
+ return {};
183
+ }
184
+ export { loadFile, getKdfRoot };
@@ -0,0 +1,44 @@
1
+ /** A design token value — className string, @reference, or object with component binding */
2
+ export type DesignTokenValue = string | {
3
+ /** Component binding — WHAT to render: "Button", "h1", "Image" */
4
+ $?: string;
5
+ /** CSS class string. Supports @refs: "@button.cta shadow-xl" */
6
+ className?: string;
7
+ /** CSS custom properties generated into konde.css */
8
+ css?: Record<string, string>;
9
+ /** Pass-through props for the bound component (variant, size, etc.) */
10
+ [key: string]: unknown;
11
+ };
12
+ /** A design token file — nested keys to values */
13
+ export interface DesignTokenFile {
14
+ /** Page section ordering — which sections appear and in what order */
15
+ $layout?: string[];
16
+ [key: string]: DesignTokenValue | DesignTokenGroup | string[] | undefined;
17
+ }
18
+ export interface DesignTokenGroup {
19
+ [key: string]: DesignTokenValue | DesignTokenGroup;
20
+ }
21
+ /** Resolved design accessor for a page */
22
+ export interface DesignAccessor {
23
+ /** Get className for a dot-path: d("hero.title") */
24
+ (path: string): string;
25
+ /** Get CSS custom properties for a dot-path */
26
+ css(path: string): Record<string, string>;
27
+ }
28
+ export type KdfCacheMode = "auto" | "always" | "none";
29
+ export interface GetDesignOptions {
30
+ /**
31
+ * Cache policy for design JSON files.
32
+ *
33
+ * - auto: production caches forever; development caches with mtime revalidation.
34
+ * - always: cache for the lifetime of the process until clearKdfCache() is called.
35
+ * - none: read from disk on every access.
36
+ */
37
+ cache?: KdfCacheMode;
38
+ /**
39
+ * Development revalidation window for cache="auto".
40
+ * Defaults to 250ms to avoid render-time disk-read storms while keeping
41
+ * design JSON edits responsive during local development.
42
+ */
43
+ maxAgeMs?: number;
44
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};