zark-design 2.0.0 → 3.0.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/bin/cli.js +363 -90
- package/package.json +2 -2
- package/templates/{README.md → README.md.hbs} +7 -7
- package/templates/_shared/ASSETS-README.md.hbs +39 -0
- package/templates/_shared/tokens.css.hbs +202 -0
- package/templates/_shared/tokens.js.hbs +34 -0
- package/templates/html/components.css +21 -21
- package/templates/html/showcase.html +94 -94
- package/templates/jsx/App.example.jsx +3 -3
- package/templates/jsx/components/Avatar.jsx +2 -2
- package/templates/jsx/components/Funnel.jsx +2 -2
- package/templates/jsx/components/Sidebar.jsx +1 -1
- package/templates/jsx/components.css +21 -21
- package/templates/jsx/icons.jsx +1 -1
- package/templates/presets/zark/preset.json +26 -0
- package/templates/html/tokens.css +0 -283
- package/templates/jsx/assets/logo-zark-laranja.png +0 -0
- package/templates/jsx/assets/zark-icon.png +0 -0
- package/templates/jsx/tokens.css +0 -283
- package/templates/jsx/tokens.js +0 -62
- /package/templates/{html → presets/zark}/assets/logo-zark-laranja.png +0 -0
- /package/templates/{html → presets/zark}/assets/zark-icon.png +0 -0
package/bin/cli.js
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
import { promises as fs } from 'node:fs';
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import readline from 'node:readline';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
8
|
|
|
8
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
const TEMPLATES_DIR = path.resolve(__dirname, '../templates');
|
|
11
|
+
const PRESETS_DIR = path.join(TEMPLATES_DIR, 'presets');
|
|
12
|
+
const SHARED_DIR = path.join(TEMPLATES_DIR, '_shared');
|
|
10
13
|
const PKG_PATH = path.resolve(__dirname, '../package.json');
|
|
11
14
|
|
|
12
15
|
const c = {
|
|
@@ -27,147 +30,417 @@ function banner(title) {
|
|
|
27
30
|
|
|
28
31
|
async function getVersion() {
|
|
29
32
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
return JSON.parse(await fs.readFile(PKG_PATH, 'utf-8')).version;
|
|
34
|
+
} catch { return 'unknown'; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Color utils: hex ↔ HSL, palette generation ──────────────────────────
|
|
38
|
+
|
|
39
|
+
function hexToRgb(hex) {
|
|
40
|
+
const h = hex.replace('#', '').trim();
|
|
41
|
+
const s = h.length === 3 ? h.split('').map(x => x + x).join('') : h;
|
|
42
|
+
if (s.length !== 6 || !/^[0-9a-f]{6}$/i.test(s)) {
|
|
43
|
+
throw new Error(`Hex inválido: "${hex}" (esperado #RRGGBB ou #RGB)`);
|
|
34
44
|
}
|
|
45
|
+
return [parseInt(s.slice(0, 2), 16), parseInt(s.slice(2, 4), 16), parseInt(s.slice(4, 6), 16)];
|
|
35
46
|
}
|
|
36
47
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
function rgbToHex(r, g, b) {
|
|
49
|
+
const cl = (v) => Math.max(0, Math.min(255, Math.round(v)));
|
|
50
|
+
return '#' + [cl(r), cl(g), cl(b)].map(v => v.toString(16).padStart(2, '0')).join('');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function rgbToHsl(r, g, b) {
|
|
54
|
+
r /= 255; g /= 255; b /= 255;
|
|
55
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
56
|
+
let h = 0, s = 0; const l = (max + min) / 2;
|
|
57
|
+
if (max !== min) {
|
|
58
|
+
const d = max - min;
|
|
59
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
60
|
+
switch (max) {
|
|
61
|
+
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
|
62
|
+
case g: h = (b - r) / d + 2; break;
|
|
63
|
+
case b: h = (r - g) / d + 4; break;
|
|
47
64
|
}
|
|
65
|
+
h /= 6;
|
|
48
66
|
}
|
|
67
|
+
return [h * 360, s * 100, l * 100];
|
|
49
68
|
}
|
|
50
69
|
|
|
51
|
-
|
|
52
|
-
|
|
70
|
+
function hslToRgb(h, s, l) {
|
|
71
|
+
h /= 360; s /= 100; l /= 100;
|
|
72
|
+
if (s === 0) { const v = l * 255; return [v, v, v]; }
|
|
73
|
+
const hue2rgb = (p, q, t) => {
|
|
74
|
+
if (t < 0) t += 1; if (t > 1) t -= 1;
|
|
75
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
76
|
+
if (t < 1 / 2) return q;
|
|
77
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
78
|
+
return p;
|
|
79
|
+
};
|
|
80
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
81
|
+
const p = 2 * l - q;
|
|
82
|
+
return [hue2rgb(p, q, h + 1 / 3) * 255, hue2rgb(p, q, h) * 255, hue2rgb(p, q, h - 1 / 3) * 255];
|
|
83
|
+
}
|
|
53
84
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
85
|
+
// Gera escala 50→900 a partir de uma cor base (assumida como 500/primary).
|
|
86
|
+
// Mantém o matiz, varia luminosidade e ajusta saturação por tier.
|
|
87
|
+
function generatePalette(baseHex) {
|
|
88
|
+
const [h, s, l] = rgbToHsl(...hexToRgb(baseHex));
|
|
89
|
+
// Tabela de target lightness por tier; saturação ajustada nos extremos
|
|
90
|
+
const tiers = [
|
|
91
|
+
{ k: 50, L: 95, sScale: 0.55 },
|
|
92
|
+
{ k: 100, L: 88, sScale: 0.70 },
|
|
93
|
+
{ k: 200, L: 78, sScale: 0.85 },
|
|
94
|
+
{ k: 300, L: 68, sScale: 0.95 },
|
|
95
|
+
{ k: 400, L: 58, sScale: 1.00 },
|
|
96
|
+
{ k: 500, L: Math.round(l), sScale: 1.00 }, // mantém a cor base
|
|
97
|
+
{ k: 600, L: Math.max(l - 8, 38), sScale: 1.00 },
|
|
98
|
+
{ k: 700, L: Math.max(l - 18, 28), sScale: 1.00 },
|
|
99
|
+
{ k: 800, L: Math.max(l - 28, 20), sScale: 0.95 },
|
|
100
|
+
{ k: 900, L: Math.max(l - 38, 12), sScale: 0.85 },
|
|
57
101
|
];
|
|
102
|
+
const out = {};
|
|
103
|
+
for (const t of tiers) {
|
|
104
|
+
if (t.k === 500) { out[500] = baseHex.toLowerCase(); continue; }
|
|
105
|
+
const tS = Math.min(100, s * t.sScale);
|
|
106
|
+
const [r, g, b] = hslToRgb(h, tS, t.L);
|
|
107
|
+
out[t.k] = rgbToHex(r, g, b);
|
|
108
|
+
}
|
|
109
|
+
// Rings (rgba)
|
|
110
|
+
const [r5, g5, b5] = hexToRgb(out[500]);
|
|
111
|
+
const [r4, g4, b4] = hexToRgb(out[400]);
|
|
112
|
+
out.ring = `rgba(${r5}, ${g5}, ${b5}, 0.20)`;
|
|
113
|
+
out.ringSoft = `rgba(${r5}, ${g5}, ${b5}, 0.12)`;
|
|
114
|
+
out.ringDark = `rgba(${r4}, ${g4}, ${b4}, 0.30)`;
|
|
115
|
+
out.ringSoftDark = `rgba(${r4}, ${g4}, ${b4}, 0.16)`;
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
58
118
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
119
|
+
// ─── Template engine: tiny Handlebars subset ─────────────────────────────
|
|
120
|
+
// Suporta {{var}}, {{var.nested}}, {{#if x}}...{{/if}}, {{#if x}}A{{else}}B{{/if}}
|
|
121
|
+
|
|
122
|
+
function renderTemplate(src, ctx) {
|
|
123
|
+
const get = (path) => path.split('.').reduce((o, k) => (o == null ? undefined : o[k]), ctx);
|
|
124
|
+
// Process {{#if cond}}...{{else}}...{{/if}} blocks (no nesting needed for our cases)
|
|
125
|
+
let out = src;
|
|
126
|
+
let prev;
|
|
127
|
+
do {
|
|
128
|
+
prev = out;
|
|
129
|
+
out = out.replace(
|
|
130
|
+
/\{\{#if\s+([\w.]+)\s*\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if\}\}/g,
|
|
131
|
+
(_, cond, a, b) => {
|
|
132
|
+
const v = get(cond);
|
|
133
|
+
return v ? a : (b || '');
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
} while (out !== prev);
|
|
137
|
+
// Replace {{var}}
|
|
138
|
+
out = out.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, key) => {
|
|
139
|
+
const v = get(key);
|
|
140
|
+
return v == null ? '' : String(v);
|
|
141
|
+
});
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── FS helpers ──────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
async function copyDir(src, dest, ctx, opts = {}) {
|
|
148
|
+
await fs.mkdir(dest, { recursive: true });
|
|
149
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
150
|
+
for (const e of entries) {
|
|
151
|
+
const sp = path.join(src, e.name);
|
|
152
|
+
const dp = path.join(dest, e.name);
|
|
153
|
+
if (e.isDirectory()) await copyDir(sp, dp, ctx, opts);
|
|
154
|
+
else if (e.isFile()) {
|
|
155
|
+
if (sp.endsWith('.hbs')) {
|
|
156
|
+
const content = await fs.readFile(sp, 'utf-8');
|
|
157
|
+
const rendered = renderTemplate(content, ctx);
|
|
158
|
+
const finalPath = dp.replace(/\.hbs$/, '');
|
|
159
|
+
await fs.writeFile(finalPath, rendered, 'utf-8');
|
|
160
|
+
opts.written?.push(path.relative(opts.targetRoot, finalPath));
|
|
161
|
+
} else {
|
|
162
|
+
await fs.copyFile(sp, dp);
|
|
163
|
+
opts.written?.push(path.relative(opts.targetRoot, dp));
|
|
69
164
|
}
|
|
70
|
-
await fs.writeFile(realFile, current + note, 'utf-8');
|
|
71
|
-
return realFile + ' (nota adicionada)';
|
|
72
|
-
} catch {
|
|
73
|
-
// continue
|
|
74
165
|
}
|
|
75
166
|
}
|
|
76
|
-
return null;
|
|
77
167
|
}
|
|
78
168
|
|
|
169
|
+
// ─── Prompts (readline nativo) ───────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
function makePrompt() {
|
|
172
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
173
|
+
return {
|
|
174
|
+
ask: (q, def) => new Promise(resolve => {
|
|
175
|
+
const prompt = def ? ` ${q} ${c.dim(`(${def})`)} ` : ` ${q} `;
|
|
176
|
+
rl.question(prompt, (a) => resolve(a.trim() || def || ''));
|
|
177
|
+
}),
|
|
178
|
+
yn: (q, def = true) => new Promise(resolve => {
|
|
179
|
+
const d = def ? 'S/n' : 's/N';
|
|
180
|
+
rl.question(` ${q} ${c.dim(`(${d})`)} `, (a) => {
|
|
181
|
+
const v = a.trim().toLowerCase();
|
|
182
|
+
if (v === '') return resolve(def);
|
|
183
|
+
resolve(v.startsWith('s') || v.startsWith('y'));
|
|
184
|
+
});
|
|
185
|
+
}),
|
|
186
|
+
close: () => rl.close(),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function slugify(s) {
|
|
191
|
+
return s.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
192
|
+
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── Help + List ─────────────────────────────────────────────────────────
|
|
196
|
+
|
|
79
197
|
function printHelp(version) {
|
|
80
198
|
banner(`zark-design v${version}`);
|
|
81
199
|
console.log(` ${c.bold('Comandos:')}\n`);
|
|
82
|
-
console.log(` ${c.green('init')} ${c.dim('[
|
|
83
|
-
console.log(` ${c.green('
|
|
84
|
-
console.log(` ${c.green('
|
|
85
|
-
console.log(`
|
|
86
|
-
console.log(`
|
|
87
|
-
console.log(`
|
|
88
|
-
console.log(`
|
|
89
|
-
console.log(` ${c.bold('
|
|
90
|
-
console.log(`
|
|
91
|
-
console.log(`
|
|
92
|
-
console.log(`
|
|
93
|
-
console.log(`
|
|
200
|
+
console.log(` ${c.green('init')} ${c.dim('[flags]')} cria design system (preset ou custom)`);
|
|
201
|
+
console.log(` ${c.green('presets')} lista presets disponíveis`);
|
|
202
|
+
console.log(` ${c.green('help')} mostra esta ajuda`);
|
|
203
|
+
console.log(` ${c.green('version')} mostra a versão\n`);
|
|
204
|
+
console.log(` ${c.bold('Modos:')}`);
|
|
205
|
+
console.log(` ${c.green('preset ZARK')} paleta + logos ZARK prontos (Liquid Lava #F56F10)`);
|
|
206
|
+
console.log(` ${c.green('custom')} sua cor primária + suas logos\n`);
|
|
207
|
+
console.log(` ${c.bold('Uso interativo:')}`);
|
|
208
|
+
console.log(` npx zark-design init ${c.dim('# pergunta tudo')}`);
|
|
209
|
+
console.log(` npx zark-design init --preset zark ${c.dim('# usa ZARK direto')}`);
|
|
210
|
+
console.log(` npx zark-design init --preset zark --yes ${c.dim('# sem confirms')}\n`);
|
|
211
|
+
console.log(` ${c.bold('Uso custom (flags):')}`);
|
|
212
|
+
console.log(` npx zark-design init --name "ALLSEC" --primary "#E74C3C" --yes`);
|
|
213
|
+
console.log(` npx zark-design init --palette ./minha-paleta.json --yes\n`);
|
|
214
|
+
console.log(` ${c.bold('Output:')}`);
|
|
215
|
+
console.log(` design-system/ ${c.dim('# default; mude com [pasta]')}`);
|
|
216
|
+
console.log(` ├── README.md`);
|
|
217
|
+
console.log(` ├── html/ (HTML puro)`);
|
|
218
|
+
console.log(` ├── jsx/ (React)`);
|
|
219
|
+
console.log(` └── assets/ (logos)\n`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function listPresets() {
|
|
223
|
+
banner('Presets disponíveis');
|
|
224
|
+
const entries = await fs.readdir(PRESETS_DIR, { withFileTypes: true });
|
|
225
|
+
for (const e of entries) {
|
|
226
|
+
if (!e.isDirectory()) continue;
|
|
227
|
+
const cfgPath = path.join(PRESETS_DIR, e.name, 'preset.json');
|
|
228
|
+
if (existsSync(cfgPath)) {
|
|
229
|
+
const cfg = JSON.parse(await fs.readFile(cfgPath, 'utf-8'));
|
|
230
|
+
console.log(` ${c.green('•')} ${c.bold(e.name.padEnd(10))} ${c.dim(`${cfg.name} · brand ${cfg.brand['500']}`)}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
console.log('');
|
|
234
|
+
console.log(c.dim(' Uso: npx zark-design init --preset <nome>'));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ─── Init logic ──────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
function parseArgs(argv) {
|
|
240
|
+
const flags = {}; const positional = [];
|
|
241
|
+
for (let i = 0; i < argv.length; i++) {
|
|
242
|
+
const a = argv[i];
|
|
243
|
+
if (a.startsWith('--')) {
|
|
244
|
+
const key = a.slice(2);
|
|
245
|
+
const next = argv[i + 1];
|
|
246
|
+
if (next === undefined || next.startsWith('--')) { flags[key] = true; }
|
|
247
|
+
else { flags[key] = next; i++; }
|
|
248
|
+
} else positional.push(a);
|
|
249
|
+
}
|
|
250
|
+
return { flags, positional };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function loadPreset(name) {
|
|
254
|
+
const cfgPath = path.join(PRESETS_DIR, name, 'preset.json');
|
|
255
|
+
if (!existsSync(cfgPath)) {
|
|
256
|
+
const available = (await fs.readdir(PRESETS_DIR)).filter(x => !x.startsWith('.'));
|
|
257
|
+
throw new Error(`Preset "${name}" não existe. Disponíveis: ${available.join(', ')}`);
|
|
258
|
+
}
|
|
259
|
+
return JSON.parse(await fs.readFile(cfgPath, 'utf-8'));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function loadCustomPalette(jsonPath) {
|
|
263
|
+
const data = JSON.parse(await fs.readFile(jsonPath, 'utf-8'));
|
|
264
|
+
if (!data.brand || !data.brand['500']) {
|
|
265
|
+
throw new Error('Paleta JSON precisa ter pelo menos { "name": ..., "brand": { "500": "#..." } }');
|
|
266
|
+
}
|
|
267
|
+
// Se faltam tiers, gera os ausentes a partir do 500
|
|
268
|
+
const filled = generatePalette(data.brand['500']);
|
|
269
|
+
data.brand = { ...filled, ...data.brand };
|
|
270
|
+
data.name = data.name || 'Custom';
|
|
271
|
+
data.slug = data.slug || slugify(data.name);
|
|
272
|
+
data.fonts = data.fonts || { ui: 'Inter', mono: 'JetBrains Mono', display: 'Space Grotesk' };
|
|
273
|
+
return data;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function buildCtx({ flags, prompt }) {
|
|
277
|
+
// 1) Preset path
|
|
278
|
+
if (flags.preset) {
|
|
279
|
+
const preset = await loadPreset(flags.preset);
|
|
280
|
+
if (!flags.yes && !prompt) return preset;
|
|
281
|
+
return preset;
|
|
282
|
+
}
|
|
283
|
+
// 2) Palette JSON path
|
|
284
|
+
if (flags.palette) {
|
|
285
|
+
const data = await loadCustomPalette(path.resolve(flags.palette));
|
|
286
|
+
data.preset = null;
|
|
287
|
+
return data;
|
|
288
|
+
}
|
|
289
|
+
// 3) Flags directos (custom não-interativo)
|
|
290
|
+
if (flags.name || flags.primary) {
|
|
291
|
+
const name = flags.name || 'Custom';
|
|
292
|
+
const slug = flags.slug || slugify(name);
|
|
293
|
+
const primary = flags.primary;
|
|
294
|
+
if (!primary) throw new Error('--primary <hex> é obrigatório quando usa --name sem --palette');
|
|
295
|
+
return {
|
|
296
|
+
name, slug, preset: null,
|
|
297
|
+
brand: generatePalette(primary),
|
|
298
|
+
fonts: { ui: 'Inter', mono: 'JetBrains Mono', display: 'Space Grotesk' },
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
// 4) Interativo
|
|
302
|
+
if (!prompt) throw new Error('Modo interativo precisa de TTY. Use --preset zark ou --primary <hex>.');
|
|
303
|
+
console.log(c.dim(' Pressione Enter pra usar o default em parênteses.\n'));
|
|
304
|
+
const usePreset = await prompt.yn('Usar preset ZARK pronto?', true);
|
|
305
|
+
if (usePreset) return await loadPreset('zark');
|
|
306
|
+
|
|
307
|
+
const name = await prompt.ask('Nome do projeto/empresa:', 'Custom');
|
|
308
|
+
const slug = await prompt.ask('Slug (lowercase, sem espaços):', slugify(name));
|
|
309
|
+
const primary = await prompt.ask('Cor primária (hex, ex: #E74C3C):');
|
|
310
|
+
if (!primary) throw new Error('Cor primária é obrigatória.');
|
|
311
|
+
const palette = generatePalette(primary);
|
|
312
|
+
return {
|
|
313
|
+
name, slug, preset: null,
|
|
314
|
+
brand: palette,
|
|
315
|
+
fonts: { ui: 'Inter', mono: 'JetBrains Mono', display: 'Space Grotesk' },
|
|
316
|
+
};
|
|
94
317
|
}
|
|
95
318
|
|
|
96
319
|
async function runInit(args) {
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const target = path.resolve(positional[0] ?? './design-system');
|
|
320
|
+
const { flags, positional } = parseArgs(args);
|
|
321
|
+
const target = path.resolve(positional[0] || './design-system');
|
|
100
322
|
const designDirName = path.basename(target);
|
|
101
|
-
const
|
|
323
|
+
const force = !!flags.force;
|
|
324
|
+
const yes = !!flags.yes;
|
|
102
325
|
|
|
103
326
|
banner('🎨 zark-design — install');
|
|
104
327
|
|
|
105
328
|
if (existsSync(target)) {
|
|
106
329
|
if (!force) {
|
|
107
|
-
console.error(c.red(`✗ Pasta '${target}' já existe.`));
|
|
108
|
-
console.error(c.dim(` Use --force pra sobrescrever, ou escolha outro nome.`));
|
|
330
|
+
console.error(c.red(`✗ Pasta '${target}' já existe. Use --force pra sobrescrever.`));
|
|
109
331
|
process.exit(1);
|
|
110
332
|
}
|
|
111
333
|
console.log(c.yellow(`⚠ Sobrescrevendo ${target} (--force)`));
|
|
112
334
|
await fs.rm(target, { recursive: true, force: true });
|
|
113
335
|
}
|
|
114
336
|
|
|
115
|
-
|
|
116
|
-
|
|
337
|
+
// Build context (preset OR custom)
|
|
338
|
+
const interactive = !yes && !flags.preset && !flags.palette && !flags.name && process.stdin.isTTY;
|
|
339
|
+
const prompt = interactive ? makePrompt() : null;
|
|
340
|
+
let ctx;
|
|
341
|
+
try {
|
|
342
|
+
ctx = await buildCtx({ flags, prompt });
|
|
343
|
+
} finally { prompt?.close(); }
|
|
117
344
|
|
|
118
|
-
|
|
345
|
+
// Defaults
|
|
346
|
+
ctx.date = new Date().toISOString().slice(0, 10);
|
|
347
|
+
ctx.nameDesc = ctx.preset === 'zark'
|
|
348
|
+
? 'Sistema de design para apps e sistemas internos da ZARK'
|
|
349
|
+
: `Sistema de design para apps e sistemas do(a) **${ctx.name}**`;
|
|
119
350
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
await walk(target);
|
|
351
|
+
console.log(c.dim(' Contexto:'));
|
|
352
|
+
console.log(c.dim(` Nome: ${ctx.name} (slug: ${ctx.slug})`));
|
|
353
|
+
console.log(c.dim(` Modo: ${ctx.preset ? 'preset ' + ctx.preset : 'custom'}`));
|
|
354
|
+
console.log(c.dim(` Brand: ${ctx.brand['500']} (paleta 50→900 gerada)`));
|
|
355
|
+
console.log(c.dim(` Fontes: ${ctx.fonts.ui} / ${ctx.fonts.mono} / ${ctx.fonts.display}`));
|
|
356
|
+
console.log(c.dim(` Destino: ${target}\n`));
|
|
129
357
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
358
|
+
const written = [];
|
|
359
|
+
const copyOpts = { targetRoot: target, written };
|
|
133
360
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
361
|
+
// 1. Copia html/ e jsx/ (estáticos, com substituição de .hbs)
|
|
362
|
+
await copyDir(path.join(TEMPLATES_DIR, 'html'), path.join(target, 'html'), ctx, copyOpts);
|
|
363
|
+
await copyDir(path.join(TEMPLATES_DIR, 'jsx'), path.join(target, 'jsx'), ctx, copyOpts);
|
|
364
|
+
|
|
365
|
+
// 2. Renderiza tokens.css.hbs e tokens.js.hbs em html/ e jsx/
|
|
366
|
+
const tokensCss = renderTemplate(await fs.readFile(path.join(SHARED_DIR, 'tokens.css.hbs'), 'utf-8'), ctx);
|
|
367
|
+
await fs.writeFile(path.join(target, 'html/tokens.css'), tokensCss);
|
|
368
|
+
await fs.writeFile(path.join(target, 'jsx/tokens.css'), tokensCss);
|
|
369
|
+
written.push('html/tokens.css', 'jsx/tokens.css');
|
|
370
|
+
const tokensJs = renderTemplate(await fs.readFile(path.join(SHARED_DIR, 'tokens.js.hbs'), 'utf-8'), ctx);
|
|
371
|
+
await fs.writeFile(path.join(target, 'jsx/tokens.js'), tokensJs);
|
|
372
|
+
written.push('jsx/tokens.js');
|
|
373
|
+
|
|
374
|
+
// 3. README.md
|
|
375
|
+
const readme = renderTemplate(await fs.readFile(path.join(TEMPLATES_DIR, 'README.md.hbs'), 'utf-8'), ctx);
|
|
376
|
+
await fs.writeFile(path.join(target, 'README.md'), readme);
|
|
377
|
+
written.push('README.md');
|
|
378
|
+
|
|
379
|
+
// 4. Assets
|
|
380
|
+
await fs.mkdir(path.join(target, 'assets'), { recursive: true });
|
|
381
|
+
if (ctx.preset && existsSync(path.join(PRESETS_DIR, ctx.preset, 'assets'))) {
|
|
382
|
+
// Preset traz assets prontos
|
|
383
|
+
await copyDir(path.join(PRESETS_DIR, ctx.preset, 'assets'), path.join(target, 'assets'), ctx, copyOpts);
|
|
138
384
|
} else {
|
|
139
|
-
|
|
385
|
+
// Custom: pasta vazia + README instrutivo
|
|
386
|
+
const assetsReadme = renderTemplate(await fs.readFile(path.join(SHARED_DIR, 'ASSETS-README.md.hbs'), 'utf-8'), ctx);
|
|
387
|
+
await fs.writeFile(path.join(target, 'assets/README.md'), assetsReadme);
|
|
388
|
+
written.push('assets/README.md (pasta pra você colar seus logos)');
|
|
140
389
|
}
|
|
141
390
|
|
|
391
|
+
// Log
|
|
392
|
+
for (const f of written.sort()) console.log(` ${c.green('✓')} ${f}`);
|
|
393
|
+
|
|
394
|
+
// Auto-note em CLAUDE.md / .ai-context/CONTEXT.md se existir
|
|
142
395
|
console.log('');
|
|
143
|
-
|
|
396
|
+
const noteResult = await appendNoteToContextFiles(path.dirname(target), designDirName, ctx);
|
|
397
|
+
if (noteResult) console.log(` ${c.green('✓')} ${noteResult}`);
|
|
398
|
+
else console.log(c.dim(` (sem CLAUDE.md ou .ai-context/CONTEXT.md no projeto — sem auto-nota)`));
|
|
399
|
+
|
|
400
|
+
console.log('');
|
|
401
|
+
console.log(c.green(`✓ Design system "${ctx.name}" instalado em ${designDirName}/`));
|
|
144
402
|
console.log('');
|
|
145
403
|
console.log(c.bold('Próximo passo:'));
|
|
404
|
+
if (!ctx.preset) {
|
|
405
|
+
console.log(` ${c.yellow('•')} Cole seus logos em ${designDirName}/assets/ (veja assets/README.md)`);
|
|
406
|
+
}
|
|
146
407
|
console.log(` ${c.dim('•')} Diga pra IA: "use o design system em ${designDirName}/ como referência (leia README.md)"`);
|
|
147
|
-
console.log(` ${c.dim('•')}
|
|
408
|
+
console.log(` ${c.dim('•')} Abra ${designDirName}/html/showcase.html no browser pra ver tudo renderizado.`);
|
|
148
409
|
}
|
|
149
410
|
|
|
411
|
+
async function appendNoteToContextFiles(targetRoot, designDirName, ctx) {
|
|
412
|
+
const note = `\n\n## Design system\nEste projeto usa o **${ctx.name} Design System** (gerado por \`zark-design\`${ctx.preset ? `, preset ${ctx.preset}` : ', modo custom'}). Antes de qualquer trabalho de UI/frontend, leia \`${designDirName}/README.md\`.\n\n2 implementações lado a lado consumindo \`tokens.css\` + \`components.css\` idênticos:\n- \`${designDirName}/html/\` — HTML puro (Blade/Django/Rails/HTML)\n- \`${designDirName}/jsx/\` — React (24 componentes individuais)\n\nVue/Svelte/Solid: use \`tokens.css\` + \`components.css\` direto, adapte os JSX mantendo as classes.\n\nCor de marca: \`var(--brand-500)\` = ${ctx.brand['500']}. Abra \`${designDirName}/html/showcase.html\` no browser pra inspeção visual.\n`;
|
|
413
|
+
const candidates = [
|
|
414
|
+
path.join(targetRoot, '.ai-context', 'CONTEXT.md'),
|
|
415
|
+
path.join(targetRoot, 'CLAUDE.md'),
|
|
416
|
+
];
|
|
417
|
+
for (const file of candidates) {
|
|
418
|
+
if (!existsSync(file)) continue;
|
|
419
|
+
try {
|
|
420
|
+
const stat = await fs.lstat(file);
|
|
421
|
+
const realFile = stat.isSymbolicLink()
|
|
422
|
+
? path.resolve(path.dirname(file), await fs.readlink(file)) : file;
|
|
423
|
+
const current = await fs.readFile(realFile, 'utf-8');
|
|
424
|
+
if (current.includes('Design System') && current.includes('design-system')) return realFile + ' (já tem nota — pulando)';
|
|
425
|
+
await fs.writeFile(realFile, current + note, 'utf-8');
|
|
426
|
+
return realFile + ' (nota adicionada)';
|
|
427
|
+
} catch { /* continue */ }
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ─── Main ────────────────────────────────────────────────────────────────
|
|
433
|
+
|
|
150
434
|
async function main() {
|
|
151
435
|
const argv = process.argv.slice(2);
|
|
152
|
-
const cmd = argv[0]
|
|
436
|
+
const cmd = argv[0] || 'help';
|
|
153
437
|
const version = await getVersion();
|
|
154
|
-
|
|
155
438
|
try {
|
|
156
439
|
switch (cmd) {
|
|
157
|
-
case 'init':
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
case '
|
|
161
|
-
case '--version':
|
|
162
|
-
case '-v':
|
|
163
|
-
console.log(version);
|
|
164
|
-
return;
|
|
165
|
-
case 'help':
|
|
166
|
-
case '--help':
|
|
167
|
-
case '-h':
|
|
168
|
-
default:
|
|
169
|
-
printHelp(version);
|
|
170
|
-
return;
|
|
440
|
+
case 'init': return await runInit(argv.slice(1));
|
|
441
|
+
case 'presets': return await listPresets();
|
|
442
|
+
case 'version': case '--version': case '-v': return console.log(version);
|
|
443
|
+
case 'help': case '--help': case '-h': default: return printHelp(version);
|
|
171
444
|
}
|
|
172
445
|
} catch (e) {
|
|
173
446
|
console.error(c.red(`✗ ${e.message ?? e}`));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zark-design",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Design system scaffolder seguindo método ZARK — `npx zark-design init` em modo preset (ZARK pronto) ou custom (cor e logos suas, paleta 50→900 gerada). HTML + JSX side-by-side. Dark mode nativo. Sem dependência runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"zark-design": "bin/cli.js"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# {{name}} Design System — Apps
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
{{nameDesc}}. Disponível em **HTML puro** e **React (JSX)**, ambos consumindo as mesmas folhas de estilo (`tokens.css` + `components.css`).
|
|
4
4
|
|
|
5
|
-
Versão: **v3 ·
|
|
5
|
+
Versão: **v3 · {{date}}** · gerado por `npx zark-design init{{#if preset}} --preset {{preset}}{{/if}}` · App Patterns derivados do CRM real.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -65,7 +65,7 @@ design-system-apps/
|
|
|
65
65
|
|
|
66
66
|
**Warm, neutral, ember-on-paper.**
|
|
67
67
|
- `--paper` (#fafaf8) cremoso, NÃO branco puro.
|
|
68
|
-
- `--
|
|
68
|
+
- `--brand-500` (#f56f10 — Liquid Lava) é a única cor de marca. Use com **moderação** em CTAs, focus, brand moments.
|
|
69
69
|
- Hairlines de 1px em `--line-200`. Nunca borders grossos.
|
|
70
70
|
- Sombras warm (`rgba(20,17,12,...)`), nunca azuis-frias.
|
|
71
71
|
- Tipografia editorial: **Inter** pra UI e números · **JetBrains Mono** pra código e labels · **Space Grotesk** pra títulos.
|
|
@@ -233,14 +233,14 @@ document.documentElement.dataset.theme = 'light'; // força light
|
|
|
233
233
|
delete document.documentElement.dataset.theme; // segue OS
|
|
234
234
|
```
|
|
235
235
|
|
|
236
|
-
A cor de marca (`--
|
|
236
|
+
A cor de marca (`--brand-500`) **NUNCA muda no dark mode** — é assinatura ZARK. Apenas neutros, semânticos suaves e sombras ajustam.
|
|
237
237
|
|
|
238
238
|
---
|
|
239
239
|
|
|
240
240
|
## Regras importantes (NÃO fazer)
|
|
241
241
|
|
|
242
|
-
- ❌ Trocar `--
|
|
243
|
-
- ❌ Hardcode de hex (`#f56f10` no código). SEMPRE use `var(--
|
|
242
|
+
- ❌ Trocar `--brand-500` por outra cor.
|
|
243
|
+
- ❌ Hardcode de hex (`#f56f10` no código). SEMPRE use `var(--brand-500)`.
|
|
244
244
|
- ❌ Sombras frias (`rgba(0,0,0,...)`). Use as warm `rgba(20,17,12,...)`.
|
|
245
245
|
- ❌ `border-radius: 999px` ou `--r-pill`. Pill foi removido v3.
|
|
246
246
|
- ❌ Display font (Space Grotesk) em UI body — só títulos h1/h2/h3, modal titles, brand.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# assets/ — Logos e ícones do {{name}}
|
|
2
|
+
|
|
3
|
+
Esta pasta está vazia porque você usou o modo **custom** do `zark-design init`. Cole aqui as imagens da marca do seu sistema.
|
|
4
|
+
|
|
5
|
+
## Arquivos recomendados
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
assets/
|
|
9
|
+
├── logo-primary.png ← logo horizontal completo (cor, fundo claro)
|
|
10
|
+
├── logo-primary-dark.png ← logo horizontal completo (fundo escuro)
|
|
11
|
+
├── logo-mark.png ← só o símbolo/mark (sem texto)
|
|
12
|
+
├── icon.png ← ícone quadrado (favicon, app icon)
|
|
13
|
+
└── (opcional) logo.svg ← versão vetorial pra escalabilidade
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Tamanhos sugeridos
|
|
17
|
+
|
|
18
|
+
| Arquivo | Tamanho | Uso |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `logo-primary.png` | ~600×180px | Sidebar, topbar, marketing |
|
|
21
|
+
| `logo-mark.png` | ~256×256px | Avatares, badges, espaço apertado |
|
|
22
|
+
| `icon.png` | 512×512px | Favicon, PWA manifest, app store |
|
|
23
|
+
|
|
24
|
+
## Convenção de nomenclatura nos componentes
|
|
25
|
+
|
|
26
|
+
Os JSX usam paths relativos. Exemplo em `jsx/components/Sidebar.jsx`:
|
|
27
|
+
|
|
28
|
+
```jsx
|
|
29
|
+
<img src="../assets/logo-primary.png" alt="{{name}}" />
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Mantenha os nomes acima ou ajuste as referências em:
|
|
33
|
+
- `jsx/components/Sidebar.jsx`
|
|
34
|
+
- `html/showcase.html`
|
|
35
|
+
- `html/index.html`
|
|
36
|
+
|
|
37
|
+
## Logos do {{name}}
|
|
38
|
+
|
|
39
|
+
> **Nota:** se você não tem os arquivos finais ainda, use placeholders temporários. O design system funciona sem as imagens (o JSX vai mostrar alt-text), mas a sidebar fica esquisita.
|