@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 +186 -57
- package/dist/cli/bin/cli.js +124 -0
- package/dist/cli/src/commands/add-theme.js +57 -0
- package/dist/cli/src/commands/diff.js +72 -0
- package/dist/cli/src/commands/install.js +275 -0
- package/dist/cli/src/commands/list.js +31 -0
- package/dist/cli/src/utils/banner.js +8 -0
- package/dist/cli/src/utils/prompts.js +67 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,82 +1,136 @@
|
|
|
1
1
|
# @lemmo-lab/tokens
|
|
2
2
|
|
|
3
|
-
> The
|
|
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
|
+
[](https://www.npmjs.com/package/@lemmo-lab/tokens)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
[](#-ci-governance-rules)
|
|
9
|
+
[](#-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
|
-
##
|
|
47
|
+
## 🎨 Shipped Themes (Tier 3)
|
|
8
48
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 &
|
|
60
|
+
## 📦 Installation & Quick Start
|
|
21
61
|
|
|
22
62
|
```bash
|
|
23
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
113
|
+
Activate themes dynamically by setting `data-theme` on the root `<html>`, `<body>`, or any nested container:
|
|
51
114
|
|
|
52
115
|
```html
|
|
53
|
-
<!-- Default
|
|
54
|
-
<html
|
|
116
|
+
<!-- Default Dark Mode -->
|
|
117
|
+
<html>
|
|
55
118
|
|
|
56
|
-
<!-- Day / Light
|
|
57
|
-
<html
|
|
119
|
+
<!-- Day / Light Mode -->
|
|
120
|
+
<html data-theme="light">
|
|
58
121
|
|
|
59
|
-
<!-- Neon
|
|
60
|
-
<html
|
|
122
|
+
<!-- Neon Preset -->
|
|
123
|
+
<html data-theme="neon">
|
|
61
124
|
|
|
62
|
-
<!-- Midnight OLED
|
|
63
|
-
<html
|
|
125
|
+
<!-- Midnight OLED Preset -->
|
|
126
|
+
<html data-theme="midnight">
|
|
64
127
|
```
|
|
65
128
|
|
|
66
|
-
|
|
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
|
-
|
|
76
|
-
console.log(tokens.core.spacing[200]); // ".5rem"
|
|
77
|
-
```
|
|
131
|
+
### 3. Tailwind CSS Preset
|
|
78
132
|
|
|
79
|
-
|
|
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
|
-
##
|
|
177
|
+
## 🛠️ CLI Tool (`@lemmo-lab/tokens-cli`)
|
|
94
178
|
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
229
|
+
pnpm validate
|
|
109
230
|
```
|
|
110
231
|
|
|
111
232
|
---
|
|
112
233
|
|
|
113
|
-
## 📊
|
|
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
|
-
|
|
244
|
+
## 📄 License
|
|
116
245
|
|
|
117
|
-
|
|
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.
|
|
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",
|