vibe-ui-suite 4.0.0 → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -77,7 +77,7 @@ In your global stylesheet (e.g. `app/globals.css` or `src/index.css`):
77
77
 
78
78
  Zero JavaScript configuration files required. Instantly unlocks:
79
79
  - **Semantic Color Utilities:** `bg-vibe-canvas`, `bg-vibe-surface`, `text-vibe-primary`, `border-vibe-border`
80
- - **Physics Springs:** `transition-vibe-spring`, `transition-vibe-snap`, `transition-vibe-glide`
80
+ - **Physics Springs:** `ease-vibe-spring`, `ease-vibe-snap`, `ease-vibe-glide` (or utility `.vibe-spring`)
81
81
  - **Surface Elevation:** `shadow-vibe-brutal`, `shadow-vibe-glass`
82
82
 
83
83
  ---
package/dist/cli.js CHANGED
@@ -39,7 +39,7 @@ const path = __importStar(require("path"));
39
39
  const readline = __importStar(require("readline"));
40
40
  const components_1 = require("./registry/components");
41
41
  const index_1 = require("./index");
42
- const VERSION = '4.0.0';
42
+ const VERSION = '4.1.1';
43
43
  function printBanner() {
44
44
  console.log(`
45
45
  \x1b[35m ▲ VIBE UI CLI v${VERSION}\x1b[0m
@@ -50,7 +50,6 @@ function printHelp() {
50
50
  printBanner();
51
51
  console.log(`\x1b[1mUSAGE:\x1b[0m
52
52
  npx vibe-ui-suite <command> [options]
53
- npx @omid-io/tokens <command> [options]
54
53
  vibe-ui <command> [options]
55
54
 
56
55
  \x1b[1mCOMMANDS:\x1b[0m
@@ -60,14 +59,19 @@ function printHelp() {
60
59
 
61
60
  \x1b[1mOPTIONS:\x1b[0m
62
61
  -f, --force Overwrite existing files (creates automated .bak backups)
62
+ -y, --yes Non-interactive mode (use defaults without prompting)
63
+ --chemistry <1-5> Select visual chemistry in non-interactive mode
64
+ --editor <1-4> Select target AI editor in non-interactive mode (1: Cursor, 2: Claude, 3: Windsurf, 4: All)
63
65
  --dry-run Preview file operations without making actual modifications
64
66
  -v, --version Show CLI version
65
67
  -h, --help Show help menu
66
68
 
67
69
  \x1b[1mEXAMPLES:\x1b[0m
68
70
  npx vibe-ui-suite init
71
+ npx vibe-ui-suite init -y
69
72
  npx vibe-ui-suite init --dry-run
70
73
  npx vibe-ui-suite add thinking-drawer
74
+ npx vibe-ui-suite add theme-toggle
71
75
  npx vibe-ui-suite add thinking-drawer --force
72
76
  `);
73
77
  }
@@ -109,100 +113,124 @@ async function handleInit(options) {
109
113
  if (options.dryRun) {
110
114
  console.log('\x1b[34m[DRY-RUN MODE ACTIVATED: No files will be modified on disk]\x1b[0m\n');
111
115
  }
112
- const rl = readline.createInterface({
113
- input: process.stdin,
114
- output: process.stdout,
115
- });
116
- try {
117
- console.log('\x1b[1mInitialize Vibe UI in current workspace:\x1b[0m\n');
118
- // 1. Select Visual Chemistry
119
- console.log('\x1b[36m1. Select Visual Chemistry:\x1b[0m');
120
- console.log(' [1] Minimalist SaaS (Monochrome restraint, high signal - Recommended)');
121
- console.log(' [2] Luxury Glass 2.0 (Specular Fresnel, deep dark, gold accents)');
122
- console.log(' [3] Neobrutalism (Hard 3px black offset shadows, bold high saturation)');
123
- console.log(' [4] Swiss Editorial (Asymmetric grid, typography-first, high contrast)');
124
- console.log(' [5] Stripe Crisp Light (Developer docs, precision micro-borders)');
125
- const chemChoice = (await prompt(rl, 'Choice [1-5] (default: 1): ')).trim() || '1';
126
- const chemMap = {
127
- '1': 'MINIMALIST_SAAS',
128
- '2': 'LUXURY_GLASS_2',
129
- '3': 'NEOBRUTALISM',
130
- '4': 'SWISS_EDITORIAL',
131
- '5': 'STRIPE_CRISP_LIGHT',
132
- };
133
- const selectedChemKey = chemMap[chemChoice] || 'MINIMALIST_SAAS';
134
- const selectedChem = index_1.VISUAL_CHEMISTRIES[selectedChemKey];
135
- // 2. Select AI Editor
136
- console.log('\n\x1b[36m2. Select AI Coding Environment:\x1b[0m');
137
- console.log(' [1] Cursor (.cursorrules - Recommended)');
138
- console.log(' [2] Claude Code (CLAUDE.md)');
139
- console.log(' [3] Windsurf (.windsurfrules)');
140
- console.log(' [4] All of the above');
141
- const editorChoice = (await prompt(rl, 'Choice [1-4] (default: 1): ')).trim() || '1';
142
- // 3. Write contract rules safely
143
- const cwd = process.cwd();
144
- const createdFiles = [];
145
- if (editorChoice === '1' || editorChoice === '4') {
146
- const res = safeWriteFile(path.join(cwd, '.cursorrules'), CONTRACT_RULES, options);
147
- if (res.written)
148
- createdFiles.push('.cursorrules');
116
+ const isInteractive = !options.yes && Boolean(process.stdin.isTTY);
117
+ let chemChoice = options.chemistry || '1';
118
+ let editorChoice = options.editor || '4';
119
+ const chemMap = {
120
+ '1': 'MINIMALIST_SAAS',
121
+ '2': 'LUXURY_GLASS_2',
122
+ '3': 'NEOBRUTALISM',
123
+ '4': 'SWISS_EDITORIAL',
124
+ '5': 'STRIPE_CRISP_LIGHT',
125
+ };
126
+ if (isInteractive) {
127
+ const rl = readline.createInterface({
128
+ input: process.stdin,
129
+ output: process.stdout,
130
+ });
131
+ try {
132
+ console.log('\x1b[1mInitialize Vibe UI in current workspace:\x1b[0m\n');
133
+ // 1. Select Visual Chemistry
134
+ console.log('\x1b[36m1. Select Visual Chemistry:\x1b[0m');
135
+ console.log(' [1] Minimalist SaaS (Monochrome restraint, high signal - Recommended)');
136
+ console.log(' [2] Luxury Glass 2.0 (Specular Fresnel, deep dark, gold accents)');
137
+ console.log(' [3] Neobrutalism (Hard 3px black offset shadows, bold high saturation)');
138
+ console.log(' [4] Swiss Editorial (Asymmetric grid, typography-first, high contrast)');
139
+ console.log(' [5] Stripe Crisp Light (Developer docs, precision micro-borders)');
140
+ chemChoice = (await prompt(rl, 'Choice [1-5] (default: 1): ')).trim() || '1';
141
+ // 2. Select AI Editor
142
+ console.log('\n\x1b[36m2. Select AI Coding Environment:\x1b[0m');
143
+ console.log(' [1] Cursor (.cursorrules - Recommended)');
144
+ console.log(' [2] Claude Code (CLAUDE.md)');
145
+ console.log(' [3] Windsurf (.windsurfrules)');
146
+ console.log(' [4] All of the above');
147
+ editorChoice = (await prompt(rl, 'Choice [1-4] (default: 1): ')).trim() || '1';
148
+ }
149
+ finally {
150
+ rl.close();
151
+ }
152
+ }
153
+ else {
154
+ console.log('\x1b[36m [auto]\x1b[0m Non-interactive mode activated. Using defaults.');
155
+ }
156
+ const selectedChemKey = chemMap[chemChoice] || 'MINIMALIST_SAAS';
157
+ const selectedChem = index_1.VISUAL_CHEMISTRIES[selectedChemKey];
158
+ const darkColors = selectedChem.darkColors || selectedChem.colors;
159
+ console.log(` \x1b[32m✔\x1b[0m Visual Chemistry: \x1b[1m${selectedChem.name}\x1b[0m (Dual Light & Dark Themes)`);
160
+ // 3. Write contract rules safely
161
+ const cwd = process.cwd();
162
+ const createdFiles = [];
163
+ if (editorChoice === '1' || editorChoice === '4') {
164
+ const res = safeWriteFile(path.join(cwd, '.cursorrules'), CONTRACT_RULES, options);
165
+ if (res.written)
166
+ createdFiles.push('.cursorrules');
167
+ }
168
+ if (editorChoice === '2' || editorChoice === '4') {
169
+ const claudePath = path.join(cwd, 'CLAUDE.md');
170
+ if (options.dryRun) {
171
+ console.log(' \x1b[34m[dry-run]\x1b[0m Would update CLAUDE.md');
149
172
  }
150
- if (editorChoice === '2' || editorChoice === '4') {
151
- const claudePath = path.join(cwd, 'CLAUDE.md');
152
- if (options.dryRun) {
153
- console.log(' \x1b[34m[dry-run]\x1b[0m Would update CLAUDE.md');
154
- }
155
- else if (fs.existsSync(claudePath)) {
156
- fs.appendFileSync(claudePath, `\n\n${CONTRACT_RULES}`, 'utf-8');
157
- createdFiles.push('CLAUDE.md (appended)');
158
- }
159
- else {
160
- const res = safeWriteFile(claudePath, CONTRACT_RULES, options);
161
- if (res.written)
162
- createdFiles.push('CLAUDE.md');
163
- }
173
+ else if (fs.existsSync(claudePath)) {
174
+ fs.appendFileSync(claudePath, `\n\n${CONTRACT_RULES}`, 'utf-8');
175
+ createdFiles.push('CLAUDE.md (appended)');
164
176
  }
165
- if (editorChoice === '3' || editorChoice === '4') {
166
- const res = safeWriteFile(path.join(cwd, '.windsurfrules'), CONTRACT_RULES, options);
177
+ else {
178
+ const res = safeWriteFile(claudePath, CONTRACT_RULES, options);
167
179
  if (res.written)
168
- createdFiles.push('.windsurfrules');
180
+ createdFiles.push('CLAUDE.md');
169
181
  }
170
- // 4. Generate CSS Tokens file safely
171
- const cssContent = `:root {
172
- /* Vibe UI Chemistry: ${selectedChem.name} (${selectedChem.id}) */
182
+ }
183
+ if (editorChoice === '3' || editorChoice === '4') {
184
+ const res = safeWriteFile(path.join(cwd, '.windsurfrules'), CONTRACT_RULES, options);
185
+ if (res.written)
186
+ createdFiles.push('.windsurfrules');
187
+ }
188
+ // 4. Generate Dual-Theme CSS Tokens file safely
189
+ const cssContent = `:root {
190
+ /* Vibe UI Chemistry: ${selectedChem.name} (${selectedChem.id}) - Light Theme */
173
191
  --vibe-canvas: ${selectedChem.colors.canvas};
174
192
  --vibe-surface: ${selectedChem.colors.surface};
175
193
  --vibe-border: ${selectedChem.colors.border};
176
194
  --vibe-primary-accent: ${selectedChem.colors.primaryAccent};
195
+ --vibe-on-primary: ${selectedChem.colors.onPrimaryAccent};
177
196
  --vibe-text-primary: ${selectedChem.colors.textPrimary};
178
197
  --vibe-text-muted: ${selectedChem.colors.textMuted};
179
198
  --vibe-ring: ${selectedChem.colors.ring};
180
199
  }
200
+
201
+ .dark, [data-theme="dark"] {
202
+ /* Vibe UI Chemistry: ${selectedChem.name} (${selectedChem.id}) - Dark Theme */
203
+ --vibe-canvas: ${darkColors.canvas};
204
+ --vibe-surface: ${darkColors.surface};
205
+ --vibe-border: ${darkColors.border};
206
+ --vibe-primary-accent: ${darkColors.primaryAccent};
207
+ --vibe-on-primary: ${darkColors.onPrimaryAccent};
208
+ --vibe-text-primary: ${darkColors.textPrimary};
209
+ --vibe-text-muted: ${darkColors.textMuted};
210
+ --vibe-ring: ${darkColors.ring};
211
+ }
181
212
  `;
182
- const cssPath = path.join(cwd, 'vibe-tokens.css');
183
- const cssRes = safeWriteFile(cssPath, cssContent, options);
184
- if (cssRes.written)
185
- createdFiles.push('vibe-tokens.css');
186
- console.log('\n\x1b[32m✔ Initialized successfully!\x1b[0m');
187
- if (createdFiles.length > 0) {
188
- console.log('\x1b[90mGenerated/Updated files:\x1b[0m');
189
- createdFiles.forEach((f) => console.log(` + ${f}`));
190
- }
191
- console.log(`
213
+ const cssPath = path.join(cwd, 'vibe-tokens.css');
214
+ const cssRes = safeWriteFile(cssPath, cssContent, options);
215
+ if (cssRes.written)
216
+ createdFiles.push('vibe-tokens.css');
217
+ console.log('\n\x1b[32m✔ Initialized successfully!\x1b[0m');
218
+ if (createdFiles.length > 0) {
219
+ console.log('\x1b[90mGenerated/Updated files:\x1b[0m');
220
+ createdFiles.forEach((f) => console.log(` + ${f}`));
221
+ }
222
+ console.log(`
192
223
  \x1b[1mNext Steps:\x1b[0m
193
224
  1. Import \x1b[35mvibe-tokens.css\x1b[0m into your layout or globals.css
194
- 2. Run \x1b[32mnpx @omid-io/tokens add thinking-drawer\x1b[0m to add your first AI component
225
+ 2. Run \x1b[32mnpx vibe-ui-suite add thinking-drawer\x1b[0m to add your first AI component
226
+ 3. Run \x1b[32mnpx vibe-ui-suite add theme-toggle\x1b[0m to add accessible Dark/Light theme switch
195
227
  `);
196
- }
197
- finally {
198
- rl.close();
199
- }
200
228
  }
201
229
  function handleAdd(componentName, options = { force: false, dryRun: false }) {
202
230
  if (!componentName) {
203
231
  console.log('\x1b[33mError: Please specify a component to add.\x1b[0m\n');
204
232
  handleList();
205
- console.log('\nUsage: npx @omid-io/tokens add <component> [--force]');
233
+ console.log('\nUsage: npx vibe-ui-suite add <component> [--force]');
206
234
  process.exit(1);
207
235
  return;
208
236
  }
@@ -223,7 +251,7 @@ function handleAdd(componentName, options = { force: false, dryRun: false }) {
223
251
  if (writeRes.skipped) {
224
252
  console.log(`\n\x1b[33mWarning: Component already exists at components/vibe-ui/${comp.filename}\x1b[0m`);
225
253
  console.log(`To overwrite with automated .bak backup, re-run with:`);
226
- console.log(` \x1b[32mnpx @omid-io/tokens add ${componentName} --force\x1b[0m\n`);
254
+ console.log(` \x1b[32mnpx vibe-ui-suite add ${componentName} --force\x1b[0m\n`);
227
255
  return;
228
256
  }
229
257
  if (writeRes.written) {
@@ -244,9 +272,18 @@ async function main() {
244
272
  const rawArgs = process.argv.slice(2);
245
273
  const force = rawArgs.includes('--force') || rawArgs.includes('-f');
246
274
  const dryRun = rawArgs.includes('--dry-run');
247
- const filteredArgs = rawArgs.filter((a) => !['--force', '-f', '--dry-run'].includes(a));
275
+ const yes = rawArgs.includes('--yes') || rawArgs.includes('-y');
276
+ let chemistry;
277
+ let editor;
278
+ for (let i = 0; i < rawArgs.length; i++) {
279
+ if (rawArgs[i] === '--chemistry' && rawArgs[i + 1])
280
+ chemistry = rawArgs[i + 1];
281
+ if (rawArgs[i] === '--editor' && rawArgs[i + 1])
282
+ editor = rawArgs[i + 1];
283
+ }
284
+ const filteredArgs = rawArgs.filter((a) => !['--force', '-f', '--dry-run', '--yes', '-y', '--chemistry', '--editor'].includes(a) && a !== chemistry && a !== editor);
248
285
  const cmd = filteredArgs[0]?.toLowerCase();
249
- const options = { force, dryRun };
286
+ const options = { force, dryRun, yes, chemistry, editor };
250
287
  if (!cmd || cmd === '--help' || cmd === '-h') {
251
288
  printHelp();
252
289
  return;
package/dist/cli.mjs CHANGED
@@ -4,7 +4,7 @@ import * as path from 'path';
4
4
  import * as readline from 'readline';
5
5
  import { COMPONENT_REGISTRY } from './registry/components';
6
6
  import { VISUAL_CHEMISTRIES } from './index';
7
- const VERSION = '4.0.0';
7
+ const VERSION = '4.1.1';
8
8
  function printBanner() {
9
9
  console.log(`
10
10
  \x1b[35m ▲ VIBE UI CLI v${VERSION}\x1b[0m
@@ -15,7 +15,6 @@ function printHelp() {
15
15
  printBanner();
16
16
  console.log(`\x1b[1mUSAGE:\x1b[0m
17
17
  npx vibe-ui-suite <command> [options]
18
- npx @omid-io/tokens <command> [options]
19
18
  vibe-ui <command> [options]
20
19
 
21
20
  \x1b[1mCOMMANDS:\x1b[0m
@@ -25,14 +24,19 @@ function printHelp() {
25
24
 
26
25
  \x1b[1mOPTIONS:\x1b[0m
27
26
  -f, --force Overwrite existing files (creates automated .bak backups)
27
+ -y, --yes Non-interactive mode (use defaults without prompting)
28
+ --chemistry <1-5> Select visual chemistry in non-interactive mode
29
+ --editor <1-4> Select target AI editor in non-interactive mode (1: Cursor, 2: Claude, 3: Windsurf, 4: All)
28
30
  --dry-run Preview file operations without making actual modifications
29
31
  -v, --version Show CLI version
30
32
  -h, --help Show help menu
31
33
 
32
34
  \x1b[1mEXAMPLES:\x1b[0m
33
35
  npx vibe-ui-suite init
36
+ npx vibe-ui-suite init -y
34
37
  npx vibe-ui-suite init --dry-run
35
38
  npx vibe-ui-suite add thinking-drawer
39
+ npx vibe-ui-suite add theme-toggle
36
40
  npx vibe-ui-suite add thinking-drawer --force
37
41
  `);
38
42
  }
@@ -74,100 +78,124 @@ async function handleInit(options) {
74
78
  if (options.dryRun) {
75
79
  console.log('\x1b[34m[DRY-RUN MODE ACTIVATED: No files will be modified on disk]\x1b[0m\n');
76
80
  }
77
- const rl = readline.createInterface({
78
- input: process.stdin,
79
- output: process.stdout,
80
- });
81
- try {
82
- console.log('\x1b[1mInitialize Vibe UI in current workspace:\x1b[0m\n');
83
- // 1. Select Visual Chemistry
84
- console.log('\x1b[36m1. Select Visual Chemistry:\x1b[0m');
85
- console.log(' [1] Minimalist SaaS (Monochrome restraint, high signal - Recommended)');
86
- console.log(' [2] Luxury Glass 2.0 (Specular Fresnel, deep dark, gold accents)');
87
- console.log(' [3] Neobrutalism (Hard 3px black offset shadows, bold high saturation)');
88
- console.log(' [4] Swiss Editorial (Asymmetric grid, typography-first, high contrast)');
89
- console.log(' [5] Stripe Crisp Light (Developer docs, precision micro-borders)');
90
- const chemChoice = (await prompt(rl, 'Choice [1-5] (default: 1): ')).trim() || '1';
91
- const chemMap = {
92
- '1': 'MINIMALIST_SAAS',
93
- '2': 'LUXURY_GLASS_2',
94
- '3': 'NEOBRUTALISM',
95
- '4': 'SWISS_EDITORIAL',
96
- '5': 'STRIPE_CRISP_LIGHT',
97
- };
98
- const selectedChemKey = chemMap[chemChoice] || 'MINIMALIST_SAAS';
99
- const selectedChem = VISUAL_CHEMISTRIES[selectedChemKey];
100
- // 2. Select AI Editor
101
- console.log('\n\x1b[36m2. Select AI Coding Environment:\x1b[0m');
102
- console.log(' [1] Cursor (.cursorrules - Recommended)');
103
- console.log(' [2] Claude Code (CLAUDE.md)');
104
- console.log(' [3] Windsurf (.windsurfrules)');
105
- console.log(' [4] All of the above');
106
- const editorChoice = (await prompt(rl, 'Choice [1-4] (default: 1): ')).trim() || '1';
107
- // 3. Write contract rules safely
108
- const cwd = process.cwd();
109
- const createdFiles = [];
110
- if (editorChoice === '1' || editorChoice === '4') {
111
- const res = safeWriteFile(path.join(cwd, '.cursorrules'), CONTRACT_RULES, options);
112
- if (res.written)
113
- createdFiles.push('.cursorrules');
81
+ const isInteractive = !options.yes && Boolean(process.stdin.isTTY);
82
+ let chemChoice = options.chemistry || '1';
83
+ let editorChoice = options.editor || '4';
84
+ const chemMap = {
85
+ '1': 'MINIMALIST_SAAS',
86
+ '2': 'LUXURY_GLASS_2',
87
+ '3': 'NEOBRUTALISM',
88
+ '4': 'SWISS_EDITORIAL',
89
+ '5': 'STRIPE_CRISP_LIGHT',
90
+ };
91
+ if (isInteractive) {
92
+ const rl = readline.createInterface({
93
+ input: process.stdin,
94
+ output: process.stdout,
95
+ });
96
+ try {
97
+ console.log('\x1b[1mInitialize Vibe UI in current workspace:\x1b[0m\n');
98
+ // 1. Select Visual Chemistry
99
+ console.log('\x1b[36m1. Select Visual Chemistry:\x1b[0m');
100
+ console.log(' [1] Minimalist SaaS (Monochrome restraint, high signal - Recommended)');
101
+ console.log(' [2] Luxury Glass 2.0 (Specular Fresnel, deep dark, gold accents)');
102
+ console.log(' [3] Neobrutalism (Hard 3px black offset shadows, bold high saturation)');
103
+ console.log(' [4] Swiss Editorial (Asymmetric grid, typography-first, high contrast)');
104
+ console.log(' [5] Stripe Crisp Light (Developer docs, precision micro-borders)');
105
+ chemChoice = (await prompt(rl, 'Choice [1-5] (default: 1): ')).trim() || '1';
106
+ // 2. Select AI Editor
107
+ console.log('\n\x1b[36m2. Select AI Coding Environment:\x1b[0m');
108
+ console.log(' [1] Cursor (.cursorrules - Recommended)');
109
+ console.log(' [2] Claude Code (CLAUDE.md)');
110
+ console.log(' [3] Windsurf (.windsurfrules)');
111
+ console.log(' [4] All of the above');
112
+ editorChoice = (await prompt(rl, 'Choice [1-4] (default: 1): ')).trim() || '1';
114
113
  }
115
- if (editorChoice === '2' || editorChoice === '4') {
116
- const claudePath = path.join(cwd, 'CLAUDE.md');
117
- if (options.dryRun) {
118
- console.log(' \x1b[34m[dry-run]\x1b[0m Would update CLAUDE.md');
119
- }
120
- else if (fs.existsSync(claudePath)) {
121
- fs.appendFileSync(claudePath, `\n\n${CONTRACT_RULES}`, 'utf-8');
122
- createdFiles.push('CLAUDE.md (appended)');
123
- }
124
- else {
125
- const res = safeWriteFile(claudePath, CONTRACT_RULES, options);
126
- if (res.written)
127
- createdFiles.push('CLAUDE.md');
128
- }
114
+ finally {
115
+ rl.close();
129
116
  }
130
- if (editorChoice === '3' || editorChoice === '4') {
131
- const res = safeWriteFile(path.join(cwd, '.windsurfrules'), CONTRACT_RULES, options);
117
+ }
118
+ else {
119
+ console.log('\x1b[36m [auto]\x1b[0m Non-interactive mode activated. Using defaults.');
120
+ }
121
+ const selectedChemKey = chemMap[chemChoice] || 'MINIMALIST_SAAS';
122
+ const selectedChem = VISUAL_CHEMISTRIES[selectedChemKey];
123
+ const darkColors = selectedChem.darkColors || selectedChem.colors;
124
+ console.log(` \x1b[32m✔\x1b[0m Visual Chemistry: \x1b[1m${selectedChem.name}\x1b[0m (Dual Light & Dark Themes)`);
125
+ // 3. Write contract rules safely
126
+ const cwd = process.cwd();
127
+ const createdFiles = [];
128
+ if (editorChoice === '1' || editorChoice === '4') {
129
+ const res = safeWriteFile(path.join(cwd, '.cursorrules'), CONTRACT_RULES, options);
130
+ if (res.written)
131
+ createdFiles.push('.cursorrules');
132
+ }
133
+ if (editorChoice === '2' || editorChoice === '4') {
134
+ const claudePath = path.join(cwd, 'CLAUDE.md');
135
+ if (options.dryRun) {
136
+ console.log(' \x1b[34m[dry-run]\x1b[0m Would update CLAUDE.md');
137
+ }
138
+ else if (fs.existsSync(claudePath)) {
139
+ fs.appendFileSync(claudePath, `\n\n${CONTRACT_RULES}`, 'utf-8');
140
+ createdFiles.push('CLAUDE.md (appended)');
141
+ }
142
+ else {
143
+ const res = safeWriteFile(claudePath, CONTRACT_RULES, options);
132
144
  if (res.written)
133
- createdFiles.push('.windsurfrules');
145
+ createdFiles.push('CLAUDE.md');
134
146
  }
135
- // 4. Generate CSS Tokens file safely
136
- const cssContent = `:root {
137
- /* Vibe UI Chemistry: ${selectedChem.name} (${selectedChem.id}) */
147
+ }
148
+ if (editorChoice === '3' || editorChoice === '4') {
149
+ const res = safeWriteFile(path.join(cwd, '.windsurfrules'), CONTRACT_RULES, options);
150
+ if (res.written)
151
+ createdFiles.push('.windsurfrules');
152
+ }
153
+ // 4. Generate Dual-Theme CSS Tokens file safely
154
+ const cssContent = `:root {
155
+ /* Vibe UI Chemistry: ${selectedChem.name} (${selectedChem.id}) - Light Theme */
138
156
  --vibe-canvas: ${selectedChem.colors.canvas};
139
157
  --vibe-surface: ${selectedChem.colors.surface};
140
158
  --vibe-border: ${selectedChem.colors.border};
141
159
  --vibe-primary-accent: ${selectedChem.colors.primaryAccent};
160
+ --vibe-on-primary: ${selectedChem.colors.onPrimaryAccent};
142
161
  --vibe-text-primary: ${selectedChem.colors.textPrimary};
143
162
  --vibe-text-muted: ${selectedChem.colors.textMuted};
144
163
  --vibe-ring: ${selectedChem.colors.ring};
145
164
  }
165
+
166
+ .dark, [data-theme="dark"] {
167
+ /* Vibe UI Chemistry: ${selectedChem.name} (${selectedChem.id}) - Dark Theme */
168
+ --vibe-canvas: ${darkColors.canvas};
169
+ --vibe-surface: ${darkColors.surface};
170
+ --vibe-border: ${darkColors.border};
171
+ --vibe-primary-accent: ${darkColors.primaryAccent};
172
+ --vibe-on-primary: ${darkColors.onPrimaryAccent};
173
+ --vibe-text-primary: ${darkColors.textPrimary};
174
+ --vibe-text-muted: ${darkColors.textMuted};
175
+ --vibe-ring: ${darkColors.ring};
176
+ }
146
177
  `;
147
- const cssPath = path.join(cwd, 'vibe-tokens.css');
148
- const cssRes = safeWriteFile(cssPath, cssContent, options);
149
- if (cssRes.written)
150
- createdFiles.push('vibe-tokens.css');
151
- console.log('\n\x1b[32m✔ Initialized successfully!\x1b[0m');
152
- if (createdFiles.length > 0) {
153
- console.log('\x1b[90mGenerated/Updated files:\x1b[0m');
154
- createdFiles.forEach((f) => console.log(` + ${f}`));
155
- }
156
- console.log(`
178
+ const cssPath = path.join(cwd, 'vibe-tokens.css');
179
+ const cssRes = safeWriteFile(cssPath, cssContent, options);
180
+ if (cssRes.written)
181
+ createdFiles.push('vibe-tokens.css');
182
+ console.log('\n\x1b[32m✔ Initialized successfully!\x1b[0m');
183
+ if (createdFiles.length > 0) {
184
+ console.log('\x1b[90mGenerated/Updated files:\x1b[0m');
185
+ createdFiles.forEach((f) => console.log(` + ${f}`));
186
+ }
187
+ console.log(`
157
188
  \x1b[1mNext Steps:\x1b[0m
158
189
  1. Import \x1b[35mvibe-tokens.css\x1b[0m into your layout or globals.css
159
- 2. Run \x1b[32mnpx @omid-io/tokens add thinking-drawer\x1b[0m to add your first AI component
190
+ 2. Run \x1b[32mnpx vibe-ui-suite add thinking-drawer\x1b[0m to add your first AI component
191
+ 3. Run \x1b[32mnpx vibe-ui-suite add theme-toggle\x1b[0m to add accessible Dark/Light theme switch
160
192
  `);
161
- }
162
- finally {
163
- rl.close();
164
- }
165
193
  }
166
194
  function handleAdd(componentName, options = { force: false, dryRun: false }) {
167
195
  if (!componentName) {
168
196
  console.log('\x1b[33mError: Please specify a component to add.\x1b[0m\n');
169
197
  handleList();
170
- console.log('\nUsage: npx @omid-io/tokens add <component> [--force]');
198
+ console.log('\nUsage: npx vibe-ui-suite add <component> [--force]');
171
199
  process.exit(1);
172
200
  return;
173
201
  }
@@ -188,7 +216,7 @@ function handleAdd(componentName, options = { force: false, dryRun: false }) {
188
216
  if (writeRes.skipped) {
189
217
  console.log(`\n\x1b[33mWarning: Component already exists at components/vibe-ui/${comp.filename}\x1b[0m`);
190
218
  console.log(`To overwrite with automated .bak backup, re-run with:`);
191
- console.log(` \x1b[32mnpx @omid-io/tokens add ${componentName} --force\x1b[0m\n`);
219
+ console.log(` \x1b[32mnpx vibe-ui-suite add ${componentName} --force\x1b[0m\n`);
192
220
  return;
193
221
  }
194
222
  if (writeRes.written) {
@@ -209,9 +237,18 @@ async function main() {
209
237
  const rawArgs = process.argv.slice(2);
210
238
  const force = rawArgs.includes('--force') || rawArgs.includes('-f');
211
239
  const dryRun = rawArgs.includes('--dry-run');
212
- const filteredArgs = rawArgs.filter((a) => !['--force', '-f', '--dry-run'].includes(a));
240
+ const yes = rawArgs.includes('--yes') || rawArgs.includes('-y');
241
+ let chemistry;
242
+ let editor;
243
+ for (let i = 0; i < rawArgs.length; i++) {
244
+ if (rawArgs[i] === '--chemistry' && rawArgs[i + 1])
245
+ chemistry = rawArgs[i + 1];
246
+ if (rawArgs[i] === '--editor' && rawArgs[i + 1])
247
+ editor = rawArgs[i + 1];
248
+ }
249
+ const filteredArgs = rawArgs.filter((a) => !['--force', '-f', '--dry-run', '--yes', '-y', '--chemistry', '--editor'].includes(a) && a !== chemistry && a !== editor);
213
250
  const cmd = filteredArgs[0]?.toLowerCase();
214
- const options = { force, dryRun };
251
+ const options = { force, dryRun, yes, chemistry, editor };
215
252
  if (!cmd || cmd === '--help' || cmd === '-h') {
216
253
  printHelp();
217
254
  return;
package/dist/index.d.ts CHANGED
@@ -8,6 +8,8 @@ export interface ChemistryColors {
8
8
  surface: string;
9
9
  border: string;
10
10
  primaryAccent: string;
11
+ /** Text color on primary accent guaranteeing >= 4.5:1 (AA) or >= 7:1 (AAA) contrast */
12
+ onPrimaryAccent: string;
11
13
  textPrimary: string;
12
14
  textMuted: string;
13
15
  ring: string;
@@ -27,6 +29,7 @@ export interface VisualChemistry {
27
29
  name: string;
28
30
  tagline: string;
29
31
  colors: ChemistryColors;
32
+ darkColors?: ChemistryColors;
30
33
  typography: ChemistryTypography;
31
34
  physics: ChemistryPhysics;
32
35
  }
package/dist/index.js CHANGED
@@ -13,17 +13,28 @@ exports.VISUAL_CHEMISTRIES = {
13
13
  name: 'Minimalist SaaS',
14
14
  tagline: 'High-signal B2B productivity with subtle borders and monochrome restraint',
15
15
  colors: {
16
+ canvas: 'oklch(0.985 0.002 264)',
17
+ surface: 'oklch(1 0 0)',
18
+ border: 'oklch(0.90 0.008 264)',
19
+ primaryAccent: 'oklch(0.55 0.22 260)',
20
+ onPrimaryAccent: 'oklch(0.99 0 0)',
21
+ textPrimary: 'oklch(0.14 0.005 264)',
22
+ textMuted: 'oklch(0.48 0.015 264)',
23
+ ring: 'oklch(0.55 0.22 260)',
24
+ },
25
+ darkColors: {
16
26
  canvas: 'oklch(0.14 0.005 264)',
17
27
  surface: 'oklch(0.18 0.008 264)',
18
28
  border: 'oklch(0.28 0.01 264)',
19
29
  primaryAccent: 'oklch(0.65 0.22 260)',
30
+ onPrimaryAccent: 'oklch(0.99 0 0)',
20
31
  textPrimary: 'oklch(0.98 0.002 264)',
21
32
  textMuted: 'oklch(0.65 0.015 264)',
22
33
  ring: 'oklch(0.65 0.22 260)',
23
34
  },
24
35
  typography: {
25
- display: 'Inter, system-ui, sans-serif',
26
- body: 'Inter, system-ui, sans-serif',
36
+ display: 'Vazirmatn, Inter, system-ui, sans-serif',
37
+ body: 'Vazirmatn, Inter, system-ui, sans-serif',
27
38
  mono: 'JetBrains Mono, Menlo, monospace',
28
39
  },
29
40
  physics: {
@@ -37,17 +48,28 @@ exports.VISUAL_CHEMISTRIES = {
37
48
  name: 'Luxury Glassmorphism 2.0',
38
49
  tagline: 'Deep dark substrates with specular highlights and gold accents',
39
50
  colors: {
51
+ canvas: 'oklch(0.97 0.01 85)',
52
+ surface: 'oklch(1 0 0 / 0.85)',
53
+ border: 'oklch(0.75 0.12 85 / 0.4)',
54
+ primaryAccent: 'oklch(0.65 0.18 85)',
55
+ onPrimaryAccent: 'oklch(0.12 0.02 85)',
56
+ textPrimary: 'oklch(0.16 0.02 85)',
57
+ textMuted: 'oklch(0.45 0.03 85)',
58
+ ring: 'oklch(0.65 0.18 85)',
59
+ },
60
+ darkColors: {
40
61
  canvas: 'oklch(0.12 0.015 280)',
41
62
  surface: 'oklch(0.18 0.02 280 / 0.75)',
42
63
  border: 'oklch(0.75 0.15 85 / 0.35)',
43
64
  primaryAccent: 'oklch(0.78 0.16 85)',
65
+ onPrimaryAccent: 'oklch(0.12 0.02 85)',
44
66
  textPrimary: 'oklch(0.98 0.005 85)',
45
67
  textMuted: 'oklch(0.70 0.03 85)',
46
68
  ring: 'oklch(0.78 0.16 85)',
47
69
  },
48
70
  typography: {
49
- display: 'Cinzel, Playfair Display, serif',
50
- body: 'Plus Jakarta Sans, sans-serif',
71
+ display: 'Cinzel, Playfair Display, Vazirmatn, serif',
72
+ body: 'Vazirmatn, Plus Jakarta Sans, sans-serif',
51
73
  mono: 'JetBrains Mono, monospace',
52
74
  },
53
75
  physics: {
@@ -65,13 +87,24 @@ exports.VISUAL_CHEMISTRIES = {
65
87
  surface: 'oklch(0.99 0.002 95)',
66
88
  border: 'oklch(0.15 0.01 95)',
67
89
  primaryAccent: 'oklch(0.82 0.20 135)',
90
+ onPrimaryAccent: 'oklch(0.10 0.01 95)',
68
91
  textPrimary: 'oklch(0.12 0.01 95)',
69
92
  textMuted: 'oklch(0.35 0.02 95)',
70
93
  ring: 'oklch(0.15 0.01 95)',
71
94
  },
95
+ darkColors: {
96
+ canvas: 'oklch(0.15 0.01 95)',
97
+ surface: 'oklch(0.20 0.015 95)',
98
+ border: 'oklch(0.95 0.01 95)',
99
+ primaryAccent: 'oklch(0.82 0.20 135)',
100
+ onPrimaryAccent: 'oklch(0.10 0.01 95)',
101
+ textPrimary: 'oklch(0.96 0.01 95)',
102
+ textMuted: 'oklch(0.70 0.02 95)',
103
+ ring: 'oklch(0.82 0.20 135)',
104
+ },
72
105
  typography: {
73
- display: 'Space Grotesk, Syne, sans-serif',
74
- body: 'Space Grotesk, sans-serif',
106
+ display: 'Space Grotesk, Syne, Vazirmatn, sans-serif',
107
+ body: 'Vazirmatn, Space Grotesk, sans-serif',
75
108
  mono: 'Space Mono, monospace',
76
109
  },
77
110
  physics: {
@@ -89,13 +122,24 @@ exports.VISUAL_CHEMISTRIES = {
89
122
  surface: 'oklch(0.93 0.008 80)',
90
123
  border: 'oklch(0.20 0.005 80)',
91
124
  primaryAccent: 'oklch(0.58 0.24 28)',
125
+ onPrimaryAccent: 'oklch(0.99 0 0)',
92
126
  textPrimary: 'oklch(0.15 0.005 80)',
93
127
  textMuted: 'oklch(0.42 0.01 80)',
94
128
  ring: 'oklch(0.58 0.24 28)',
95
129
  },
130
+ darkColors: {
131
+ canvas: 'oklch(0.13 0.005 80)',
132
+ surface: 'oklch(0.17 0.008 80)',
133
+ border: 'oklch(0.35 0.005 80)',
134
+ primaryAccent: 'oklch(0.68 0.22 28)',
135
+ onPrimaryAccent: 'oklch(0.12 0.005 80)',
136
+ textPrimary: 'oklch(0.95 0.005 80)',
137
+ textMuted: 'oklch(0.65 0.01 80)',
138
+ ring: 'oklch(0.68 0.22 28)',
139
+ },
96
140
  typography: {
97
- display: 'Helvetica Neue, Arial, sans-serif',
98
- body: 'Newsreader, Georgia, serif',
141
+ display: 'Helvetica Neue, Arial, Vazirmatn, sans-serif',
142
+ body: 'Newsreader, Georgia, Vazirmatn, serif',
99
143
  mono: 'Courier New, monospace',
100
144
  },
101
145
  physics: {
@@ -113,13 +157,24 @@ exports.VISUAL_CHEMISTRIES = {
113
157
  surface: 'oklch(1 0 0)',
114
158
  border: 'oklch(0.90 0.008 247.8)',
115
159
  primaryAccent: 'oklch(0.55 0.22 265)',
160
+ onPrimaryAccent: 'oklch(0.99 0 0)',
116
161
  textPrimary: 'oklch(0.20 0.02 265)',
117
162
  textMuted: 'oklch(0.50 0.02 265)',
118
163
  ring: 'oklch(0.55 0.22 265)',
119
164
  },
165
+ darkColors: {
166
+ canvas: 'oklch(0.13 0.005 265)',
167
+ surface: 'oklch(0.17 0.008 265)',
168
+ border: 'oklch(0.26 0.01 265)',
169
+ primaryAccent: 'oklch(0.65 0.22 265)',
170
+ onPrimaryAccent: 'oklch(0.99 0 0)',
171
+ textPrimary: 'oklch(0.98 0 0)',
172
+ textMuted: 'oklch(0.65 0.01 265)',
173
+ ring: 'oklch(0.65 0.22 265)',
174
+ },
120
175
  typography: {
121
- display: 'Inter, system-ui, sans-serif',
122
- body: 'Inter, system-ui, sans-serif',
176
+ display: 'Vazirmatn, Inter, system-ui, sans-serif',
177
+ body: 'Vazirmatn, Inter, system-ui, sans-serif',
123
178
  mono: 'Fira Code, monospace',
124
179
  },
125
180
  physics: {
package/dist/index.mjs CHANGED
@@ -8,17 +8,28 @@ export const VISUAL_CHEMISTRIES = {
8
8
  name: 'Minimalist SaaS',
9
9
  tagline: 'High-signal B2B productivity with subtle borders and monochrome restraint',
10
10
  colors: {
11
+ canvas: 'oklch(0.985 0.002 264)',
12
+ surface: 'oklch(1 0 0)',
13
+ border: 'oklch(0.90 0.008 264)',
14
+ primaryAccent: 'oklch(0.55 0.22 260)',
15
+ onPrimaryAccent: 'oklch(0.99 0 0)',
16
+ textPrimary: 'oklch(0.14 0.005 264)',
17
+ textMuted: 'oklch(0.48 0.015 264)',
18
+ ring: 'oklch(0.55 0.22 260)',
19
+ },
20
+ darkColors: {
11
21
  canvas: 'oklch(0.14 0.005 264)',
12
22
  surface: 'oklch(0.18 0.008 264)',
13
23
  border: 'oklch(0.28 0.01 264)',
14
24
  primaryAccent: 'oklch(0.65 0.22 260)',
25
+ onPrimaryAccent: 'oklch(0.99 0 0)',
15
26
  textPrimary: 'oklch(0.98 0.002 264)',
16
27
  textMuted: 'oklch(0.65 0.015 264)',
17
28
  ring: 'oklch(0.65 0.22 260)',
18
29
  },
19
30
  typography: {
20
- display: 'Inter, system-ui, sans-serif',
21
- body: 'Inter, system-ui, sans-serif',
31
+ display: 'Vazirmatn, Inter, system-ui, sans-serif',
32
+ body: 'Vazirmatn, Inter, system-ui, sans-serif',
22
33
  mono: 'JetBrains Mono, Menlo, monospace',
23
34
  },
24
35
  physics: {
@@ -32,17 +43,28 @@ export const VISUAL_CHEMISTRIES = {
32
43
  name: 'Luxury Glassmorphism 2.0',
33
44
  tagline: 'Deep dark substrates with specular highlights and gold accents',
34
45
  colors: {
46
+ canvas: 'oklch(0.97 0.01 85)',
47
+ surface: 'oklch(1 0 0 / 0.85)',
48
+ border: 'oklch(0.75 0.12 85 / 0.4)',
49
+ primaryAccent: 'oklch(0.65 0.18 85)',
50
+ onPrimaryAccent: 'oklch(0.12 0.02 85)',
51
+ textPrimary: 'oklch(0.16 0.02 85)',
52
+ textMuted: 'oklch(0.45 0.03 85)',
53
+ ring: 'oklch(0.65 0.18 85)',
54
+ },
55
+ darkColors: {
35
56
  canvas: 'oklch(0.12 0.015 280)',
36
57
  surface: 'oklch(0.18 0.02 280 / 0.75)',
37
58
  border: 'oklch(0.75 0.15 85 / 0.35)',
38
59
  primaryAccent: 'oklch(0.78 0.16 85)',
60
+ onPrimaryAccent: 'oklch(0.12 0.02 85)',
39
61
  textPrimary: 'oklch(0.98 0.005 85)',
40
62
  textMuted: 'oklch(0.70 0.03 85)',
41
63
  ring: 'oklch(0.78 0.16 85)',
42
64
  },
43
65
  typography: {
44
- display: 'Cinzel, Playfair Display, serif',
45
- body: 'Plus Jakarta Sans, sans-serif',
66
+ display: 'Cinzel, Playfair Display, Vazirmatn, serif',
67
+ body: 'Vazirmatn, Plus Jakarta Sans, sans-serif',
46
68
  mono: 'JetBrains Mono, monospace',
47
69
  },
48
70
  physics: {
@@ -60,13 +82,24 @@ export const VISUAL_CHEMISTRIES = {
60
82
  surface: 'oklch(0.99 0.002 95)',
61
83
  border: 'oklch(0.15 0.01 95)',
62
84
  primaryAccent: 'oklch(0.82 0.20 135)',
85
+ onPrimaryAccent: 'oklch(0.10 0.01 95)',
63
86
  textPrimary: 'oklch(0.12 0.01 95)',
64
87
  textMuted: 'oklch(0.35 0.02 95)',
65
88
  ring: 'oklch(0.15 0.01 95)',
66
89
  },
90
+ darkColors: {
91
+ canvas: 'oklch(0.15 0.01 95)',
92
+ surface: 'oklch(0.20 0.015 95)',
93
+ border: 'oklch(0.95 0.01 95)',
94
+ primaryAccent: 'oklch(0.82 0.20 135)',
95
+ onPrimaryAccent: 'oklch(0.10 0.01 95)',
96
+ textPrimary: 'oklch(0.96 0.01 95)',
97
+ textMuted: 'oklch(0.70 0.02 95)',
98
+ ring: 'oklch(0.82 0.20 135)',
99
+ },
67
100
  typography: {
68
- display: 'Space Grotesk, Syne, sans-serif',
69
- body: 'Space Grotesk, sans-serif',
101
+ display: 'Space Grotesk, Syne, Vazirmatn, sans-serif',
102
+ body: 'Vazirmatn, Space Grotesk, sans-serif',
70
103
  mono: 'Space Mono, monospace',
71
104
  },
72
105
  physics: {
@@ -84,13 +117,24 @@ export const VISUAL_CHEMISTRIES = {
84
117
  surface: 'oklch(0.93 0.008 80)',
85
118
  border: 'oklch(0.20 0.005 80)',
86
119
  primaryAccent: 'oklch(0.58 0.24 28)',
120
+ onPrimaryAccent: 'oklch(0.99 0 0)',
87
121
  textPrimary: 'oklch(0.15 0.005 80)',
88
122
  textMuted: 'oklch(0.42 0.01 80)',
89
123
  ring: 'oklch(0.58 0.24 28)',
90
124
  },
125
+ darkColors: {
126
+ canvas: 'oklch(0.13 0.005 80)',
127
+ surface: 'oklch(0.17 0.008 80)',
128
+ border: 'oklch(0.35 0.005 80)',
129
+ primaryAccent: 'oklch(0.68 0.22 28)',
130
+ onPrimaryAccent: 'oklch(0.12 0.005 80)',
131
+ textPrimary: 'oklch(0.95 0.005 80)',
132
+ textMuted: 'oklch(0.65 0.01 80)',
133
+ ring: 'oklch(0.68 0.22 28)',
134
+ },
91
135
  typography: {
92
- display: 'Helvetica Neue, Arial, sans-serif',
93
- body: 'Newsreader, Georgia, serif',
136
+ display: 'Helvetica Neue, Arial, Vazirmatn, sans-serif',
137
+ body: 'Newsreader, Georgia, Vazirmatn, serif',
94
138
  mono: 'Courier New, monospace',
95
139
  },
96
140
  physics: {
@@ -108,13 +152,24 @@ export const VISUAL_CHEMISTRIES = {
108
152
  surface: 'oklch(1 0 0)',
109
153
  border: 'oklch(0.90 0.008 247.8)',
110
154
  primaryAccent: 'oklch(0.55 0.22 265)',
155
+ onPrimaryAccent: 'oklch(0.99 0 0)',
111
156
  textPrimary: 'oklch(0.20 0.02 265)',
112
157
  textMuted: 'oklch(0.50 0.02 265)',
113
158
  ring: 'oklch(0.55 0.22 265)',
114
159
  },
160
+ darkColors: {
161
+ canvas: 'oklch(0.13 0.005 265)',
162
+ surface: 'oklch(0.17 0.008 265)',
163
+ border: 'oklch(0.26 0.01 265)',
164
+ primaryAccent: 'oklch(0.65 0.22 265)',
165
+ onPrimaryAccent: 'oklch(0.99 0 0)',
166
+ textPrimary: 'oklch(0.98 0 0)',
167
+ textMuted: 'oklch(0.65 0.01 265)',
168
+ ring: 'oklch(0.65 0.22 265)',
169
+ },
115
170
  typography: {
116
- display: 'Inter, system-ui, sans-serif',
117
- body: 'Inter, system-ui, sans-serif',
171
+ display: 'Vazirmatn, Inter, system-ui, sans-serif',
172
+ body: 'Vazirmatn, Inter, system-ui, sans-serif',
118
173
  mono: 'Fira Code, monospace',
119
174
  },
120
175
  physics: {
@@ -218,6 +218,90 @@ export function ContrastBadge({ ratio, label = 'WCAG 2.2', className = '' }: Con
218
218
  </span>
219
219
  );
220
220
  }
221
+ `,
222
+ },
223
+ 'theme-toggle': {
224
+ name: 'theme-toggle',
225
+ filename: 'ThemeToggle.tsx',
226
+ description: 'Accessible dual-theme switch (light/dark) with SVG vector icons, focus ring, and localStorage persistence',
227
+ code: `'use client';
228
+
229
+ import React, { useEffect, useState } from 'react';
230
+
231
+ export interface ThemeToggleProps {
232
+ className?: string;
233
+ defaultTheme?: 'light' | 'dark' | 'system';
234
+ }
235
+
236
+ export function ThemeToggle({ className = '', defaultTheme = 'system' }: ThemeToggleProps) {
237
+ const [theme, setTheme] = useState<'light' | 'dark'>('dark');
238
+ const [mounted, setMounted] = useState(false);
239
+
240
+ useEffect(() => {
241
+ setMounted(true);
242
+ const stored = localStorage.getItem('vibe-theme');
243
+ if (stored === 'light' || stored === 'dark') {
244
+ setTheme(stored);
245
+ document.documentElement.classList.toggle('dark', stored === 'dark');
246
+ document.documentElement.setAttribute('data-theme', stored);
247
+ } else {
248
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
249
+ const initial = defaultTheme === 'system' ? (prefersDark ? 'dark' : 'light') : defaultTheme;
250
+ setTheme(initial);
251
+ document.documentElement.classList.toggle('dark', initial === 'dark');
252
+ document.documentElement.setAttribute('data-theme', initial);
253
+ }
254
+ }, [defaultTheme]);
255
+
256
+ const toggleTheme = () => {
257
+ const next = theme === 'dark' ? 'light' : 'dark';
258
+ setTheme(next);
259
+ localStorage.setItem('vibe-theme', next);
260
+ document.documentElement.classList.toggle('dark', next === 'dark');
261
+ document.documentElement.setAttribute('data-theme', next);
262
+ };
263
+
264
+ if (!mounted) {
265
+ return (
266
+ <button
267
+ type="button"
268
+ aria-label="Toggle theme"
269
+ className={\`inline-flex items-center justify-center min-h-[44px] min-w-[44px] p-2.5 rounded-lg border border-neutral-800 bg-neutral-900/50 text-neutral-400 opacity-60 \${className}\`}
270
+ disabled
271
+ >
272
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
273
+ <circle cx="12" cy="12" r="4" strokeWidth="2" />
274
+ </svg>
275
+ </button>
276
+ );
277
+ }
278
+
279
+ return (
280
+ <button
281
+ type="button"
282
+ onClick={toggleTheme}
283
+ aria-label={\`Switch to \${theme === 'dark' ? 'light' : 'dark'} mode\`}
284
+ className={\`inline-flex items-center justify-center min-h-[44px] min-w-[44px] p-2.5 rounded-lg border transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 \${
285
+ theme === 'dark'
286
+ ? 'border-neutral-800 bg-neutral-900/80 text-neutral-200 hover:bg-neutral-800 hover:text-white'
287
+ : 'border-neutral-200 bg-white text-neutral-700 hover:bg-neutral-100 hover:text-neutral-900'
288
+ } \${className}\`}
289
+ >
290
+ {theme === 'dark' ? (
291
+ /* Sun Icon for switching to light mode */
292
+ <svg className="w-5 h-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2" aria-hidden="true">
293
+ <circle cx="12" cy="12" r="4" />
294
+ <path strokeLinecap="round" d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M6.34 17.66l-1.41 1.41m14.14-14.14l-1.41 1.41" />
295
+ </svg>
296
+ ) : (
297
+ /* Moon Icon for switching to dark mode */
298
+ <svg className="w-5 h-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2" aria-hidden="true">
299
+ <path strokeLinecap="round" strokeLinejoin="round" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
300
+ </svg>
301
+ )}
302
+ </button>
303
+ );
304
+ }
221
305
  `,
222
306
  },
223
307
  };
@@ -215,6 +215,90 @@ export function ContrastBadge({ ratio, label = 'WCAG 2.2', className = '' }: Con
215
215
  </span>
216
216
  );
217
217
  }
218
+ `,
219
+ },
220
+ 'theme-toggle': {
221
+ name: 'theme-toggle',
222
+ filename: 'ThemeToggle.tsx',
223
+ description: 'Accessible dual-theme switch (light/dark) with SVG vector icons, focus ring, and localStorage persistence',
224
+ code: `'use client';
225
+
226
+ import React, { useEffect, useState } from 'react';
227
+
228
+ export interface ThemeToggleProps {
229
+ className?: string;
230
+ defaultTheme?: 'light' | 'dark' | 'system';
231
+ }
232
+
233
+ export function ThemeToggle({ className = '', defaultTheme = 'system' }: ThemeToggleProps) {
234
+ const [theme, setTheme] = useState<'light' | 'dark'>('dark');
235
+ const [mounted, setMounted] = useState(false);
236
+
237
+ useEffect(() => {
238
+ setMounted(true);
239
+ const stored = localStorage.getItem('vibe-theme');
240
+ if (stored === 'light' || stored === 'dark') {
241
+ setTheme(stored);
242
+ document.documentElement.classList.toggle('dark', stored === 'dark');
243
+ document.documentElement.setAttribute('data-theme', stored);
244
+ } else {
245
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
246
+ const initial = defaultTheme === 'system' ? (prefersDark ? 'dark' : 'light') : defaultTheme;
247
+ setTheme(initial);
248
+ document.documentElement.classList.toggle('dark', initial === 'dark');
249
+ document.documentElement.setAttribute('data-theme', initial);
250
+ }
251
+ }, [defaultTheme]);
252
+
253
+ const toggleTheme = () => {
254
+ const next = theme === 'dark' ? 'light' : 'dark';
255
+ setTheme(next);
256
+ localStorage.setItem('vibe-theme', next);
257
+ document.documentElement.classList.toggle('dark', next === 'dark');
258
+ document.documentElement.setAttribute('data-theme', next);
259
+ };
260
+
261
+ if (!mounted) {
262
+ return (
263
+ <button
264
+ type="button"
265
+ aria-label="Toggle theme"
266
+ className={\`inline-flex items-center justify-center min-h-[44px] min-w-[44px] p-2.5 rounded-lg border border-neutral-800 bg-neutral-900/50 text-neutral-400 opacity-60 \${className}\`}
267
+ disabled
268
+ >
269
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
270
+ <circle cx="12" cy="12" r="4" strokeWidth="2" />
271
+ </svg>
272
+ </button>
273
+ );
274
+ }
275
+
276
+ return (
277
+ <button
278
+ type="button"
279
+ onClick={toggleTheme}
280
+ aria-label={\`Switch to \${theme === 'dark' ? 'light' : 'dark'} mode\`}
281
+ className={\`inline-flex items-center justify-center min-h-[44px] min-w-[44px] p-2.5 rounded-lg border transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 \${
282
+ theme === 'dark'
283
+ ? 'border-neutral-800 bg-neutral-900/80 text-neutral-200 hover:bg-neutral-800 hover:text-white'
284
+ : 'border-neutral-200 bg-white text-neutral-700 hover:bg-neutral-100 hover:text-neutral-900'
285
+ } \${className}\`}
286
+ >
287
+ {theme === 'dark' ? (
288
+ /* Sun Icon for switching to light mode */
289
+ <svg className="w-5 h-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2" aria-hidden="true">
290
+ <circle cx="12" cy="12" r="4" />
291
+ <path strokeLinecap="round" d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M6.34 17.66l-1.41 1.41m14.14-14.14l-1.41 1.41" />
292
+ </svg>
293
+ ) : (
294
+ /* Moon Icon for switching to dark mode */
295
+ <svg className="w-5 h-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2" aria-hidden="true">
296
+ <path strokeLinecap="round" strokeLinejoin="round" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
297
+ </svg>
298
+ )}
299
+ </button>
300
+ );
301
+ }
218
302
  `,
219
303
  },
220
304
  };
package/dist/tailwind.js CHANGED
@@ -16,6 +16,7 @@ function createVibeUiPlugin(options = {}) {
16
16
  '--vibe-surface': chem.colors.surface,
17
17
  '--vibe-border': chem.colors.border,
18
18
  '--vibe-primary': chem.colors.primaryAccent,
19
+ '--vibe-on-primary': chem.colors.onPrimaryAccent,
19
20
  '--vibe-text-primary': chem.colors.textPrimary,
20
21
  '--vibe-text-muted': chem.colors.textMuted,
21
22
  '--vibe-ring': chem.colors.ring,
@@ -33,9 +34,18 @@ function createVibeUiPlugin(options = {}) {
33
34
  '.vibe-spring': {
34
35
  'transition-timing-function': index_1.MOTION_CURVES.naturalSpring,
35
36
  },
37
+ '.ease-vibe-spring': {
38
+ 'transition-timing-function': index_1.MOTION_CURVES.naturalSpring,
39
+ },
36
40
  '.vibe-snap': {
37
41
  'transition-timing-function': index_1.MOTION_CURVES.responsiveSnap,
38
42
  },
43
+ '.ease-vibe-snap': {
44
+ 'transition-timing-function': index_1.MOTION_CURVES.responsiveSnap,
45
+ },
46
+ '.ease-vibe-glide': {
47
+ 'transition-timing-function': 'cubic-bezier(0.25, 1, 0.5, 1)',
48
+ },
39
49
  '.vibe-glass': {
40
50
  'backdrop-filter': 'blur(12px)',
41
51
  '-webkit-backdrop-filter': 'blur(12px)',
package/dist/tailwind.mjs CHANGED
@@ -12,6 +12,7 @@ export function createVibeUiPlugin(options = {}) {
12
12
  '--vibe-surface': chem.colors.surface,
13
13
  '--vibe-border': chem.colors.border,
14
14
  '--vibe-primary': chem.colors.primaryAccent,
15
+ '--vibe-on-primary': chem.colors.onPrimaryAccent,
15
16
  '--vibe-text-primary': chem.colors.textPrimary,
16
17
  '--vibe-text-muted': chem.colors.textMuted,
17
18
  '--vibe-ring': chem.colors.ring,
@@ -29,9 +30,18 @@ export function createVibeUiPlugin(options = {}) {
29
30
  '.vibe-spring': {
30
31
  'transition-timing-function': MOTION_CURVES.naturalSpring,
31
32
  },
33
+ '.ease-vibe-spring': {
34
+ 'transition-timing-function': MOTION_CURVES.naturalSpring,
35
+ },
32
36
  '.vibe-snap': {
33
37
  'transition-timing-function': MOTION_CURVES.responsiveSnap,
34
38
  },
39
+ '.ease-vibe-snap': {
40
+ 'transition-timing-function': MOTION_CURVES.responsiveSnap,
41
+ },
42
+ '.ease-vibe-glide': {
43
+ 'transition-timing-function': 'cubic-bezier(0.25, 1, 0.5, 1)',
44
+ },
35
45
  '.vibe-glass': {
36
46
  'backdrop-filter': 'blur(12px)',
37
47
  '-webkit-backdrop-filter': 'blur(12px)',
package/dist/v4.css CHANGED
@@ -1,20 +1,26 @@
1
1
  /**
2
- * @omid-io/tokens/v4.css — Native Tailwind CSS v4 Theme Preset
2
+ * vibe-ui-suite/v4.css — Native Tailwind CSS v4 Theme Preset
3
3
  * Provides zero-config @theme tokens with perceptual OKLCH color spaces and physics curves.
4
4
  * Usage:
5
5
  * @import "tailwindcss";
6
- * @import "@omid-io/tokens/v4.css";
6
+ * @import "vibe-ui-suite/v4.css";
7
7
  */
8
8
 
9
9
  @theme {
10
- /* OKLCH Perceptual Colors — Semantic Palette */
11
- --color-vibe-canvas: var(--vibe-canvas, oklch(0.14 0.005 260));
12
- --color-vibe-surface: var(--vibe-surface, oklch(0.18 0.008 260));
13
- --color-vibe-border: var(--vibe-border, oklch(0.28 0.01 260));
14
- --color-vibe-primary: var(--vibe-primary, oklch(0.65 0.22 265));
15
- --color-vibe-text-primary: var(--vibe-text-primary, oklch(0.98 0 0));
16
- --color-vibe-text-muted: var(--vibe-text-muted, oklch(0.70 0.01 260));
17
- --color-vibe-ring: var(--vibe-ring, oklch(0.65 0.22 265));
10
+ /* OKLCH Perceptual Colors — Semantic Palette (Defaults to Light Mode) */
11
+ --color-vibe-canvas: var(--vibe-canvas, oklch(0.985 0.002 264));
12
+ --color-vibe-surface: var(--vibe-surface, oklch(1 0 0));
13
+ --color-vibe-border: var(--vibe-border, oklch(0.90 0.008 264));
14
+ --color-vibe-primary: var(--vibe-primary-accent, var(--vibe-primary, oklch(0.55 0.22 260)));
15
+ --color-vibe-on-primary: var(--vibe-on-primary, oklch(0.99 0 0));
16
+ --color-vibe-text-primary: var(--vibe-text-primary, oklch(0.14 0.005 264));
17
+ --color-vibe-text-muted: var(--vibe-text-muted, oklch(0.48 0.015 264));
18
+ --color-vibe-ring: var(--vibe-ring, oklch(0.55 0.22 260));
19
+
20
+ /* Typography Stacks with Vazirmatn Persian Web Font */
21
+ --font-vibe-sans: Vazirmatn, Inter, system-ui, -apple-system, sans-serif;
22
+ --font-vibe-display: Vazirmatn, Inter, system-ui, sans-serif;
23
+ --font-vibe-mono: JetBrains Mono, Menlo, monospace;
18
24
 
19
25
  /* Spring & Snap Physics Curves */
20
26
  --ease-vibe-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
@@ -27,6 +33,19 @@
27
33
  --shadow-vibe-glass: inset 0 1px 1px rgba(255, 255, 255, 0.15);
28
34
  }
29
35
 
36
+ /* Fallback Dark Theme Variables when .dark or [data-theme="dark"] is applied */
37
+ .dark, [data-theme="dark"] {
38
+ --vibe-canvas: oklch(0.14 0.005 264);
39
+ --vibe-surface: oklch(0.18 0.008 264);
40
+ --vibe-border: oklch(0.28 0.01 264);
41
+ --vibe-primary: oklch(0.65 0.22 260);
42
+ --vibe-primary-accent: oklch(0.65 0.22 260);
43
+ --vibe-on-primary: oklch(0.99 0 0);
44
+ --vibe-text-primary: oklch(0.98 0 0);
45
+ --vibe-text-muted: oklch(0.65 0.015 264);
46
+ --vibe-ring: oklch(0.65 0.22 260);
47
+ }
48
+
30
49
  /* BiDi Isolation Utility */
31
50
  [dir="rtl"] {
32
51
  letter-spacing: normal !important;
@@ -37,3 +56,16 @@
37
56
  backdrop-filter: blur(12px);
38
57
  -webkit-backdrop-filter: blur(12px);
39
58
  }
59
+
60
+ /* Tailwind CSS v4 Motion & Transition Utilities */
61
+ @utility transition-vibe-spring {
62
+ transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
63
+ }
64
+
65
+ @utility transition-vibe-snap {
66
+ transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
67
+ }
68
+
69
+ @utility transition-vibe-glide {
70
+ transition-timing-function: cubic-bezier(0.25, 1, 0.5, 1);
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-ui-suite",
3
- "version": "4.0.0",
3
+ "version": "4.8.0",
4
4
  "description": "Deterministic design contracts, component recipes, and WCAG AA evaluation gates for AI coding agents (Cursor, Claude Code, Windsurf, Antigravity).",
5
5
  "bin": {
6
6
  "vibe-ui-suite": "./dist/cli.js",
@@ -23,6 +23,10 @@
23
23
  "./v4": "./dist/v4.css",
24
24
  "./v4.css": "./dist/v4.css"
25
25
  },
26
+ "files": [
27
+ "dist",
28
+ "README.md"
29
+ ],
26
30
  "scripts": {
27
31
  "build": "node build.js"
28
32
  },
package/build.js DELETED
@@ -1,23 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
-
4
- const srcDistDir = path.resolve(__dirname, '../tokens/dist');
5
- const targetDistDir = path.resolve(__dirname, 'dist');
6
-
7
- if (!fs.existsSync(srcDistDir)) {
8
- console.error('Error: ../tokens/dist does not exist. Please build @omid-io/tokens first.');
9
- process.exit(1);
10
- }
11
-
12
- // Copy recursively from tokens/dist to vibe-ui-suite/dist
13
- fs.cpSync(srcDistDir, targetDistDir, { recursive: true });
14
-
15
- // Ensure executable permission on dist/cli.js
16
- const cliPath = path.join(targetDistDir, 'cli.js');
17
- if (fs.existsSync(cliPath)) {
18
- try {
19
- fs.chmodSync(cliPath, 0o755);
20
- } catch (e) {}
21
- }
22
-
23
- console.log('✅ vibe-ui-suite build complete: synchronized dist/ from tokens package.');