@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 +17 -0
- package/README.md +61 -16
- package/dist/class-compose.d.ts +35 -0
- package/dist/class-compose.js +40 -0
- package/dist/create-design.d.ts +18 -0
- package/dist/create-design.js +108 -0
- package/dist/edge.d.ts +3 -0
- package/dist/edge.js +2 -0
- package/dist/index.d.ts +3 -36
- package/dist/index.js +2 -40
- package/dist/resolver.d.ts +2 -2
- package/dist/resolver.js +14 -85
- package/dist/types.d.ts +2 -0
- package/docs/doc.md +34 -3
- package/docs/skill.md +40 -6
- package/package.json +5 -1
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
|
|
39
|
-
from disk with Node `fs`.
|
|
38
|
+
KDF has two runtime modes:
|
|
40
39
|
|
|
41
|
-
-
|
|
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()
|
|
45
|
-
|
|
46
|
-
|
|
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 |
|
|
51
|
-
| Astro | Tested |
|
|
52
|
-
| Hono | Tested |
|
|
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()
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
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
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
|
|
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;
|
package/dist/resolver.d.ts
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
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
|
|
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`
|
|
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.
|
|
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"
|