@lemmo-lab/tokens 2.1.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/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
|
@@ -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.1.
|
|
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",
|