@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.
@@ -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,10 @@
1
+ import postcssImport from 'postcss-import';
2
+ import discardComments from 'postcss-discard-comments';
3
+
4
+ export default {
5
+ plugins: [
6
+ postcssImport(),
7
+ discardComments({ removeAll: true }),
8
+ ],
9
+ };
10
+
@@ -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
+