@kondeio/kdf 0.1.2 → 0.1.3

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/CHANGELOG.md CHANGED
@@ -3,6 +3,23 @@
3
3
  All notable changes to KDF are documented here. Format loosely follows
4
4
  [Keep a Changelog](https://keepachangelog.com); versions follow semver.
5
5
 
6
+ ## [0.1.3] - 2026-06-29
7
+
8
+ ### Added
9
+ - Added `createDesign(pageTokens, shared)` for imported JSON token objects. This
10
+ is the preferred API for Astro SSR, Hono, Next.js, Cloudflare Workers, and
11
+ other bundled runtimes because JSON files become build dependencies instead of
12
+ runtime filesystem paths.
13
+ - Added the pure `@kondeio/kdf/edge` export for runtimes that reject Node
14
+ built-ins entirely.
15
+ - Added `DesignSharedTokens` type for shared token maps such as
16
+ `{ button: buttonTokens }`.
17
+
18
+ ### Changed
19
+ - Documentation now distinguishes the Node file API (`getDesign(page)`) from the
20
+ imported JSON API (`createDesign(tokens, shared)`), including Cloudflare
21
+ Workers guidance.
22
+
6
23
  ## [0.1.2] - 2026-06-22
7
24
 
8
25
  ### Changed
package/README.md CHANGED
@@ -35,21 +35,28 @@ not a CSS engine.
35
35
 
36
36
  ## Runtime Support
37
37
 
38
- KDF core runs in Node/server-side JavaScript environments because it reads JSON
39
- from disk with Node `fs`.
38
+ KDF has two runtime modes:
40
39
 
41
- - Works with server-rendered Next.js, Astro, Hono, or similar Node runtimes.
40
+ - `createDesign(tokens, shared)` uses JSON objects imported by the host app.
41
+ This is the preferred mode for Astro, Next.js, Hono, Cloudflare Workers, and
42
+ other edge/serverless runtimes because bundlers can see and include the JSON.
43
+ - `getDesign(page)` reads `kdf/<page>.json` from disk with Node `fs`. This is
44
+ convenient for Node/server apps and local tooling, but it is not the right
45
+ API for edge runtimes that do not have your local filesystem.
46
+
47
+ - Works with server-rendered Next.js, Astro, Hono, Cloudflare Workers, or similar runtimes.
42
48
  - The included `@kondeio/kdf/plugin` export is the official Next.js integration.
43
49
  - Next.js plugin target: App Router, Next.js 14+ (`next >=14`).
44
- - `getDesign()`, `d()`, and `d.css()` are server-only. Browser/client
45
- components cannot call them directly; resolve classes server-side and pass
46
- class names down when needed.
50
+ - `getDesign()` is server-only. Browser/client components should use resolved
51
+ class strings from a server boundary or use imported JSON with `createDesign()`
52
+ when the bundler/runtime supports it.
47
53
 
48
54
  | Framework | Status | How KDF is used |
49
55
  | --- | --- | --- |
50
- | Next.js | Tested | Core API in server-rendered code, plus the official `@kondeio/kdf/plugin` integration. |
51
- | Astro | Tested | Core API in server-rendered code. |
52
- | Hono | Tested | Core API in server handlers. |
56
+ | Next.js | Tested | `createDesign()` with imported JSON, or `getDesign()` in Node/server-rendered code plus the optional plugin. |
57
+ | Astro | Tested | `createDesign()` with imported JSON for SSR/edge builds. |
58
+ | Hono | Tested | `createDesign()` with imported JSON in handlers, or `getDesign()` in Node handlers. |
59
+ | Cloudflare Workers | Supported | `createDesign()` with imported JSON. Do not pass local absolute file paths. |
53
60
 
54
61
  ## Install
55
62
 
@@ -172,6 +179,41 @@ Example token:
172
179
 
173
180
  ## Usage
174
181
 
182
+ ### Imported JSON API
183
+
184
+ Use `createDesign()` when the app is bundled for Astro, Next.js, Hono, or
185
+ Cloudflare Workers:
186
+
187
+ ```tsx
188
+ import { createDesign } from "@kondeio/kdf";
189
+ import homepageTokens from "../kdf/homepage.json";
190
+ import buttonTokens from "../kdf/shared/button.json";
191
+ import typographyTokens from "../kdf/shared/typography.json";
192
+
193
+ const d = createDesign(homepageTokens, {
194
+ button: buttonTokens,
195
+ typography: typographyTokens,
196
+ });
197
+
198
+ <h1 data-kdf="hero.title" className={d("hero.title")}>
199
+ {t("hero.headline")}
200
+ </h1>
201
+ ```
202
+
203
+ This makes the JSON a normal build dependency. The bundler includes it in the
204
+ output instead of KDF trying to read a machine-local path at runtime.
205
+
206
+ If a runtime or bundler rejects Node built-ins entirely, import the pure entry:
207
+
208
+ ```ts
209
+ import { createDesign } from "@kondeio/kdf/edge";
210
+ ```
211
+
212
+ ### File API
213
+
214
+ Use `getDesign()` in Node/server environments where reading from `kdf/*.json` at
215
+ runtime is intentional:
216
+
175
217
  ```tsx
176
218
  import { getDesign } from "@kondeio/kdf";
177
219
 
@@ -235,11 +277,11 @@ const cn = createClassComposer({
235
277
 
236
278
  ## Server-only
237
279
 
238
- `getDesign()`, `d()`, and `d.css()` read JSON from disk via Node `fs`, so they
239
- run on the **server only**: Next.js Server Components, Astro server rendering,
240
- Hono handlers, or equivalent Node/server-rendered code. They do **not** work
241
- inside browser-only code or a Next.js Client Component (`"use client"`), which
242
- has no filesystem.
280
+ `getDesign()` reads JSON from disk via Node `fs`, so it runs on the **server
281
+ only**: Next.js Server Components, Astro Node server rendering, Hono Node
282
+ handlers, or equivalent Node/server-rendered code. It does **not** work inside
283
+ browser-only code or a Next.js Client Component (`"use client"`), which has no
284
+ filesystem.
243
285
 
244
286
  For client components, resolve on the server and pass the resulting className
245
287
  string down as a prop:
@@ -250,6 +292,9 @@ const d = getDesign("homepage");
250
292
  return <ClientThing className={d("hero.cta")} />;
251
293
  ```
252
294
 
295
+ For edge/serverless deploys, prefer `createDesign(importedJson, shared)` so the
296
+ tokens are bundled as code/data instead of looked up from a runtime path.
297
+
253
298
  ## References
254
299
 
255
300
  Reference shared tokens from shared/:
@@ -283,8 +328,8 @@ Playwright checks.
283
328
 
284
329
  ## Cache Behavior
285
330
 
286
- KDF caches design JSON files by default. In development it revalidates with
287
- file `mtime`/`size` checks so repeated `d()` calls do not create disk-read
331
+ `getDesign()` caches design JSON files by default. In development it revalidates
332
+ with file `mtime`/`size` checks so repeated `d()` calls do not create disk-read
288
333
  storms during HMR.
289
334
 
290
335
  ```tsx
@@ -0,0 +1,35 @@
1
+ import { type ClassValue } from "clsx";
2
+ export type ClassMergeFunction = (className: string) => string;
3
+ export interface ClassComposerOptions {
4
+ /**
5
+ * Optional app-defined semantic merge step.
6
+ *
7
+ * KDF's default merge only removes exact duplicate classes. If an app wants
8
+ * semantic rules such as "keep the last class in this project-specific group",
9
+ * inject that logic here.
10
+ */
11
+ merge?: ClassMergeFunction;
12
+ }
13
+ /**
14
+ * Remove exact duplicate class names while preserving first-seen order.
15
+ *
16
+ * This is intentionally semantic-free: it does not try to understand any CSS
17
+ * framework. It only turns "btn btn btn-primary" into "btn btn-primary".
18
+ */
19
+ export declare function dedupeClasses(className: string): string;
20
+ /**
21
+ * Create a UI-library agnostic class composer.
22
+ *
23
+ * Default behavior:
24
+ * - flatten strings, arrays, and objects through clsx
25
+ * - drop falsy values
26
+ * - normalize whitespace
27
+ * - remove exact duplicate class names
28
+ *
29
+ * Apps that need semantic class conflict handling can inject their own merge
30
+ * function. KDF does not ship CSS-framework-specific class rules.
31
+ */
32
+ export declare function createClassComposer(options?: ClassComposerOptions): (...inputs: ClassValue[]) => string;
33
+ export declare const composeClasses: (...inputs: ClassValue[]) => string;
34
+ export declare const cx: (...inputs: ClassValue[]) => string;
35
+ export declare const cn: (...inputs: ClassValue[]) => string;
@@ -0,0 +1,40 @@
1
+ import { clsx } from "clsx";
2
+ /**
3
+ * Remove exact duplicate class names while preserving first-seen order.
4
+ *
5
+ * This is intentionally semantic-free: it does not try to understand any CSS
6
+ * framework. It only turns "btn btn btn-primary" into "btn btn-primary".
7
+ */
8
+ export function dedupeClasses(className) {
9
+ const seen = new Set();
10
+ const output = [];
11
+ for (const part of className.trim().split(/\s+/)) {
12
+ if (!part || seen.has(part))
13
+ continue;
14
+ seen.add(part);
15
+ output.push(part);
16
+ }
17
+ return output.join(" ");
18
+ }
19
+ /**
20
+ * Create a UI-library agnostic class composer.
21
+ *
22
+ * Default behavior:
23
+ * - flatten strings, arrays, and objects through clsx
24
+ * - drop falsy values
25
+ * - normalize whitespace
26
+ * - remove exact duplicate class names
27
+ *
28
+ * Apps that need semantic class conflict handling can inject their own merge
29
+ * function. KDF does not ship CSS-framework-specific class rules.
30
+ */
31
+ export function createClassComposer(options = {}) {
32
+ const merge = options.merge ?? dedupeClasses;
33
+ return (...inputs) => {
34
+ const joined = clsx(...inputs);
35
+ return merge(joined);
36
+ };
37
+ }
38
+ export const composeClasses = createClassComposer();
39
+ export const cx = composeClasses;
40
+ export const cn = composeClasses;
@@ -0,0 +1,18 @@
1
+ import type { DesignAccessor, DesignSharedTokens, DesignTokenFile } from "./types.js";
2
+ interface ResolveRuntimeOptions {
3
+ shared?: DesignSharedTokens;
4
+ resolveRef?: (component: string, key: string) => unknown;
5
+ }
6
+ /** Resolve className for a token path. */
7
+ export declare function resolveClassName(path: string, pageTokens: DesignTokenFile | null, options?: ResolveRuntimeOptions): string;
8
+ /** Resolve CSS custom properties for a token path */
9
+ export declare function resolveCSS(path: string, pageTokens: DesignTokenFile | null): Record<string, string>;
10
+ /**
11
+ * Create a design accessor from imported JSON token objects.
12
+ *
13
+ * This is the preferred API for edge runtimes and build-time bundlers because
14
+ * the host app imports JSON explicitly instead of asking KDF to read a path at
15
+ * runtime.
16
+ */
17
+ export declare function createDesign(pageTokens: DesignTokenFile | null, shared?: DesignSharedTokens): DesignAccessor;
18
+ export {};
@@ -0,0 +1,108 @@
1
+ /** Get value from nested object by dot-path */
2
+ function getByPath(obj, path) {
3
+ return path.split(".").reduce((acc, key) => {
4
+ if (acc && typeof acc === "object" && key in acc) {
5
+ return acc[key];
6
+ }
7
+ return undefined;
8
+ }, obj);
9
+ }
10
+ /**
11
+ * Resolve a single @reference: "@button.cta"
12
+ * @button.cta -> shared.button.cta, external resolver, or page-level fallback.
13
+ */
14
+ function resolveSingleRef(ref, pageTokens, depth, options) {
15
+ if (depth > 5)
16
+ return "";
17
+ const refPart = ref.slice(1); // remove @
18
+ const dotIdx = refPart.indexOf(".");
19
+ const component = dotIdx > 0 ? refPart.slice(0, dotIdx) : refPart;
20
+ const key = dotIdx > 0 ? refPart.slice(dotIdx + 1) : "";
21
+ // Reject path-traversal when a file-backed resolver is used.
22
+ if (!/^[A-Za-z0-9_-]+$/.test(component)) {
23
+ if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
24
+ console.warn(`[kdf] ignoring unsafe @ref component: "${component}"`);
25
+ }
26
+ return "";
27
+ }
28
+ let resolved = undefined;
29
+ const sharedToken = options?.shared?.[component];
30
+ if (key && sharedToken) {
31
+ resolved = getByPath(sharedToken, key);
32
+ }
33
+ if (resolved === undefined && key && options?.resolveRef) {
34
+ resolved = options.resolveRef(component, key);
35
+ }
36
+ if (resolved === undefined && pageTokens) {
37
+ resolved = getByPath(pageTokens, refPart);
38
+ }
39
+ if (typeof resolved === "string") {
40
+ return resolveTokenString(resolved, pageTokens, depth + 1, options);
41
+ }
42
+ if (resolved && typeof resolved === "object" && "className" in resolved) {
43
+ const className = resolved.className;
44
+ return resolveTokenString(className, pageTokens, depth + 1, options);
45
+ }
46
+ return "";
47
+ }
48
+ /**
49
+ * Resolve a token string that may contain multiple @refs and plain classes.
50
+ * Examples:
51
+ * "@button.cta" -> resolves single ref
52
+ * "@button.cta shadow-xl" -> resolves ref + extra classes
53
+ * "@button.base @button.ghost @button.sm" -> resolves all refs
54
+ */
55
+ function resolveTokenString(value, pageTokens, depth = 0, options) {
56
+ if (depth > 5)
57
+ return value;
58
+ const parts = value.split(/\s+/).filter(Boolean);
59
+ const resolved = parts.map((part) => {
60
+ if (part.startsWith("@")) {
61
+ return resolveSingleRef(part, pageTokens, depth, options);
62
+ }
63
+ return part;
64
+ });
65
+ return resolved.filter(Boolean).join(" ");
66
+ }
67
+ /** Resolve className for a token path. */
68
+ export function resolveClassName(path, pageTokens, options) {
69
+ const val = pageTokens
70
+ ? getByPath(pageTokens, path)
71
+ : undefined;
72
+ if (val === undefined)
73
+ return "";
74
+ if (typeof val === "string") {
75
+ return resolveTokenString(val, pageTokens, 0, options);
76
+ }
77
+ if (typeof val === "object" && val && "className" in val) {
78
+ const className = val.className;
79
+ return resolveTokenString(className, pageTokens, 0, options);
80
+ }
81
+ return "";
82
+ }
83
+ /** Resolve CSS custom properties for a token path */
84
+ export function resolveCSS(path, pageTokens) {
85
+ const val = pageTokens
86
+ ? getByPath(pageTokens, path)
87
+ : undefined;
88
+ if (val && typeof val === "object" && "css" in val) {
89
+ return val.css;
90
+ }
91
+ return {};
92
+ }
93
+ /**
94
+ * Create a design accessor from imported JSON token objects.
95
+ *
96
+ * This is the preferred API for edge runtimes and build-time bundlers because
97
+ * the host app imports JSON explicitly instead of asking KDF to read a path at
98
+ * runtime.
99
+ */
100
+ export function createDesign(pageTokens, shared = {}) {
101
+ const accessor = ((path) => {
102
+ return resolveClassName(path, pageTokens, { shared });
103
+ });
104
+ accessor.css = (path) => {
105
+ return resolveCSS(path, pageTokens);
106
+ };
107
+ return accessor;
108
+ }
package/dist/edge.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { cn, composeClasses, createClassComposer, cx, dedupeClasses, type ClassComposerOptions, type ClassMergeFunction, } from "./class-compose.js";
2
+ export { createDesign } from "./create-design.js";
3
+ export type { DesignAccessor, DesignSharedTokens, DesignTokenFile, DesignTokenValue, } from "./types.js";
package/dist/edge.js ADDED
@@ -0,0 +1,2 @@
1
+ export { cn, composeClasses, createClassComposer, cx, dedupeClasses, } from "./class-compose.js";
2
+ export { createDesign } from "./create-design.js";
package/dist/index.d.ts CHANGED
@@ -1,18 +1,8 @@
1
- import { type ClassValue } from "clsx";
2
1
  import type { DesignAccessor, GetDesignOptions } from "./types.js";
3
- export type { DesignAccessor, DesignTokenFile, DesignTokenValue, GetDesignOptions, KdfCacheMode, } from "./types.js";
2
+ export { cn, composeClasses, createClassComposer, cx, dedupeClasses, type ClassComposerOptions, type ClassMergeFunction, } from "./class-compose.js";
3
+ export { createDesign } from "./create-design.js";
4
+ export type { DesignAccessor, DesignSharedTokens, DesignTokenFile, DesignTokenValue, GetDesignOptions, KdfCacheMode, } from "./types.js";
4
5
  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
6
  /**
17
7
  * Get design accessor for a page.
18
8
  *
@@ -23,26 +13,3 @@ export interface ClassComposerOptions {
23
13
  * Resolution order: kdf/<page>.json -> kdf/shared/
24
14
  */
25
15
  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 CHANGED
@@ -1,5 +1,6 @@
1
- import { clsx } from "clsx";
2
1
  import { loadFile, resolveClassName, resolveCSS } from "./resolver.js";
2
+ export { cn, composeClasses, createClassComposer, cx, dedupeClasses, } from "./class-compose.js";
3
+ export { createDesign } from "./create-design.js";
3
4
  export { clearKdfCache } from "./resolver.js";
4
5
  /**
5
6
  * Get design accessor for a page.
@@ -21,42 +22,3 @@ export function getDesign(page, options) {
21
22
  };
22
23
  return accessor;
23
24
  }
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;
@@ -1,4 +1,5 @@
1
1
  import type { DesignTokenFile, GetDesignOptions } from "./types.js";
2
+ import { resolveCSS } from "./create-design.js";
2
3
  /** Returns KDF root path — reads env at call time, not module load time */
3
4
  declare function getKdfRoot(): string;
4
5
  export declare function clearKdfCache(): void;
@@ -6,6 +7,5 @@ declare function loadFile(name: string, options?: GetDesignOptions): DesignToken
6
7
  /** Resolve className for a token path.
7
8
  * Looks in page JSON first. @references resolve from shared/ files. */
8
9
  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
10
  export { loadFile, getKdfRoot };
11
+ export { resolveCSS };
package/dist/resolver.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, statSync } from "fs";
2
2
  import { isAbsolute, join } from "path";
3
+ import { resolveClassName as resolveClassNameFromTokens, resolveCSS, } from "./create-design.js";
3
4
  /** Returns KDF root path — reads env at call time, not module load time */
4
5
  function getKdfRoot() {
5
6
  const dir = process.env.KDF_DIR || "kdf";
@@ -78,7 +79,6 @@ function loadFile(name, options) {
78
79
  function loadFileAbsolute(filePath, options) {
79
80
  return loadJsonFile(filePath, options);
80
81
  }
81
- /** Get value from nested object by dot-path */
82
82
  function getByPath(obj, path) {
83
83
  return path.split(".").reduce((acc, key) => {
84
84
  if (acc && typeof acc === "object" && key in acc) {
@@ -87,98 +87,27 @@ function getByPath(obj, path) {
87
87
  return undefined;
88
88
  }, obj);
89
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;
90
+ function resolveFileRef(component, key, options) {
112
91
  // 1. Template-specific shared (e.g., designs/lander/shared/button.json)
113
92
  const templateShared = loadFile(`shared/${component}`, options);
114
- if (key && templateShared) {
115
- resolved = getByPath(templateShared, key);
93
+ if (templateShared) {
94
+ const resolved = getByPath(templateShared, key);
95
+ if (resolved !== undefined)
96
+ return resolved;
116
97
  }
117
98
  // 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);
99
+ const rootShared = loadFileAbsolute(join(getKdfRoot(), "..", "shared", `${component}.json`), options);
100
+ if (rootShared) {
101
+ return getByPath(rootShared, key);
127
102
  }
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(" ");
103
+ return undefined;
156
104
  }
157
105
  /** Resolve className for a token path.
158
106
  * Looks in page JSON first. @references resolve from shared/ files. */
159
107
  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 {};
108
+ return resolveClassNameFromTokens(path, pageTokens, {
109
+ resolveRef: (component, key) => resolveFileRef(component, key, options),
110
+ });
183
111
  }
184
112
  export { loadFile, getKdfRoot };
113
+ export { resolveCSS };
package/dist/types.d.ts CHANGED
@@ -18,6 +18,8 @@ export interface DesignTokenFile {
18
18
  export interface DesignTokenGroup {
19
19
  [key: string]: DesignTokenValue | DesignTokenGroup;
20
20
  }
21
+ /** Shared design token objects keyed by @ref component name, e.g. { button } */
22
+ export type DesignSharedTokens = Record<string, DesignTokenFile | null | undefined>;
21
23
  /** Resolved design accessor for a page */
22
24
  export interface DesignAccessor {
23
25
  /** Get className for a dot-path: d("hero.title") */
package/docs/doc.md CHANGED
@@ -466,7 +466,37 @@ export default {
466
466
 
467
467
  ## Runtime API
468
468
 
469
- KDF core runs in Node/server-side JavaScript because it reads JSON from disk.
469
+ KDF has two runtime APIs.
470
+
471
+ ### Imported JSON / Edge-Safe API
472
+
473
+ Use `createDesign()` when the host app is built by Vite, Astro, Next.js, Hono,
474
+ or Cloudflare Workers:
475
+
476
+ ```ts
477
+ import { createDesign, cn } from "@kondeio/kdf";
478
+ import homepageTokens from "../kdf/homepage.json";
479
+ import buttonTokens from "../kdf/shared/button.json";
480
+ import typographyTokens from "../kdf/shared/typography.json";
481
+
482
+ const d = createDesign(homepageTokens, {
483
+ button: buttonTokens,
484
+ typography: typographyTokens,
485
+ });
486
+ ```
487
+
488
+ This API does not read design JSON from a path. The app imports JSON explicitly,
489
+ so the bundler can include the token files in the deployment artifact.
490
+
491
+ For runtimes that reject Node built-ins completely, use the pure subpath:
492
+
493
+ ```ts
494
+ import { createDesign, cn } from "@kondeio/kdf/edge";
495
+ ```
496
+
497
+ ### Node File API
498
+
499
+ Use `getDesign()` when runtime filesystem reads are intentional:
470
500
 
471
501
  ```ts
472
502
  import { getDesign, cn, clearKdfCache } from "@kondeio/kdf";
@@ -481,11 +511,12 @@ d("hero.title"); // resolved className string
481
511
  d.css("hero.title"); // CSS custom properties object
482
512
  ```
483
513
 
484
- Server-only rule:
514
+ Server-only rule for `getDesign()`:
485
515
 
486
- - use `getDesign()` in server-rendered code
516
+ - use `getDesign()` in Node/server-rendered code
487
517
  - do not call it directly inside browser-only Client Components
488
518
  - resolve classes server-side and pass strings down when needed
519
+ - use `createDesign(importedJson, shared)` for edge/serverless builds
489
520
 
490
521
  Cache options:
491
522
 
package/docs/skill.md CHANGED
@@ -3,8 +3,8 @@
3
3
  Use this skill whenever you build, review, or modify UI that uses Konde Design Framework.
4
4
 
5
5
  Package: `@kondeio/kdf`
6
- Runtime target: Node/server-side JavaScript. Tested with Next.js, Astro, and Hono.
7
- Primary API: `getDesign`, `cn`, `cx`, `composeClasses`, `dedupeClasses`, `createClassComposer`, `clearKdfCache`
6
+ Runtime target: Node/server-side JavaScript plus edge/serverless builds with imported JSON. Tested with Next.js, Astro, Hono, and Cloudflare Workers.
7
+ Primary API: `createDesign`, `getDesign`, `cn`, `cx`, `composeClasses`, `dedupeClasses`, `createClassComposer`, `clearKdfCache`
8
8
  Required convention: every `d()` usage must have matching `data-kdf`
9
9
 
10
10
  ## Goal
@@ -50,7 +50,26 @@ kdf/shared/*.json
50
50
 
51
51
  2. Locate the relevant UI component or page.
52
52
 
53
- 3. Confirm package import:
53
+ 3. Confirm package import.
54
+
55
+ For bundled SSR/edge runtimes such as Astro SSR or Cloudflare Workers, prefer
56
+ imported JSON:
57
+
58
+ ```ts
59
+ import { createDesign, cn } from "@kondeio/kdf";
60
+ import pageTokens from "../kdf/homepage.json";
61
+ import buttonTokens from "../kdf/shared/button.json";
62
+
63
+ const d = createDesign(pageTokens, { button: buttonTokens });
64
+ ```
65
+
66
+ If the runtime rejects Node built-ins entirely, use:
67
+
68
+ ```ts
69
+ import { createDesign, cn } from "@kondeio/kdf/edge";
70
+ ```
71
+
72
+ For Node/server-only apps that intentionally read files at runtime:
54
73
 
55
74
  ```ts
56
75
  import { getDesign, cn } from "@kondeio/kdf";
@@ -62,6 +81,12 @@ import { getDesign, cn } from "@kondeio/kdf";
62
81
  const d = getDesign("homepage");
63
82
  ```
64
83
 
84
+ or:
85
+
86
+ ```ts
87
+ const d = createDesign(pageTokens, { button: buttonTokens });
88
+ ```
89
+
65
90
  5. Confirm the styling framework scans KDF JSON if it generates CSS by scanning source files:
66
91
 
67
92
  ```css
@@ -72,6 +97,15 @@ or equivalent framework/source scanning config.
72
97
 
73
98
  ## Non-negotiable Rules
74
99
 
100
+ ### 0. Pick the Right Runtime API
101
+
102
+ Use `createDesign(importedJson, shared)` for Astro SSR, Cloudflare Workers, and
103
+ other bundled edge/serverless deployments. Do not pass local absolute paths such
104
+ as `/Volumes/.../designs/homepage.json` into deployed code.
105
+
106
+ Use `getDesign(page)` only when runtime filesystem reads from `kdf/*.json` are
107
+ intentional and the target runtime is Node/server.
108
+
75
109
  ### 1. Do Not Guess Design
76
110
 
77
111
  If a class belongs to the design system, put it in JSON and read it with `d()`.
@@ -325,8 +359,8 @@ clearKdfCache();
325
359
 
326
360
  Use this checklist before finishing KDF-related UI work.
327
361
 
328
- - [ ] Page imports `getDesign` from `@kondeio/kdf`.
329
- - [ ] Page creates one accessor per design page, for example `getDesign("homepage")`.
362
+ - [ ] Page imports the correct runtime API: `createDesign` for imported JSON/edge, `getDesign` for Node file mode.
363
+ - [ ] Page creates one accessor per design page, for example `createDesign(pageTokens, sharedTokens)` or `getDesign("homepage")`.
330
364
  - [ ] Every `d()` usage has matching `data-kdf`.
331
365
  - [ ] Shared repeated styles live in `kdf/shared`.
332
366
  - [ ] One-off page styles live in `kdf/<page>.json`.
@@ -347,7 +381,7 @@ Use this when reviewing code written by another agent.
347
381
  - [ ] `data-kdf` values match the token paths exactly.
348
382
  - [ ] No design drift through arbitrary inline styling classes.
349
383
  - [ ] KDF JSON remains valid JSON.
350
- - [ ] Shared refs point to existing shared token files.
384
+ - [ ] Shared refs point to existing shared token files or imported shared token objects.
351
385
  - [ ] `@` refs are not used for business logic.
352
386
  - [ ] No non-English or informal comments are introduced into user-facing app source.
353
387
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kondeio/kdf",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Agent-first design consistency for Node-powered web apps. KDF gives AI agents a JSON source of truth for consistent UI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,6 +14,10 @@
14
14
  "import": "./dist/index.js",
15
15
  "types": "./dist/index.d.ts"
16
16
  },
17
+ "./edge": {
18
+ "import": "./dist/edge.js",
19
+ "types": "./dist/edge.d.ts"
20
+ },
17
21
  "./plugin": {
18
22
  "import": "./dist/plugin.js",
19
23
  "types": "./dist/plugin.d.ts"