@apollion-dsi/tokens 4.0.0 → 4.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.
package/README.md CHANGED
@@ -19,6 +19,15 @@ import { TOKENS_RUNTIME_API_VERSION } from '@apollion-dsi/tokens/runtime/v1';
19
19
  - **In:** OKLch palette derivation (S2/S4), Dimension axis (S3), Foundation/Surface builders (S4), build-time CSS/JSON/TS variant emission (S5), Config-First loader (S6).
20
20
  - **Out:** Runtime React components (lives in `@apollion-dsi/core`), framework adapters (Vue/Flutter consumers consume `dist/json/*.json` directly).
21
21
 
22
+ ## DTCG output (`dist/json/*.json`)
23
+
24
+ Emitted per [Design Tokens Format Module 2025.10](https://www.designtokens.org/TR/drafts/format/) — values are **structured objects**, not shorthand strings:
25
+
26
+ - `color` → `{ "colorSpace": "oklch", "components": [L, C, H], "hex": "#rrggbb" }` — OKLch working space (ADR-006 E1) + sRGB `hex` fallback. `alpha` emitted only when below 1.
27
+ - `dimension` → `{ "value": <number>, "unit": "rem" }`.
28
+
29
+ `dist/css/*.css` and `dist/ts/*.d.ts` stay string-based (hex / `rem`) — JSON is the spec-interop surface; CSS/TS are the resolved consumer surfaces.
30
+
22
31
  ## Coupling to `@apollion-dsi/core`
23
32
 
24
33
  Hard rule per ADR-006 §3.1:
@@ -29,15 +38,15 @@ Hard rule per ADR-006 §3.1:
29
38
 
30
39
  ## Slicing (ADR-006 §6)
31
40
 
32
- | Slice | Status | Deliverable |
33
- |---|---|---|
34
- | S1 — scaffold + versioned API | ✅ this commit | `runtime/v1` namespace, esbuild ESM/CJS dual emit, smoke test |
35
- | S2 — culori vendor + OKLch lerp (in core) | pending | spike `spike/culori-oklch-lerp-parity` validated 0 regressions |
36
- | S3 — Dimension axis | pending | `compact/normal/spacious` multipliers |
37
- | S4 — builders duplicated here via Strangler Fig | pending | parity tests vs core |
38
- | S5 — atomic+idempotent build CLI | pending | `apollion-tokens build` + `dist/manifest.json` |
39
- | S6 — Config-First + sandboxed loader | pending | `node:vm` + zod schema |
40
- | S7 — release `4.0.0` | pending | first major + lockstep changesets |
41
+ | Slice | Status | Deliverable |
42
+ | ----------------------------------------------- | -------------- | -------------------------------------------------------------- |
43
+ | S1 — scaffold + versioned API | ✅ this commit | `runtime/v1` namespace, esbuild ESM/CJS dual emit, smoke test |
44
+ | S2 — culori vendor + OKLch lerp (in core) | pending | spike `spike/culori-oklch-lerp-parity` validated 0 regressions |
45
+ | S3 — Dimension axis | pending | `compact/normal/spacious` multipliers |
46
+ | S4 — builders duplicated here via Strangler Fig | pending | parity tests vs core |
47
+ | S5 — atomic+idempotent build CLI | pending | `apollion-tokens build` + `dist/manifest.json` |
48
+ | S6 — Config-First + sandboxed loader | pending | `node:vm` + zod schema |
49
+ | S7 — release `4.0.0` | pending | first major + lockstep changesets |
41
50
 
42
51
  ## Refs
43
52
 
package/lib/build.cjs CHANGED
@@ -1,6 +1,5 @@
1
- "use strict";var k=Object.defineProperty;var q=Object.getOwnPropertyDescriptor;var K=Object.getOwnPropertyNames;var Q=Object.prototype.hasOwnProperty;var s=(o,e)=>k(o,"name",{value:e,configurable:!0});var W=(o,e)=>{for(var n in e)k(o,n,{get:e[n],enumerable:!0})},X=(o,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let t of K(e))!Q.call(o,t)&&t!==n&&k(o,t,{get:()=>e[t],enumerable:!(r=q(e,t))||r.enumerable});return o};var Y=o=>X(k({},"__esModule",{value:!0}),o);var lo={};W(lo,{build:()=>co,check:()=>po});module.exports=Y(lo);var c=require("node:fs/promises"),Z=require("node:os"),y=require("node:path");var oo="/* Generated by apollion-tokens build \u2014 DO NOT EDIT. See apollion.config.mjs. */";function I(o,e){let n=[oo,""];n.push(`/* Variant: brand=${e.brand} mode=${e.mode} surface=${e.surface} dimension=${e.dimension} */`,"",":root {");for(let[r,t]of Object.entries(o.bg))n.push(` --apollion-bg-${r}: ${t};`);for(let[r,t]of Object.entries(o.text)){let i=r.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase();n.push(` --apollion-text-${i}: ${t};`)}for(let[r,t]of Object.entries(o.spacing))n.push(` --apollion-spacing-${r}: ${t};`);n.push("}",""),n.push("@supports (background: paint(squircle)) or (color: oklch(0% 0 0)) {");for(let[r,t]of Object.entries(o.bg))n.push(` @property --apollion-bg-${r} { syntax: '<color>'; inherits: true; initial-value: ${t}; }`);for(let r of Object.keys(o.text)){let t=r.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase(),i=o.text[r];n.push(` @property --apollion-text-${t} { syntax: '<color>'; inherits: true; initial-value: ${i}; }`)}return n.push("}",""),n.join(`
2
- `)}s(I,"renderCss");function A(o){let e={};for(let[n,r]of Object.entries(o)){let t=n.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase();e[t]={$value:r,$type:"color"}}return e}s(A,"toColorGroup");function eo(o){let e={};for(let[n,r]of Object.entries(o))e[n]={$value:r,$type:"dimension"};return e}s(eo,"toDimensionGroup");function F(o,e){let n={$description:`Apollion DS tokens \u2014 DTCG v1. Variant: ${e.brand}/${e.mode}/${e.surface}/${e.dimension}.`,bg:A(o.bg),text:A(o.text),spacing:eo(o.spacing),$extensions:{"com.apollion.variant":{brand:e.brand,mode:e.mode,surface:e.surface,dimension:e.dimension}}};return`${JSON.stringify(n,null,2)}
3
- `}s(F,"renderJson");var no="// Generated by apollion-tokens build \u2014 DO NOT EDIT. See apollion.config.mjs.";function S(o,e){let n=" ".repeat(e);return Object.entries(o).map(([t,i])=>`${n}${JSON.stringify(t)}: ${JSON.stringify(i)},`).join(`
4
- `)}s(S,"serializeRecord");function j(o,e){return[no,`// Variant: brand=${e.brand} mode=${e.mode} surface=${e.surface} dimension=${e.dimension}`,"","export const tokens = {"," bg: {",S(o.bg,4)," },"," text: {",S(o.text,4)," },"," spacing: {",S(o.spacing,4)," },","} as const;","","export type Tokens = typeof tokens;",""].join(`
5
- `)}s(j,"renderTs");var to=["light","dark"],ro=["positive","negative"],io=["compact","normal","spacious"];function v(o){let e=o.modes??to,n=o.surfaces??ro,r=o.dimensions??io,t=Object.entries(o.brands).sort(([p],[l])=>p.localeCompare(l)),i=[];for(let[p,l]of t)for(let m of e)for(let $ of n)for(let x of r)i.push({brand:p,colors:l,mode:m,surface:$,dimension:x});return i}s(v,"expandVariants");function L(o){return`${o.brand}.${o.mode}.${o.surface}.${o.dimension}`}s(L,"variantName");var R=require("node:crypto");function b(o){return(0,R.createHash)("sha256").update(o).digest("hex")}s(b,"sha256");function T(o){return b(w(o))}s(T,"hashConfig");function w(o){return o===null||typeof o!="object"?JSON.stringify(o):Array.isArray(o)?`[${o.map(w).join(",")}]`:`{${Object.keys(o).sort().map(r=>`${JSON.stringify(r)}:${w(o[r])}`).join(",")}}`}s(w,"stableStringify");function M(o){try{let{execSync:e}=require("node:child_process");return e("git rev-parse --short HEAD",{cwd:o,stdio:["ignore","pipe","ignore"],encoding:"utf8"}).trim()}catch{return""}}s(M,"detectGitCommit");function N(){return{node:process.version,platform:`${process.platform}-${process.arch}`}}s(N,"buildEnv");function V(o){let e=[...o.files].sort((r,t)=>r.path.localeCompare(t.path)),n={configHash:o.configHash,files:e,gitCommit:o.gitCommit,buildEnv:o.buildEnv};return`${JSON.stringify(n,null,2)}
6
- `}s(V,"serializeManifest");var O=require("@apollion-dsi/core/themes/foundation"),z=require("@apollion-dsi/core/themes/semantic"),P=require("@apollion-dsi/core/themes/spacing"),_=require("@apollion-dsi/core/themes/surface");var g=require("culori");var Do=(0,g.converter)("oklch");function a(o,e,n){let r=(0,g.parse)(o),t=(0,g.parse)(e);if(!r||!t)return o;let i=(0,g.interpolate)([r,t],"oklch");return(0,g.formatHex)(i(n))??o}s(a,"mixOklch");function so(o){return o?typeof o=="string"?o:o.base:"#000"}s(so,"coerceToHex");function u(o,e){let n=so(o),r=e.deepDark,t=e.deepLight;return{base:n,dark:a(n,r,.6),action:a(n,r,.2),active:a(n,t,.6),light:a(n,t,.9)}}s(u,"mountSingleColor");function G(o){let e=o.deepLight,n=o.deepDark;return{0:e,5:a(e,n,.05),10:a(e,n,.1),20:a(e,n,.2),30:a(e,n,.3),40:a(e,n,.4),50:a(e,n,.5),60:a(e,n,.6),70:a(e,n,.7),80:a(e,n,.8),90:a(e,n,.9),100:a(e,n,1)}}s(G,"mountGreyscaleColors");function B(o){let e=o.deepLight,n=o.deepDark,r=o.primary,t=s(p=>1/17*(p-1),"interpolation"),i=a(r,n,.5);return{0:a(i,e,t(18)),5:a(i,e,t(17.5)),10:a(i,e,t(17)),20:a(i,e,t(16)),30:a(i,e,t(15)),40:a(i,e,t(14)),50:a(i,e,t(13)),60:a(i,e,t(12)),70:a(i,e,t(11)),80:a(i,e,t(10)),90:a(i,e,t(9)),100:a(i,e,t(8)),110:a(i,e,t(7)),120:a(i,e,t(6)),130:a(i,e,t(5)),140:a(i,e,t(4)),150:a(i,e,t(3)),160:a(i,e,t(2)),170:i,180:a(i,n,.25)}}s(B,"mountNeutralColors");var H=require("@apollion-dsi/core/themes/dimension");function J({multiplier:o=.25,alias:e},n){let r=n!==void 0?H.DIMENSION_MULTIPLIERS[n]:o;return(...t)=>t.map(i=>{let p=typeof i=="string"?e[i]:i;return`${r*p}rem`}).join(" ")}s(J,"createSpacing");function ao(o){return{baseDark:o.baseDark,baseLight:o.baseLight,deepDark:o.deepDark,deepLight:o.deepLight,transparent:o.transparent??"transparent",neutral:B(o),grayscale:G(o),main:u(o.main,o),opposite:u(o.opposite,o),complementary:u(o.complementary,o),danger:u(o.danger,o),information:u(o.information,o),success:u(o.success,o),warning:u(o.warning,o),primary:u(o.primary,o),secondary:u(o.secondary,o),tertiary:u(o.tertiary,o)}}s(ao,"composeColors");function U(o,e=P.defaultInputSpacing){let n=ao(o.colors),r=J(e,o.dimension),t=(0,z.createSemantic)({colors:n,spacing:r}),i=(0,O.createFoundation)(t);if(o.surface==="negative"){let l=(0,_.invertForSurface)({semantic:t,colors:n},"negative");return(0,O.createFoundation)(l.semantic)}return i}s(U,"buildFoundationForVariant");async function co(o){let{config:e,outDir:n,cwd:r=process.cwd(),verbose:t=!1}=o,i=v(e),p=e.output??{};if(i.length===0)throw new Error("apollion-tokens build: no variants (config.brands is empty)");let l=await(0,c.mkdtemp)((0,y.join)((0,Z.tmpdir)(),"apollion-tokens-"));try{let m=[];for(let h of i){let C=U(h),D=L(h);if(p.css){let d=I(C,h),f=`css/${D}.css`;await E(l,f,d),m.push({path:f,sha256:b(d)}),t&&console.log(` \u2713 ${f}`)}if(p.json){let d=F(C,h),f=`json/${D}.json`;await E(l,f,d),m.push({path:f,sha256:b(d)}),t&&console.log(` \u2713 ${f}`)}if(p.ts){let d=j(C,h),f=`ts/${D}.d.ts`;await E(l,f,d),m.push({path:f,sha256:b(d)}),t&&console.log(` \u2713 ${f}`)}}let $={configHash:T(e),files:m,gitCommit:M(r),buildEnv:N()},x=V($);return await(0,c.writeFile)((0,y.join)(l,"manifest.json"),x),await(0,c.rm)(n,{recursive:!0,force:!0}),await(0,c.mkdir)((0,y.dirname)(n),{recursive:!0}),await(0,c.rename)(l,n),t&&console.log(`\u2713 ${m.length} files \u2192 ${n}`),{manifest:$,outDir:n}}catch(m){throw await(0,c.rm)(l,{recursive:!0,force:!0}).catch(()=>{}),m}}s(co,"build");async function po(o){let{config:e,outDir:n}=o,r;try{r=JSON.parse(await(0,c.readFile)((0,y.join)(n,"manifest.json"),"utf8"))}catch{return!1}return r.configHash===T(e)}s(po,"check");async function E(o,e,n){let r=(0,y.join)(o,e);await(0,c.mkdir)((0,y.dirname)(r),{recursive:!0}),await(0,c.writeFile)(r,n)}s(E,"writeAt");0&&(module.exports={build,check});
1
+ "use strict";var w=Object.defineProperty;var ye=Object.getOwnPropertyDescriptor;var he=Object.getOwnPropertyNames;var be=Object.prototype.hasOwnProperty;var s=(e,o)=>w(e,"name",{value:o,configurable:!0});var Re=(e,o)=>{for(var n in o)w(e,n,{get:o[n],enumerable:!0})},ke=(e,o,n,r)=>{if(o&&typeof o=="object"||typeof o=="function")for(let i of he(o))!be.call(e,i)&&i!==n&&w(e,i,{get:()=>o[i],enumerable:!(r=ye(o,i))||r.enumerable});return e};var Ie=e=>ke(w({},"__esModule",{value:!0}),e);var ze={};Re(ze,{build:()=>_e,check:()=>Je});module.exports=Ie(ze);var u=require("node:fs/promises"),de=require("node:os"),b=require("node:path");var C=require("@apollion-dsi/core/themes/border"),v=require("@apollion-dsi/core/themes/depth"),h=require("@apollion-dsi/core/themes/font"),k=require("culori");var xe=(0,k.converter)("oklch");function R(e){return typeof e.type=="string"&&"value"in e}s(R,"isToken");function I(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}s(I,"kebab");function W(e){return e.split(".").map(I).join(".")}s(W,"kebabPath");function x(e){return e.kind==="ref"?e.resolved.raw:e.raw}s(x,"rawValue");function S(e,o){return Number(e.toFixed(o))||0}s(S,"round");function G(e){let o=xe((0,k.parse)(e));if(!o)return{kind:"color",raw:e,components:[0,0,0],hex:"#000000"};let n=[S(o.l,4),S(o.c,4),S(o.h??0,2)],r=(0,k.formatHex)(o)??"#000000";return o.alpha!==void 0&&o.alpha!==1?{kind:"color",raw:e,components:n,alpha:S(o.alpha,4),hex:r}:{kind:"color",raw:e,components:n,hex:r}}s(G,"toColorValue");var _=/^(-?(?:\d+\.?\d*|\.\d+))(px|rem)$/;function $(e){let o=_.exec(e.trim());if(o)return{kind:"dimension",raw:e,value:Number(o[1]),unit:o[2]};let n=Number.parseFloat(e);return{kind:"dimension",raw:e,value:Number.isFinite(n)?n:0,unit:"rem"}}s($,"toDimensionValue");function d(e){return{type:"color",value:G(e)}}s(d,"colorToken");function H(e){let o={};for(let[n,r]of Object.entries(e))o[n]=d(r);return o}s(H,"recordColorGroup");var De=["main","opposite","complementary","primary","secondary","tertiary","success","danger","warning","information"];function Ve(e){let o={};for(let n of De){let r=e[n];o[n]={base:d(r.base),dark:d(r.dark),action:d(r.action),active:d(r.active),light:d(r.light)}}return o.grayscale=H(e.grayscale),o.neutral=H(e.neutral),o.baseDark=d(e.baseDark??"#000000"),o.baseLight=d(e.baseLight??"#ffffff"),o.deepDark=d(e.deepDark??"#000000"),o.deepLight=d(e.deepLight??"#ffffff"),o}s(Ve,"buildPrimitives");var Te={primary:"color.primary.base",secondary:"color.secondary.base",tertiary:"color.tertiary.base",success:"color.success.base",danger:"color.danger.base",warning:"color.warning.base",info:"color.information.base"},we={onPrimary:"color.baseLight",onSecondary:"color.baseLight",onTertiary:"color.baseDark",onSuccess:"color.baseLight",onDanger:"color.baseLight",onWarning:"color.baseDark",onInfo:"color.baseLight"};function Se(e,o){let n=e;for(let r of o.split(".").slice(1))if(n&&typeof n=="object"&&r in n)n=n[r];else return;return typeof n=="string"?n:void 0}s(Se,"lookupPrimitive");function P(e,o,n){let r={};for(let[i,t]of Object.entries(e)){let c=n[i],l=c?Se(o,c):void 0;r[i]=c&&l===t?{type:"color",value:{kind:"ref",path:c,resolved:G(t)}}:d(t)}return r}s(P,"colorRefGroup");function Ce(e){let o={};for(let[n,r]of Object.entries(e))o[n]={type:"dimension",value:$(r)};return o}s(Ce,"dimensionGroup");function $e(){let e={},o={};for(let[i,t]of Object.entries(C.defaultInputBorder.borderRadius))_.test(t)?e[i]={type:"dimension",value:$(t)}:o[i]=t;let n={};for(let[i,t]of Object.entries(C.defaultInputBorder.borderWidth))n[i]={type:"dimension",value:$(t)};let r={};for(let[i,t]of Object.entries(C.defaultInputBorder.borderStyle))r[i]={type:"strokeStyle",value:{kind:"strokeStyle",raw:t,style:t}};return{group:{radius:e,width:n,style:r},extensions:o}}s($e,"buildBorder");function ve(){let e={};for(let[t,c]of Object.entries(h.defaultInputFont.fontFamily)){let l=c.replace(/;\s*$/,"").trim();e[t]={type:"fontFamily",value:{kind:"fontFamily",raw:l,family:l}}}let o={};for(let[t,c]of Object.entries(h.defaultInputFont.fontSize))o[t]={type:"dimension",value:{kind:"dimension",raw:`${c}px`,value:c,unit:"px"}};let n={};for(let[t,c]of Object.entries(h.defaultInputFont.fontWeight))n[t]={type:"fontWeight",value:{kind:"fontWeight",raw:`${c}`,weight:c}};let r={};for(let[t,c]of Object.entries(h.defaultInputFont.lineHeight))r[t]={type:"number",value:{kind:"number",raw:`${c}`,number:c}};let i={letterSpacing:h.defaultInputFont.letterSpacing,textTransform:h.defaultInputFont.textTransform,fontStyle:h.defaultInputFont.fontStyle};return{group:{family:e,size:o,weight:n,lineHeight:r},extensions:i}}s(ve,"buildFont");var Oe=/rgba?\([^)]*\)|#[0-9a-fA-F]+|[a-zA-Z]+$/;function Fe(e){let o=e.trim(),n=o.startsWith("inset");n&&(o=o.slice(5).trim());let r=o.match(Oe),i=r?r[0]:"#000000",c=(r?o.slice(0,r.index).trim():o).split(/\s+/).filter(Boolean),l=s(p=>$(c[p]??"0px"),"dim");return{kind:"shadow",raw:e,shadow:{color:G(i),offsetX:l(0),offsetY:l(1),blur:l(2),spread:l(3),inset:n}}}s(Fe,"parseShadow");function je(e){let o=(0,v.createDeth)(e),n={};for(let r of Object.keys(v.levelValues))n[r]={type:"shadow",value:Fe(o(r))};return n}s(je,"buildShadow");function J(e,o,n){let r={brand:n.brand,mode:n.mode,surface:n.surface,dimension:n.dimension},i=$e(),t=ve(),c={"com.apollion.variant":r,"com.apollion.font":t.extensions};return Object.keys(i.extensions).length>0&&(c["com.apollion.border"]={radius:i.extensions}),{description:`Apollion DS tokens \u2014 DTCG 2025.10. Variant: ${r.brand}/${r.mode}/${r.surface}/${r.dimension}.`,meta:r,primitives:Ve(o),groups:{bg:P(e.bg,o,Te),text:P(e.text,o,we),spacing:Ce(e.spacing),border:i.group,font:t.group,shadow:je(o)},extensions:c}}s(J,"buildIR");var Ee="/* Generated by apollion-tokens build \u2014 DO NOT EDIT. See apollion.config.mjs. */";function z(e,o,n){for(let[r,i]of Object.entries(e)){let t=[...o,I(r)];R(i)?n.push(` --apollion-${t.join("-")}: ${x(i.value)};`):z(i,t,n)}}s(z,"emitVars");function X(e,o,n){for(let[r,i]of Object.entries(e)){let t=[...o,I(r)];R(i)?i.type==="color"&&n.push(` @property --apollion-${t.join("-")} { syntax: '<color>'; inherits: true; initial-value: ${x(i.value)}; }`):X(i,t,n)}}s(X,"emitProperties");function U(e){let{brand:o,mode:n,surface:r,dimension:i}=e.meta,t=[Ee,""];return t.push(`/* Variant: brand=${o} mode=${n} surface=${r} dimension=${i} */`,"",":root {"),z(e.groups,[],t),t.push("}",""),t.push("@supports (background: paint(squircle)) or (color: oklch(0% 0 0)) {"),X(e.groups,[],t),t.push("}",""),t.join(`
2
+ `)}s(U,"renderCss");function Y(e){return e.alpha!==void 0?{colorSpace:"oklch",components:[...e.components],alpha:e.alpha,hex:e.hex}:{colorSpace:"oklch",components:[...e.components],hex:e.hex}}s(Y,"colorValue");function D(e){return{value:e.value,unit:e.unit}}s(D,"dimensionValue");function Ge(e){let o=e.shadow,n={color:Y(o.color),offsetX:D(o.offsetX),offsetY:D(o.offsetY),blur:D(o.blur),spread:D(o.spread)};return o.inset&&(n.inset=!0),n}s(Ge,"shadowValue");function Le(e){switch(e.kind){case"ref":return`{${W(e.path)}}`;case"color":return Y(e);case"dimension":return D(e);case"fontFamily":return e.family;case"fontWeight":return e.weight;case"number":return e.number;case"strokeStyle":return e.style;case"shadow":return Ge(e)}}s(Le,"projectValue");function Ae(e){let o={$value:Le(e.value),$type:e.type};return e.description!==void 0&&(o.$description=e.description),e.deprecated!==void 0&&(o.$deprecated=e.deprecated),o}s(Ae,"projectToken");function L(e){let o={};for(let[n,r]of Object.entries(e))o[I(n)]=R(r)?Ae(r):L(r);return o}s(L,"projectGroup");function q(e){let o={$description:e.description,color:L(e.primitives),...L(e.groups),$extensions:e.extensions};return`${JSON.stringify(o,null,2)}
3
+ `}s(q,"renderJson");var Ne="// Generated by apollion-tokens build \u2014 DO NOT EDIT. See apollion.config.mjs.";function Z(e,o,n){let r=" ".repeat(o);for(let[i,t]of Object.entries(e))R(t)?n.push(`${r}${JSON.stringify(i)}: ${JSON.stringify(x(t.value))},`):(n.push(`${r}${i}: {`),Z(t,o+1,n),n.push(`${r}},`))}s(Z,"emitGroup");function K(e){let{brand:o,mode:n,surface:r,dimension:i}=e.meta,t=[Ne,`// Variant: brand=${o} mode=${n} surface=${r} dimension=${i}`,"","export const tokens = {"];return Z(e.groups,1,t),t.push("} as const;","","export type Tokens = typeof tokens;",""),t.join(`
4
+ `)}s(K,"renderTs");var Me=["light","dark"],Be=["positive","negative"],He=["compact","normal","spacious"];function Q(e){let o=e.modes??Me,n=e.surfaces??Be,r=e.dimensions??He,i=Object.entries(e.brands).sort(([c],[l])=>c.localeCompare(l)),t=[];for(let[c,l]of i)for(let p of o)for(let T of n)for(let O of r)t.push({brand:c,colors:l,mode:p,surface:T,dimension:O});return t}s(Q,"expandVariants");function ee(e){return`${e.brand}.${e.mode}.${e.surface}.${e.dimension}`}s(ee,"variantName");var oe=require("node:crypto");function V(e){return(0,oe.createHash)("sha256").update(e).digest("hex")}s(V,"sha256");function N(e){return V(A(e))}s(N,"hashConfig");function A(e){return e===null||typeof e!="object"?JSON.stringify(e):Array.isArray(e)?`[${e.map(A).join(",")}]`:`{${Object.keys(e).sort().map(r=>`${JSON.stringify(r)}:${A(e[r])}`).join(",")}}`}s(A,"stableStringify");function ne(e){try{let{execSync:o}=require("node:child_process");return o("git rev-parse --short HEAD",{cwd:e,stdio:["ignore","pipe","ignore"],encoding:"utf8"}).trim()}catch{return""}}s(ne,"detectGitCommit");function re(){return{node:process.version,platform:`${process.platform}-${process.arch}`}}s(re,"buildEnv");function te(e){let o=[...e.files].sort((r,i)=>r.path.localeCompare(i.path)),n={configHash:e.configHash,files:o,gitCommit:e.gitCommit,buildEnv:e.buildEnv};return`${JSON.stringify(n,null,2)}
5
+ `}s(te,"serializeManifest");var M=require("@apollion-dsi/core/themes/foundation"),le=require("@apollion-dsi/core/themes/semantic"),ue=require("@apollion-dsi/core/themes/spacing"),pe=require("@apollion-dsi/core/themes/surface");var g=require("culori");var lo=(0,g.converter)("oklch");function a(e,o,n){let r=(0,g.parse)(e),i=(0,g.parse)(o);if(!r||!i)return e;let t=(0,g.interpolate)([r,i],"oklch");return(0,g.formatHex)(t(n))??e}s(a,"mixOklch");function Pe(e){return e?typeof e=="string"?e:e.base:"#000"}s(Pe,"coerceToHex");function m(e,o){let n=Pe(e),r=o.deepDark,i=o.deepLight;return{base:n,dark:a(n,r,.6),action:a(n,r,.2),active:a(n,i,.6),light:a(n,i,.9)}}s(m,"mountSingleColor");function ie(e){let o=e.deepLight,n=e.deepDark;return{0:o,5:a(o,n,.05),10:a(o,n,.1),20:a(o,n,.2),30:a(o,n,.3),40:a(o,n,.4),50:a(o,n,.5),60:a(o,n,.6),70:a(o,n,.7),80:a(o,n,.8),90:a(o,n,.9),100:a(o,n,1)}}s(ie,"mountGreyscaleColors");function se(e){let o=e.deepLight,n=e.deepDark,r=e.primary,i=s(c=>1/17*(c-1),"interpolation"),t=a(r,n,.5);return{0:a(t,o,i(18)),5:a(t,o,i(17.5)),10:a(t,o,i(17)),20:a(t,o,i(16)),30:a(t,o,i(15)),40:a(t,o,i(14)),50:a(t,o,i(13)),60:a(t,o,i(12)),70:a(t,o,i(11)),80:a(t,o,i(10)),90:a(t,o,i(9)),100:a(t,o,i(8)),110:a(t,o,i(7)),120:a(t,o,i(6)),130:a(t,o,i(5)),140:a(t,o,i(4)),150:a(t,o,i(3)),160:a(t,o,i(2)),170:t,180:a(t,n,.25)}}s(se,"mountNeutralColors");var ae=require("@apollion-dsi/core/themes/dimension");function ce({multiplier:e=.25,alias:o},n){let r=n!==void 0?ae.DIMENSION_MULTIPLIERS[n]:e;return(...i)=>i.map(t=>{let c=typeof t=="string"?o[t]:t;return`${r*c}rem`}).join(" ")}s(ce,"createSpacing");function We(e){return{baseDark:e.baseDark,baseLight:e.baseLight,deepDark:e.deepDark,deepLight:e.deepLight,transparent:e.transparent??"transparent",neutral:se(e),grayscale:ie(e),main:m(e.main,e),opposite:m(e.opposite,e),complementary:m(e.complementary,e),danger:m(e.danger,e),information:m(e.information,e),success:m(e.success,e),warning:m(e.warning,e),primary:m(e.primary,e),secondary:m(e.secondary,e),tertiary:m(e.tertiary,e)}}s(We,"composeColors");function fe(e,o=ue.defaultInputSpacing){let n=We(e.colors),r=ce(o,e.dimension),i=(0,le.createSemantic)({colors:n,spacing:r}),t=(0,M.createFoundation)(i);return e.surface==="negative"&&(t=(0,M.createFoundation)((0,pe.invertForSurface)({semantic:i,colors:n},"negative").semantic)),{foundation:t,colors:n}}s(fe,"buildVariantThemes");async function _e(e){let{config:o,outDir:n,cwd:r=process.cwd(),verbose:i=!1}=e,t=Q(o),c=o.output??{};if(t.length===0)throw new Error("apollion-tokens build: no variants (config.brands is empty)");let l=await(0,u.mkdtemp)((0,b.join)((0,de.tmpdir)(),"apollion-tokens-"));try{let p=[];for(let F of t){let{foundation:me,colors:ge}=fe(F),j=J(me,ge,F),E=ee(F);if(c.css){let y=U(j),f=`css/${E}.css`;await B(l,f,y),p.push({path:f,sha256:V(y)}),i&&console.log(` \u2713 ${f}`)}if(c.json){let y=q(j),f=`json/${E}.json`;await B(l,f,y),p.push({path:f,sha256:V(y)}),i&&console.log(` \u2713 ${f}`)}if(c.ts){let y=K(j),f=`ts/${E}.d.ts`;await B(l,f,y),p.push({path:f,sha256:V(y)}),i&&console.log(` \u2713 ${f}`)}}let T={configHash:N(o),files:p,gitCommit:ne(r),buildEnv:re()},O=te(T);return await(0,u.writeFile)((0,b.join)(l,"manifest.json"),O),await(0,u.rm)(n,{recursive:!0,force:!0}),await(0,u.mkdir)((0,b.dirname)(n),{recursive:!0}),await(0,u.rename)(l,n),i&&console.log(`\u2713 ${p.length} files \u2192 ${n}`),{manifest:T,outDir:n}}catch(p){throw await(0,u.rm)(l,{recursive:!0,force:!0}).catch(()=>{}),p}}s(_e,"build");async function Je(e){let{config:o,outDir:n}=e,r;try{r=JSON.parse(await(0,u.readFile)((0,b.join)(n,"manifest.json"),"utf8"))}catch{return!1}return r.configHash===N(o)}s(Je,"check");async function B(e,o,n){let r=(0,b.join)(e,o);await(0,u.mkdir)((0,b.dirname)(r),{recursive:!0}),await(0,u.writeFile)(r,n)}s(B,"writeAt");0&&(module.exports={build,check});
package/lib/build.esm.js CHANGED
@@ -1,6 +1,5 @@
1
- var P=Object.defineProperty;var s=(o,e)=>P(o,"name",{value:e,configurable:!0}),_=(o=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(o,{get:(e,n)=>(typeof require<"u"?require:e)[n]}):o)(function(o){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+o+'" is not supported')});import{mkdir as H,mkdtemp as co,readFile as po,rename as lo,rm as B,writeFile as J}from"node:fs/promises";import{tmpdir as fo}from"node:os";import{dirname as z,join as h}from"node:path";var U="/* Generated by apollion-tokens build \u2014 DO NOT EDIT. See apollion.config.mjs. */";function w(o,e){let n=[U,""];n.push(`/* Variant: brand=${e.brand} mode=${e.mode} surface=${e.surface} dimension=${e.dimension} */`,"",":root {");for(let[r,t]of Object.entries(o.bg))n.push(` --apollion-bg-${r}: ${t};`);for(let[r,t]of Object.entries(o.text)){let i=r.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase();n.push(` --apollion-text-${i}: ${t};`)}for(let[r,t]of Object.entries(o.spacing))n.push(` --apollion-spacing-${r}: ${t};`);n.push("}",""),n.push("@supports (background: paint(squircle)) or (color: oklch(0% 0 0)) {");for(let[r,t]of Object.entries(o.bg))n.push(` @property --apollion-bg-${r} { syntax: '<color>'; inherits: true; initial-value: ${t}; }`);for(let r of Object.keys(o.text)){let t=r.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase(),i=o.text[r];n.push(` @property --apollion-text-${t} { syntax: '<color>'; inherits: true; initial-value: ${i}; }`)}return n.push("}",""),n.join(`
2
- `)}s(w,"renderCss");function T(o){let e={};for(let[n,r]of Object.entries(o)){let t=n.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase();e[t]={$value:r,$type:"color"}}return e}s(T,"toColorGroup");function Z(o){let e={};for(let[n,r]of Object.entries(o))e[n]={$value:r,$type:"dimension"};return e}s(Z,"toDimensionGroup");function O(o,e){let n={$description:`Apollion DS tokens \u2014 DTCG v1. Variant: ${e.brand}/${e.mode}/${e.surface}/${e.dimension}.`,bg:T(o.bg),text:T(o.text),spacing:Z(o.spacing),$extensions:{"com.apollion.variant":{brand:e.brand,mode:e.mode,surface:e.surface,dimension:e.dimension}}};return`${JSON.stringify(n,null,2)}
3
- `}s(O,"renderJson");var q="// Generated by apollion-tokens build \u2014 DO NOT EDIT. See apollion.config.mjs.";function x(o,e){let n=" ".repeat(e);return Object.entries(o).map(([t,i])=>`${n}${JSON.stringify(t)}: ${JSON.stringify(i)},`).join(`
4
- `)}s(x,"serializeRecord");function E(o,e){return[q,`// Variant: brand=${e.brand} mode=${e.mode} surface=${e.surface} dimension=${e.dimension}`,"","export const tokens = {"," bg: {",x(o.bg,4)," },"," text: {",x(o.text,4)," },"," spacing: {",x(o.spacing,4)," },","} as const;","","export type Tokens = typeof tokens;",""].join(`
5
- `)}s(E,"renderTs");var K=["light","dark"],Q=["positive","negative"],W=["compact","normal","spacious"];function I(o){let e=o.modes??K,n=o.surfaces??Q,r=o.dimensions??W,t=Object.entries(o.brands).sort(([c],[p])=>c.localeCompare(p)),i=[];for(let[c,p]of t)for(let u of e)for(let y of n)for(let b of r)i.push({brand:c,colors:p,mode:u,surface:y,dimension:b});return i}s(I,"expandVariants");function A(o){return`${o.brand}.${o.mode}.${o.surface}.${o.dimension}`}s(A,"variantName");import{createHash as X}from"node:crypto";function d(o){return X("sha256").update(o).digest("hex")}s(d,"sha256");function D(o){return d(C(o))}s(D,"hashConfig");function C(o){return o===null||typeof o!="object"?JSON.stringify(o):Array.isArray(o)?`[${o.map(C).join(",")}]`:`{${Object.keys(o).sort().map(r=>`${JSON.stringify(r)}:${C(o[r])}`).join(",")}}`}s(C,"stableStringify");function F(o){try{let{execSync:e}=_("node:child_process");return e("git rev-parse --short HEAD",{cwd:o,stdio:["ignore","pipe","ignore"],encoding:"utf8"}).trim()}catch{return""}}s(F,"detectGitCommit");function j(){return{node:process.version,platform:`${process.platform}-${process.arch}`}}s(j,"buildEnv");function v(o){let e=[...o.files].sort((r,t)=>r.path.localeCompare(t.path)),n={configHash:o.configHash,files:e,gitCommit:o.gitCommit,buildEnv:o.buildEnv};return`${JSON.stringify(n,null,2)}
6
- `}s(v,"serializeManifest");import{createFoundation as V}from"@apollion-dsi/core/themes/foundation";import{createSemantic as ro}from"@apollion-dsi/core/themes/semantic";import{defaultInputSpacing as io}from"@apollion-dsi/core/themes/spacing";import{invertForSurface as so}from"@apollion-dsi/core/themes/surface";import{converter as Y,formatHex as oo,interpolate as eo,parse as L}from"culori";var To=Y("oklch");function a(o,e,n){let r=L(o),t=L(e);if(!r||!t)return o;let i=eo([r,t],"oklch");return oo(i(n))??o}s(a,"mixOklch");function no(o){return o?typeof o=="string"?o:o.base:"#000"}s(no,"coerceToHex");function f(o,e){let n=no(o),r=e.deepDark,t=e.deepLight;return{base:n,dark:a(n,r,.6),action:a(n,r,.2),active:a(n,t,.6),light:a(n,t,.9)}}s(f,"mountSingleColor");function R(o){let e=o.deepLight,n=o.deepDark;return{0:e,5:a(e,n,.05),10:a(e,n,.1),20:a(e,n,.2),30:a(e,n,.3),40:a(e,n,.4),50:a(e,n,.5),60:a(e,n,.6),70:a(e,n,.7),80:a(e,n,.8),90:a(e,n,.9),100:a(e,n,1)}}s(R,"mountGreyscaleColors");function M(o){let e=o.deepLight,n=o.deepDark,r=o.primary,t=s(c=>1/17*(c-1),"interpolation"),i=a(r,n,.5);return{0:a(i,e,t(18)),5:a(i,e,t(17.5)),10:a(i,e,t(17)),20:a(i,e,t(16)),30:a(i,e,t(15)),40:a(i,e,t(14)),50:a(i,e,t(13)),60:a(i,e,t(12)),70:a(i,e,t(11)),80:a(i,e,t(10)),90:a(i,e,t(9)),100:a(i,e,t(8)),110:a(i,e,t(7)),120:a(i,e,t(6)),130:a(i,e,t(5)),140:a(i,e,t(4)),150:a(i,e,t(3)),160:a(i,e,t(2)),170:i,180:a(i,n,.25)}}s(M,"mountNeutralColors");import{DIMENSION_MULTIPLIERS as to}from"@apollion-dsi/core/themes/dimension";function N({multiplier:o=.25,alias:e},n){let r=n!==void 0?to[n]:o;return(...t)=>t.map(i=>{let c=typeof i=="string"?e[i]:i;return`${r*c}rem`}).join(" ")}s(N,"createSpacing");function ao(o){return{baseDark:o.baseDark,baseLight:o.baseLight,deepDark:o.deepDark,deepLight:o.deepLight,transparent:o.transparent??"transparent",neutral:M(o),grayscale:R(o),main:f(o.main,o),opposite:f(o.opposite,o),complementary:f(o.complementary,o),danger:f(o.danger,o),information:f(o.information,o),success:f(o.success,o),warning:f(o.warning,o),primary:f(o.primary,o),secondary:f(o.secondary,o),tertiary:f(o.tertiary,o)}}s(ao,"composeColors");function G(o,e=io){let n=ao(o.colors),r=N(e,o.dimension),t=ro({colors:n,spacing:r}),i=V(t);if(o.surface==="negative"){let p=so({semantic:t,colors:n},"negative");return V(p.semantic)}return i}s(G,"buildFoundationForVariant");async function Ko(o){let{config:e,outDir:n,cwd:r=process.cwd(),verbose:t=!1}=o,i=I(e),c=e.output??{};if(i.length===0)throw new Error("apollion-tokens build: no variants (config.brands is empty)");let p=await co(h(fo(),"apollion-tokens-"));try{let u=[];for(let g of i){let $=G(g),k=A(g);if(c.css){let m=w($,g),l=`css/${k}.css`;await S(p,l,m),u.push({path:l,sha256:d(m)}),t&&console.log(` \u2713 ${l}`)}if(c.json){let m=O($,g),l=`json/${k}.json`;await S(p,l,m),u.push({path:l,sha256:d(m)}),t&&console.log(` \u2713 ${l}`)}if(c.ts){let m=E($,g),l=`ts/${k}.d.ts`;await S(p,l,m),u.push({path:l,sha256:d(m)}),t&&console.log(` \u2713 ${l}`)}}let y={configHash:D(e),files:u,gitCommit:F(r),buildEnv:j()},b=v(y);return await J(h(p,"manifest.json"),b),await B(n,{recursive:!0,force:!0}),await H(z(n),{recursive:!0}),await lo(p,n),t&&console.log(`\u2713 ${u.length} files \u2192 ${n}`),{manifest:y,outDir:n}}catch(u){throw await B(p,{recursive:!0,force:!0}).catch(()=>{}),u}}s(Ko,"build");async function Qo(o){let{config:e,outDir:n}=o,r;try{r=JSON.parse(await po(h(n,"manifest.json"),"utf8"))}catch{return!1}return r.configHash===D(e)}s(Qo,"check");async function S(o,e,n){let r=h(o,e);await H(z(r),{recursive:!0}),await J(r,n)}s(S,"writeAt");export{Ko as build,Qo as check};
1
+ var ue=Object.defineProperty;var s=(e,o)=>ue(e,"name",{value:o,configurable:!0}),pe=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(o,n)=>(typeof require<"u"?require:o)[n]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});import{mkdir as ie,mkdtemp as Xe,readFile as Ue,rename as Ye,rm as te,writeFile as se}from"node:fs/promises";import{tmpdir as qe}from"node:os";import{dirname as ae,join as V}from"node:path";import{defaultInputBorder as $}from"@apollion-dsi/core/themes/border";import{createDeth as fe,levelValues as de}from"@apollion-dsi/core/themes/depth";import{defaultInputFont as g}from"@apollion-dsi/core/themes/font";import{converter as me,formatHex as ge,parse as ye}from"culori";var he=me("oklch");function y(e){return typeof e.type=="string"&&"value"in e}s(y,"isToken");function h(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}s(h,"kebab");function A(e){return e.split(".").map(h).join(".")}s(A,"kebabPath");function b(e){return e.kind==="ref"?e.resolved.raw:e.raw}s(b,"rawValue");function x(e,o){return Number(e.toFixed(o))||0}s(x,"round");function v(e){let o=he(ye(e));if(!o)return{kind:"color",raw:e,components:[0,0,0],hex:"#000000"};let n=[x(o.l,4),x(o.c,4),x(o.h??0,2)],r=ge(o)??"#000000";return o.alpha!==void 0&&o.alpha!==1?{kind:"color",raw:e,components:n,alpha:x(o.alpha,4),hex:r}:{kind:"color",raw:e,components:n,hex:r}}s(v,"toColorValue");var N=/^(-?(?:\d+\.?\d*|\.\d+))(px|rem)$/;function D(e){let o=N.exec(e.trim());if(o)return{kind:"dimension",raw:e,value:Number(o[1]),unit:o[2]};let n=Number.parseFloat(e);return{kind:"dimension",raw:e,value:Number.isFinite(n)?n:0,unit:"rem"}}s(D,"toDimensionValue");function f(e){return{type:"color",value:v(e)}}s(f,"colorToken");function G(e){let o={};for(let[n,r]of Object.entries(e))o[n]=f(r);return o}s(G,"recordColorGroup");var be=["main","opposite","complementary","primary","secondary","tertiary","success","danger","warning","information"];function Re(e){let o={};for(let n of be){let r=e[n];o[n]={base:f(r.base),dark:f(r.dark),action:f(r.action),active:f(r.active),light:f(r.light)}}return o.grayscale=G(e.grayscale),o.neutral=G(e.neutral),o.baseDark=f(e.baseDark??"#000000"),o.baseLight=f(e.baseLight??"#ffffff"),o.deepDark=f(e.deepDark??"#000000"),o.deepLight=f(e.deepLight??"#ffffff"),o}s(Re,"buildPrimitives");var ke={primary:"color.primary.base",secondary:"color.secondary.base",tertiary:"color.tertiary.base",success:"color.success.base",danger:"color.danger.base",warning:"color.warning.base",info:"color.information.base"},Ie={onPrimary:"color.baseLight",onSecondary:"color.baseLight",onTertiary:"color.baseDark",onSuccess:"color.baseLight",onDanger:"color.baseLight",onWarning:"color.baseDark",onInfo:"color.baseLight"};function xe(e,o){let n=e;for(let r of o.split(".").slice(1))if(n&&typeof n=="object"&&r in n)n=n[r];else return;return typeof n=="string"?n:void 0}s(xe,"lookupPrimitive");function L(e,o,n){let r={};for(let[i,t]of Object.entries(e)){let c=n[i],l=c?xe(o,c):void 0;r[i]=c&&l===t?{type:"color",value:{kind:"ref",path:c,resolved:v(t)}}:f(t)}return r}s(L,"colorRefGroup");function De(e){let o={};for(let[n,r]of Object.entries(e))o[n]={type:"dimension",value:D(r)};return o}s(De,"dimensionGroup");function Ve(){let e={},o={};for(let[i,t]of Object.entries($.borderRadius))N.test(t)?e[i]={type:"dimension",value:D(t)}:o[i]=t;let n={};for(let[i,t]of Object.entries($.borderWidth))n[i]={type:"dimension",value:D(t)};let r={};for(let[i,t]of Object.entries($.borderStyle))r[i]={type:"strokeStyle",value:{kind:"strokeStyle",raw:t,style:t}};return{group:{radius:e,width:n,style:r},extensions:o}}s(Ve,"buildBorder");function Te(){let e={};for(let[t,c]of Object.entries(g.fontFamily)){let l=c.replace(/;\s*$/,"").trim();e[t]={type:"fontFamily",value:{kind:"fontFamily",raw:l,family:l}}}let o={};for(let[t,c]of Object.entries(g.fontSize))o[t]={type:"dimension",value:{kind:"dimension",raw:`${c}px`,value:c,unit:"px"}};let n={};for(let[t,c]of Object.entries(g.fontWeight))n[t]={type:"fontWeight",value:{kind:"fontWeight",raw:`${c}`,weight:c}};let r={};for(let[t,c]of Object.entries(g.lineHeight))r[t]={type:"number",value:{kind:"number",raw:`${c}`,number:c}};let i={letterSpacing:g.letterSpacing,textTransform:g.textTransform,fontStyle:g.fontStyle};return{group:{family:e,size:o,weight:n,lineHeight:r},extensions:i}}s(Te,"buildFont");var we=/rgba?\([^)]*\)|#[0-9a-fA-F]+|[a-zA-Z]+$/;function Se(e){let o=e.trim(),n=o.startsWith("inset");n&&(o=o.slice(5).trim());let r=o.match(we),i=r?r[0]:"#000000",c=(r?o.slice(0,r.index).trim():o).split(/\s+/).filter(Boolean),l=s(u=>D(c[u]??"0px"),"dim");return{kind:"shadow",raw:e,shadow:{color:v(i),offsetX:l(0),offsetY:l(1),blur:l(2),spread:l(3),inset:n}}}s(Se,"parseShadow");function Ce(e){let o=fe(e),n={};for(let r of Object.keys(de))n[r]={type:"shadow",value:Se(o(r))};return n}s(Ce,"buildShadow");function M(e,o,n){let r={brand:n.brand,mode:n.mode,surface:n.surface,dimension:n.dimension},i=Ve(),t=Te(),c={"com.apollion.variant":r,"com.apollion.font":t.extensions};return Object.keys(i.extensions).length>0&&(c["com.apollion.border"]={radius:i.extensions}),{description:`Apollion DS tokens \u2014 DTCG 2025.10. Variant: ${r.brand}/${r.mode}/${r.surface}/${r.dimension}.`,meta:r,primitives:Re(o),groups:{bg:L(e.bg,o,ke),text:L(e.text,o,Ie),spacing:De(e.spacing),border:i.group,font:t.group,shadow:Ce(o)},extensions:c}}s(M,"buildIR");var $e="/* Generated by apollion-tokens build \u2014 DO NOT EDIT. See apollion.config.mjs. */";function B(e,o,n){for(let[r,i]of Object.entries(e)){let t=[...o,h(r)];y(i)?n.push(` --apollion-${t.join("-")}: ${b(i.value)};`):B(i,t,n)}}s(B,"emitVars");function H(e,o,n){for(let[r,i]of Object.entries(e)){let t=[...o,h(r)];y(i)?i.type==="color"&&n.push(` @property --apollion-${t.join("-")} { syntax: '<color>'; inherits: true; initial-value: ${b(i.value)}; }`):H(i,t,n)}}s(H,"emitProperties");function P(e){let{brand:o,mode:n,surface:r,dimension:i}=e.meta,t=[$e,""];return t.push(`/* Variant: brand=${o} mode=${n} surface=${r} dimension=${i} */`,"",":root {"),B(e.groups,[],t),t.push("}",""),t.push("@supports (background: paint(squircle)) or (color: oklch(0% 0 0)) {"),H(e.groups,[],t),t.push("}",""),t.join(`
2
+ `)}s(P,"renderCss");function W(e){return e.alpha!==void 0?{colorSpace:"oklch",components:[...e.components],alpha:e.alpha,hex:e.hex}:{colorSpace:"oklch",components:[...e.components],hex:e.hex}}s(W,"colorValue");function R(e){return{value:e.value,unit:e.unit}}s(R,"dimensionValue");function ve(e){let o=e.shadow,n={color:W(o.color),offsetX:R(o.offsetX),offsetY:R(o.offsetY),blur:R(o.blur),spread:R(o.spread)};return o.inset&&(n.inset=!0),n}s(ve,"shadowValue");function Oe(e){switch(e.kind){case"ref":return`{${A(e.path)}}`;case"color":return W(e);case"dimension":return R(e);case"fontFamily":return e.family;case"fontWeight":return e.weight;case"number":return e.number;case"strokeStyle":return e.style;case"shadow":return ve(e)}}s(Oe,"projectValue");function Fe(e){let o={$value:Oe(e.value),$type:e.type};return e.description!==void 0&&(o.$description=e.description),e.deprecated!==void 0&&(o.$deprecated=e.deprecated),o}s(Fe,"projectToken");function O(e){let o={};for(let[n,r]of Object.entries(e))o[h(n)]=y(r)?Fe(r):O(r);return o}s(O,"projectGroup");function _(e){let o={$description:e.description,color:O(e.primitives),...O(e.groups),$extensions:e.extensions};return`${JSON.stringify(o,null,2)}
3
+ `}s(_,"renderJson");var je="// Generated by apollion-tokens build \u2014 DO NOT EDIT. See apollion.config.mjs.";function J(e,o,n){let r=" ".repeat(o);for(let[i,t]of Object.entries(e))y(t)?n.push(`${r}${JSON.stringify(i)}: ${JSON.stringify(b(t.value))},`):(n.push(`${r}${i}: {`),J(t,o+1,n),n.push(`${r}},`))}s(J,"emitGroup");function z(e){let{brand:o,mode:n,surface:r,dimension:i}=e.meta,t=[je,`// Variant: brand=${o} mode=${n} surface=${r} dimension=${i}`,"","export const tokens = {"];return J(e.groups,1,t),t.push("} as const;","","export type Tokens = typeof tokens;",""),t.join(`
4
+ `)}s(z,"renderTs");var Ee=["light","dark"],Ge=["positive","negative"],Le=["compact","normal","spacious"];function X(e){let o=e.modes??Ee,n=e.surfaces??Ge,r=e.dimensions??Le,i=Object.entries(e.brands).sort(([c],[l])=>c.localeCompare(l)),t=[];for(let[c,l]of i)for(let u of o)for(let I of n)for(let T of r)t.push({brand:c,colors:l,mode:u,surface:I,dimension:T});return t}s(X,"expandVariants");function U(e){return`${e.brand}.${e.mode}.${e.surface}.${e.dimension}`}s(U,"variantName");import{createHash as Ae}from"node:crypto";function k(e){return Ae("sha256").update(e).digest("hex")}s(k,"sha256");function j(e){return k(F(e))}s(j,"hashConfig");function F(e){return e===null||typeof e!="object"?JSON.stringify(e):Array.isArray(e)?`[${e.map(F).join(",")}]`:`{${Object.keys(e).sort().map(r=>`${JSON.stringify(r)}:${F(e[r])}`).join(",")}}`}s(F,"stableStringify");function Y(e){try{let{execSync:o}=pe("node:child_process");return o("git rev-parse --short HEAD",{cwd:e,stdio:["ignore","pipe","ignore"],encoding:"utf8"}).trim()}catch{return""}}s(Y,"detectGitCommit");function q(){return{node:process.version,platform:`${process.platform}-${process.arch}`}}s(q,"buildEnv");function Z(e){let o=[...e.files].sort((r,i)=>r.path.localeCompare(i.path)),n={configHash:e.configHash,files:o,gitCommit:e.gitCommit,buildEnv:e.buildEnv};return`${JSON.stringify(n,null,2)}
5
+ `}s(Z,"serializeManifest");import{createFoundation as ne}from"@apollion-dsi/core/themes/foundation";import{createSemantic as We}from"@apollion-dsi/core/themes/semantic";import{defaultInputSpacing as _e}from"@apollion-dsi/core/themes/spacing";import{invertForSurface as Je}from"@apollion-dsi/core/themes/surface";import{converter as Ne,formatHex as Me,interpolate as Be,parse as K}from"culori";var ko=Ne("oklch");function a(e,o,n){let r=K(e),i=K(o);if(!r||!i)return e;let t=Be([r,i],"oklch");return Me(t(n))??e}s(a,"mixOklch");function He(e){return e?typeof e=="string"?e:e.base:"#000"}s(He,"coerceToHex");function d(e,o){let n=He(e),r=o.deepDark,i=o.deepLight;return{base:n,dark:a(n,r,.6),action:a(n,r,.2),active:a(n,i,.6),light:a(n,i,.9)}}s(d,"mountSingleColor");function Q(e){let o=e.deepLight,n=e.deepDark;return{0:o,5:a(o,n,.05),10:a(o,n,.1),20:a(o,n,.2),30:a(o,n,.3),40:a(o,n,.4),50:a(o,n,.5),60:a(o,n,.6),70:a(o,n,.7),80:a(o,n,.8),90:a(o,n,.9),100:a(o,n,1)}}s(Q,"mountGreyscaleColors");function ee(e){let o=e.deepLight,n=e.deepDark,r=e.primary,i=s(c=>1/17*(c-1),"interpolation"),t=a(r,n,.5);return{0:a(t,o,i(18)),5:a(t,o,i(17.5)),10:a(t,o,i(17)),20:a(t,o,i(16)),30:a(t,o,i(15)),40:a(t,o,i(14)),50:a(t,o,i(13)),60:a(t,o,i(12)),70:a(t,o,i(11)),80:a(t,o,i(10)),90:a(t,o,i(9)),100:a(t,o,i(8)),110:a(t,o,i(7)),120:a(t,o,i(6)),130:a(t,o,i(5)),140:a(t,o,i(4)),150:a(t,o,i(3)),160:a(t,o,i(2)),170:t,180:a(t,n,.25)}}s(ee,"mountNeutralColors");import{DIMENSION_MULTIPLIERS as Pe}from"@apollion-dsi/core/themes/dimension";function oe({multiplier:e=.25,alias:o},n){let r=n!==void 0?Pe[n]:e;return(...i)=>i.map(t=>{let c=typeof t=="string"?o[t]:t;return`${r*c}rem`}).join(" ")}s(oe,"createSpacing");function ze(e){return{baseDark:e.baseDark,baseLight:e.baseLight,deepDark:e.deepDark,deepLight:e.deepLight,transparent:e.transparent??"transparent",neutral:ee(e),grayscale:Q(e),main:d(e.main,e),opposite:d(e.opposite,e),complementary:d(e.complementary,e),danger:d(e.danger,e),information:d(e.information,e),success:d(e.success,e),warning:d(e.warning,e),primary:d(e.primary,e),secondary:d(e.secondary,e),tertiary:d(e.tertiary,e)}}s(ze,"composeColors");function re(e,o=_e){let n=ze(e.colors),r=oe(o,e.dimension),i=We({colors:n,spacing:r}),t=ne(i);return e.surface==="negative"&&(t=ne(Je({semantic:i,colors:n},"negative").semantic)),{foundation:t,colors:n}}s(re,"buildVariantThemes");async function _o(e){let{config:o,outDir:n,cwd:r=process.cwd(),verbose:i=!1}=e,t=X(o),c=o.output??{};if(t.length===0)throw new Error("apollion-tokens build: no variants (config.brands is empty)");let l=await Xe(V(qe(),"apollion-tokens-"));try{let u=[];for(let w of t){let{foundation:ce,colors:le}=re(w),S=M(ce,le,w),C=U(w);if(c.css){let m=P(S),p=`css/${C}.css`;await E(l,p,m),u.push({path:p,sha256:k(m)}),i&&console.log(` \u2713 ${p}`)}if(c.json){let m=_(S),p=`json/${C}.json`;await E(l,p,m),u.push({path:p,sha256:k(m)}),i&&console.log(` \u2713 ${p}`)}if(c.ts){let m=z(S),p=`ts/${C}.d.ts`;await E(l,p,m),u.push({path:p,sha256:k(m)}),i&&console.log(` \u2713 ${p}`)}}let I={configHash:j(o),files:u,gitCommit:Y(r),buildEnv:q()},T=Z(I);return await se(V(l,"manifest.json"),T),await te(n,{recursive:!0,force:!0}),await ie(ae(n),{recursive:!0}),await Ye(l,n),i&&console.log(`\u2713 ${u.length} files \u2192 ${n}`),{manifest:I,outDir:n}}catch(u){throw await te(l,{recursive:!0,force:!0}).catch(()=>{}),u}}s(_o,"build");async function Jo(e){let{config:o,outDir:n}=e,r;try{r=JSON.parse(await Ue(V(n,"manifest.json"),"utf8"))}catch{return!1}return r.configHash===j(o)}s(Jo,"check");async function E(e,o,n){let r=V(e,o);await ie(ae(r),{recursive:!0}),await se(r,n)}s(E,"writeAt");export{_o as build,Jo as check};
package/lib/cli.mjs CHANGED
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env node
2
- var oo=Object.defineProperty;var i=(o,n)=>oo(o,"name",{value:n,configurable:!0}),no=(o=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(o,{get:(n,e)=>(typeof require<"u"?require:n)[e]}):o)(function(o){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+o+'" is not supported')});import{readFile as Eo}from"node:fs/promises";import{resolve as j}from"node:path";import{mkdir as q,mkdtemp as ko,readFile as xo,rename as $o,rm as U,writeFile as Z}from"node:fs/promises";import{tmpdir as Co}from"node:os";import{dirname as W,join as x}from"node:path";var eo="/* Generated by apollion-tokens build \u2014 DO NOT EDIT. See apollion.config.mjs. */";function E(o,n){let e=[eo,""];e.push(`/* Variant: brand=${n.brand} mode=${n.mode} surface=${n.surface} dimension=${n.dimension} */`,"",":root {");for(let[r,t]of Object.entries(o.bg))e.push(` --apollion-bg-${r}: ${t};`);for(let[r,t]of Object.entries(o.text)){let s=r.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase();e.push(` --apollion-text-${s}: ${t};`)}for(let[r,t]of Object.entries(o.spacing))e.push(` --apollion-spacing-${r}: ${t};`);e.push("}",""),e.push("@supports (background: paint(squircle)) or (color: oklch(0% 0 0)) {");for(let[r,t]of Object.entries(o.bg))e.push(` @property --apollion-bg-${r} { syntax: '<color>'; inherits: true; initial-value: ${t}; }`);for(let r of Object.keys(o.text)){let t=r.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase(),s=o.text[r];e.push(` @property --apollion-text-${t} { syntax: '<color>'; inherits: true; initial-value: ${s}; }`)}return e.push("}",""),e.join(`
3
- `)}i(E,"renderCss");function I(o){let n={};for(let[e,r]of Object.entries(o)){let t=e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase();n[t]={$value:r,$type:"color"}}return n}i(I,"toColorGroup");function to(o){let n={};for(let[e,r]of Object.entries(o))n[e]={$value:r,$type:"dimension"};return n}i(to,"toDimensionGroup");function F(o,n){let e={$description:`Apollion DS tokens \u2014 DTCG v1. Variant: ${n.brand}/${n.mode}/${n.surface}/${n.dimension}.`,bg:I(o.bg),text:I(o.text),spacing:to(o.spacing),$extensions:{"com.apollion.variant":{brand:n.brand,mode:n.mode,surface:n.surface,dimension:n.dimension}}};return`${JSON.stringify(e,null,2)}
4
- `}i(F,"renderJson");var ro="// Generated by apollion-tokens build \u2014 DO NOT EDIT. See apollion.config.mjs.";function S(o,n){let e=" ".repeat(n);return Object.entries(o).map(([t,s])=>`${e}${JSON.stringify(t)}: ${JSON.stringify(s)},`).join(`
5
- `)}i(S,"serializeRecord");function R(o,n){return[ro,`// Variant: brand=${n.brand} mode=${n.mode} surface=${n.surface} dimension=${n.dimension}`,"","export const tokens = {"," bg: {",S(o.bg,4)," },"," text: {",S(o.text,4)," },"," spacing: {",S(o.spacing,4)," },","} as const;","","export type Tokens = typeof tokens;",""].join(`
6
- `)}i(R,"renderTs");var io=["light","dark"],so=["positive","negative"],ao=["compact","normal","spacious"];function L(o){let n=o.modes??io,e=o.surfaces??so,r=o.dimensions??ao,t=Object.entries(o.brands).sort(([f],[l])=>f.localeCompare(l)),s=[];for(let[f,l]of t)for(let c of n)for(let d of e)for(let C of r)s.push({brand:f,colors:l,mode:c,surface:d,dimension:C});return s}i(L,"expandVariants");function M(o){return`${o.brand}.${o.mode}.${o.surface}.${o.dimension}`}i(M,"variantName");import{createHash as co}from"node:crypto";function k(o){return co("sha256").update(o).digest("hex")}i(k,"sha256");function v(o){return k(A(o))}i(v,"hashConfig");function A(o){return o===null||typeof o!="object"?JSON.stringify(o):Array.isArray(o)?`[${o.map(A).join(",")}]`:`{${Object.keys(o).sort().map(r=>`${JSON.stringify(r)}:${A(o[r])}`).join(",")}}`}i(A,"stableStringify");function N(o){try{let{execSync:n}=no("node:child_process");return n("git rev-parse --short HEAD",{cwd:o,stdio:["ignore","pipe","ignore"],encoding:"utf8"}).trim()}catch{return""}}i(N,"detectGitCommit");function P(){return{node:process.version,platform:`${process.platform}-${process.arch}`}}i(P,"buildEnv");function V(o){let n=[...o.files].sort((r,t)=>r.path.localeCompare(t.path)),e={configHash:o.configHash,files:n,gitCommit:o.gitCommit,buildEnv:o.buildEnv};return`${JSON.stringify(e,null,2)}
7
- `}i(V,"serializeManifest");import{createFoundation as J}from"@apollion-dsi/core/themes/foundation";import{createSemantic as go}from"@apollion-dsi/core/themes/semantic";import{defaultInputSpacing as ho}from"@apollion-dsi/core/themes/spacing";import{invertForSurface as yo}from"@apollion-dsi/core/themes/surface";import{converter as lo,formatHex as po,interpolate as fo,parse as _}from"culori";var Zo=lo("oklch");function a(o,n,e){let r=_(o),t=_(n);if(!r||!t)return o;let s=fo([r,t],"oklch");return po(s(e))??o}i(a,"mixOklch");function mo(o){return o?typeof o=="string"?o:o.base:"#000"}i(mo,"coerceToHex");function g(o,n){let e=mo(o),r=n.deepDark,t=n.deepLight;return{base:e,dark:a(e,r,.6),action:a(e,r,.2),active:a(e,t,.6),light:a(e,t,.9)}}i(g,"mountSingleColor");function G(o){let n=o.deepLight,e=o.deepDark;return{0:n,5:a(n,e,.05),10:a(n,e,.1),20:a(n,e,.2),30:a(n,e,.3),40:a(n,e,.4),50:a(n,e,.5),60:a(n,e,.6),70:a(n,e,.7),80:a(n,e,.8),90:a(n,e,.9),100:a(n,e,1)}}i(G,"mountGreyscaleColors");function B(o){let n=o.deepLight,e=o.deepDark,r=o.primary,t=i(f=>1/17*(f-1),"interpolation"),s=a(r,e,.5);return{0:a(s,n,t(18)),5:a(s,n,t(17.5)),10:a(s,n,t(17)),20:a(s,n,t(16)),30:a(s,n,t(15)),40:a(s,n,t(14)),50:a(s,n,t(13)),60:a(s,n,t(12)),70:a(s,n,t(11)),80:a(s,n,t(10)),90:a(s,n,t(9)),100:a(s,n,t(8)),110:a(s,n,t(7)),120:a(s,n,t(6)),130:a(s,n,t(5)),140:a(s,n,t(4)),150:a(s,n,t(3)),160:a(s,n,t(2)),170:s,180:a(s,e,.25)}}i(B,"mountNeutralColors");import{DIMENSION_MULTIPLIERS as uo}from"@apollion-dsi/core/themes/dimension";function H({multiplier:o=.25,alias:n},e){let r=e!==void 0?uo[e]:o;return(...t)=>t.map(s=>{let f=typeof s=="string"?n[s]:s;return`${r*f}rem`}).join(" ")}i(H,"createSpacing");function bo(o){return{baseDark:o.baseDark,baseLight:o.baseLight,deepDark:o.deepDark,deepLight:o.deepLight,transparent:o.transparent??"transparent",neutral:B(o),grayscale:G(o),main:g(o.main,o),opposite:g(o.opposite,o),complementary:g(o.complementary,o),danger:g(o.danger,o),information:g(o.information,o),success:g(o.success,o),warning:g(o.warning,o),primary:g(o.primary,o),secondary:g(o.secondary,o),tertiary:g(o.tertiary,o)}}i(bo,"composeColors");function z(o,n=ho){let e=bo(o.colors),r=H(n,o.dimension),t=go({colors:e,spacing:r}),s=J(t);if(o.surface==="negative"){let l=yo({semantic:t,colors:e},"negative");return J(l.semantic)}return s}i(z,"buildFoundationForVariant");async function X(o){let{config:n,outDir:e,cwd:r=process.cwd(),verbose:t=!1}=o,s=L(n),f=n.output??{};if(s.length===0)throw new Error("apollion-tokens build: no variants (config.brands is empty)");let l=await ko(x(Co(),"apollion-tokens-"));try{let c=[];for(let b of s){let w=z(b),D=M(b);if(f.css){let h=E(w,b),u=`css/${D}.css`;await O(l,u,h),c.push({path:u,sha256:k(h)}),t&&console.log(` \u2713 ${u}`)}if(f.json){let h=F(w,b),u=`json/${D}.json`;await O(l,u,h),c.push({path:u,sha256:k(h)}),t&&console.log(` \u2713 ${u}`)}if(f.ts){let h=R(w,b),u=`ts/${D}.d.ts`;await O(l,u,h),c.push({path:u,sha256:k(h)}),t&&console.log(` \u2713 ${u}`)}}let d={configHash:v(n),files:c,gitCommit:N(r),buildEnv:P()},C=V(d);return await Z(x(l,"manifest.json"),C),await U(e,{recursive:!0,force:!0}),await q(W(e),{recursive:!0}),await $o(l,e),t&&console.log(`\u2713 ${c.length} files \u2192 ${e}`),{manifest:d,outDir:e}}catch(c){throw await U(l,{recursive:!0,force:!0}).catch(()=>{}),c}}i(X,"build");async function K(o){let{config:n,outDir:e}=o,r;try{r=JSON.parse(await xo(x(e,"manifest.json"),"utf8"))}catch{return!1}return r.configHash===v(n)}i(K,"check");async function O(o,n,e){let r=x(o,n);await q(W(r),{recursive:!0}),await Z(r,e)}i(O,"writeAt");import{readFile as Do}from"node:fs/promises";import{resolve as So}from"node:path";import{createContext as Ao,Script as vo}from"node:vm";import{z as p}from"zod";var m=p.string().regex(/^#([0-9a-fA-F]{3}){1,2}$|^transparent$|^rgb\(.*\)$|^oklch\(.*\)$/,{message:'Must be hex, rgb(), oklch(), or "transparent"'}),wo=p.object({baseDark:p.string().optional(),baseLight:p.string().optional(),deepDark:m,deepLight:m,transparent:p.string().optional(),main:m.optional(),opposite:m.optional(),complementary:m.optional(),information:m.optional(),success:m.optional(),danger:m.optional(),warning:m.optional(),primary:m.optional(),secondary:m.optional(),tertiary:m.optional()}).passthrough(),Q=p.object({brands:p.record(p.string(),wo).refine(o=>Object.keys(o).length>0,{message:"apollion.config.mjs: brands must contain at least 1 entry"}),modes:p.array(p.enum(["light","dark"])).optional(),surfaces:p.array(p.enum(["positive","negative"])).optional(),dimensions:p.array(p.enum(["compact","normal","spacious"])).optional(),output:p.object({runtime:p.boolean().optional(),css:p.boolean().optional(),json:p.boolean().optional(),ts:p.boolean().optional()}).passthrough().optional()}).passthrough();var Oo=[/\brequire\s*\(/,/\bimport\s*\(/,/^\s*import\s+/m],To=1e3;var T=class T extends Error{constructor(e,r){super(e);this.configPath=r;this.name="ConfigLoadError"}};i(T,"ConfigLoadError");var y=T;async function Y(o){let n=So(o.configPath),e=await Do(n,"utf8");for(let c of Oo)if(c.test(e))throw new y(`apollion.config: dynamic require/import not allowed (matched ${c}). Declarative config only \u2014 see ADR-006 \xA73.8 B3.`,n);let r=e.replace(/(^|\n)\s*export\s+default\s+/,(c,d)=>`${d}__apollion_config_export = `);if(r===e)throw new y("apollion.config: missing `export default <object>;` \u2014 config must export a default object literal.",n);let t={__apollion_config_export:void 0,console:jo(),Symbol,Number,String,Boolean,Array,Object,JSON,Math,defineConfig:i(c=>c,"defineConfig")},s=Ao(t);try{new vo(r,{filename:n}).runInContext(s,{timeout:To})}catch(c){throw new y(`apollion.config: evaluation failed \u2014 ${c instanceof Error?c.message:String(c)}`,n)}let f=t.__apollion_config_export;if(f===void 0)throw new y("apollion.config: `export default` produced undefined.",n);let l=Q.safeParse(f);if(!l.success){let c=l.error.issues.map(d=>` ${d.path.join(".")||"(root)"}: ${d.message}`).join(`
8
- `);throw new y(`apollion.config: schema validation failed \u2014
9
- ${c}`,n)}return l.data}i(Y,"loadConfig");function jo(){return{log:i((...o)=>console.log("[apollion.config]",...o),"log"),warn:i((...o)=>console.warn("[apollion.config]",...o),"warn"),error:i((...o)=>console.error("[apollion.config]",...o),"error")}}i(jo,"makeReadOnlyConsole");function Io(o){let n=o.slice(2),e=n[0];e!=="build"&&$(`Unknown command "${e??""}". Usage: apollion-tokens build [--check] [--verbose] --config <path> [--out <dir>]`);let r="apollion.config.mjs",t="dist",s=!1,f=!1;for(let l=1;l<n.length;l++){let c=n[l];switch(c){case"--config":r=n[++l]??$("--config needs a path");break;case"--out":t=n[++l]??$("--out needs a path");break;case"--check":s=!0;break;case"--verbose":f=!0;break;default:$(`Unknown flag "${c}"`)}}return{command:e,configPath:r,outDir:t,check:s,verbose:f}}i(Io,"parseArgs");function $(o){console.error(`apollion-tokens: ${o}`),process.exit(2)}i($,"fail");async function Fo(o){let n=j(o);if(n.endsWith(".json")){let e=await Eo(n,"utf8");return JSON.parse(e)}return Y({configPath:n})}i(Fo,"loadConfigFromPath");async function Ro(){let o=Io(process.argv),n=await Fo(o.configPath);if(o.check){await K({config:n,outDir:j(o.outDir)})||(console.error("apollion-tokens: dist/ is stale or missing (configHash mismatch). Re-run `apollion-tokens build` without --check."),process.exit(1)),console.log("apollion-tokens: dist/ is up-to-date with config.");return}let{manifest:e}=await X({config:n,outDir:j(o.outDir),verbose:o.verbose});console.log(`apollion-tokens: wrote ${e.files.length} file(s) \u2014 configHash=${e.configHash.slice(0,12)}\u2026`)}i(Ro,"main");Ro().catch(o=>{console.error(`apollion-tokens: ${o instanceof Error?o.message:String(o)}`),process.exit(1)});
2
+ var Re=Object.defineProperty;var s=(e,o)=>Re(e,"name",{value:o,configurable:!0}),Ie=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(o,n)=>(typeof require<"u"?require:o)[n]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});import{readFile as ho}from"node:fs/promises";import{resolve as P}from"node:path";import{mkdir as pe,mkdtemp as to,readFile as ro,rename as io,rm as ue,writeFile as fe}from"node:fs/promises";import{tmpdir as so}from"node:os";import{dirname as de,join as S}from"node:path";import{defaultInputBorder as A}from"@apollion-dsi/core/themes/border";import{createDeth as xe,levelValues as we}from"@apollion-dsi/core/themes/depth";import{defaultInputFont as b}from"@apollion-dsi/core/themes/font";import{converter as De,formatHex as Ce,parse as Te}from"culori";var Se=De("oklch");function k(e){return typeof e.type=="string"&&"value"in e}s(k,"isToken");function I(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}s(I,"kebab");function H(e){return e.split(".").map(I).join(".")}s(H,"kebabPath");function x(e){return e.kind==="ref"?e.resolved.raw:e.raw}s(x,"rawValue");function C(e,o){return Number(e.toFixed(o))||0}s(C,"round");function F(e){let o=Se(Te(e));if(!o)return{kind:"color",raw:e,components:[0,0,0],hex:"#000000"};let n=[C(o.l,4),C(o.c,4),C(o.h??0,2)],t=Ce(o)??"#000000";return o.alpha!==void 0&&o.alpha!==1?{kind:"color",raw:e,components:n,alpha:C(o.alpha,4),hex:t}:{kind:"color",raw:e,components:n,hex:t}}s(F,"toColorValue");var W=/^(-?(?:\d+\.?\d*|\.\d+))(px|rem)$/;function T(e){let o=W.exec(e.trim());if(o)return{kind:"dimension",raw:e,value:Number(o[1]),unit:o[2]};let n=Number.parseFloat(e);return{kind:"dimension",raw:e,value:Number.isFinite(n)?n:0,unit:"rem"}}s(T,"toDimensionValue");function m(e){return{type:"color",value:F(e)}}s(m,"colorToken");function _(e){let o={};for(let[n,t]of Object.entries(e))o[n]=m(t);return o}s(_,"recordColorGroup");var Ve=["main","opposite","complementary","primary","secondary","tertiary","success","danger","warning","information"];function $e(e){let o={};for(let n of Ve){let t=e[n];o[n]={base:m(t.base),dark:m(t.dark),action:m(t.action),active:m(t.active),light:m(t.light)}}return o.grayscale=_(e.grayscale),o.neutral=_(e.neutral),o.baseDark=m(e.baseDark??"#000000"),o.baseLight=m(e.baseLight??"#ffffff"),o.deepDark=m(e.deepDark??"#000000"),o.deepLight=m(e.deepLight??"#ffffff"),o}s($e,"buildPrimitives");var ve={primary:"color.primary.base",secondary:"color.secondary.base",tertiary:"color.tertiary.base",success:"color.success.base",danger:"color.danger.base",warning:"color.warning.base",info:"color.information.base"},Oe={onPrimary:"color.baseLight",onSecondary:"color.baseLight",onTertiary:"color.baseDark",onSuccess:"color.baseLight",onDanger:"color.baseLight",onWarning:"color.baseDark",onInfo:"color.baseLight"};function je(e,o){let n=e;for(let t of o.split(".").slice(1))if(n&&typeof n=="object"&&t in n)n=n[t];else return;return typeof n=="string"?n:void 0}s(je,"lookupPrimitive");function B(e,o,n){let t={};for(let[i,r]of Object.entries(e)){let a=n[i],l=a?je(o,a):void 0;t[i]=a&&l===r?{type:"color",value:{kind:"ref",path:a,resolved:F(r)}}:m(r)}return t}s(B,"colorRefGroup");function Ae(e){let o={};for(let[n,t]of Object.entries(e))o[n]={type:"dimension",value:T(t)};return o}s(Ae,"dimensionGroup");function Fe(){let e={},o={};for(let[i,r]of Object.entries(A.borderRadius))W.test(r)?e[i]={type:"dimension",value:T(r)}:o[i]=r;let n={};for(let[i,r]of Object.entries(A.borderWidth))n[i]={type:"dimension",value:T(r)};let t={};for(let[i,r]of Object.entries(A.borderStyle))t[i]={type:"strokeStyle",value:{kind:"strokeStyle",raw:r,style:r}};return{group:{radius:e,width:n,style:t},extensions:o}}s(Fe,"buildBorder");function Ee(){let e={};for(let[r,a]of Object.entries(b.fontFamily)){let l=a.replace(/;\s*$/,"").trim();e[r]={type:"fontFamily",value:{kind:"fontFamily",raw:l,family:l}}}let o={};for(let[r,a]of Object.entries(b.fontSize))o[r]={type:"dimension",value:{kind:"dimension",raw:`${a}px`,value:a,unit:"px"}};let n={};for(let[r,a]of Object.entries(b.fontWeight))n[r]={type:"fontWeight",value:{kind:"fontWeight",raw:`${a}`,weight:a}};let t={};for(let[r,a]of Object.entries(b.lineHeight))t[r]={type:"number",value:{kind:"number",raw:`${a}`,number:a}};let i={letterSpacing:b.letterSpacing,textTransform:b.textTransform,fontStyle:b.fontStyle};return{group:{family:e,size:o,weight:n,lineHeight:t},extensions:i}}s(Ee,"buildFont");var Ge=/rgba?\([^)]*\)|#[0-9a-fA-F]+|[a-zA-Z]+$/;function Ne(e){let o=e.trim(),n=o.startsWith("inset");n&&(o=o.slice(5).trim());let t=o.match(Ge),i=t?t[0]:"#000000",a=(t?o.slice(0,t.index).trim():o).split(/\s+/).filter(Boolean),l=s(u=>T(a[u]??"0px"),"dim");return{kind:"shadow",raw:e,shadow:{color:F(i),offsetX:l(0),offsetY:l(1),blur:l(2),spread:l(3),inset:n}}}s(Ne,"parseShadow");function Le(e){let o=xe(e),n={};for(let t of Object.keys(we))n[t]={type:"shadow",value:Ne(o(t))};return n}s(Le,"buildShadow");function J(e,o,n){let t={brand:n.brand,mode:n.mode,surface:n.surface,dimension:n.dimension},i=Fe(),r=Ee(),a={"com.apollion.variant":t,"com.apollion.font":r.extensions};return Object.keys(i.extensions).length>0&&(a["com.apollion.border"]={radius:i.extensions}),{description:`Apollion DS tokens \u2014 DTCG 2025.10. Variant: ${t.brand}/${t.mode}/${t.surface}/${t.dimension}.`,meta:t,primitives:$e(o),groups:{bg:B(e.bg,o,ve),text:B(e.text,o,Oe),spacing:Ae(e.spacing),border:i.group,font:r.group,shadow:Le(o)},extensions:a}}s(J,"buildIR");var Me="/* Generated by apollion-tokens build \u2014 DO NOT EDIT. See apollion.config.mjs. */";function z(e,o,n){for(let[t,i]of Object.entries(e)){let r=[...o,I(t)];k(i)?n.push(` --apollion-${r.join("-")}: ${x(i.value)};`):z(i,r,n)}}s(z,"emitVars");function U(e,o,n){for(let[t,i]of Object.entries(e)){let r=[...o,I(t)];k(i)?i.type==="color"&&n.push(` @property --apollion-${r.join("-")} { syntax: '<color>'; inherits: true; initial-value: ${x(i.value)}; }`):U(i,r,n)}}s(U,"emitProperties");function X(e){let{brand:o,mode:n,surface:t,dimension:i}=e.meta,r=[Me,""];return r.push(`/* Variant: brand=${o} mode=${n} surface=${t} dimension=${i} */`,"",":root {"),z(e.groups,[],r),r.push("}",""),r.push("@supports (background: paint(squircle)) or (color: oklch(0% 0 0)) {"),U(e.groups,[],r),r.push("}",""),r.join(`
3
+ `)}s(X,"renderCss");function Y(e){return e.alpha!==void 0?{colorSpace:"oklch",components:[...e.components],alpha:e.alpha,hex:e.hex}:{colorSpace:"oklch",components:[...e.components],hex:e.hex}}s(Y,"colorValue");function w(e){return{value:e.value,unit:e.unit}}s(w,"dimensionValue");function Pe(e){let o=e.shadow,n={color:Y(o.color),offsetX:w(o.offsetX),offsetY:w(o.offsetY),blur:w(o.blur),spread:w(o.spread)};return o.inset&&(n.inset=!0),n}s(Pe,"shadowValue");function _e(e){switch(e.kind){case"ref":return`{${H(e.path)}}`;case"color":return Y(e);case"dimension":return w(e);case"fontFamily":return e.family;case"fontWeight":return e.weight;case"number":return e.number;case"strokeStyle":return e.style;case"shadow":return Pe(e)}}s(_e,"projectValue");function Be(e){let o={$value:_e(e.value),$type:e.type};return e.description!==void 0&&(o.$description=e.description),e.deprecated!==void 0&&(o.$deprecated=e.deprecated),o}s(Be,"projectToken");function E(e){let o={};for(let[n,t]of Object.entries(e))o[I(n)]=k(t)?Be(t):E(t);return o}s(E,"projectGroup");function q(e){let o={$description:e.description,color:E(e.primitives),...E(e.groups),$extensions:e.extensions};return`${JSON.stringify(o,null,2)}
4
+ `}s(q,"renderJson");var He="// Generated by apollion-tokens build \u2014 DO NOT EDIT. See apollion.config.mjs.";function Z(e,o,n){let t=" ".repeat(o);for(let[i,r]of Object.entries(e))k(r)?n.push(`${t}${JSON.stringify(i)}: ${JSON.stringify(x(r.value))},`):(n.push(`${t}${i}: {`),Z(r,o+1,n),n.push(`${t}},`))}s(Z,"emitGroup");function K(e){let{brand:o,mode:n,surface:t,dimension:i}=e.meta,r=[He,`// Variant: brand=${o} mode=${n} surface=${t} dimension=${i}`,"","export const tokens = {"];return Z(e.groups,1,r),r.push("} as const;","","export type Tokens = typeof tokens;",""),r.join(`
5
+ `)}s(K,"renderTs");var We=["light","dark"],Je=["positive","negative"],ze=["compact","normal","spacious"];function Q(e){let o=e.modes??We,n=e.surfaces??Je,t=e.dimensions??ze,i=Object.entries(e.brands).sort(([a],[l])=>a.localeCompare(l)),r=[];for(let[a,l]of i)for(let u of o)for(let h of n)for(let $ of t)r.push({brand:a,colors:l,mode:u,surface:h,dimension:$});return r}s(Q,"expandVariants");function ee(e){return`${e.brand}.${e.mode}.${e.surface}.${e.dimension}`}s(ee,"variantName");import{createHash as Ue}from"node:crypto";function D(e){return Ue("sha256").update(e).digest("hex")}s(D,"sha256");function N(e){return D(G(e))}s(N,"hashConfig");function G(e){return e===null||typeof e!="object"?JSON.stringify(e):Array.isArray(e)?`[${e.map(G).join(",")}]`:`{${Object.keys(e).sort().map(t=>`${JSON.stringify(t)}:${G(e[t])}`).join(",")}}`}s(G,"stableStringify");function oe(e){try{let{execSync:o}=Ie("node:child_process");return o("git rev-parse --short HEAD",{cwd:e,stdio:["ignore","pipe","ignore"],encoding:"utf8"}).trim()}catch{return""}}s(oe,"detectGitCommit");function ne(){return{node:process.version,platform:`${process.platform}-${process.arch}`}}s(ne,"buildEnv");function te(e){let o=[...e.files].sort((t,i)=>t.path.localeCompare(i.path)),n={configHash:e.configHash,files:o,gitCommit:e.gitCommit,buildEnv:e.buildEnv};return`${JSON.stringify(n,null,2)}
6
+ `}s(te,"serializeManifest");import{createFoundation as ce}from"@apollion-dsi/core/themes/foundation";import{createSemantic as Qe}from"@apollion-dsi/core/themes/semantic";import{defaultInputSpacing as eo}from"@apollion-dsi/core/themes/spacing";import{invertForSurface as oo}from"@apollion-dsi/core/themes/surface";import{converter as Xe,formatHex as Ye,interpolate as qe,parse as re}from"culori";var Bo=Xe("oklch");function c(e,o,n){let t=re(e),i=re(o);if(!t||!i)return e;let r=qe([t,i],"oklch");return Ye(r(n))??e}s(c,"mixOklch");function Ze(e){return e?typeof e=="string"?e:e.base:"#000"}s(Ze,"coerceToHex");function g(e,o){let n=Ze(e),t=o.deepDark,i=o.deepLight;return{base:n,dark:c(n,t,.6),action:c(n,t,.2),active:c(n,i,.6),light:c(n,i,.9)}}s(g,"mountSingleColor");function ie(e){let o=e.deepLight,n=e.deepDark;return{0:o,5:c(o,n,.05),10:c(o,n,.1),20:c(o,n,.2),30:c(o,n,.3),40:c(o,n,.4),50:c(o,n,.5),60:c(o,n,.6),70:c(o,n,.7),80:c(o,n,.8),90:c(o,n,.9),100:c(o,n,1)}}s(ie,"mountGreyscaleColors");function se(e){let o=e.deepLight,n=e.deepDark,t=e.primary,i=s(a=>1/17*(a-1),"interpolation"),r=c(t,n,.5);return{0:c(r,o,i(18)),5:c(r,o,i(17.5)),10:c(r,o,i(17)),20:c(r,o,i(16)),30:c(r,o,i(15)),40:c(r,o,i(14)),50:c(r,o,i(13)),60:c(r,o,i(12)),70:c(r,o,i(11)),80:c(r,o,i(10)),90:c(r,o,i(9)),100:c(r,o,i(8)),110:c(r,o,i(7)),120:c(r,o,i(6)),130:c(r,o,i(5)),140:c(r,o,i(4)),150:c(r,o,i(3)),160:c(r,o,i(2)),170:r,180:c(r,n,.25)}}s(se,"mountNeutralColors");import{DIMENSION_MULTIPLIERS as Ke}from"@apollion-dsi/core/themes/dimension";function ae({multiplier:e=.25,alias:o},n){let t=n!==void 0?Ke[n]:e;return(...i)=>i.map(r=>{let a=typeof r=="string"?o[r]:r;return`${t*a}rem`}).join(" ")}s(ae,"createSpacing");function no(e){return{baseDark:e.baseDark,baseLight:e.baseLight,deepDark:e.deepDark,deepLight:e.deepLight,transparent:e.transparent??"transparent",neutral:se(e),grayscale:ie(e),main:g(e.main,e),opposite:g(e.opposite,e),complementary:g(e.complementary,e),danger:g(e.danger,e),information:g(e.information,e),success:g(e.success,e),warning:g(e.warning,e),primary:g(e.primary,e),secondary:g(e.secondary,e),tertiary:g(e.tertiary,e)}}s(no,"composeColors");function le(e,o=eo){let n=no(e.colors),t=ae(o,e.dimension),i=Qe({colors:n,spacing:t}),r=ce(i);return e.surface==="negative"&&(r=ce(oo({semantic:i,colors:n},"negative").semantic)),{foundation:r,colors:n}}s(le,"buildVariantThemes");async function me(e){let{config:o,outDir:n,cwd:t=process.cwd(),verbose:i=!1}=e,r=Q(o),a=o.output??{};if(r.length===0)throw new Error("apollion-tokens build: no variants (config.brands is empty)");let l=await to(S(so(),"apollion-tokens-"));try{let u=[];for(let v of r){let{foundation:be,colors:ke}=le(v),O=J(be,ke,v),j=ee(v);if(a.css){let y=X(O),d=`css/${j}.css`;await L(l,d,y),u.push({path:d,sha256:D(y)}),i&&console.log(` \u2713 ${d}`)}if(a.json){let y=q(O),d=`json/${j}.json`;await L(l,d,y),u.push({path:d,sha256:D(y)}),i&&console.log(` \u2713 ${d}`)}if(a.ts){let y=K(O),d=`ts/${j}.d.ts`;await L(l,d,y),u.push({path:d,sha256:D(y)}),i&&console.log(` \u2713 ${d}`)}}let h={configHash:N(o),files:u,gitCommit:oe(t),buildEnv:ne()},$=te(h);return await fe(S(l,"manifest.json"),$),await ue(n,{recursive:!0,force:!0}),await pe(de(n),{recursive:!0}),await io(l,n),i&&console.log(`\u2713 ${u.length} files \u2192 ${n}`),{manifest:h,outDir:n}}catch(u){throw await ue(l,{recursive:!0,force:!0}).catch(()=>{}),u}}s(me,"build");async function ge(e){let{config:o,outDir:n}=e,t;try{t=JSON.parse(await ro(S(n,"manifest.json"),"utf8"))}catch{return!1}return t.configHash===N(o)}s(ge,"check");async function L(e,o,n){let t=S(e,o);await pe(de(t),{recursive:!0}),await fe(t,n)}s(L,"writeAt");import{readFile as co}from"node:fs/promises";import{resolve as lo}from"node:path";import{createContext as uo,Script as po}from"node:vm";import{z as p}from"zod";var f=p.string().regex(/^#([0-9a-fA-F]{3}){1,2}$|^transparent$|^rgb\(.*\)$|^oklch\(.*\)$/,{message:'Must be hex, rgb(), oklch(), or "transparent"'}),ao=p.object({baseDark:p.string().optional(),baseLight:p.string().optional(),deepDark:f,deepLight:f,transparent:p.string().optional(),main:f.optional(),opposite:f.optional(),complementary:f.optional(),information:f.optional(),success:f.optional(),danger:f.optional(),warning:f.optional(),primary:f.optional(),secondary:f.optional(),tertiary:f.optional()}).passthrough(),he=p.object({brands:p.record(p.string(),ao).refine(e=>Object.keys(e).length>0,{message:"apollion.config.mjs: brands must contain at least 1 entry"}),modes:p.array(p.enum(["light","dark"])).optional(),surfaces:p.array(p.enum(["positive","negative"])).optional(),dimensions:p.array(p.enum(["compact","normal","spacious"])).optional(),output:p.object({runtime:p.boolean().optional(),css:p.boolean().optional(),json:p.boolean().optional(),ts:p.boolean().optional()}).passthrough().optional()}).passthrough();var fo=[/\brequire\s*\(/,/\bimport\s*\(/,/^\s*import\s+/m],mo=1e3;var M=class M extends Error{constructor(n,t){super(n);this.configPath=t;this.name="ConfigLoadError"}};s(M,"ConfigLoadError");var R=M;async function ye(e){let o=lo(e.configPath),n=await co(o,"utf8");for(let u of fo)if(u.test(n))throw new R(`apollion.config: dynamic require/import not allowed (matched ${u}). Declarative config only \u2014 see ADR-006 \xA73.8 B3.`,o);let t=n.replace(/(^|\n)\s*export\s+default\s+/,(u,h)=>`${h}__apollion_config_export = `);if(t===n)throw new R("apollion.config: missing `export default <object>;` \u2014 config must export a default object literal.",o);let i={__apollion_config_export:void 0,console:go(),Symbol,Number,String,Boolean,Array,Object,JSON,Math,defineConfig:s(u=>u,"defineConfig")},r=uo(i);try{new po(t,{filename:o}).runInContext(r,{timeout:mo})}catch(u){throw new R(`apollion.config: evaluation failed \u2014 ${u instanceof Error?u.message:String(u)}`,o)}let a=i.__apollion_config_export;if(a===void 0)throw new R("apollion.config: `export default` produced undefined.",o);let l=he.safeParse(a);if(!l.success){let u=l.error.issues.map(h=>` ${h.path.join(".")||"(root)"}: ${h.message}`).join(`
7
+ `);throw new R(`apollion.config: schema validation failed \u2014
8
+ ${u}`,o)}return l.data}s(ye,"loadConfig");function go(){return{log:s((...e)=>console.log("[apollion.config]",...e),"log"),warn:s((...e)=>console.warn("[apollion.config]",...e),"warn"),error:s((...e)=>console.error("[apollion.config]",...e),"error")}}s(go,"makeReadOnlyConsole");function yo(e){let o=e.slice(2),n=o[0];n!=="build"&&V(`Unknown command "${n??""}". Usage: apollion-tokens build [--check] [--verbose] --config <path> [--out <dir>]`);let t="apollion.config.mjs",i="dist",r=!1,a=!1;for(let l=1;l<o.length;l++){let u=o[l];switch(u){case"--config":t=o[++l]??V("--config needs a path");break;case"--out":i=o[++l]??V("--out needs a path");break;case"--check":r=!0;break;case"--verbose":a=!0;break;default:V(`Unknown flag "${u}"`)}}return{command:n,configPath:t,outDir:i,check:r,verbose:a}}s(yo,"parseArgs");function V(e){console.error(`apollion-tokens: ${e}`),process.exit(2)}s(V,"fail");async function bo(e){let o=P(e);if(o.endsWith(".json")){let n=await ho(o,"utf8");return JSON.parse(n)}return ye({configPath:o})}s(bo,"loadConfigFromPath");async function ko(){let e=yo(process.argv),o=await bo(e.configPath);if(e.check){await ge({config:o,outDir:P(e.outDir)})||(console.error("apollion-tokens: dist/ is stale or missing (configHash mismatch). Re-run `apollion-tokens build` without --check."),process.exit(1)),console.log("apollion-tokens: dist/ is up-to-date with config.");return}let{manifest:n}=await me({config:o,outDir:P(e.outDir),verbose:e.verbose});console.log(`apollion-tokens: wrote ${n.files.length} file(s) \u2014 configHash=${n.configHash.slice(0,12)}\u2026`)}s(ko,"main");ko().catch(e=>{console.error(`apollion-tokens: ${e instanceof Error?e.message:String(e)}`),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apollion-dsi/tokens",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Apollion Design System — Framework-agnostic design tokens",
5
5
  "homepage": "https://github.com/apollion-ds/apollion",
6
6
  "license": "MIT",
package/src/build.ts CHANGED
@@ -28,9 +28,10 @@ import { renderJson } from './renderers/json';
28
28
  import { renderTs } from './renderers/ts';
29
29
  import type { ApollionConfig, Variant } from './config-schema';
30
30
  import { expandVariants, variantName } from './config-schema';
31
+ import { buildIR } from './ir';
31
32
  import type { BuildManifest, ManifestFileEntry } from './manifest';
32
33
  import { buildEnv, detectGitCommit, hashConfig, serializeManifest, sha256 } from './manifest';
33
- import { buildFoundationForVariant } from './theme-factory';
34
+ import { buildVariantThemes } from './theme-factory';
34
35
 
35
36
  export interface BuildOptions {
36
37
  config: ApollionConfig;
@@ -60,11 +61,12 @@ export async function build(opts: BuildOptions): Promise<BuildResult> {
60
61
  const files: ManifestFileEntry[] = [];
61
62
 
62
63
  for (const variant of variants) {
63
- const foundation = buildFoundationForVariant(variant);
64
+ const { foundation, colors } = buildVariantThemes(variant);
65
+ const ir = buildIR(foundation, colors, variant);
64
66
  const baseName = variantName(variant);
65
67
 
66
68
  if (output.css) {
67
- const content = renderCss(foundation, variant);
69
+ const content = renderCss(ir);
68
70
  const path = `css/${baseName}.css`;
69
71
  await writeAt(tmpRoot, path, content);
70
72
  files.push({ path, sha256: sha256(content) });
@@ -72,7 +74,7 @@ export async function build(opts: BuildOptions): Promise<BuildResult> {
72
74
  }
73
75
 
74
76
  if (output.json) {
75
- const content = renderJson(foundation, variant);
77
+ const content = renderJson(ir);
76
78
  const path = `json/${baseName}.json`;
77
79
  await writeAt(tmpRoot, path, content);
78
80
  files.push({ path, sha256: sha256(content) });
@@ -80,7 +82,7 @@ export async function build(opts: BuildOptions): Promise<BuildResult> {
80
82
  }
81
83
 
82
84
  if (output.ts) {
83
- const content = renderTs(foundation, variant);
85
+ const content = renderTs(ir);
84
86
  const path = `ts/${baseName}.d.ts`;
85
87
  await writeAt(tmpRoot, path, content);
86
88
  files.push({ path, sha256: sha256(content) });
package/src/ir.ts ADDED
@@ -0,0 +1,405 @@
1
+ /**
2
+ * Token Intermediate Representation (IR) — the typed, tool-agnostic model that
3
+ * every renderer (JSON/CSS/TS) projects from.
4
+ *
5
+ * **Why (improve-architecture B3):** the IR centralizes token typing so a
6
+ * renderer is a pure projection — a new token type or output format is added in
7
+ * one place.
8
+ *
9
+ * **Layers:**
10
+ * - `primitives` — the structural palette as the colour SSOT (`color.*`, R4).
11
+ * - `groups` — foundation aliases (`bg`/`text`) + scales (`spacing`) +
12
+ * composite/scale layers (`border`/`font`/`shadow`, S4). Foundation colours
13
+ * reference primitives (`{color.x.y}`); diverging values fall back to inline.
14
+ *
15
+ * **DTCG-expressibility (S4):** values DTCG cannot model (font `letterSpacing`
16
+ * in `em`, `textTransform`, `fontStyle`, border `circular: 100%`) are carried in
17
+ * the document `$extensions` under `com.apollion.*` rather than dropped.
18
+ *
19
+ * **Byte-fidelity:** each value keeps its original resolved CSS string (`raw`)
20
+ * so CSS/TS projections (which resolve references) stay faithful, while JSON
21
+ * emits the DTCG-structured + referenced form.
22
+ *
23
+ * @see renderers/json.ts · renderers/css.ts · renderers/ts.ts
24
+ * @see ADR-006 §3.10 · tech-radar R1/R2 (structured) + R4 (references) + R3/B3
25
+ */
26
+
27
+ import { defaultInputBorder } from '@apollion-dsi/core/themes/border';
28
+ import { createDeth, levelValues } from '@apollion-dsi/core/themes/depth';
29
+ import { defaultInputFont } from '@apollion-dsi/core/themes/font';
30
+ import type { FoundationLayer } from '@apollion-dsi/core/themes/foundation';
31
+ import { converter, formatHex, parse } from 'culori';
32
+
33
+ import type { Variant } from './config-schema';
34
+ import type { ColorsThemeInterface } from './theme-factory';
35
+
36
+ const toOklch = converter('oklch');
37
+
38
+ export type IRTokenType = 'color' | 'dimension' | 'fontFamily' | 'fontWeight' | 'number' | 'strokeStyle' | 'shadow';
39
+
40
+ export interface IRColorValue {
41
+ readonly kind: 'color';
42
+ /** Original resolved CSS color string — preserves CSS/TS byte-fidelity. */
43
+ readonly raw: string;
44
+ /** OKLch components `[L, C, H]` — L ∈ [0,1], C ≥ 0, H in degrees. */
45
+ readonly components: readonly [number, number, number];
46
+ readonly alpha?: number;
47
+ /** sRGB hex fallback (normalized). */
48
+ readonly hex: string;
49
+ }
50
+
51
+ export interface IRDimensionValue {
52
+ readonly kind: 'dimension';
53
+ readonly raw: string;
54
+ readonly value: number;
55
+ readonly unit: 'px' | 'rem';
56
+ }
57
+
58
+ export interface IRFontFamilyValue {
59
+ readonly kind: 'fontFamily';
60
+ readonly raw: string;
61
+ readonly family: string;
62
+ }
63
+
64
+ export interface IRFontWeightValue {
65
+ readonly kind: 'fontWeight';
66
+ readonly raw: string;
67
+ readonly weight: number;
68
+ }
69
+
70
+ export interface IRNumberValue {
71
+ readonly kind: 'number';
72
+ readonly raw: string;
73
+ readonly number: number;
74
+ }
75
+
76
+ export interface IRStrokeStyleValue {
77
+ readonly kind: 'strokeStyle';
78
+ readonly raw: string;
79
+ readonly style: string;
80
+ }
81
+
82
+ export interface IRShadowLayer {
83
+ readonly color: IRColorValue;
84
+ readonly offsetX: IRDimensionValue;
85
+ readonly offsetY: IRDimensionValue;
86
+ readonly blur: IRDimensionValue;
87
+ readonly spread: IRDimensionValue;
88
+ readonly inset: boolean;
89
+ }
90
+
91
+ export interface IRShadowValue {
92
+ readonly kind: 'shadow';
93
+ readonly raw: string;
94
+ readonly shadow: IRShadowLayer;
95
+ }
96
+
97
+ /** A reference to another token (DTCG alias). `resolved` carries the target's
98
+ * value so CSS/TS can project a concrete value without graph traversal. */
99
+ export interface IRReference {
100
+ readonly kind: 'ref';
101
+ readonly path: string;
102
+ readonly resolved: IRColorValue | IRDimensionValue;
103
+ }
104
+
105
+ export type IRValue =
106
+ | IRColorValue
107
+ | IRDimensionValue
108
+ | IRFontFamilyValue
109
+ | IRFontWeightValue
110
+ | IRNumberValue
111
+ | IRStrokeStyleValue
112
+ | IRShadowValue
113
+ | IRReference;
114
+
115
+ export interface IRToken {
116
+ readonly type: IRTokenType;
117
+ readonly value: IRValue;
118
+ readonly description?: string;
119
+ readonly deprecated?: boolean | string;
120
+ }
121
+
122
+ export interface IRGroup {
123
+ readonly [key: string]: IRToken | IRGroup;
124
+ }
125
+
126
+ export interface IRVariantMeta {
127
+ readonly brand: string;
128
+ readonly mode: string;
129
+ readonly surface: string;
130
+ readonly dimension: string;
131
+ }
132
+
133
+ export interface IRDocument {
134
+ readonly description: string;
135
+ readonly meta: IRVariantMeta;
136
+ /** Colour SSOT — structural palette primitives (`color.*`). */
137
+ readonly primitives: IRGroup;
138
+ /** Foundation + scale + composite layers (`bg`/`text`/`spacing`/`border`/`font`/`shadow`). */
139
+ readonly groups: IRGroup;
140
+ readonly extensions: Readonly<Record<string, unknown>>;
141
+ }
142
+
143
+ /** Narrow an IR node to a token (vs a nested group). */
144
+ export function isToken(node: IRToken | IRGroup): node is IRToken {
145
+ return typeof (node as IRToken).type === 'string' && 'value' in node;
146
+ }
147
+
148
+ /** `camelCase` → `kebab-case` token-name normalization (JSON/CSS). */
149
+ export function kebab(key: string): string {
150
+ return key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
151
+ }
152
+
153
+ /** Kebab-case each segment of a dotted IR path (for DTCG `{...}` aliases). */
154
+ export function kebabPath(path: string): string {
155
+ return path.split('.').map(kebab).join('.');
156
+ }
157
+
158
+ /** Concrete value of an IR value — resolving references to their target. */
159
+ export function rawValue(value: IRValue): string {
160
+ return value.kind === 'ref' ? value.resolved.raw : value.raw;
161
+ }
162
+
163
+ function round(value: number, dp: number): number {
164
+ return Number(value.toFixed(dp)) || 0;
165
+ }
166
+
167
+ /** Build an OKLch IR color value from a resolved CSS color string. */
168
+ export function toColorValue(raw: string): IRColorValue {
169
+ const oklch = toOklch(parse(raw));
170
+ if (!oklch) return { kind: 'color', raw, components: [0, 0, 0], hex: '#000000' };
171
+
172
+ const components: readonly [number, number, number] = [round(oklch.l, 4), round(oklch.c, 4), round(oklch.h ?? 0, 2)];
173
+ const hex = formatHex(oklch) ?? '#000000';
174
+
175
+ return oklch.alpha !== undefined && oklch.alpha !== 1
176
+ ? { kind: 'color', raw, components, alpha: round(oklch.alpha, 4), hex }
177
+ : { kind: 'color', raw, components, hex };
178
+ }
179
+
180
+ const DIMENSION_RE = /^(-?(?:\d+\.?\d*|\.\d+))(px|rem)$/;
181
+
182
+ /** Build an IR dimension value from a resolved CSS length string. */
183
+ export function toDimensionValue(raw: string): IRDimensionValue {
184
+ const match = DIMENSION_RE.exec(raw.trim());
185
+ if (match) return { kind: 'dimension', raw, value: Number(match[1]), unit: match[2] as 'px' | 'rem' };
186
+
187
+ const value = Number.parseFloat(raw);
188
+ return { kind: 'dimension', raw, value: Number.isFinite(value) ? value : 0, unit: 'rem' };
189
+ }
190
+
191
+ function colorToken(raw: string): IRToken {
192
+ return { type: 'color', value: toColorValue(raw) };
193
+ }
194
+
195
+ function recordColorGroup(record: Record<string, string>): IRGroup {
196
+ const out: Record<string, IRToken> = {};
197
+ for (const [key, value] of Object.entries(record)) out[key] = colorToken(value);
198
+ return out;
199
+ }
200
+
201
+ const NAMED_PALETTES = [
202
+ 'main',
203
+ 'opposite',
204
+ 'complementary',
205
+ 'primary',
206
+ 'secondary',
207
+ 'tertiary',
208
+ 'success',
209
+ 'danger',
210
+ 'warning',
211
+ 'information',
212
+ ] as const;
213
+
214
+ /** Structural palette → colour primitives (`color.*`). The SSOT layer. */
215
+ function buildPrimitives(colors: ColorsThemeInterface): IRGroup {
216
+ const out: Record<string, IRToken | IRGroup> = {};
217
+ for (const name of NAMED_PALETTES) {
218
+ const palette = colors[name];
219
+ out[name] = {
220
+ base: colorToken(palette.base),
221
+ dark: colorToken(palette.dark),
222
+ action: colorToken(palette.action),
223
+ active: colorToken(palette.active),
224
+ light: colorToken(palette.light),
225
+ };
226
+ }
227
+ out.grayscale = recordColorGroup(colors.grayscale);
228
+ out.neutral = recordColorGroup(colors.neutral);
229
+ out.baseDark = colorToken(colors.baseDark ?? '#000000');
230
+ out.baseLight = colorToken(colors.baseLight ?? '#ffffff');
231
+ out.deepDark = colorToken(colors.deepDark ?? '#000000');
232
+ out.deepLight = colorToken(colors.deepLight ?? '#ffffff');
233
+ return out;
234
+ }
235
+
236
+ /** Foundation role → primitive IR path (static mapping, verified vs core). */
237
+ const BG_REF: Readonly<Record<string, string>> = {
238
+ primary: 'color.primary.base',
239
+ secondary: 'color.secondary.base',
240
+ tertiary: 'color.tertiary.base',
241
+ success: 'color.success.base',
242
+ danger: 'color.danger.base',
243
+ warning: 'color.warning.base',
244
+ info: 'color.information.base',
245
+ };
246
+
247
+ const TEXT_REF: Readonly<Record<string, string>> = {
248
+ onPrimary: 'color.baseLight',
249
+ onSecondary: 'color.baseLight',
250
+ onTertiary: 'color.baseDark',
251
+ onSuccess: 'color.baseLight',
252
+ onDanger: 'color.baseLight',
253
+ onWarning: 'color.baseDark',
254
+ onInfo: 'color.baseLight',
255
+ };
256
+
257
+ /** Navigate the structural palette by IR path (`color.x.y`). */
258
+ function lookupPrimitive(colors: ColorsThemeInterface, path: string): string | undefined {
259
+ let node: unknown = colors;
260
+ for (const segment of path.split('.').slice(1)) {
261
+ if (node && typeof node === 'object' && segment in node) {
262
+ node = (node as Record<string, unknown>)[segment];
263
+ } else {
264
+ return undefined;
265
+ }
266
+ }
267
+ return typeof node === 'string' ? node : undefined;
268
+ }
269
+
270
+ /** Colour layer: reference the primitive when the resolved value matches, else inline. */
271
+ function colorRefGroup(
272
+ layer: Record<string, string>,
273
+ colors: ColorsThemeInterface,
274
+ refMap: Readonly<Record<string, string>>,
275
+ ): IRGroup {
276
+ const out: Record<string, IRToken> = {};
277
+ for (const [key, value] of Object.entries(layer)) {
278
+ const path = refMap[key];
279
+ const primitive = path ? lookupPrimitive(colors, path) : undefined;
280
+ out[key] =
281
+ path && primitive === value
282
+ ? { type: 'color', value: { kind: 'ref', path, resolved: toColorValue(value) } }
283
+ : colorToken(value);
284
+ }
285
+ return out;
286
+ }
287
+
288
+ function dimensionGroup(layer: Record<string, string>): IRGroup {
289
+ const out: Record<string, IRToken> = {};
290
+ for (const [key, value] of Object.entries(layer)) {
291
+ out[key] = { type: 'dimension', value: toDimensionValue(value) };
292
+ }
293
+ return out;
294
+ }
295
+
296
+ /** Border scales: px → dimension, style → strokeStyle. Non-px (e.g. `100%`)
297
+ * goes to `$extensions`. */
298
+ function buildBorder(): { group: IRGroup; extensions: Record<string, string> } {
299
+ const radius: Record<string, IRToken> = {};
300
+ const extRadius: Record<string, string> = {};
301
+ for (const [key, value] of Object.entries(defaultInputBorder.borderRadius)) {
302
+ if (DIMENSION_RE.test(value)) radius[key] = { type: 'dimension', value: toDimensionValue(value) };
303
+ else extRadius[key] = value;
304
+ }
305
+ const width: Record<string, IRToken> = {};
306
+ for (const [key, value] of Object.entries(defaultInputBorder.borderWidth)) {
307
+ width[key] = { type: 'dimension', value: toDimensionValue(value) };
308
+ }
309
+ const style: Record<string, IRToken> = {};
310
+ for (const [key, value] of Object.entries(defaultInputBorder.borderStyle)) {
311
+ style[key] = { type: 'strokeStyle', value: { kind: 'strokeStyle', raw: value, style: value } };
312
+ }
313
+ return { group: { radius, width, style }, extensions: extRadius };
314
+ }
315
+
316
+ /** Font scales: family/size/weight/lineHeight as DTCG types; non-DTCG bits
317
+ * (letterSpacing/textTransform/fontStyle) go to `$extensions`. */
318
+ function buildFont(): { group: IRGroup; extensions: Record<string, unknown> } {
319
+ const family: Record<string, IRToken> = {};
320
+ for (const [key, value] of Object.entries(defaultInputFont.fontFamily)) {
321
+ const clean = value.replace(/;\s*$/, '').trim();
322
+ family[key] = { type: 'fontFamily', value: { kind: 'fontFamily', raw: clean, family: clean } };
323
+ }
324
+ const size: Record<string, IRToken> = {};
325
+ for (const [key, value] of Object.entries(defaultInputFont.fontSize)) {
326
+ size[key] = { type: 'dimension', value: { kind: 'dimension', raw: `${value}px`, value, unit: 'px' } };
327
+ }
328
+ const weight: Record<string, IRToken> = {};
329
+ for (const [key, value] of Object.entries(defaultInputFont.fontWeight)) {
330
+ weight[key] = { type: 'fontWeight', value: { kind: 'fontWeight', raw: `${value}`, weight: value } };
331
+ }
332
+ const lineHeight: Record<string, IRToken> = {};
333
+ for (const [key, value] of Object.entries(defaultInputFont.lineHeight)) {
334
+ lineHeight[key] = { type: 'number', value: { kind: 'number', raw: `${value}`, number: value } };
335
+ }
336
+ const extensions = {
337
+ letterSpacing: defaultInputFont.letterSpacing,
338
+ textTransform: defaultInputFont.textTransform,
339
+ fontStyle: defaultInputFont.fontStyle,
340
+ };
341
+ return { group: { family, size, weight, lineHeight }, extensions };
342
+ }
343
+
344
+ const SHADOW_COLOR_RE = /rgba?\([^)]*\)|#[0-9a-fA-F]+|[a-zA-Z]+$/;
345
+
346
+ /** Parse a CSS box-shadow string into a DTCG-shaped shadow layer. */
347
+ function parseShadow(css: string): IRShadowValue {
348
+ let body = css.trim();
349
+ const inset = body.startsWith('inset');
350
+ if (inset) body = body.slice('inset'.length).trim();
351
+
352
+ const colorMatch = body.match(SHADOW_COLOR_RE);
353
+ const colorStr = colorMatch ? colorMatch[0] : '#000000';
354
+ const lengthsPart = colorMatch ? body.slice(0, colorMatch.index).trim() : body;
355
+ const lengths = lengthsPart.split(/\s+/).filter(Boolean);
356
+ const dim = (i: number): IRDimensionValue => toDimensionValue(lengths[i] ?? '0px');
357
+
358
+ return {
359
+ kind: 'shadow',
360
+ raw: css,
361
+ shadow: { color: toColorValue(colorStr), offsetX: dim(0), offsetY: dim(1), blur: dim(2), spread: dim(3), inset },
362
+ };
363
+ }
364
+
365
+ /** Depth levels → DTCG shadow composites (geometry from core, colour resolved). */
366
+ function buildShadow(colors: ColorsThemeInterface): IRGroup {
367
+ const depth = createDeth(colors as never);
368
+ const out: Record<string, IRToken> = {};
369
+ for (const level of Object.keys(levelValues)) {
370
+ out[level] = { type: 'shadow', value: parseShadow(depth(level as never)) };
371
+ }
372
+ return out;
373
+ }
374
+
375
+ /** Build the IR document for one resolved variant. */
376
+ export function buildIR(foundation: FoundationLayer, colors: ColorsThemeInterface, variant: Variant): IRDocument {
377
+ const meta: IRVariantMeta = {
378
+ brand: variant.brand,
379
+ mode: variant.mode,
380
+ surface: variant.surface,
381
+ dimension: variant.dimension,
382
+ };
383
+ const border = buildBorder();
384
+ const font = buildFont();
385
+
386
+ const extensions: Record<string, unknown> = { 'com.apollion.variant': meta, 'com.apollion.font': font.extensions };
387
+ if (Object.keys(border.extensions).length > 0) {
388
+ extensions['com.apollion.border'] = { radius: border.extensions };
389
+ }
390
+
391
+ return {
392
+ description: `Apollion DS tokens — DTCG 2025.10. Variant: ${meta.brand}/${meta.mode}/${meta.surface}/${meta.dimension}.`,
393
+ meta,
394
+ primitives: buildPrimitives(colors),
395
+ groups: {
396
+ bg: colorRefGroup(foundation.bg as unknown as Record<string, string>, colors, BG_REF),
397
+ text: colorRefGroup(foundation.text as unknown as Record<string, string>, colors, TEXT_REF),
398
+ spacing: dimensionGroup(foundation.spacing as unknown as Record<string, string>),
399
+ border: border.group,
400
+ font: font.group,
401
+ shadow: buildShadow(colors),
402
+ },
403
+ extensions,
404
+ };
405
+ }
@@ -1,64 +1,56 @@
1
1
  /**
2
- * CSS renderer — emit Foundation tokens as CSS custom properties.
2
+ * CSS renderer — project the token IR to CSS custom properties (the resolved
3
+ * consumer surface).
3
4
  *
4
- * Format: `--apollion-{layer}-{role}-{variant}` per ADR-006 §3.3.
5
+ * Format: `--apollion-{group}-{...path}-{token}` per ADR-006 §3.3. References
6
+ * are resolved to literal values and the primitives layer is omitted — CSS
7
+ * stays fully resolved (JSON is the SSOT surface). Nested groups (border/font/
8
+ * shadow) flatten into dashed paths. Color tokens additionally get `@property`
9
+ * declarations (tech-radar R3) wrapped in `@supports`.
5
10
  *
6
- * **tech-radar R3:** also emits `@property` declarations for typed tokens
7
- * (color/length) — enables native CSS transitions (View Transitions API
8
- * friendly). `@property` is browser-supported 84%+ mid-2024; wrapped in
9
- * `@supports` for graceful degradation.
11
+ * Output is deterministic: groups/tokens iterate in IR insertion order.
10
12
  *
11
- * Output is deterministic: keys iterated in insertion order from the
12
- * FoundationLayer object; no timestamps, no run-id.
13
- *
14
- * @see ADR-006 §3.3 + §3.10 + tech-radar R3
15
- * @see PRD-002 §S5
13
+ * @see ../ir.ts (source of truth) · ADR-006 §3.3 + §3.10 · tech-radar R3
16
14
  */
17
15
 
18
- import type { FoundationLayer } from '@apollion-dsi/core/themes/foundation';
19
-
20
- import type { Variant } from '../config-schema';
16
+ import type { IRDocument, IRGroup } from '../ir';
17
+ import { isToken, kebab, rawValue } from '../ir';
21
18
 
22
19
  const HEADER_LINE = '/* Generated by apollion-tokens build — DO NOT EDIT. See apollion.config.mjs. */';
23
20
 
24
- export function renderCss(foundation: FoundationLayer, variant: Variant): string {
25
- const lines: string[] = [HEADER_LINE, ''];
26
-
27
- // Variant marker as a comment (NOT a var — keep cascade clean).
28
- lines.push(
29
- `/* Variant: brand=${variant.brand} mode=${variant.mode} surface=${variant.surface} dimension=${variant.dimension} */`,
30
- '',
31
- ':root {',
32
- );
33
-
34
- // bg layer
35
- for (const [key, value] of Object.entries(foundation.bg)) {
36
- lines.push(` --apollion-bg-${key}: ${value};`);
21
+ function emitVars(group: IRGroup, prefix: string[], lines: string[]): void {
22
+ for (const [key, node] of Object.entries(group)) {
23
+ const path = [...prefix, kebab(key)];
24
+ if (isToken(node)) lines.push(` --apollion-${path.join('-')}: ${rawValue(node.value)};`);
25
+ else emitVars(node, path, lines);
37
26
  }
27
+ }
38
28
 
39
- // text layer
40
- for (const [key, value] of Object.entries(foundation.text)) {
41
- const kebab = key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
42
- lines.push(` --apollion-text-${kebab}: ${value};`);
29
+ function emitProperties(group: IRGroup, prefix: string[], lines: string[]): void {
30
+ for (const [key, node] of Object.entries(group)) {
31
+ const path = [...prefix, kebab(key)];
32
+ if (!isToken(node)) {
33
+ emitProperties(node, path, lines);
34
+ } else if (node.type === 'color') {
35
+ lines.push(
36
+ ` @property --apollion-${path.join('-')} { syntax: '<color>'; inherits: true; initial-value: ${rawValue(node.value)}; }`,
37
+ );
38
+ }
43
39
  }
40
+ }
44
41
 
45
- // spacing layer
46
- for (const [key, value] of Object.entries(foundation.spacing)) {
47
- lines.push(` --apollion-spacing-${key}: ${value};`);
48
- }
42
+ export function renderCss(ir: IRDocument): string {
43
+ const { brand, mode, surface, dimension } = ir.meta;
44
+ const lines: string[] = [HEADER_LINE, ''];
49
45
 
46
+ // Variant marker as a comment (NOT a var — keep cascade clean).
47
+ lines.push(`/* Variant: brand=${brand} mode=${mode} surface=${surface} dimension=${dimension} */`, '', ':root {');
48
+ emitVars(ir.groups, [], lines);
50
49
  lines.push('}', '');
51
50
 
52
- // @property declarations for color tokens — opt-in via @supports
51
+ // @property declarations for color tokens — opt-in via @supports.
53
52
  lines.push('@supports (background: paint(squircle)) or (color: oklch(0% 0 0)) {');
54
- for (const [key, value] of Object.entries(foundation.bg)) {
55
- lines.push(` @property --apollion-bg-${key} { syntax: '<color>'; inherits: true; initial-value: ${value}; }`);
56
- }
57
- for (const key of Object.keys(foundation.text)) {
58
- const kebab = key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
59
- const value = (foundation.text as unknown as Record<string, string>)[key];
60
- lines.push(` @property --apollion-text-${kebab} { syntax: '<color>'; inherits: true; initial-value: ${value}; }`);
61
- }
53
+ emitProperties(ir.groups, [], lines);
62
54
  lines.push('}', '');
63
55
 
64
56
  return lines.join('\n');
@@ -1,81 +1,123 @@
1
1
  /**
2
- * JSON renderer — emit Foundation tokens as DTCG-compliant JSON.
2
+ * JSON renderer — project the token IR to DTCG-compliant JSON.
3
3
  *
4
- * **Spec:** [Design Tokens Community Group Format Module](https://design-tokens.github.io/community-group/format/)
5
- * (W3C draft track, mid-2024).
4
+ * **Spec:** [Design Tokens Format Module 2025.10](https://www.designtokens.org/TR/drafts/format/)
5
+ * (DTCG / W3C Community Group draft track).
6
6
  *
7
- * Each token is a leaf with `$value` + `$type`. Groups nest naturally:
8
- * ```json
9
- * { "bg": { "primary": { "$value": "#003750", "$type": "color" } } }
10
- * ```
7
+ * Pure projection of the IR (`ir.ts`):
8
+ * - `primitives` → the `color` group (structural SSOT) of OKLch objects.
9
+ * - colour/dimension `{ colorSpace, components, hex, alpha? }` / `{ value, unit }`.
10
+ * - references → DTCG aliases, e.g. `"$value": "{color.primary.base}"`.
11
+ * - composites/scales (S4) → `fontFamily`/`fontWeight`/`number`/`strokeStyle`
12
+ * primitives and the `shadow` composite `{ color, offsetX, offsetY, blur, spread, inset? }`.
11
13
  *
12
- * Enables interop with Tokens Studio (Figma), Style Dictionary downstream,
13
- * Specify, and any tool consuming the DTCG spec.
14
+ * Non-DTCG values travel in the document `$extensions` (`com.apollion.*`).
14
15
  *
15
- * @see tech-radar R1 (DTCG compliance)
16
- * @see ADR-006 §3.10 + PRD-002 §S5
16
+ * @see ../ir.ts (source of truth) · tech-radar R1/R2/R3/R4 · ADR-006 §3.10
17
17
  */
18
18
 
19
- import type { FoundationLayer } from '@apollion-dsi/core/themes/foundation';
19
+ import type { IRColorValue, IRDimensionValue, IRDocument, IRGroup, IRShadowValue, IRToken, IRValue } from '../ir';
20
+ import { isToken, kebab, kebabPath } from '../ir';
20
21
 
21
- import type { Variant } from '../config-schema';
22
+ interface DtcgColorValue {
23
+ colorSpace: 'oklch';
24
+ components: number[];
25
+ alpha?: number;
26
+ hex: string;
27
+ }
28
+
29
+ interface DtcgDimensionValue {
30
+ value: number;
31
+ unit: 'px' | 'rem';
32
+ }
22
33
 
23
- interface DtcgToken<T extends string = string> {
24
- $value: string;
25
- $type: T;
34
+ interface DtcgShadowValue {
35
+ color: DtcgColorValue;
36
+ offsetX: DtcgDimensionValue;
37
+ offsetY: DtcgDimensionValue;
38
+ blur: DtcgDimensionValue;
39
+ spread: DtcgDimensionValue;
40
+ inset?: boolean;
41
+ }
42
+
43
+ type DtcgTokenValue = DtcgColorValue | DtcgDimensionValue | DtcgShadowValue | string | number;
44
+
45
+ interface DtcgToken {
46
+ $value: DtcgTokenValue;
47
+ $type: string;
48
+ $description?: string;
49
+ $deprecated?: boolean | string;
26
50
  }
27
51
 
28
52
  interface DtcgGroup {
29
53
  [key: string]: DtcgToken | DtcgGroup;
30
54
  }
31
55
 
32
- export interface DtcgDocument {
33
- $description: string;
34
- bg: DtcgGroup;
35
- text: DtcgGroup;
36
- spacing: DtcgGroup;
37
- /** Variant metadata — non-standard extension under DTCG `$extensions` namespace. */
38
- $extensions: {
39
- 'com.apollion.variant': {
40
- brand: string;
41
- mode: string;
42
- surface: string;
43
- dimension: string;
44
- };
56
+ function colorValue(v: IRColorValue): DtcgColorValue {
57
+ return v.alpha !== undefined
58
+ ? { colorSpace: 'oklch', components: [...v.components], alpha: v.alpha, hex: v.hex }
59
+ : { colorSpace: 'oklch', components: [...v.components], hex: v.hex };
60
+ }
61
+
62
+ function dimensionValue(v: IRDimensionValue): DtcgDimensionValue {
63
+ return { value: v.value, unit: v.unit };
64
+ }
65
+
66
+ function shadowValue(v: IRShadowValue): DtcgShadowValue {
67
+ const s = v.shadow;
68
+ const out: DtcgShadowValue = {
69
+ color: colorValue(s.color),
70
+ offsetX: dimensionValue(s.offsetX),
71
+ offsetY: dimensionValue(s.offsetY),
72
+ blur: dimensionValue(s.blur),
73
+ spread: dimensionValue(s.spread),
45
74
  };
75
+ if (s.inset) out.inset = true;
76
+ return out;
46
77
  }
47
78
 
48
- function toColorGroup(layer: Record<string, string>): DtcgGroup {
49
- const out: DtcgGroup = {};
50
- for (const [key, value] of Object.entries(layer)) {
51
- const kebab = key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
52
- out[kebab] = { $value: value, $type: 'color' };
79
+ function projectValue(value: IRValue): DtcgTokenValue {
80
+ switch (value.kind) {
81
+ case 'ref':
82
+ return `{${kebabPath(value.path)}}`;
83
+ case 'color':
84
+ return colorValue(value);
85
+ case 'dimension':
86
+ return dimensionValue(value);
87
+ case 'fontFamily':
88
+ return value.family;
89
+ case 'fontWeight':
90
+ return value.weight;
91
+ case 'number':
92
+ return value.number;
93
+ case 'strokeStyle':
94
+ return value.style;
95
+ case 'shadow':
96
+ return shadowValue(value);
53
97
  }
98
+ }
99
+
100
+ function projectToken(token: IRToken): DtcgToken {
101
+ const out: DtcgToken = { $value: projectValue(token.value), $type: token.type };
102
+ if (token.description !== undefined) out.$description = token.description;
103
+ if (token.deprecated !== undefined) out.$deprecated = token.deprecated;
54
104
  return out;
55
105
  }
56
106
 
57
- function toDimensionGroup(layer: Record<string, string>): DtcgGroup {
107
+ function projectGroup(group: IRGroup): DtcgGroup {
58
108
  const out: DtcgGroup = {};
59
- for (const [key, value] of Object.entries(layer)) {
60
- out[key] = { $value: value, $type: 'dimension' };
109
+ for (const [key, node] of Object.entries(group)) {
110
+ out[kebab(key)] = isToken(node) ? projectToken(node) : projectGroup(node);
61
111
  }
62
112
  return out;
63
113
  }
64
114
 
65
- export function renderJson(foundation: FoundationLayer, variant: Variant): string {
66
- const doc: DtcgDocument = {
67
- $description: `Apollion DS tokens — DTCG v1. Variant: ${variant.brand}/${variant.mode}/${variant.surface}/${variant.dimension}.`,
68
- bg: toColorGroup(foundation.bg as unknown as Record<string, string>),
69
- text: toColorGroup(foundation.text as unknown as Record<string, string>),
70
- spacing: toDimensionGroup(foundation.spacing as unknown as Record<string, string>),
71
- $extensions: {
72
- 'com.apollion.variant': {
73
- brand: variant.brand,
74
- mode: variant.mode,
75
- surface: variant.surface,
76
- dimension: variant.dimension,
77
- },
78
- },
115
+ export function renderJson(ir: IRDocument): string {
116
+ const doc = {
117
+ $description: ir.description,
118
+ color: projectGroup(ir.primitives),
119
+ ...projectGroup(ir.groups),
120
+ $extensions: ir.extensions,
79
121
  };
80
122
  return `${JSON.stringify(doc, null, 2)}\n`;
81
123
  }
@@ -1,46 +1,45 @@
1
1
  /**
2
- * TypeScript renderer — emit Foundation tokens as `as const` literal.
2
+ * TypeScript renderer — project the token IR to an `as const` literal (the
3
+ * resolved consumer surface).
3
4
  *
4
- * **tech-radar R4:** consumer importing the generated `.d.ts` gets literal
5
- * types (e.g. `tokens.bg.primary: 'oklch(...)'` not `string`) — autocomplete
6
- * + compile-time invariant checks.
5
+ * Consumers importing the generated `.d.ts` get literal types (e.g.
6
+ * `tokens.bg.primary: '#003750'` not `string`) — autocomplete + compile-time
7
+ * invariant checks (tech-radar R4). References resolve to literal values, the
8
+ * primitives layer is omitted, and nested groups (border/font/shadow) nest as
9
+ * nested objects.
7
10
  *
8
- * @see tech-radar R4
9
- * @see ADR-006 §3.10 + PRD-002 §S5
11
+ * @see ../ir.ts (source of truth) · tech-radar R4 · ADR-006 §3.10
10
12
  */
11
13
 
12
- import type { FoundationLayer } from '@apollion-dsi/core/themes/foundation';
13
-
14
- import type { Variant } from '../config-schema';
14
+ import type { IRDocument, IRGroup } from '../ir';
15
+ import { isToken, rawValue } from '../ir';
15
16
 
16
17
  const HEADER_LINE = '// Generated by apollion-tokens build — DO NOT EDIT. See apollion.config.mjs.';
17
18
 
18
- function serializeRecord(record: Record<string, string>, indent: number): string {
19
- const pad = ' '.repeat(indent);
20
- const entries = Object.entries(record).map(
21
- ([key, value]) => `${pad}${JSON.stringify(key)}: ${JSON.stringify(value)},`,
22
- );
23
- return entries.join('\n');
19
+ function emitGroup(group: IRGroup, depth: number, out: string[]): void {
20
+ const pad = ' '.repeat(depth);
21
+ for (const [key, node] of Object.entries(group)) {
22
+ if (isToken(node)) {
23
+ out.push(`${pad}${JSON.stringify(key)}: ${JSON.stringify(rawValue(node.value))},`);
24
+ } else {
25
+ out.push(`${pad}${key}: {`);
26
+ emitGroup(node, depth + 1, out);
27
+ out.push(`${pad}},`);
28
+ }
29
+ }
24
30
  }
25
31
 
26
- export function renderTs(foundation: FoundationLayer, variant: Variant): string {
27
- return [
32
+ export function renderTs(ir: IRDocument): string {
33
+ const { brand, mode, surface, dimension } = ir.meta;
34
+ const out: string[] = [
28
35
  HEADER_LINE,
29
- `// Variant: brand=${variant.brand} mode=${variant.mode} surface=${variant.surface} dimension=${variant.dimension}`,
36
+ `// Variant: brand=${brand} mode=${mode} surface=${surface} dimension=${dimension}`,
30
37
  '',
31
38
  'export const tokens = {',
32
- ' bg: {',
33
- serializeRecord(foundation.bg as unknown as Record<string, string>, 4),
34
- ' },',
35
- ' text: {',
36
- serializeRecord(foundation.text as unknown as Record<string, string>, 4),
37
- ' },',
38
- ' spacing: {',
39
- serializeRecord(foundation.spacing as unknown as Record<string, string>, 4),
40
- ' },',
41
- '} as const;',
42
- '',
43
- 'export type Tokens = typeof tokens;',
44
- '',
45
- ].join('\n');
39
+ ];
40
+
41
+ emitGroup(ir.groups, 1, out);
42
+
43
+ out.push('} as const;', '', 'export type Tokens = typeof tokens;', '');
44
+ return out.join('\n');
46
45
  }
@@ -31,7 +31,14 @@ import { mountGreyscaleColors, mountNeutralColors, mountSingleColor } from './bu
31
31
  import { createSpacing } from './builders/spacing';
32
32
  import type { Variant } from './config-schema';
33
33
 
34
- type ColorsThemeInterface = ReturnType<typeof composeColors>;
34
+ export type ColorsThemeInterface = ReturnType<typeof composeColors>;
35
+
36
+ /** A variant's resolved themes: the consumer Foundation layer + the structural
37
+ * palette (colour primitives / SSOT). */
38
+ export interface VariantThemes {
39
+ foundation: FoundationLayer;
40
+ colors: ColorsThemeInterface;
41
+ }
35
42
 
36
43
  function composeColors(input: Variant['colors']) {
37
44
  return {
@@ -56,25 +63,33 @@ function composeColors(input: Variant['colors']) {
56
63
  }
57
64
 
58
65
  /**
59
- * Build the Foundation layer for one variant. Pure function, deterministic.
66
+ * Build a variant's resolved themes — the Foundation layer plus the structural
67
+ * palette (colour SSOT consumed by the IR for reference emission). Pure +
68
+ * deterministic.
60
69
  */
61
- export function buildFoundationForVariant(
62
- variant: Variant,
63
- spacingInput: SpacingInput = defaultInputSpacing,
64
- ): FoundationLayer {
70
+ export function buildVariantThemes(variant: Variant, spacingInput: SpacingInput = defaultInputSpacing): VariantThemes {
65
71
  const themeColors = composeColors(variant.colors) as unknown as ColorsThemeInterface;
66
72
  const themeSpacing = createSpacing(spacingInput, variant.dimension);
67
73
  const semantic = createSemantic({ colors: themeColors as never, spacing: themeSpacing as never });
68
74
 
69
- const baseFoundation = createFoundation(semantic);
75
+ let foundation = createFoundation(semantic);
70
76
 
71
77
  if (variant.surface === 'negative') {
72
78
  // invertForSurface acts on the full Theme — assemble a minimal shim.
73
79
  // S6 will refactor to accept (semantic, surface) directly.
74
80
  const fullTheme = { semantic, colors: themeColors as never } as never;
75
- const inverted = invertForSurface(fullTheme, 'negative');
76
- return createFoundation(inverted.semantic);
81
+ foundation = createFoundation(invertForSurface(fullTheme, 'negative').semantic);
77
82
  }
78
83
 
79
- return baseFoundation;
84
+ return { foundation, colors: themeColors };
85
+ }
86
+
87
+ /**
88
+ * Build the Foundation layer for one variant. Pure function, deterministic.
89
+ */
90
+ export function buildFoundationForVariant(
91
+ variant: Variant,
92
+ spacingInput: SpacingInput = defaultInputSpacing,
93
+ ): FoundationLayer {
94
+ return buildVariantThemes(variant, spacingInput).foundation;
80
95
  }