@lemmo-lab/tokens 2.0.0 → 2.1.1

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
@@ -1,82 +1,136 @@
1
1
  # @lemmo-lab/tokens
2
2
 
3
- > The **Single Source of Truth** design tokens package for the Lemmo ecosystem. Authored in W3C DTCG format, strictly governed by CI validators, and built for multi-theme consumption.
3
+ > **The Single Source of Truth Design Tokens Package for the Lemmo Ecosystem.**
4
+ > Authored in W3C DTCG format, strictly governed by 9 CI automated rules, and engineered for accessible multi-theme consumption.
5
+
6
+ [![npm version](https://img.shields.io/npm/v/@lemmo-lab/tokens.svg?color=386b00)](https://www.npmjs.com/package/@lemmo-lab/tokens)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
+ [![CI Governance](https://img.shields.io/badge/CI%20Governance-9%20Rules%20Passing-brightgreen.svg)](#-ci-governance-rules)
9
+ [![WCAG AA](https://img.shields.io/badge/Accessibility-WCAG%20AA%20%E2%89%A5%204.5%3A1-success.svg)](#-accessibility--contrast-compliance-wcag-aa)
10
+
11
+ ---
12
+
13
+ ## 📑 Table of Contents
14
+ - [Architecture Overview](#-architecture-overview)
15
+ - [Shipped Themes (Tier 3)](#-shipped-themes-tier-3)
16
+ - [Installation & Quick Start](#-installation--quick-start)
17
+ - [Consuming Tokens](#-consuming-tokens)
18
+ - [1. CSS Custom Properties](#1-css-custom-properties)
19
+ - [2. Multi-Theme Switching](#2-multi-theme-switching)
20
+ - [3. Tailwind CSS Preset](#3-tailwind-css-preset)
21
+ - [4. JavaScript & TypeScript](#4-javascript--typescript)
22
+ - [CLI Tool (@lemmo-lab/tokens-cli)](#-cli-tool-lemmo-labtokens-cli)
23
+ - [Accessibility & Contrast Compliance (WCAG AA)](#-accessibility--contrast-compliance-wcag-aa)
24
+ - [Typography & Persian Script Protection](#-typography--persian-script-protection)
25
+ - [CI Governance Rules](#-ci-governance-rules)
26
+ - [Audit & Parity Guarantee](#-audit--parity-guarantee)
27
+
28
+ ---
29
+
30
+ ## 🌟 Architecture Overview
31
+
32
+ `@lemmo-lab/tokens` adheres strictly to the **Three-Tier Design Tokens Community Group (W3C DTCG)** model:
33
+
34
+ ```
35
+ packages/tokens/src/
36
+ ├── core/ ← Tier 1: Raw Primitives (Palettes, 4px spacing ladder, radius, motion)
37
+ ├── semantic/ ← Tier 2: Semantic Contract (--lemmo-surface-*, --lemmo-text-*, --lemmo-border-*)
38
+ └── themes/ ← Tier 3: Named Theme Presets (Mapping Tier 1 onto Tier 2 contracts)
39
+ ```
40
+
41
+ 1. **Tier 1 — Primitives (`core/`)**: Immutable foundation values. Color palettes (dark, light, neon, midnight, brand, status, alpha), spacing (4px rhythm from `0` to `2400`), typography scales, motion curves, and shadow definitions.
42
+ 2. **Tier 2 — Semantic Contract (`semantic/`)**: Component-facing design tokens. Components never reference raw hex/px values directly; they reference semantic roles (`--lemmo-surface-primary`, `--lemmo-text-primary`, `--lemmo-border-subtle`).
43
+ 3. **Tier 3 — Themes (`themes/`)**: Multi-theme presets that map raw primitives onto the semantic contract. Themes are strictly cosmetic and bounded; they may never override geometric properties like spacing or layout.
4
44
 
5
45
  ---
6
46
 
7
- ## 🌟 Architecture & Multi-Theme System
47
+ ## 🎨 Shipped Themes (Tier 3)
8
48
 
9
- Lemmo adopts a **Three-Tier Token Model**:
10
- 1. **Tier 1 — Core Primitives (`packages/tokens/src/core/`)**: Raw palettes, 4px spacing ladder, typography scales, radius, elevation, motion, breakpoints.
11
- 2. **Tier 2 Semantic Contract (`packages/tokens/src/semantic/`)**: Named component roles (`--surface-primary`, `--text-primary`, `--border-default`, etc.) that alias Tier 1 primitives.
12
- 3. **Tier 3 Themes (`packages/tokens/src/themes/`)**:
13
- - **Default (Dark Theme)**: The default dark theme (`#131517` page, dark elevated surfaces, AA contrast text).
14
- - **Light (Day Theme)**: Fully derived from the dark theme with inverted luminosities, clean off-white canvas (`#f8f9fa`), crisp white cards (`#ffffff`), and dark accessible text (`#131517`).
15
- - **Neon Preset**: High-energy cyberpunk contrast with electric green, neon cyan, and hot magenta accents.
16
- - **Midnight Preset**: OLED minimal pure-black (`#000000`) theme.
49
+ `@lemmo-lab/tokens` ships with 4 production-ready, fully accessible theme presets:
50
+
51
+ | Theme Preset | Environment | Primary Canvas | Elevation / Cards | Key Accent | WCAG Contrast |
52
+ | :--- | :--- | :---: | :---: | :---: | :---: |
53
+ | **`default`** | Dark Mode (Default) | `#131517` | `#1c1e20` / `#23262a` | Lime `#d1fe17` | 14.02:1 (AAA) |
54
+ | **`light`** | Day / Light Mode | `#f8f9fa` | `#ffffff` / `#e9ecef` | LimeContrast `#386b00` | 6.42:1 (AA) |
55
+ | **`neon`** | Cyberpunk Contrast | `#08090a` | `#12151a` / `#181c24` | Electric Green `#00ff66` | 13.51:1 (AAA) |
56
+ | **`midnight`** | OLED Pure Black | `#000000` | `#0c0c0c` / `#161616` | Minimal White `#ffffff` | 21.00:1 (AAA) |
17
57
 
18
58
  ---
19
59
 
20
- ## 📦 Installation & Consumption
60
+ ## 📦 Installation & Quick Start
21
61
 
22
62
  ```bash
23
- pnpm add @lemmo-lab/tokens
24
- # or
63
+ # Using npm
25
64
  npm install @lemmo-lab/tokens
65
+
66
+ # Using pnpm
67
+ pnpm add @lemmo-lab/tokens
68
+
69
+ # Using yarn
70
+ yarn add @lemmo-lab/tokens
26
71
  ```
27
72
 
28
- ### 1. CSS Variables (Default Dark Theme)
29
- Import the core variables in your app's global stylesheet or root entry:
73
+ ---
74
+
75
+ ## 💻 Consuming Tokens
76
+
77
+ ### 1. CSS Custom Properties
78
+
79
+ Import the base CSS bundle in your application's entry point (e.g. `index.css`, `App.css`, or `layout.tsx`):
30
80
 
31
81
  ```css
32
- /* Loads default dark theme and all base tokens */
82
+ /* Loads base variables, typography, font resets, and default dark theme */
33
83
  @import "@lemmo-lab/tokens/css/variables.css";
34
84
  ```
35
85
 
36
- ### 2. Multi-Theme Switching (Light / Neon / Midnight)
86
+ All variables use the official, standardized prefix:
87
+ ```css
88
+ .card {
89
+ background-color: var(--lemmo-surface-primary);
90
+ color: var(--lemmo-text-primary);
91
+ border: 1px solid var(--lemmo-border-subtle);
92
+ border-radius: var(--lemmo-radius-card);
93
+ padding: var(--lemmo-space-400);
94
+ }
95
+ ```
37
96
 
38
- You can import specific themes or the entire theme index:
97
+ ---
98
+
99
+ ### 2. Multi-Theme Switching
100
+
101
+ To enable theme switching, import the modular theme stylesheets:
39
102
 
40
103
  ```css
41
- /* Option A: Import individual themes */
104
+ /* Option A: Import all themes bundled */
105
+ @import "@lemmo-lab/tokens/css/themes";
106
+
107
+ /* Option B: Import individual themes on demand */
42
108
  @import "@lemmo-lab/tokens/css/themes/light.css";
43
109
  @import "@lemmo-lab/tokens/css/themes/neon.css";
44
110
  @import "@lemmo-lab/tokens/css/themes/midnight.css";
45
-
46
- /* Option B: Import all themes at once */
47
- @import "@lemmo-lab/tokens/css/themes";
48
111
  ```
49
112
 
50
- To activate a theme, simply set `data-theme` or a class on `<html>` or any container:
113
+ Activate themes dynamically by setting `data-theme` on the root `<html>`, `<body>`, or any nested container:
51
114
 
52
115
  ```html
53
- <!-- Default (Dark) -->
54
- <html lang="en">
116
+ <!-- Default Dark Mode -->
117
+ <html>
55
118
 
56
- <!-- Day / Light Theme -->
57
- <html lang="en" data-theme="light">
119
+ <!-- Day / Light Mode -->
120
+ <html data-theme="light">
58
121
 
59
- <!-- Neon Theme -->
60
- <html lang="en" data-theme="neon">
122
+ <!-- Neon Preset -->
123
+ <html data-theme="neon">
61
124
 
62
- <!-- Midnight OLED Theme -->
63
- <html lang="en" data-theme="midnight">
125
+ <!-- Midnight OLED Preset -->
126
+ <html data-theme="midnight">
64
127
  ```
65
128
 
66
- ### 3. JavaScript / TypeScript
67
-
68
- ```typescript
69
- import { tokens, themes } from '@lemmo-lab/tokens';
70
-
71
- // Access themes
72
- const darkTheme = themes.default;
73
- const lightTheme = themes.light;
129
+ ---
74
130
 
75
- // Access primitives directly
76
- console.log(tokens.core.spacing[200]); // ".5rem"
77
- ```
131
+ ### 3. Tailwind CSS Preset
78
132
 
79
- ### 4. Tailwind CSS v4 Preset
133
+ `@lemmo-lab/tokens` includes a first-class Tailwind CSS preset compatible with Tailwind v3 and v4:
80
134
 
81
135
  ```javascript
82
136
  // tailwind.config.js
@@ -84,34 +138,109 @@ import lemmoPreset from '@lemmo-lab/tokens/tailwind';
84
138
 
85
139
  export default {
86
140
  presets: [lemmoPreset],
87
- // ...
141
+ content: [
142
+ './src/**/*.{js,jsx,ts,tsx,vue,svelte,html}',
143
+ ],
144
+ // Your customizations...
88
145
  };
89
146
  ```
90
147
 
148
+ You can now use utility classes matching the design system:
149
+ ```html
150
+ <div class="bg-[var(--lemmo-surface-primary)] text-[var(--lemmo-text-primary)] rounded-[var(--lemmo-radius-card)] p-4">
151
+ <button class="bg-[var(--lemmo-interactive-primary-background)] text-[var(--lemmo-interactive-primary-foreground)]">
152
+ Submit
153
+ </button>
154
+ </div>
155
+ ```
156
+
157
+ ---
158
+
159
+ ### 4. JavaScript & TypeScript
160
+
161
+ Strongly typed token definitions for React, Vue, Svelte, or Node.js services:
162
+
163
+ ```typescript
164
+ import { tokens, themes } from '@lemmo-lab/tokens';
165
+
166
+ // Access theme-specific semantic contracts
167
+ const currentTheme = themes.light;
168
+ console.log(currentTheme.color.surface.primary.background); // "#ffffff"
169
+
170
+ // Access primitive scale values
171
+ console.log(tokens.core.spacing[400]); // "1rem"
172
+ console.log(tokens.core.radius.base); // "0.5rem"
173
+ ```
174
+
91
175
  ---
92
176
 
93
- ## 🛡️ CI Governance Rules (AGENTS.md)
177
+ ## 🛠️ CLI Tool (`@lemmo-lab/tokens-cli`)
94
178
 
95
- 1. **Cosmetics vs. Geometry**: Themes may only override visual styles (colors, radius base, shadow, fonts). They may **never** touch geometry/spacing.
96
- 2. **Mandatory Paired Tokens**: Every `*-background` token must have a paired `*-foreground` token.
97
- 3. **Relative Radius**: Only `--radius-base` may be set by themes; `--radius-control`, `--radius-card`, etc. are strictly derived via `calc()`.
98
- 4. **No Orphan Tokens**: Every semantic contract key must exist across all shipped themes.
99
- 5. **DTCG Standard**: All source tokens follow W3C DTCG format (`$type`, `$value`).
179
+ For projects that require standalone extraction, scaffolding, or theme inspection:
100
180
 
101
- To validate all rules locally:
102
181
  ```bash
103
- pnpm validate
182
+ # Launch the interactive setup wizard
183
+ npx @lemmo-lab/tokens-cli install
184
+
185
+ # Install only the Light theme to a specific folder
186
+ npx @lemmo-lab/tokens-cli install --theme light --out ./src/styles/tokens
187
+
188
+ # Compare token differences between two themes
189
+ npx @lemmo-lab/tokens-cli diff default light
190
+
191
+ # Scaffold a new theme adhering to DTCG rules
192
+ npx @lemmo-lab/tokens-cli add-theme ocean
104
193
  ```
105
194
 
106
- To build output tokens:
195
+ ---
196
+
197
+ ## ♿ Accessibility & Contrast Compliance (WCAG AA)
198
+
199
+ - **Lime Light-Theme Contrast Fix**: Traditional brand lime (`#d1fe17`) drops to `1.28:1` on light backgrounds. To guarantee strict accessibility, `@lemmo-lab/tokens` introduces `color.brand.limeContrast` (`#386b00`), providing **6.42:1** contrast against white (`#ffffff`) and **6.09:1** against off-white (`#f8f9fa`), exceeding WCAG AA minimums.
200
+ - **Mandatory Token Pairing**: Every `*-background` token is guaranteed to ship with a paired `*-foreground` token verified to satisfy at least **4.5:1** contrast across all 4 themes.
201
+ - **Delta-E Discriminability (Delta-E >= 2.0)**: Eliminates imperceptible color duplicates (e.g. merging `#131416` into `#131517`).
202
+
203
+ ---
204
+
205
+ ## 🔤 Typography & Persian Script Protection
206
+
207
+ - **Proportional Unitless Line-Heights**: All line-heights are defined as unitless ratios (`1.25`, `1.43`, etc.) ensuring proper scaling when font sizes change.
208
+ - **Persian Glyphic Integrity**: Negative letter-spacing breaks cursive Persian and Arabic connections (Nastaliq & Naskh). In `@lemmo-lab/tokens`, tracking is strictly reset to `letter-spacing: normal` under `:root[lang="fa"], [lang="fa"]`.
209
+ - **Decoupled Display Stack**: Hero font `Oddval` is isolated into `--lemmo-font-display`, preventing unintentional font fallbacks.
210
+
211
+ ---
212
+
213
+ ## 🛡️ CI Governance Rules
214
+
215
+ Every token commit is validated automatically against 9 strict governance rules:
216
+
217
+ 1. **Cosmetics vs. Geometry**: Themes may only override visual aesthetics (color, base radius, shadow, font, motion). Geometry (spacing, layout, width) is strictly forbidden in themes.
218
+ 2. **Mandatory Paired Tokens**: Background tokens must have matching foreground tokens.
219
+ 3. **Relative Radius Scale**: Radius scales must be derived from `--radius-base` via `calc()`.
220
+ 4. **Zero Orphan Tokens**: All Tier 2 semantic keys must resolve in every shipped theme.
221
+ 5. **W3C DTCG Compliance**: All source tokens require `$type` and `$value`.
222
+ 6. **Minimum Contrast**: Paired foreground/background tokens must satisfy WCAG AA contrast (>= 4.5:1).
223
+ 7. **Color Discriminability**: Adjacent palette steps must maintain Delta-E CIE76 >= 2.0.
224
+ 8. **Zero Raw Hex in Gradients**: All 26 gradients must be 100% tokenized via `{...}` aliases per theme.
225
+ 9. **Zero `lemu` Policy**: The legacy prefix `--lemu-*` is permanently eliminated in favor of `--lemmo-*`.
226
+
227
+ Run validator locally:
107
228
  ```bash
108
- pnpm build
229
+ pnpm validate
109
230
  ```
110
231
 
111
232
  ---
112
233
 
113
- ## 📊 System Design Synchronization & Audit
234
+ ## 📊 Audit & Parity Guarantee
235
+
236
+ `@lemmo-lab/tokens` is verified to be in **100% complete synchronization** with `System_design`. All 361 CSS variables, brand gradients, typography scales, and responsive breakpoints match with zero missing tokens and zero drift.
237
+
238
+ For technical deep-dives:
239
+ - [Audit & Parity Report](https://github.com/lemmo-lab/tokens/blob/main/docs/audit-system-design-sync.md)
240
+ - [Executive Architecture Decisions](https://github.com/lemmo-lab/tokens/blob/main/docs/EXECUTIVE-AUDIT-REPORT.md)
241
+
242
+ ---
114
243
 
115
- `@lemmo-lab/tokens` is verified to be in **100% complete synchronization** with `System_design`. All 361 CSS variables, 4 brand fonts, status colors, and responsive scales match with zero discrepancies.
244
+ ## 📄 License
116
245
 
117
- See full report: [docs/audit-system-design-sync.md](docs/audit-system-design-sync.md).
246
+ MIT © [Lemmo Lab](https://github.com/lemmo-lab)
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { printBanner } from '../src/utils/banner.js';
4
+ import { commandList } from '../src/commands/list.js';
5
+ import { commandInstall } from '../src/commands/install.js';
6
+ import { commandAddTheme } from '../src/commands/add-theme.js';
7
+ import { commandDiff } from '../src/commands/diff.js';
8
+
9
+ function printHelp() {
10
+ printBanner();
11
+ console.log(`Usage:
12
+ tokens <command> [options]
13
+ npx @lemmo-lab/tokens-cli <command> [options]
14
+
15
+ Commands:
16
+ install, init, pull Install token styles & themes (Interactive or Flags)
17
+ list, ls List available themes, formats, and token scales
18
+ add-theme <name> Scaffold a new theme adhering to Tier 2 contract
19
+ diff <theme1> <theme2> Compare two theme definitions and display differences
20
+ help Show this help message
21
+
22
+ Options for 'install':
23
+ --interactive, -i Launch the interactive installation wizard
24
+ --theme, -t <names> Target theme(s): default, light, neon, midnight, all
25
+ --format, -f <formats> Output format(s): css, tailwind, js, ts, all
26
+ --out, -o <dir> Destination directory [default: ./tokens]
27
+ --yes, -y Accept all default configuration without prompting
28
+ --dry-run Preview files to be installed without writing
29
+
30
+ Examples:
31
+ # Interactive wizard (Recommended for new projects):
32
+ tokens install
33
+
34
+ # Install only light theme CSS to ./src/styles/tokens:
35
+ tokens install --theme light --out ./src/styles/tokens
36
+
37
+ # Install default & neon themes with Tailwind preset:
38
+ tokens install --theme default,neon --format css,tailwind --out ./tokens
39
+
40
+ # Compare dark (default) and light themes:
41
+ tokens diff default light
42
+ `);
43
+ }
44
+
45
+ function parseArgs(rawArgs) {
46
+ const parsed = {
47
+ command: null,
48
+ target: null,
49
+ target2: null,
50
+ theme: null,
51
+ format: null,
52
+ out: null,
53
+ interactive: false,
54
+ yes: false,
55
+ dryRun: false
56
+ };
57
+
58
+ let i = 0;
59
+ while (i < rawArgs.length) {
60
+ const arg = rawArgs[i];
61
+ if (!arg.startsWith('-')) {
62
+ if (!parsed.command) parsed.command = arg;
63
+ else if (!parsed.target) parsed.target = arg;
64
+ else if (!parsed.target2) parsed.target2 = arg;
65
+ } else if (arg === '--help' || arg === '-h') {
66
+ parsed.command = 'help';
67
+ } else if (arg === '--theme' || arg === '-t') {
68
+ parsed.theme = rawArgs[++i];
69
+ } else if (arg === '--format' || arg === '-f') {
70
+ parsed.format = rawArgs[++i];
71
+ } else if (arg === '--out' || arg === '-o') {
72
+ parsed.out = rawArgs[++i];
73
+ } else if (arg === '--interactive' || arg === '-i') {
74
+ parsed.interactive = true;
75
+ } else if (arg === '--yes' || arg === '-y') {
76
+ parsed.yes = true;
77
+ } else if (arg === '--dry-run') {
78
+ parsed.dryRun = true;
79
+ }
80
+ i++;
81
+ }
82
+ return parsed;
83
+ }
84
+
85
+ async function main() {
86
+ const args = process.argv.slice(2);
87
+ const options = parseArgs(args);
88
+
89
+ switch (options.command) {
90
+ case 'list':
91
+ case 'ls':
92
+ commandList();
93
+ break;
94
+ case 'install':
95
+ case 'init':
96
+ case 'pull':
97
+ await commandInstall(options);
98
+ break;
99
+ case 'add-theme':
100
+ await commandAddTheme(options.target);
101
+ break;
102
+ case 'diff':
103
+ commandDiff(options.target, options.target2);
104
+ break;
105
+ case 'help':
106
+ case '--help':
107
+ case '-h':
108
+ printHelp();
109
+ break;
110
+ default:
111
+ if (!options.command) {
112
+ // If run with no arguments, launch interactive installation
113
+ await commandInstall({ interactive: true });
114
+ } else {
115
+ printHelp();
116
+ }
117
+ break;
118
+ }
119
+ }
120
+
121
+ main().catch(err => {
122
+ console.error('\n❌ CLI Error:', err.message);
123
+ process.exit(1);
124
+ });
@@ -0,0 +1,57 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { printBanner } from '../utils/banner.js';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const CLI_ROOT = path.resolve(__dirname, '../../');
9
+ const REPO_ROOT = path.resolve(CLI_ROOT, '../../');
10
+
11
+ export async function commandAddTheme(themeName) {
12
+ printBanner();
13
+
14
+ if (!themeName) {
15
+ console.error('❌ Please provide a theme name: `tokens add-theme <theme-name>`');
16
+ process.exit(1);
17
+ }
18
+
19
+ const cleanName = themeName.toLowerCase().replace(/[^a-z0-9_-]/g, '');
20
+ const themesDir = path.join(REPO_ROOT, 'packages/tokens/src/themes');
21
+
22
+ if (!fs.existsSync(themesDir)) {
23
+ console.error('❌ Must be executed within the tokens monorepo to scaffold themes.');
24
+ process.exit(1);
25
+ }
26
+
27
+ const newThemePath = path.join(themesDir, `${cleanName}.json`);
28
+ if (fs.existsSync(newThemePath)) {
29
+ console.error(`❌ Theme '${cleanName}' already exists at: ${newThemePath}`);
30
+ process.exit(1);
31
+ }
32
+
33
+ // Load semantic contract template to pre-populate all required contract keys
34
+ const semanticContractPath = path.join(REPO_ROOT, 'packages/tokens/src/semantic/default.json');
35
+ const contract = JSON.parse(fs.readFileSync(semanticContractPath, 'utf8'));
36
+
37
+ // Scaffold theme conforming 100% to governance rules
38
+ const scaffoldTheme = {
39
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
40
+ theme: {
41
+ color: contract.semantic?.color || {},
42
+ gradient: contract.semantic?.gradient || {},
43
+ radius: {
44
+ base: { $type: 'dimension', $value: '0.5rem' }
45
+ }
46
+ }
47
+ };
48
+
49
+ fs.writeFileSync(newThemePath, JSON.stringify(scaffoldTheme, null, 2) + '\n', 'utf8');
50
+
51
+ console.log(`🎉 Theme \x1b[32m${cleanName}\x1b[0m scaffolded successfully!`);
52
+ console.log(`📁 File created: \x1b[36m${newThemePath}\x1b[0m`);
53
+ console.log(`\nNext Steps:`);
54
+ console.log(` 1. Open ${newThemePath} and update Tier 1 alias references.`);
55
+ console.log(` 2. Run \x1b[1mpnpm validate\x1b[0m to ensure it passes all 9 governance rules.`);
56
+ console.log(` 3. Run \x1b[1mpnpm build\x1b[0m to regenerate CSS and Tailwind outputs.`);
57
+ }
@@ -0,0 +1,72 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { printBanner } from '../utils/banner.js';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const CLI_ROOT = path.resolve(__dirname, '../../');
9
+ const REPO_ROOT = path.resolve(CLI_ROOT, '../../');
10
+
11
+ function collectLeafTokens(obj, prefix = '') {
12
+ let res = {};
13
+ for (const [k, v] of Object.entries(obj)) {
14
+ if (k.startsWith('$')) continue;
15
+ const curPath = prefix ? `${prefix}.${k}` : k;
16
+ if (v && typeof v === 'object') {
17
+ if (v.$value !== undefined) {
18
+ res[curPath] = v.$value;
19
+ } else {
20
+ Object.assign(res, collectLeafTokens(v, curPath));
21
+ }
22
+ }
23
+ }
24
+ return res;
25
+ }
26
+
27
+ export function commandDiff(theme1, theme2) {
28
+ printBanner();
29
+
30
+ if (!theme1 || !theme2) {
31
+ console.error('❌ Usage: `tokens diff <theme1> <theme2>` (e.g. `tokens diff default light`)');
32
+ process.exit(1);
33
+ }
34
+
35
+ const themesDir = path.join(REPO_ROOT, 'packages/tokens/src/themes');
36
+ const t1File = path.join(themesDir, `${theme1.toLowerCase()}.json`);
37
+ const t2File = path.join(themesDir, `${theme2.toLowerCase()}.json`);
38
+
39
+ if (!fs.existsSync(t1File)) {
40
+ console.error(`❌ Theme '${theme1}' not found at: ${t1File}`);
41
+ process.exit(1);
42
+ }
43
+ if (!fs.existsSync(t2File)) {
44
+ console.error(`❌ Theme '${theme2}' not found at: ${t2File}`);
45
+ process.exit(1);
46
+ }
47
+
48
+ const t1Data = JSON.parse(fs.readFileSync(t1File, 'utf8'));
49
+ const t2Data = JSON.parse(fs.readFileSync(t2File, 'utf8'));
50
+
51
+ const t1Tokens = collectLeafTokens(t1Data.theme || {});
52
+ const t2Tokens = collectLeafTokens(t2Data.theme || {});
53
+
54
+ const allKeys = Array.from(new Set([...Object.keys(t1Tokens), ...Object.keys(t2Tokens)])).sort();
55
+
56
+ console.log(`📊 Comparing themes: \x1b[33m${theme1}\x1b[0m vs \x1b[36m${theme2}\x1b[0m\n`);
57
+
58
+ let diffCount = 0;
59
+ for (const k of allKeys) {
60
+ const v1 = t1Tokens[k];
61
+ const v2 = t2Tokens[k];
62
+ if (v1 !== v2) {
63
+ diffCount++;
64
+ console.log(` • \x1b[1m${k}\x1b[0m:`);
65
+ console.log(` \x1b[33m${theme1}\x1b[0m: ${v1 || '<undefined>'}`);
66
+ console.log(` \x1b[36m${theme2}\x1b[0m: ${v2 || '<undefined>'}\n`);
67
+ }
68
+ }
69
+
70
+ console.log(`=============================================================`);
71
+ console.log(`Total differences found: ${diffCount} token(s).`);
72
+ }
@@ -0,0 +1,275 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { printBanner } from '../utils/banner.js';
5
+ import { createPromptSession } from '../utils/prompts.js';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const CLI_ROOT = path.resolve(__dirname, '../../');
10
+ const REPO_ROOT = path.resolve(CLI_ROOT, '../../');
11
+
12
+ const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/lemmo-lab/tokens/main/packages/tokens-build/dist';
13
+
14
+ const ALL_THEMES = ['default', 'light', 'neon', 'midnight'];
15
+ const ALL_FORMATS = ['css', 'tailwind', 'js', 'ts'];
16
+
17
+ async function getAssetContent(relPath) {
18
+ // 1. Direct dist folder (when bundled inside @lemmo-lab/tokens package)
19
+ const distDirect = path.resolve(__dirname, '../../../');
20
+ if (fs.existsSync(path.join(distDirect, relPath))) {
21
+ return fs.readFileSync(path.join(distDirect, relPath), 'utf8');
22
+ }
23
+
24
+ // 2. Check local assets dir inside CLI package
25
+ const cliAssetPath = path.join(CLI_ROOT, 'assets', relPath);
26
+ if (fs.existsSync(cliAssetPath)) {
27
+ return fs.readFileSync(cliAssetPath, 'utf8');
28
+ }
29
+
30
+ // 3. Check monorepo dist
31
+ const distPath = path.join(REPO_ROOT, 'packages/tokens-build/dist', relPath);
32
+ if (fs.existsSync(distPath)) {
33
+ return fs.readFileSync(distPath, 'utf8');
34
+ }
35
+
36
+ // 3. Fallback to remote GitHub raw
37
+ const remoteUrl = `${GITHUB_RAW_BASE}/${relPath}`;
38
+ try {
39
+ const res = await fetch(remoteUrl);
40
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
41
+ return await res.text();
42
+ } catch (err) {
43
+ throw new Error(`Failed to load asset ${relPath}: ${err.message}`);
44
+ }
45
+ }
46
+
47
+ export async function commandInstall(options) {
48
+ printBanner();
49
+
50
+ let targetOut = options.out || './tokens';
51
+ let targetThemes = [];
52
+ let targetFormats = [];
53
+ let generateRootIndex = true;
54
+ let isDryRun = options.dryRun || false;
55
+
56
+ // Determine if interactive session is needed
57
+ const isInteractive = options.interactive || (!options.theme && !options.format && !options.yes);
58
+
59
+ if (isInteractive) {
60
+ console.log('🔮 \x1b[1mInteractive Lemmo Tokens Installation Wizard\x1b[0m\n');
61
+ const prompts = await createPromptSession();
62
+
63
+ try {
64
+ // 1. Output directory
65
+ targetOut = await prompts.askText('📁 Where should the tokens be installed?', targetOut);
66
+
67
+ // 2. Format selection
68
+ targetFormats = await prompts.askMultiChoice(
69
+ '📦 Select the formats you want to install:',
70
+ [
71
+ { label: 'CSS Variables (variables.css & theme overrides)', value: 'css' },
72
+ { label: 'Tailwind CSS Preset (tailwind.preset.js)', value: 'tailwind' },
73
+ { label: 'JavaScript / TypeScript Dictionaries (tokens.js & tokens.d.ts)', value: 'js' }
74
+ ],
75
+ [0] // default CSS
76
+ );
77
+
78
+ // 3. Theme selection
79
+ targetThemes = await prompts.askMultiChoice(
80
+ '🎨 Select which theme presets to install:',
81
+ [
82
+ { label: 'All themes (Default Dark, Light Day, Neon, Midnight)', value: 'all' },
83
+ { label: 'Default (Dark theme — #131517 canvas)', value: 'default' },
84
+ { label: 'Light (Day theme — #f8f9fa canvas, high contrast lime)', value: 'light' },
85
+ { label: 'Neon (Cyberpunk high contrast — #00ff66 & #00f0ff)', value: 'neon' },
86
+ { label: 'Midnight (OLED Pure Black — #000000)', value: 'midnight' }
87
+ ],
88
+ [0]
89
+ );
90
+
91
+ if (targetThemes.includes('all')) {
92
+ targetThemes = ['default', 'light', 'neon', 'midnight'];
93
+ }
94
+
95
+ // 4. Generate root index.css
96
+ if (targetFormats.includes('css')) {
97
+ generateRootIndex = await prompts.askConfirm('📄 Generate unified index.css with all selected imports?', true);
98
+ }
99
+
100
+ console.log('\nConfiguration Summary:');
101
+ console.log(` • Output: ${targetOut}`);
102
+ console.log(` • Formats: ${targetFormats.join(', ')}`);
103
+ console.log(` • Themes: ${targetThemes.join(', ')}`);
104
+
105
+ const proceed = await prompts.askConfirm('Proceed with installation?', true);
106
+ if (!proceed) {
107
+ console.log('Installation aborted.');
108
+ prompts.close();
109
+ return;
110
+ }
111
+ } finally {
112
+ prompts.close();
113
+ }
114
+ } else {
115
+ // Non-interactive / CLI flag mode
116
+ if (options.theme) {
117
+ if (options.theme === 'all') {
118
+ targetThemes = ALL_THEMES;
119
+ } else {
120
+ targetThemes = options.theme.split(',').map(s => s.trim().toLowerCase());
121
+ }
122
+ } else {
123
+ targetThemes = ALL_THEMES;
124
+ }
125
+
126
+ if (options.format) {
127
+ if (options.format === 'all') {
128
+ targetFormats = ['css', 'tailwind', 'js'];
129
+ } else {
130
+ targetFormats = options.format.split(',').map(s => s.trim().toLowerCase());
131
+ }
132
+ } else {
133
+ targetFormats = ['css'];
134
+ }
135
+ }
136
+
137
+ // Normalize JS/TS
138
+ if (targetFormats.includes('js') || targetFormats.includes('ts')) {
139
+ if (!targetFormats.includes('js')) targetFormats.push('js');
140
+ if (!targetFormats.includes('ts')) targetFormats.push('ts');
141
+ }
142
+
143
+ console.log(`\n🚀 Installing tokens to: \x1b[36m${path.resolve(process.cwd(), targetOut)}\x1b[0m\n`);
144
+
145
+ const destRoot = path.resolve(process.cwd(), targetOut);
146
+ if (!isDryRun && !fs.existsSync(destRoot)) {
147
+ fs.mkdirSync(destRoot, { recursive: true });
148
+ }
149
+
150
+ const installedFiles = [];
151
+
152
+ // ==========================================================================
153
+ // 1. Install CSS Variables & Selected Themes
154
+ // ==========================================================================
155
+ if (targetFormats.includes('css')) {
156
+ const cssDestDir = path.join(destRoot, 'css');
157
+ const themesDestDir = path.join(cssDestDir, 'themes');
158
+
159
+ if (!isDryRun) {
160
+ fs.mkdirSync(themesDestDir, { recursive: true });
161
+ }
162
+
163
+ // A. Base variables.css (includes Tier 1 primitives, Tier 2 semantic roles, and Default Dark theme)
164
+ const varContent = await getAssetContent('css/variables.css');
165
+ const varDestFile = path.join(cssDestDir, 'variables.css');
166
+ if (!isDryRun) {
167
+ fs.writeFileSync(varDestFile, varContent, 'utf8');
168
+ }
169
+ installedFiles.push({ path: varDestFile, label: 'Base CSS Custom Properties' });
170
+
171
+ // B. Selected Themes
172
+ const themeImports = [];
173
+ themeImports.push(`@import './variables.css';`);
174
+
175
+ for (const t of targetThemes) {
176
+ if (t === 'default') {
177
+ // default theme is baked directly into variables.css :root
178
+ continue;
179
+ }
180
+ const themeFileName = `${t}.css`;
181
+ try {
182
+ const themeContent = await getAssetContent(`css/themes/${themeFileName}`);
183
+ const themeDestFile = path.join(themesDestDir, themeFileName);
184
+ if (!isDryRun) {
185
+ fs.writeFileSync(themeDestFile, themeContent, 'utf8');
186
+ }
187
+ installedFiles.push({ path: themeDestFile, label: `Theme override: ${t}` });
188
+ themeImports.push(`@import './themes/${themeFileName}';`);
189
+ } catch (e) {
190
+ console.warn(` ⚠️ Could not find theme file for: ${t}`);
191
+ }
192
+ }
193
+
194
+ // C. Unified index.css
195
+ if (generateRootIndex) {
196
+ const indexContent = `/**\n * Lemmo Design Tokens — Selected Theme Entry\n * Auto-generated by @lemmo-lab/tokens-cli\n */\n\n` +
197
+ themeImports.join('\n') + '\n';
198
+ const indexDestFile = path.join(cssDestDir, 'index.css');
199
+ if (!isDryRun) {
200
+ fs.writeFileSync(indexDestFile, indexContent, 'utf8');
201
+ }
202
+ installedFiles.push({ path: indexDestFile, label: 'Master CSS Entry Point' });
203
+ }
204
+ }
205
+
206
+ // ==========================================================================
207
+ // 2. Install Tailwind Preset
208
+ // ==========================================================================
209
+ if (targetFormats.includes('tailwind')) {
210
+ const tailwindDestDir = path.join(destRoot, 'tailwind');
211
+ if (!isDryRun) {
212
+ fs.mkdirSync(tailwindDestDir, { recursive: true });
213
+ }
214
+ const twContent = await getAssetContent('tailwind/preset.js');
215
+ const twDestFile = path.join(tailwindDestDir, 'preset.js');
216
+ if (!isDryRun) {
217
+ fs.writeFileSync(twDestFile, twContent, 'utf8');
218
+ }
219
+ installedFiles.push({ path: twDestFile, label: 'Tailwind CSS Preset' });
220
+ }
221
+
222
+ // ==========================================================================
223
+ // 3. Install JavaScript / TypeScript Tokens
224
+ // ==========================================================================
225
+ if (targetFormats.includes('js') || targetFormats.includes('ts')) {
226
+ const jsDestDir = path.join(destRoot, 'js');
227
+ if (!isDryRun) {
228
+ fs.mkdirSync(jsDestDir, { recursive: true });
229
+ }
230
+
231
+ const jsContent = await getAssetContent('js/tokens.js');
232
+ const jsDestFile = path.join(jsDestDir, 'tokens.js');
233
+ if (!isDryRun) {
234
+ fs.writeFileSync(jsDestFile, jsContent, 'utf8');
235
+ }
236
+ installedFiles.push({ path: jsDestFile, label: 'JavaScript ESM Tokens' });
237
+
238
+ const dtsContent = await getAssetContent('js/tokens.d.ts');
239
+ const dtsDestFile = path.join(jsDestDir, 'tokens.d.ts');
240
+ if (!isDryRun) {
241
+ fs.writeFileSync(dtsDestFile, dtsContent, 'utf8');
242
+ }
243
+ installedFiles.push({ path: dtsDestFile, label: 'TypeScript Token Definitions' });
244
+ }
245
+
246
+ // Summary
247
+ console.log('=============================================================');
248
+ console.log(`🎉 \x1b[32mInstallation complete!\x1b[0m ${installedFiles.length} file(s) installed:\n`);
249
+ for (const f of installedFiles) {
250
+ const rel = path.relative(process.cwd(), f.path);
251
+ console.log(` ✓ \x1b[36m${rel}\x1b[0m \x1b[90m(${f.label})\x1b[0m`);
252
+ }
253
+
254
+ console.log('\n💡 \x1b[1mQuick Start Integration Guide:\x1b[0m');
255
+ if (targetFormats.includes('css')) {
256
+ console.log(` In your global CSS file:`);
257
+ console.log(` \x1b[33m@import './${path.posix.join(targetOut, 'css/index.css')}';\x1b[0m\n`);
258
+ console.log(` To activate a theme on a container or page:`);
259
+ for (const t of targetThemes) {
260
+ if (t === 'default') {
261
+ console.log(` <html data-theme="default"> <!-- Dark Mode (Default) -->`);
262
+ } else {
263
+ console.log(` <html data-theme="${t}"> <!-- ${t.toUpperCase()} Mode -->`);
264
+ }
265
+ }
266
+ }
267
+
268
+ if (targetFormats.includes('tailwind')) {
269
+ console.log(`\n In your tailwind.config.js:`);
270
+ console.log(` module.exports = {`);
271
+ console.log(` presets: [require('./${path.posix.join(targetOut, 'tailwind/preset.js')}')],`);
272
+ console.log(` // ...`);
273
+ console.log(` };`);
274
+ }
275
+ }
@@ -0,0 +1,31 @@
1
+ import { printBanner } from '../utils/banner.js';
2
+
3
+ export function commandList() {
4
+ printBanner();
5
+ console.log(`Available Themes & Formats in @lemmo-lab/tokens v2.0.0:\n`);
6
+
7
+ console.log(`🎨 \x1b[1m\x1b[36mShipped Themes (Tier 3 Presets):\x1b[0m`);
8
+ console.log(` 1. \x1b[33mdefault\x1b[0m - Official Dark Mode (#131517 canvas, dark elevated surfaces, WCAG AA contrast)`);
9
+ console.log(` 2. \x1b[33mlight\x1b[0m - Day / Light Mode (#f8f9fa canvas, #ffffff cards, limeContrast #386b00)`);
10
+ console.log(` 3. \x1b[33mneon\x1b[0m - High-energy Cyberpunk preset (electric green #00ff66, cyan #00f0ff glow)`);
11
+ console.log(` 4. \x1b[33mmidnight\x1b[0m - OLED Pure Black minimal theme (#000000 canvas, ultra-high contrast)\n`);
12
+
13
+ console.log(`📦 \x1b[1m\x1b[36mSupported Output Formats:\x1b[0m`);
14
+ console.log(` • \x1b[32mcss\x1b[0m - Standard CSS Custom Properties (--lemmo-*) with responsive themes`);
15
+ console.log(` • \x1b[32mtailwind\x1b[0m - Tailwind CSS v3 & v4 compatible preset (preset.js)`);
16
+ console.log(` • \x1b[32mjs / ts\x1b[0m - Type-safe JavaScript / TypeScript token dictionary\n`);
17
+
18
+ console.log(`📐 \x1b[1m\x1b[36mToken Scales Included:\x1b[0m`);
19
+ console.log(` • Colors: Brand, Surface, Text, Border, Status, Alpha overlays`);
20
+ console.log(` • Gradients: All 26 fully-tokenized gradients`);
21
+ console.log(` • Typography: Satoshi, IRANSans, Morabba, Oddval with unitless leading ratios`);
22
+ console.log(` • Spacing: 4px rhythmic scale (0 to 2400)`);
23
+ console.log(` • Radius: Relative scale computed from base (--radius)`);
24
+ console.log(` • Elevation: Ambient & drop shadows, inner rims, glass reflections`);
25
+ console.log(` • Motion: Fast, Normal, Slow durations and cubic bezier curves\n`);
26
+
27
+ console.log(`Usage:`);
28
+ console.log(` \x1b[1mtokens install\x1b[0m # Interactive setup`);
29
+ console.log(` \x1b[1mtokens install --theme light\x1b[0m # Install only light theme`);
30
+ console.log(` \x1b[1mtokens install --theme neon,midnight\x1b[0m # Install specific themes`);
31
+ }
@@ -0,0 +1,8 @@
1
+ export function printBanner() {
2
+ console.log(`
3
+ ┌─────────────────────────────────────────────────────────────┐
4
+ │ ⚡ Lemmo Design Tokens CLI Tool (v2.0.0) │
5
+ │ Enterprise asset generator, theme installer & synchronizer │
6
+ └─────────────────────────────────────────────────────────────┘
7
+ `);
8
+ }
@@ -0,0 +1,67 @@
1
+ import readline from 'node:readline/promises';
2
+ import { stdin as input, stdout as output } from 'node:process';
3
+
4
+ export async function createPromptSession() {
5
+ const rl = readline.createInterface({ input, output });
6
+
7
+ return {
8
+ async askText(question, defaultValue = '') {
9
+ const promptStr = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
10
+ const answer = await rl.question(promptStr);
11
+ return answer.trim() || defaultValue;
12
+ },
13
+
14
+ async askConfirm(question, defaultYes = true) {
15
+ const hint = defaultYes ? '(Y/n)' : '(y/N)';
16
+ const answer = await rl.question(`${question} ${hint} `);
17
+ const cleaned = answer.trim().toLowerCase();
18
+ if (!cleaned) return defaultYes;
19
+ return cleaned === 'y' || cleaned === 'yes';
20
+ },
21
+
22
+ async askChoice(question, options, defaultIndex = 0) {
23
+ console.log(`\n${question}`);
24
+ options.forEach((opt, idx) => {
25
+ const marker = idx === defaultIndex ? '➔' : ' ';
26
+ console.log(` ${marker} ${idx + 1}) ${opt.label || opt}`);
27
+ });
28
+ const answer = await rl.question(`Select [1-${options.length}] (default: ${defaultIndex + 1}): `);
29
+ const num = parseInt(answer.trim(), 10);
30
+ if (isNaN(num) || num < 1 || num > options.length) {
31
+ return options[defaultIndex].value !== undefined ? options[defaultIndex].value : options[defaultIndex];
32
+ }
33
+ const sel = options[num - 1];
34
+ return sel.value !== undefined ? sel.value : sel;
35
+ },
36
+
37
+ async askMultiChoice(question, options, defaultSelectedIndices = []) {
38
+ console.log(`\n${question} (enter comma-separated numbers, e.g. 1,2 or hit enter for default):`);
39
+ options.forEach((opt, idx) => {
40
+ const isDef = defaultSelectedIndices.includes(idx);
41
+ const marker = isDef ? '✓' : ' ';
42
+ console.log(` [${marker}] ${idx + 1}) ${opt.label || opt}`);
43
+ });
44
+
45
+ const answer = await rl.question(`Select numbers (default: ${defaultSelectedIndices.map(i => i + 1).join(',')}): `);
46
+ const cleaned = answer.trim();
47
+ if (!cleaned) {
48
+ return defaultSelectedIndices.map(i => options[i].value !== undefined ? options[i].value : options[i]);
49
+ }
50
+
51
+ const indices = cleaned
52
+ .split(',')
53
+ .map(s => parseInt(s.trim(), 10) - 1)
54
+ .filter(i => !isNaN(i) && i >= 0 && i < options.length);
55
+
56
+ if (indices.length === 0) {
57
+ return defaultSelectedIndices.map(i => options[i].value !== undefined ? options[i].value : options[i]);
58
+ }
59
+
60
+ return indices.map(i => options[i].value !== undefined ? options[i].value : options[i]);
61
+ },
62
+
63
+ close() {
64
+ rl.close();
65
+ }
66
+ };
67
+ }
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "@lemmo-lab/tokens",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "Single source of truth design tokens for Lemmo products",
5
5
  "type": "module",
6
6
  "main": "./dist/js/tokens.js",
7
7
  "module": "./dist/js/tokens.js",
8
8
  "types": "./dist/js/tokens.d.ts",
9
+ "bin": {
10
+ "lemmo-tokens": "./dist/cli/bin/cli.js",
11
+ "tokens": "./dist/cli/bin/cli.js"
12
+ },
9
13
  "exports": {
10
14
  ".": {
11
15
  "types": "./dist/js/tokens.d.ts",