@oneli8/tokens 1.0.0-beta.10

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.
Binary file
Binary file
Binary file
Binary file
package/fonts.css ADDED
@@ -0,0 +1,63 @@
1
+ /* OneLi8 brand faces. Self hosted so the type system works offline and without
2
+ a third party request. Syne and AR One Sans are both SIL OFL 1.1; see
3
+ FONTS-LICENSE.txt. Both are variable: one file carries the whole weight range. */
4
+
5
+ /* Syne · greek */
6
+ @font-face {
7
+ font-family: "Syne";
8
+ font-style: normal;
9
+ font-weight: 400 800;
10
+ font-display: swap;
11
+ src: url("./fonts/syne-greek.woff2") format("woff2");
12
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
13
+ }
14
+
15
+ /* Syne · latin-ext */
16
+ @font-face {
17
+ font-family: "Syne";
18
+ font-style: normal;
19
+ font-weight: 400 800;
20
+ font-display: swap;
21
+ src: url("./fonts/syne-latin-ext.woff2") format("woff2");
22
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
23
+ }
24
+
25
+ /* Syne · latin */
26
+ @font-face {
27
+ font-family: "Syne";
28
+ font-style: normal;
29
+ font-weight: 400 800;
30
+ font-display: swap;
31
+ src: url("./fonts/syne-latin.woff2") format("woff2");
32
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
33
+ }
34
+
35
+ /* AR One Sans · vietnamese */
36
+ @font-face {
37
+ font-family: "AR One Sans";
38
+ font-style: normal;
39
+ font-weight: 400 700;
40
+ font-display: swap;
41
+ src: url("./fonts/ar-one-sans-vietnamese.woff2") format("woff2");
42
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
43
+ }
44
+
45
+ /* AR One Sans · latin-ext */
46
+ @font-face {
47
+ font-family: "AR One Sans";
48
+ font-style: normal;
49
+ font-weight: 400 700;
50
+ font-display: swap;
51
+ src: url("./fonts/ar-one-sans-latin-ext.woff2") format("woff2");
52
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
53
+ }
54
+
55
+ /* AR One Sans · latin */
56
+ @font-face {
57
+ font-family: "AR One Sans";
58
+ font-style: normal;
59
+ font-weight: 400 700;
60
+ font-display: swap;
61
+ src: url("./fonts/ar-one-sans-latin.woff2") format("woff2");
62
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
63
+ }
@@ -0,0 +1,53 @@
1
+ /* OneLi8 override surface.
2
+ Copy what you need into your own stylesheet, after importing the tokens.
3
+ Nothing here is required. Every value below is already set by the system;
4
+ these are the supported places to change it. */
5
+
6
+ :root {
7
+ /* ---- Type ------------------------------------------------------------
8
+ Two brand faces and one shared fallback. Replace a face and the fallback
9
+ still applies, because the fallback is referenced rather than inlined.
10
+ Drop the @oneli8/tokens/fonts.css import if you are supplying your own. */
11
+
12
+ /* --ol8-font-family-display: "Your Display Face", var(--ol8-font-family-fallback); */
13
+ /* --ol8-font-family-functional: "Your Text Face", var(--ol8-font-family-fallback); */
14
+ /* --ol8-font-family-fallback: ui-sans-serif, system-ui, sans-serif; */
15
+
16
+ /* ---- Colour ----------------------------------------------------------
17
+ data-ol8-primary switches between the three built families. For any other
18
+ hue, repoint the eleven primary steps here instead. 47 semantic roles read
19
+ this ramp, so buttons, links, focus, selection and the Gem material all
20
+ follow without touching a component.
21
+
22
+ Keep the tonal relationships: 030 lightest through 930 darkest, with 600
23
+ carrying the default action surface and 120 doing the same job in dark. */
24
+
25
+ /* --ol8-color-primary-030: #f6f2ff; */
26
+ /* --ol8-color-primary-060: #ece4ff; */
27
+ /* --ol8-color-primary-090: #d9c9ff; */
28
+ /* --ol8-color-primary-120: #c0a6ff; */
29
+ /* --ol8-color-primary-240: #a077f2; */
30
+ /* --ol8-color-primary-360: #8250d6; */
31
+ /* --ol8-color-primary-480: #6838b2; */
32
+ /* --ol8-color-primary-600: #542e8e; */
33
+ /* --ol8-color-primary-720: #452771; */
34
+ /* --ol8-color-primary-840: #38225b; */
35
+ /* --ol8-color-primary-930: #221238; */
36
+ }
37
+
38
+ /* Override at :root, not on a subtree.
39
+ A role such as --ol8-color-action-primary-default is declared as
40
+ var(--ol8-color-primary-600) on :root, so CSS substitutes it there and
41
+ descendants inherit an already resolved colour. Re-pointing the ramp deeper
42
+ in the tree changes the ramp but not the roles that read it.
43
+
44
+ Verified: overriding --ol8-color-primary-600 on :root repaints the primary
45
+ action surface. The same override on a wrapper element does nothing.
46
+
47
+ To theme one region, re-declare the roles you want on that region:
48
+
49
+ .marketing-section {
50
+ --ol8-color-action-primary-default: #0f766e;
51
+ --ol8-color-action-primary-hover: #115e59;
52
+ }
53
+ */
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@oneli8/tokens",
3
+ "version": "1.0.0-beta.10",
4
+ "description": "OneLi8 Design System — design tokens compiled from Figma. CSS custom properties and a flat JS object.",
5
+ "keywords": [
6
+ "design-tokens",
7
+ "css-variables",
8
+ "design-system",
9
+ "figma",
10
+ "oneli8"
11
+ ],
12
+ "homepage": "https://github.com/Akshay007ad/OneLi8-Design-System#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/Akshay007ad/OneLi8-Design-System/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/Akshay007ad/OneLi8-Design-System.git",
19
+ "directory": "packages/tokens"
20
+ },
21
+ "license": "Apache-2.0",
22
+ "author": "Akshay Dhore",
23
+ "type": "module",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./build/typescript/index.d.ts",
27
+ "import": "./build/typescript/index.js",
28
+ "default": "./build/typescript/index.js"
29
+ },
30
+ "./css": "./build/css/tokens.css",
31
+ "./fonts.css": "./fonts.css",
32
+ "./fonts/*": "./fonts/*",
33
+ "./overrides.example.css": "./overrides.example.css",
34
+ "./tailwind": "./build/tailwind/preset.mjs",
35
+ "./tailwind.css": "./build/tailwind/theme.css",
36
+ "./figma": "./build/figma/variables.json",
37
+ "./status": "./build/status/components.json",
38
+ "./source": "./src/semantic.json",
39
+ "./package.json": "./package.json",
40
+ "./ai-context.json": "./ai-context.json"
41
+ },
42
+ "types": "./build/typescript/index.d.ts",
43
+ "sideEffects": [
44
+ "./build/css/tokens.css",
45
+ "./fonts.css"
46
+ ],
47
+ "files": [
48
+ "build",
49
+ "src",
50
+ "status",
51
+ "schema",
52
+ "scripts",
53
+ "fonts",
54
+ "fonts.css",
55
+ "overrides.example.css",
56
+ "ai-context.json",
57
+ "PRINCIPLES.md",
58
+ "AGENTS.md",
59
+ "README.md",
60
+ "LICENSE",
61
+ "NOTICE",
62
+ "FONTS-LICENSE.txt"
63
+ ],
64
+ "engines": {
65
+ "node": ">=20"
66
+ },
67
+ "publishConfig": {
68
+ "access": "public"
69
+ }
70
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema":"https://json-schema.org/draft/2020-12/schema",
3
+ "$id":"https://oneli8.org/schema/component-status.schema.json",
4
+ "title":"Oneli8 component status registry",
5
+ "type":"object",
6
+ "required":["version","components"],
7
+ "properties":{
8
+ "$schema":{"type":"string"},
9
+ "generated":{"type":"boolean"},
10
+ "version":{"type":"string","minLength":1},
11
+ "components":{"type":"array","minItems":1,"items":{"$ref":"#/$defs/record"}}
12
+ },
13
+ "additionalProperties":false,
14
+ "$defs":{
15
+ "record":{
16
+ "type":"object",
17
+ "required":["name","status","designerApproved","implementationCertified","contractPath","knownEvidence","missingEvidence"],
18
+ "properties":{
19
+ "name":{"type":"string","minLength":1},
20
+ "status":{"enum":["Stable","Candidate","Labs","Experimental","Deprecated"]},
21
+ "designerApproved":{"type":"boolean"},
22
+ "implementationCertified":{"type":"boolean"},
23
+ "contractPath":{"type":"string","minLength":1},
24
+ "implementationPath":{"type":["string","null"]},
25
+ "figmaComponentKey":{"type":["string","null"]},
26
+ "knownEvidence":{"type":"array","items":{"type":"string"}},
27
+ "missingEvidence":{"type":"array","items":{"type":"string"}}
28
+ },
29
+ "additionalProperties":true
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema":"https://json-schema.org/draft/2020-12/schema",
3
+ "$id":"https://oneli8.org/schema/token-file.schema.json",
4
+ "title":"Oneli8 DTCG-compatible token file",
5
+ "type":"object",
6
+ "properties":{
7
+ "$schema":{"type":"string"},
8
+ "$description":{"type":"string"},
9
+ "$extensions":{"type":"object"}
10
+ },
11
+ "additionalProperties":{"$ref":"#/$defs/node"},
12
+ "$defs":{
13
+ "node":{
14
+ "oneOf":[
15
+ {"$ref":"#/$defs/token"},
16
+ {"type":"object","additionalProperties":{"$ref":"#/$defs/node"}}
17
+ ]
18
+ },
19
+ "token":{
20
+ "type":"object",
21
+ "required":["$type","$value"],
22
+ "properties":{
23
+ "$type":{"type":"string","minLength":1},
24
+ "$value":{},
25
+ "$description":{"type":"string","minLength":1},
26
+ "$extensions":{"type":"object"}
27
+ },
28
+ "additionalProperties":false
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,246 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {fileURLToPath} from "node:url";
4
+
5
+ const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),"..");
6
+ const read=name=>JSON.parse(fs.readFileSync(path.join(root,"src",name),"utf8"));
7
+ const primitive=read("primitive.json");
8
+ const semantic=read("semantic.json");
9
+ const component=read("component.json");
10
+ const themes=read("themes.json");
11
+ const status=JSON.parse(fs.readFileSync(path.join(root,"status/components.json"),"utf8"));
12
+ const sources={primitive,semantic,component};
13
+
14
+ const isToken=value=>value&&typeof value==="object"&&Object.hasOwn(value,"$value");
15
+ const flatten=(node,prefix="",out={})=>{
16
+ for(const [key,value] of Object.entries(node)){
17
+ if(key.startsWith("$"))continue;
18
+ const tokenPath=prefix?`${prefix}.${key}`:key;
19
+ if(isToken(value))out[tokenPath]=value;
20
+ else if(value&&typeof value==="object"&&!Array.isArray(value))flatten(value,tokenPath,out);
21
+ }
22
+ return out;
23
+ };
24
+ const byTier={primitive:flatten(primitive),semantic:flatten(semantic),component:flatten(component)};
25
+ const all={...byTier.primitive,...byTier.semantic,...byTier.component};
26
+ const referencePattern=/^\{([^}]+)\}$/;
27
+
28
+ const resolveValue=(value,stack=[])=>{
29
+ if(typeof value==="string"){
30
+ const match=value.match(referencePattern);
31
+ if(!match)return value;
32
+ const ref=match[1];
33
+ if(!all[ref])throw new Error(`Missing token reference {${ref}}`);
34
+ if(stack.includes(ref))throw new Error(`Circular token reference ${[...stack,ref].join(" -> ")}`);
35
+ return resolveValue(all[ref].$value,[...stack,ref]);
36
+ }
37
+ if(Array.isArray(value))return value.map(item=>resolveValue(item,stack));
38
+ if(value&&typeof value==="object")return Object.fromEntries(Object.entries(value).map(([key,item])=>[key,resolveValue(item,stack)]));
39
+ return value;
40
+ };
41
+
42
+ const approvedDimensions=new Set([0,3,6,9,12,15,18,24,30,36,42,48,60,72,84,96,108]);
43
+ for(const [tokenPath,token] of Object.entries(byTier.primitive)){
44
+ if(JSON.stringify(token.$value).match(/#(?:000|000000|fff|ffffff)(?![0-9a-f])/i))throw new Error(`Pure black or white in ${tokenPath}`);
45
+ if(typeof token.$value==="string"&&referencePattern.test(token.$value))throw new Error(`Primitive token ${tokenPath} cannot reference another tier`);
46
+ if(tokenPath.startsWith("dimension.scale.")&&!approvedDimensions.has(token.$value.value))throw new Error(`Off-scale primitive ${tokenPath}`);
47
+ }
48
+ for(const [tokenPath,token] of Object.entries(byTier.semantic)){
49
+ if(!token.$description)throw new Error(`Public Semantic token ${tokenPath} needs a description`);
50
+ const refs=JSON.stringify(token.$value).match(/\{[a-zA-Z0-9.-]+\}/g)||[];
51
+ for(const raw of refs){const ref=raw.slice(1,-1);if(!byTier.primitive[ref]&&!byTier.semantic[ref])throw new Error(`Semantic token ${tokenPath} has invalid reference ${raw}`)}
52
+ }
53
+ for(const [tokenPath,token] of Object.entries(byTier.component)){
54
+ if(!token.$description)throw new Error(`Public Component token ${tokenPath} needs a description`);
55
+ const refs=JSON.stringify(token.$value).match(/\{[a-zA-Z0-9.-]+\}/g)||[];
56
+ for(const raw of refs){const ref=raw.slice(1,-1);if(!byTier.semantic[ref]&&!byTier.component[ref])throw new Error(`Component token ${tokenPath} has invalid reference ${raw}`)}
57
+ if(token.$type==="color"&&refs.length===0)throw new Error(`Component color token ${tokenPath} must resolve through Semantic color`);
58
+ if(token.$type==="dimension"&&token.$value&&typeof token.$value==="object"&&typeof token.$value.value==="number"&&token.$value.unit&&!token.$extensions?.["oneli8.geometry"]&&!token.$extensions?.["oneli8.exception"])throw new Error(`Literal Component geometry ${tokenPath} needs an approval record`);
59
+ }
60
+ const allowedStatuses=new Set(["Stable","Candidate","Labs","Experimental","Deprecated"]);
61
+ if(!Array.isArray(status.components)||status.components.length===0)throw new Error("Component status registry must contain public records");
62
+ const statusNames=new Set();
63
+ for(const record of status.components){
64
+ for(const field of ["name","status","designerApproved","implementationCertified","contractPath"])if(!Object.hasOwn(record,field))throw new Error(`Component status record is missing ${field}`);
65
+ if(statusNames.has(record.name))throw new Error(`Duplicate component status ${record.name}`);
66
+ statusNames.add(record.name);
67
+ if(!allowedStatuses.has(record.status))throw new Error(`Unsupported component status ${record.status}`);
68
+ if(typeof record.designerApproved!=="boolean"||typeof record.implementationCertified!=="boolean")throw new Error(`Invalid approval flags for ${record.name}`);
69
+ if(!Array.isArray(record.knownEvidence)||!Array.isArray(record.missingEvidence))throw new Error(`Evidence arrays required for ${record.name}`);
70
+ if(record.designerApproved&&record.knownEvidence.length===0)throw new Error(`Designer approval for ${record.name} requires evidence`);
71
+ if(record.implementationCertified&&!record.implementationPath)throw new Error(`Certified implementation for ${record.name} requires a path`);
72
+ const contractFile=path.resolve(root,"..","..",record.contractPath);
73
+ // Standalone token consumers do not receive the full development documentation.
74
+ // Keep repository contract validation on by default; this flag changes no tokens.
75
+ if(!process.argv.includes("--standalone")&&!fs.existsSync(contractFile))throw new Error(`Missing component contract for ${record.name}: ${record.contractPath}`);
76
+ }
77
+ for(const tokenPath of Object.keys(all))resolveValue(all[tokenPath].$value,[tokenPath]);
78
+ for(const axis of ["colorScheme","primaryFamily","typographyViewport"]){
79
+ for(const [modeName,mode] of Object.entries(themes[axis]||{})){
80
+ for(const [tokenPath,token] of Object.entries(mode.overrides||{})){
81
+ if(!all[tokenPath])throw new Error(`Unknown ${axis}/${modeName} override target ${tokenPath}`);
82
+ const refs=JSON.stringify(token.$value).match(/\{[a-zA-Z0-9.-]+\}/g)||[];
83
+ for(const raw of refs){const ref=raw.slice(1,-1);if(!all[ref])throw new Error(`Invalid ${axis}/${modeName} reference ${raw}`)}
84
+ resolveValue(token.$value,[`${axis}.${modeName}.${tokenPath}`]);
85
+ }
86
+ }
87
+ }
88
+
89
+ // Figma is the naming authority. Its variables run compound words together
90
+ // (cutedge, innerwidth, actionprimary), so a camelCase source key must NOT be
91
+ // split on the capital. Only the path separator becomes a hyphen. The authored
92
+ // JSON keeps camelCase for readability; the emitted name matches Figma exactly.
93
+ const kebab=value=>value.replace(/\./g,"-").toLowerCase();
94
+ // Figma is the naming authority, and it says so explicitly: every variable
95
+ // carries a codeSyntax.WEB field. src/figma-code-syntax.txt records that field
96
+ // verbatim. Nothing here infers a name, because Figma is not internally regular
97
+ // enough to infer from — it writes color-icon-button-quiet-hover-surface
98
+ // hyphenated and component-iconbutton-box-compact joined.
99
+ const CONTRACT=new Map(
100
+ fs.readFileSync(path.join(root,"src/figma-code-syntax.txt"),"utf8")
101
+ .split("\n").filter(l=>l && !l.startsWith("#"))
102
+ .map(l=>{const i=l.indexOf("|");return [l.slice(0,i), l.slice(i+1)];})
103
+ );
104
+ const uncontracted=new Set();
105
+ const figmaVarName=tokenPath=>{
106
+ const contracted=CONTRACT.get(tokenPath);
107
+ if(contracted)return contracted.replace(/^--ol8-/,"");
108
+ uncontracted.add(tokenPath);
109
+ return kebab(tokenPath);
110
+ };
111
+ const cssName=tokenPath=>`--ol8-${figmaVarName(tokenPath)}`;
112
+ // A composite role's sub key is a CSS property, not a Figma variable, so it
113
+ // keeps the hyphen: font-weight, line-height, letter-spacing.
114
+ const cssProperty=key=>key.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase();
115
+ const cssCompositeName=(tokenPath,key)=>`--ol8-${figmaVarName(tokenPath)}-${cssProperty(key)}`;
116
+ const unitValue=value=>value&&typeof value==="object"&&typeof value.value==="number"&&value.unit?`${value.value}${value.unit}`:null;
117
+ const hexRgb=value=>{
118
+ const match=typeof value==="string"&&value.match(/^#([0-9a-f]{6})$/i);
119
+ return match?[0,2,4].map(offset=>Number.parseInt(match[1].slice(offset,offset+2),16)):null;
120
+ };
121
+ const alphaColor=value=>{
122
+ if(!value||typeof value!=="object"||typeof value.alpha!=="number")return null;
123
+ const channels=hexRgb(value.color);
124
+ return channels?`rgb(${channels.join(" ")} / ${value.alpha})`:null;
125
+ };
126
+ const shadowValue=value=>{
127
+ if(!Array.isArray(value))return null;
128
+ if(value.length===0)return "none";
129
+ const layers=value.map(layer=>{
130
+ const color=alphaColor({color:layer.color,alpha:layer.alpha});
131
+ const geometry=[layer.offsetX,layer.offsetY,layer.blur,layer.spread].map(unitValue);
132
+ return color&&geometry.every(Boolean)?`${geometry.join(" ")} ${color}`:null;
133
+ });
134
+ return layers.every(Boolean)?layers.join(", "):null;
135
+ };
136
+ const cssScalar=(value,token)=>{
137
+ const unit=unitValue(value);if(unit)return unit;
138
+ // A bare family name is not usable CSS on its own. Quote anything with a
139
+ // space, and resolve brand faces through the shared fallback so a face that
140
+ // is missing, still loading, or deliberately replaced lands somewhere sane.
141
+ if(token?.$type==="fontFamily"){
142
+ const name=String(value);
143
+ const quoted=/[^A-Za-z0-9_-]/.test(name)?`"${name}"`:name;
144
+ return token?.$extensions?.["oneli8.role"]==="brand-face"
145
+ ?`${quoted}, var(--ol8-font-family-fallback)`:name;
146
+ }
147
+ if(token?.$extensions?.["oneli8.unit"]==="em")return `${value}em`;
148
+ if(token?.$type==="color"){const color=alphaColor(value);if(color)return color}
149
+ if(token?.$type==="shadow"){const shadow=shadowValue(value);if(shadow!==null)return shadow}
150
+ if(Array.isArray(value)&&value.length===4&&value.every(item=>typeof item==="number"))return `cubic-bezier(${value.join(", ")})`;
151
+ if(typeof value==="string"||typeof value==="number")return String(value);
152
+ return null;
153
+ };
154
+ const cssLinesFor=(tokenPath,token,{preserveColorReference=false}={})=>{
155
+ const reference=typeof token.$value==="string"?token.$value.match(referencePattern):null;
156
+ if(preserveColorReference&&token.$type==="color"&&reference)return [` ${cssName(tokenPath)}: var(${cssName(reference[1])});`];
157
+ const compositeReference=token.$type==="color"&&token.$value&&typeof token.$value==="object"&&typeof token.$value.color==="string"?token.$value.color.match(referencePattern):null;
158
+ if(preserveColorReference&&compositeReference&&typeof token.$value.alpha==="number")return [` ${cssName(tokenPath)}: color-mix(in srgb, var(${cssName(compositeReference[1])}) ${token.$value.alpha*100}%, transparent);`];
159
+ const value=resolveValue(token.$value,[tokenPath]);
160
+ const scalar=cssScalar(value,token);
161
+ if(scalar!==null)return [` ${cssName(tokenPath)}: ${scalar};`];
162
+ if(value&&typeof value==="object")return Object.entries(value).flatMap(([key,item])=>{
163
+ const rawReference=String(token.$value?.[key]||"").match(referencePattern);
164
+ // Keep the family as a reference rather than baking the resolved name.
165
+ // A composite role that inlines "AR One Sans" cannot follow a replaced
166
+ // brand face; one that points at the family token can.
167
+ if(preserveColorReference&&rawReference&&rawReference[1].startsWith("font.family."))
168
+ return [` ${cssCompositeName(tokenPath,key)}: var(${cssName(rawReference[1])});`];
169
+ const itemScalar=cssScalar(item,all[(rawReference||[])[1]]||token);
170
+ return itemScalar===null?[]:[` ${cssCompositeName(tokenPath,key)}: ${itemScalar};`];
171
+ });
172
+ return [];
173
+ };
174
+ const cssTokens={...byTier.primitive,...byTier.semantic,...byTier.component};
175
+ const css=["/* @generated by @oneli8/tokens — do not edit */",":root {",...Object.entries(cssTokens).flatMap(([p,t])=>cssLinesFor(p,t,{preserveColorReference:true})),"}"];
176
+ for(const [modeName,mode] of Object.entries(themes.colorScheme)){
177
+ if(!Object.keys(mode.overrides).length)continue;
178
+ css.push(``, `[data-ol8-color-scheme="${modeName}"] {`);
179
+ for(const [tokenPath,token] of Object.entries(mode.overrides))css.push(...cssLinesFor(tokenPath,token,{preserveColorReference:true}));
180
+ css.push("}");
181
+ }
182
+ for(const [modeName,mode] of Object.entries(themes.primaryFamily)){
183
+ css.push(``, `[data-ol8-primary="${modeName}"] {`);
184
+ for(const [tokenPath,token] of Object.entries(mode.overrides))css.push(...cssLinesFor(tokenPath,token,{preserveColorReference:true}));
185
+ css.push("}");
186
+ }
187
+ for(const mode of ["medium","expanded"]){
188
+ css.push(``, `[data-ol8-type-mode="${mode}"] {`);
189
+ for(const [tokenPath,token] of Object.entries(themes.typographyViewport[mode].overrides))css.push(...cssLinesFor(tokenPath,token));
190
+ css.push("}");
191
+ }
192
+
193
+ const resolved=Object.fromEntries(Object.entries(all).map(([tokenPath,token])=>[tokenPath,{type:token.$type,value:resolveValue(token.$value,[tokenPath]),description:token.$description||"",tier:byTier.primitive[tokenPath]?"primitive":byTier.semantic[tokenPath]?"semantic":"component"}]));
194
+ const tokenPaths=Object.keys(resolved).sort();
195
+
196
+ const dimensionScale=Object.fromEntries(Object.entries(byTier.primitive).filter(([p])=>p.startsWith("dimension.scale.")).map(([,t])=>{const value=resolveValue(t.$value);return [String(value.value/3),cssScalar(value,t)]}));
197
+ const radii=Object.fromEntries(Object.entries(byTier.semantic).filter(([p])=>p.startsWith("shape.radius.")).map(([p,t])=>[p.split(".").at(-1),cssScalar(resolveValue(t.$value),t)]));
198
+ const borderWidth=Object.fromEntries(Object.entries(byTier.semantic).filter(([p])=>p.startsWith("border.width.")).map(([p,t])=>[p.split(".").at(-1),cssScalar(resolveValue(t.$value),t)]));
199
+ const fontSize=Object.fromEntries(Object.entries(byTier.semantic).filter(([p])=>/^typography\.(display|title|body)\./.test(p)).map(([p,t])=>{const v=resolveValue(t.$value);return [p.replace("typography.","").replace(".","-"),[cssScalar(v.fontSize,t),{lineHeight:cssScalar(v.lineHeight,t),letterSpacing:`${v.letterSpacing}em`,fontWeight:String(v.fontWeight),fontFamily:v.fontFamily}]]}));
200
+ const duration=Object.fromEntries(Object.entries(byTier.primitive).filter(([p])=>p.startsWith("motion.duration.")).map(([p,t])=>[p.split(".").at(-1),cssScalar(resolveValue(t.$value),t)]));
201
+ const easing=Object.fromEntries(Object.entries(byTier.primitive).filter(([p])=>p.startsWith("motion.easing.")).map(([p,t])=>[p.split(".").at(-1),cssScalar(resolveValue(t.$value),t)]));
202
+ const boxShadow=Object.fromEntries(Object.keys(byTier.semantic).filter(p=>p.startsWith("elevation.surface.")).map(p=>[p.split(".").at(-1),`var(${cssName(p)})`]));
203
+ const zIndex=Object.fromEntries(Object.entries(byTier.semantic).filter(([p])=>p.startsWith("layer.stack.")).map(([p,t])=>[p.split(".").at(-1),String(resolveValue(t.$value))]));
204
+ // A Tailwind utility is something a person types, so it stays readable and
205
+ // hyphenated: bg-ol8-action-primary-default. The variable it points at follows
206
+ // Figma. Readable surface, exact underlying name.
207
+ const utilityName=path=>path.replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/\./g,"-").toLowerCase();
208
+ const semanticColors=Object.fromEntries(Object.keys(byTier.semantic).filter(p=>p.startsWith("color.")&&!p.startsWith("color.primary.")).map(p=>[utilityName(p.slice(6)),`var(${cssName(p)})`]));
209
+ const tailwind={theme:{extend:{colors:{ol8:semanticColors},spacing:dimensionScale,borderRadius:radii,borderWidth,boxShadow,zIndex,fontFamily:{display:["Syne","system-ui","sans-serif"],functional:["AR One Sans","system-ui","sans-serif"]},fontWeight:{regular:"450",semibold:"600",bold:"690"},fontSize,transitionDuration:duration,transitionTimingFunction:easing}}};
210
+
211
+ const title=value=>value.replace(/([a-z0-9])([A-Z])/g,"$1 $2").replace(/^./,char=>char.toUpperCase());
212
+ const figmaName=tokenPath=>tokenPath.split(".").map(title).join(" / ");
213
+ const modeData=Object.fromEntries(["colorScheme","primaryFamily","typographyViewport"].map(axis=>[axis,Object.fromEntries(Object.entries(themes[axis]).map(([modeName,mode])=>[modeName,Object.entries(mode.overrides).map(([tokenPath,token])=>({path:tokenPath,name:figmaName(tokenPath),type:token.$type,value:resolveValue(token.$value,[`${axis}.${modeName}.${tokenPath}`]),reference:(typeof token.$value==="string"&&token.$value.match(referencePattern)?.[1])||null,description:token.$description||""}))]))]));
214
+ const figma={generated:true,source:"@oneli8/tokens",collections:{primitive:[],semantic:[],component:[]},typographyStyles:[],elevationStyles:[],modeAxes:themes.$extensions["oneli8.axes"],modes:modeData};
215
+ for(const [tokenPath,record] of Object.entries(resolved)){
216
+ const sourceToken=all[tokenPath];
217
+ const entry={path:tokenPath,name:figmaName(tokenPath),type:record.type,value:record.value,reference:(typeof sourceToken.$value==="string"&&sourceToken.$value.match(referencePattern)?.[1])||null,description:record.description};
218
+ if(record.type==="typography")figma.typographyStyles.push(entry);
219
+ else if(record.type==="shadow")figma.elevationStyles.push(entry);
220
+ else figma.collections[record.tier].push(entry);
221
+ }
222
+
223
+ const ensure=relative=>fs.mkdirSync(path.dirname(path.join(root,relative)),{recursive:true});
224
+ const write=(relative,content)=>{ensure(relative);fs.writeFileSync(path.join(root,relative),content)};
225
+ write("build/css/tokens.css",`${css.join("\n")}\n`);
226
+ write("build/typescript/index.js",`// @generated by @oneli8/tokens — do not edit\nexport const tokens = ${JSON.stringify(resolved,null,2)};\nexport const tokenPaths = ${JSON.stringify(tokenPaths,null,2)};\nexport const themeAxes = ${JSON.stringify(themes.$extensions["oneli8.axes"],null,2)};\nexport const themeModes = ${JSON.stringify(modeData,null,2)};\nexport default tokens;\n`);
227
+ write("build/typescript/index.d.ts",`// @generated by @oneli8/tokens — do not edit\nexport type TokenPath = ${tokenPaths.map(p=>JSON.stringify(p)).join(" | ")};\nexport type ColorScheme = "light" | "dark";\nexport type PrimaryFamily = "blue" | "orange" | "green";\nexport type TypographyViewport = "compact" | "medium" | "expanded";\nexport declare const tokens: Readonly<Record<TokenPath,{readonly type:string;readonly value:unknown;readonly description:string;readonly tier:"primitive"|"semantic"|"component"}>>;\nexport declare const tokenPaths: readonly TokenPath[];\nexport declare const themeAxes: Readonly<Record<string,readonly string[]>>;\nexport declare const themeModes: Readonly<Record<string,unknown>>;\nexport default tokens;\n`);
228
+ write("build/tailwind/preset.mjs",`// @generated by @oneli8/tokens — do not edit\nexport default ${JSON.stringify(tailwind,null,2)};\n`);
229
+ const tw4=["/* @generated Tailwind CSS v4 theme bridge — do not edit */","@theme {",...Object.entries(semanticColors).map(([k,v])=>` --color-ol8-${k}: ${v};`),...Object.entries(dimensionScale).map(([k,v])=>` --spacing-${k}: ${v};`),...Object.entries(radii).map(([k,v])=>` --radius-${k}: ${v};`),...Object.entries(boxShadow).map(([k,v])=>` --shadow-ol8-${k}: ${v};`),...Object.entries(zIndex).map(([k,v])=>` --z-ol8-${k}: ${v};`),` --font-display: "Syne", system-ui, sans-serif;`,` --font-functional: "AR One Sans", system-ui, sans-serif;`,` --font-weight-regular: 450;`,` --font-weight-semibold: 600;`,` --font-weight-bold: 690;`,"}"];
230
+ write("build/tailwind/theme.css",`${tw4.join("\n")}\n`);
231
+ write("build/figma/variables.json",`${JSON.stringify(figma,null,2)}\n`);
232
+ write("build/status/components.json",`${JSON.stringify({...status,$schema:"https://oneli8.org/schema/component-status.schema.json",generated:true},null,2)}\n`);
233
+ write("build/manifest.json",`${JSON.stringify({generated:true,version:"1.0.0-beta.1",counts:{primitive:Object.keys(byTier.primitive).length,semantic:Object.keys(byTier.semantic).length,component:Object.keys(byTier.component).length,componentStatuses:status.components.length,figmaVariables:figma.collections.primitive.length+figma.collections.semantic.length+figma.collections.component.length,figmaTypographyStyles:figma.typographyStyles.length,figmaElevationStyles:figma.elevationStyles.length},outputs:["css/tokens.css","typescript/index.js","typescript/index.d.ts","tailwind/preset.mjs","tailwind/theme.css","figma/variables.json","status/components.json"]},null,2)}\n`);
234
+
235
+ // Drift report, both directions. A token with no contract entry means the name
236
+ // was invented here rather than agreed in Figma. A contract entry with no token
237
+ // means Figma is ahead, which is a reference for what to build next.
238
+ const contractPaths=new Set(CONTRACT.keys());
239
+ const authoredPaths=new Set(Object.keys(all));
240
+ const figmaAhead=[...contractPaths].filter(p=>!authoredPaths.has(p));
241
+ if(uncontracted.size>0){
242
+ console.log(`\u26a0 ${uncontracted.size} token(s) have no Figma codeSyntax; their names are inferred, not agreed:`);
243
+ for(const p of [...uncontracted].sort().slice(0,10))console.log(` \u00b7 ${p}`);
244
+ }
245
+ if(figmaAhead.length>0)console.log(`\u2139 Figma defines ${figmaAhead.length} variables this build does not emit yet (see src/figma-code-syntax.txt)`);
246
+ console.log(`Built ${tokenPaths.length} Oneli8 tokens for CSS, TypeScript, Tailwind, and Figma.`);