@nqlib/nqui 0.1.1 โ 0.1.3
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 +40 -2
- package/dist/styles.css +468 -21
- package/package.json +5 -1
- package/scripts/build-styles.js +0 -10
- package/scripts/examples/nextjs-layout.tsx +2 -0
- package/scripts/examples.js +76 -0
- package/scripts/framework.js +68 -0
- package/scripts/getPackageRoot.js +26 -0
- package/scripts/help.js +34 -0
- package/scripts/init-css.js +88 -543
- package/scripts/pipeline/emit.js +21 -0
- package/scripts/pipeline/extract.js +118 -0
- package/scripts/pipeline/index.js +90 -0
- package/scripts/pipeline/tokens.js +25 -0
- package/scripts/pipeline/transform.js +51 -0
- package/scripts/postcss.config.mjs +10 -0
- package/scripts/setup-helper.js +72 -0
- package/scripts/wizard.js +57 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Emit file with force and dry-run support
|
|
6
|
+
*/
|
|
7
|
+
export function emit(path, content, { force, dryRun }) {
|
|
8
|
+
if (existsSync(path) && !force) {
|
|
9
|
+
throw new Error(`File exists: ${path}. Use --force to overwrite.`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (dryRun) {
|
|
13
|
+
console.log(`๐งช Dry-run: Would write ${path} (${content.length} bytes)`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
18
|
+
writeFileSync(path, content, 'utf8');
|
|
19
|
+
console.log(`โ
Created: ${path}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extract CSS from package
|
|
6
|
+
* Handles both published (dist/styles.css) and local development (src/index.css + src/styles/colors.css)
|
|
7
|
+
*/
|
|
8
|
+
export function extractCSS(root) {
|
|
9
|
+
const dist = join(root, 'dist', 'styles.css');
|
|
10
|
+
const srcIndex = join(root, 'src', 'index.css');
|
|
11
|
+
const srcColors = join(root, 'src', 'styles', 'colors.css');
|
|
12
|
+
|
|
13
|
+
// If published package, use dist/styles.css
|
|
14
|
+
if (existsSync(dist)) {
|
|
15
|
+
return readFileSync(dist, 'utf8');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Local development: merge index.css and colors.css
|
|
19
|
+
if (existsSync(srcIndex) && existsSync(srcColors)) {
|
|
20
|
+
let indexCss = readFileSync(srcIndex, 'utf8');
|
|
21
|
+
let colorsCss = readFileSync(srcColors, 'utf8');
|
|
22
|
+
|
|
23
|
+
// Extract :root and .dark from colors.css (wrapped in @layer base)
|
|
24
|
+
const colorsRootMatch = colorsCss.match(/@layer base\s*\{\s*:root\s*\{([\s\S]*?)\}\s*\.dark\s*\{([\s\S]*?)\}\s*\}/s);
|
|
25
|
+
|
|
26
|
+
let colorsRootContent = '';
|
|
27
|
+
let colorsDarkContent = '';
|
|
28
|
+
|
|
29
|
+
if (colorsRootMatch) {
|
|
30
|
+
colorsRootContent = colorsRootMatch[1].trim();
|
|
31
|
+
colorsDarkContent = colorsRootMatch[2].trim();
|
|
32
|
+
} else {
|
|
33
|
+
// Fallback: try to extract without @layer base wrapper
|
|
34
|
+
const rootMatch = colorsCss.match(/:root\s*\{([\s\S]*?)\}/s);
|
|
35
|
+
const darkMatch = colorsCss.match(/\.dark\s*\{([\s\S]*?)\}/s);
|
|
36
|
+
if (rootMatch) colorsRootContent = rootMatch[1].trim();
|
|
37
|
+
if (darkMatch) colorsDarkContent = darkMatch[1].trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Extract from index.css
|
|
41
|
+
const indexRootMatch = indexCss.match(/:root\s*\{([\s\S]*?)\}/s);
|
|
42
|
+
const indexDarkMatch = indexCss.match(/\.dark\s*\{([\s\S]*?)\}/s);
|
|
43
|
+
|
|
44
|
+
const indexRoot = indexRootMatch ? indexRootMatch[1].trim() : '';
|
|
45
|
+
const indexDark = indexDarkMatch ? indexDarkMatch[1].trim() : '';
|
|
46
|
+
|
|
47
|
+
// Merge: colors first, then index
|
|
48
|
+
const mergedRoot = [colorsRootContent, indexRoot].filter(Boolean).join('\n\n');
|
|
49
|
+
const mergedDark = [colorsDarkContent, indexDark].filter(Boolean).join('\n\n');
|
|
50
|
+
|
|
51
|
+
// Clean up imports and Tailwind v4 directives we don't want in standalone file
|
|
52
|
+
indexCss = indexCss
|
|
53
|
+
.replace(/@import\s+["']tailwindcss["'];?\s*\n/g, '')
|
|
54
|
+
.replace(/@import\s+["']tw-animate-css["'];?\s*\n/g, '')
|
|
55
|
+
.replace(/@import\s+["'].*shadcn.*["'];?\s*\n/g, '')
|
|
56
|
+
.replace(/@import\s+["'].*@fontsource-variable\/inter["'];?\s*\n/g, '')
|
|
57
|
+
.replace(/@import\s+["'].*\/colors\.css["'];?\s*\n/g, '')
|
|
58
|
+
.replace(/\/\*\s*Import enhanced color system\s*\*\//g, '')
|
|
59
|
+
.replace(/@source\s+[^;]+;?\s*\n/g, '')
|
|
60
|
+
.replace(/@custom-variant\s+[^;]+;?\s*\n/g, '')
|
|
61
|
+
.replace(/@theme\s+inline\s*\{[\s\S]*?\}\s*/g, '');
|
|
62
|
+
|
|
63
|
+
// Helper function to extract complete CSS block (handles nested braces)
|
|
64
|
+
function extractBlock(css, pattern) {
|
|
65
|
+
const match = css.match(pattern);
|
|
66
|
+
if (!match) return null;
|
|
67
|
+
const start = match.index;
|
|
68
|
+
let depth = 0;
|
|
69
|
+
let i = start + match[0].indexOf('{');
|
|
70
|
+
while (i < css.length) {
|
|
71
|
+
if (css[i] === '{') depth++;
|
|
72
|
+
if (css[i] === '}') {
|
|
73
|
+
depth--;
|
|
74
|
+
if (depth === 0) {
|
|
75
|
+
return css.substring(start, i + 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
i++;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Extract blocks using brace counting
|
|
84
|
+
const existingRoot = extractBlock(indexCss, /:root\s*\{/s) || '';
|
|
85
|
+
const existingDark = extractBlock(indexCss, /\.dark\s*\{/s) || '';
|
|
86
|
+
const layerBaseMatch = indexCss.match(/@layer base\s*\{/g);
|
|
87
|
+
const layerBaseBlocks = layerBaseMatch ? layerBaseMatch.map(m => extractBlock(indexCss, new RegExp(m.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 's'))).filter(Boolean).join('\n\n') : '';
|
|
88
|
+
const layerUtilitiesMatch = indexCss.match(/@layer utilities\s*\{/g);
|
|
89
|
+
const layerUtilitiesBlocks = layerUtilitiesMatch ? layerUtilitiesMatch.map(m => extractBlock(indexCss, new RegExp(m.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 's'))).filter(Boolean).join('\n\n') : '';
|
|
90
|
+
|
|
91
|
+
// Remove extracted blocks to get remaining content
|
|
92
|
+
let remainingCss = indexCss;
|
|
93
|
+
if (existingRoot) remainingCss = remainingCss.replace(existingRoot, '');
|
|
94
|
+
if (existingDark) remainingCss = remainingCss.replace(existingDark, '');
|
|
95
|
+
if (layerBaseBlocks) remainingCss = remainingCss.replace(layerBaseBlocks, '');
|
|
96
|
+
if (layerUtilitiesBlocks) remainingCss = remainingCss.replace(layerUtilitiesBlocks, '');
|
|
97
|
+
remainingCss = remainingCss.trim();
|
|
98
|
+
|
|
99
|
+
// Create :root and .dark blocks from merged content (prefer merged over existing)
|
|
100
|
+
const rootBlock = mergedRoot ? `:root {\n${mergedRoot}\n}` : existingRoot;
|
|
101
|
+
const darkBlock = mergedDark ? `.dark {\n${mergedDark}\n}` : existingDark;
|
|
102
|
+
|
|
103
|
+
// Reconstruct in correct order: :root, .dark, then @layer blocks, then rest
|
|
104
|
+
// This is critical because @layer base uses @apply which needs CSS variables to be defined first
|
|
105
|
+
const reorderedCss = [
|
|
106
|
+
rootBlock,
|
|
107
|
+
darkBlock,
|
|
108
|
+
layerBaseBlocks,
|
|
109
|
+
layerUtilitiesBlocks,
|
|
110
|
+
remainingCss
|
|
111
|
+
].filter(Boolean).join('\n\n');
|
|
112
|
+
|
|
113
|
+
return reorderedCss.trim();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
throw new Error('No CSS source found. Expected dist/styles.css or src/index.css + src/styles/colors.css');
|
|
117
|
+
}
|
|
118
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { extractCSS } from './extract.js';
|
|
2
|
+
import { transformCSS } from './transform.js';
|
|
3
|
+
import { extractTokens, tokensToJS } from './tokens.js';
|
|
4
|
+
import { emit } from './emit.js';
|
|
5
|
+
|
|
6
|
+
const header = `/**
|
|
7
|
+
* nqui Design Tokens
|
|
8
|
+
*
|
|
9
|
+
* Auto-generated file โ do not edit manually.
|
|
10
|
+
* Regenerate with: npx nqui init-css
|
|
11
|
+
*/\n\n`;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Reorder CSS to ensure :root and .dark come before @layer blocks
|
|
15
|
+
*/
|
|
16
|
+
function reorderCSS(css) {
|
|
17
|
+
// Helper to extract complete block with nested braces
|
|
18
|
+
function extractBlock(text, pattern) {
|
|
19
|
+
const match = text.match(pattern);
|
|
20
|
+
if (!match) return null;
|
|
21
|
+
const start = match.index;
|
|
22
|
+
let depth = 0;
|
|
23
|
+
let i = start + match[0].indexOf('{');
|
|
24
|
+
while (i < text.length) {
|
|
25
|
+
if (text[i] === '{') depth++;
|
|
26
|
+
if (text[i] === '}') {
|
|
27
|
+
depth--;
|
|
28
|
+
if (depth === 0) {
|
|
29
|
+
return text.substring(start, i + 1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
i++;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const rootBlock = extractBlock(css, /:root\s*\{/s) || '';
|
|
38
|
+
const darkBlock = extractBlock(css, /\.dark\s*\{/s) || '';
|
|
39
|
+
|
|
40
|
+
// Remove extracted blocks
|
|
41
|
+
let remaining = css;
|
|
42
|
+
if (rootBlock) remaining = remaining.replace(rootBlock, '');
|
|
43
|
+
if (darkBlock) remaining = remaining.replace(darkBlock, '');
|
|
44
|
+
|
|
45
|
+
// Extract @layer blocks
|
|
46
|
+
const layerBaseMatches = [...remaining.matchAll(/@layer base\s*\{/g)];
|
|
47
|
+
const layerBaseBlocks = layerBaseMatches.map(m => extractBlock(remaining, new RegExp(m[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 's'))).filter(Boolean).join('\n\n');
|
|
48
|
+
|
|
49
|
+
const layerUtilitiesMatches = [...remaining.matchAll(/@layer utilities\s*\{/g)];
|
|
50
|
+
const layerUtilitiesBlocks = layerUtilitiesMatches.map(m => extractBlock(remaining, new RegExp(m[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 's'))).filter(Boolean).join('\n\n');
|
|
51
|
+
|
|
52
|
+
// Remove @layer blocks
|
|
53
|
+
if (layerBaseBlocks) remaining = remaining.replace(layerBaseBlocks, '');
|
|
54
|
+
if (layerUtilitiesBlocks) remaining = remaining.replace(layerUtilitiesBlocks, '');
|
|
55
|
+
remaining = remaining.trim();
|
|
56
|
+
|
|
57
|
+
// Reconstruct in correct order
|
|
58
|
+
return [rootBlock, darkBlock, layerBaseBlocks, layerUtilitiesBlocks, remaining]
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.join('\n\n')
|
|
61
|
+
.trim();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Run the complete pipeline: extract โ transform โ reorder โ emit
|
|
66
|
+
*/
|
|
67
|
+
export async function runPipeline({
|
|
68
|
+
root,
|
|
69
|
+
outCss,
|
|
70
|
+
outJs,
|
|
71
|
+
tokensOnly,
|
|
72
|
+
force,
|
|
73
|
+
dryRun,
|
|
74
|
+
}) {
|
|
75
|
+
const css = extractCSS(root);
|
|
76
|
+
let transformed = await transformCSS(css, { tokensOnly });
|
|
77
|
+
|
|
78
|
+
// Reorder CSS to ensure :root/.dark come before @layer blocks
|
|
79
|
+
if (!tokensOnly) {
|
|
80
|
+
transformed = reorderCSS(transformed);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
emit(outCss, header + transformed, { force, dryRun });
|
|
84
|
+
|
|
85
|
+
if (outJs) {
|
|
86
|
+
const tokens = extractTokens(transformed);
|
|
87
|
+
emit(outJs, tokensToJS(tokens), { force, dryRun });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import postcss from 'postcss';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract CSS variables (tokens) from CSS
|
|
5
|
+
*/
|
|
6
|
+
export function extractTokens(css) {
|
|
7
|
+
const tokens = {};
|
|
8
|
+
const root = postcss.parse(css);
|
|
9
|
+
|
|
10
|
+
root.walkDecls((decl) => {
|
|
11
|
+
if (decl.prop.startsWith('--')) {
|
|
12
|
+
tokens[decl.prop.slice(2)] = decl.value;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return tokens;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Convert tokens object to JS export
|
|
21
|
+
*/
|
|
22
|
+
export function tokensToJS(tokens) {
|
|
23
|
+
return `export const tokens = ${JSON.stringify(tokens, null, 2)};`;
|
|
24
|
+
}
|
|
25
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import postcss from 'postcss';
|
|
2
|
+
import { extractTokens } from './tokens.js';
|
|
3
|
+
|
|
4
|
+
// Dynamic import for PostCSS config (ESM compatibility)
|
|
5
|
+
let postcssConfig = null;
|
|
6
|
+
|
|
7
|
+
async function loadPostcssConfig() {
|
|
8
|
+
if (postcssConfig) return postcssConfig;
|
|
9
|
+
|
|
10
|
+
// Try to load postcss.config.mjs
|
|
11
|
+
try {
|
|
12
|
+
const configModule = await import('../postcss.config.mjs');
|
|
13
|
+
postcssConfig = configModule.default;
|
|
14
|
+
return postcssConfig;
|
|
15
|
+
} catch (err) {
|
|
16
|
+
// Fallback: use basic plugins
|
|
17
|
+
const postcssImport = (await import('postcss-import')).default;
|
|
18
|
+
const discardComments = (await import('postcss-discard-comments')).default;
|
|
19
|
+
postcssConfig = {
|
|
20
|
+
plugins: [
|
|
21
|
+
postcssImport(),
|
|
22
|
+
discardComments({ removeAll: true }),
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
return postcssConfig;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Transform CSS using PostCSS pipeline
|
|
31
|
+
* If tokensOnly is true, returns only CSS variables in :root block
|
|
32
|
+
*/
|
|
33
|
+
export async function transformCSS(css, { tokensOnly }) {
|
|
34
|
+
// Remove Tailwind v4 specific directives before PostCSS processing
|
|
35
|
+
// These are not standard CSS and will cause PostCSS to fail
|
|
36
|
+
const cleanedCss = css
|
|
37
|
+
.replace(/@source\s+[^;]+;?\s*\n/g, '')
|
|
38
|
+
.replace(/@custom-variant\s+[^;]+;?\s*\n/g, '')
|
|
39
|
+
.replace(/@theme\s+inline\s*\{[\s\S]*?\}\s*/g, '');
|
|
40
|
+
|
|
41
|
+
const config = await loadPostcssConfig();
|
|
42
|
+
const result = await postcss(config.plugins).process(cleanedCss, { from: undefined });
|
|
43
|
+
|
|
44
|
+
if (!tokensOnly) return result.css.trim();
|
|
45
|
+
|
|
46
|
+
const tokens = extractTokens(result.css);
|
|
47
|
+
return `:root {\n${Object.entries(tokens)
|
|
48
|
+
.map(([k, v]) => ` --${k}: ${v};`)
|
|
49
|
+
.join('\n')}\n}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { detectFramework, findMainCssFile } from './framework.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate setup helper content with framework-specific imports
|
|
6
|
+
*/
|
|
7
|
+
export function generateSetupContent(framework, nquiCssPath, useLibraryImport = true) {
|
|
8
|
+
const mainCssFile = findMainCssFile(framework);
|
|
9
|
+
const mainCssDir = mainCssFile.split('/').slice(0, -1).join('/');
|
|
10
|
+
const nquiFileName = nquiCssPath.split('/').pop();
|
|
11
|
+
|
|
12
|
+
// Calculate relative path
|
|
13
|
+
let relativeImport;
|
|
14
|
+
if (useLibraryImport) {
|
|
15
|
+
// Import directly from library package
|
|
16
|
+
relativeImport = '@nqlib/nqui/styles';
|
|
17
|
+
} else {
|
|
18
|
+
// Import from local file
|
|
19
|
+
if (mainCssDir === 'app' || mainCssDir === 'src/app') {
|
|
20
|
+
// From app/globals.css or src/app/globals.css to nqui/index.css or nqui/nqui.css
|
|
21
|
+
relativeImport = '../nqui/' + nquiFileName;
|
|
22
|
+
} else if (mainCssDir === 'src') {
|
|
23
|
+
// From src/index.css to nqui/index.css or nqui/nqui.css
|
|
24
|
+
relativeImport = '../nqui/' + nquiFileName;
|
|
25
|
+
} else if (mainCssDir === nquiCssPath.split('/').slice(0, -1).join('/')) {
|
|
26
|
+
// Same directory
|
|
27
|
+
relativeImport = './' + nquiFileName;
|
|
28
|
+
} else {
|
|
29
|
+
// Default: use path from project root
|
|
30
|
+
relativeImport = './' + nquiCssPath;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const base = `/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
35
|
+
nqui โ Recommended Tailwind & Import Setup
|
|
36
|
+
Add these lines to the TOP of your main CSS file
|
|
37
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
|
38
|
+
|
|
39
|
+
@import "tailwindcss";
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const extras = {
|
|
43
|
+
nextjs: `
|
|
44
|
+
/* Next.js + Tailwind CSS v4 โ required source directives */
|
|
45
|
+
@source "./**/*.{js,ts,jsx,tsx,mdx}";
|
|
46
|
+
@source "../components/**/*.{js,ts,jsx,tsx,mdx}";
|
|
47
|
+
@source "../node_modules/@nqlib/nqui/dist/**/*.js";
|
|
48
|
+
|
|
49
|
+
@import "tw-animate-css";
|
|
50
|
+
|
|
51
|
+
/* Better dark mode handling */
|
|
52
|
+
@custom-variant dark (&:is(.dark *));
|
|
53
|
+
`,
|
|
54
|
+
vite: `
|
|
55
|
+
@import "tw-animate-css";
|
|
56
|
+
`,
|
|
57
|
+
'create-react-app': `
|
|
58
|
+
@import "tw-animate-css";
|
|
59
|
+
`,
|
|
60
|
+
remix: `
|
|
61
|
+
@import "tw-animate-css";
|
|
62
|
+
`,
|
|
63
|
+
generic: `
|
|
64
|
+
@import "tw-animate-css";
|
|
65
|
+
`,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const finalImport = `\n/* Import nqui design tokens */\n@import "${relativeImport}";\n`;
|
|
69
|
+
|
|
70
|
+
return base + (extras[framework] || extras.generic) + finalImport;
|
|
71
|
+
}
|
|
72
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createInterface } from 'readline';
|
|
2
|
+
import { detectFramework } from './framework.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create readline interface for prompts
|
|
6
|
+
*/
|
|
7
|
+
function createRL() {
|
|
8
|
+
return createInterface({
|
|
9
|
+
input: process.stdin,
|
|
10
|
+
output: process.stdout
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Ask a question and return the answer
|
|
16
|
+
*/
|
|
17
|
+
export async function askQuestion(question) {
|
|
18
|
+
const rl = createRL();
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
rl.question(question, (answer) => {
|
|
21
|
+
rl.close();
|
|
22
|
+
resolve(answer.trim().toLowerCase());
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Interactive wizard for CLI options
|
|
29
|
+
*/
|
|
30
|
+
export async function wizard() {
|
|
31
|
+
const ask = (question) => askQuestion(question);
|
|
32
|
+
|
|
33
|
+
const tokensOnly = (await ask('Extract tokens only? (y/n): ')) === 'y';
|
|
34
|
+
const js = (await ask('Generate JS tokens? (y/n): ')) === 'y';
|
|
35
|
+
|
|
36
|
+
const framework = detectFramework();
|
|
37
|
+
let copyExamples = false;
|
|
38
|
+
|
|
39
|
+
if (framework === 'nextjs') {
|
|
40
|
+
copyExamples = (await ask('\nCopy Next.js example files (page.tsx, layout.tsx)? (y/n): ')) === 'y';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { tokensOnly, js, copyExamples };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Ask about copying examples (used in default mode for Next.js)
|
|
48
|
+
*/
|
|
49
|
+
export async function askAboutExamples(framework) {
|
|
50
|
+
if (framework !== 'nextjs') {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const answer = await askQuestion('\n๐ฆ Copy Next.js example files (page.tsx, layout.tsx)? (y/n): ');
|
|
55
|
+
return answer === 'y' || answer === 'yes';
|
|
56
|
+
}
|
|
57
|
+
|