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 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
- const pkg = JSON.parse(await fs.readFile(PKG_PATH, 'utf-8'));
31
- return pkg.version;
32
- } catch {
33
- return 'unknown';
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
- async function copyDir(src, dest) {
38
- await fs.mkdir(dest, { recursive: true });
39
- const entries = await fs.readdir(src, { withFileTypes: true });
40
- for (const entry of entries) {
41
- const sp = path.join(src, entry.name);
42
- const dp = path.join(dest, entry.name);
43
- if (entry.isDirectory()) {
44
- await copyDir(sp, dp);
45
- } else if (entry.isFile()) {
46
- await fs.copyFile(sp, dp);
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
- async function appendNoteToContextFiles(targetRoot, designDirName) {
52
- const note = `\n\n## Design system\nEste projeto usa o **ZARK Design System (Apps)**. Antes de qualquer trabalho de UI/frontend, leia \`${designDirName}/README.md\` — descreve tokens, primitivos, App Patterns (StatCard, Funnel, KanbanColumn, LeadCard, AlertCritical, EmptyState, TableActions), 35 ícones e regras de uso.\n\nDuas implementações lado a lado consumindo os MESMOS \`tokens.css\` + \`components.css\`:\n- \`${designDirName}/html/\` — HTML puro com classes (\`.btn\`, \`.tag-dot\`, \`.stat\` …). Use em projetos Blade/Django/Rails/HTML.\n- \`${designDirName}/jsx/\` — React (24 componentes individuais em \`components/\`). Use em projetos React/Next.\n\nPara Vue/Svelte/Solid, use \`tokens.css\` + \`components.css\` direto e adapte os componentes JSX mantendo as classes CSS.\n\nAbra \`${designDirName}/html/showcase.html\` no browser pra ver tudo renderizado (light/dark toggle).\n`;
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
- const candidates = [
55
- path.join(targetRoot, '.ai-context', 'CONTEXT.md'),
56
- path.join(targetRoot, 'CLAUDE.md'),
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
- for (const file of candidates) {
60
- if (!existsSync(file)) continue;
61
- try {
62
- const stat = await fs.lstat(file);
63
- const realFile = stat.isSymbolicLink()
64
- ? path.resolve(path.dirname(file), await fs.readlink(file))
65
- : file;
66
- const current = await fs.readFile(realFile, 'utf-8');
67
- if (current.includes('ZARK Design System')) {
68
- return realFile + ' (já tem nota — pulando)';
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('[pasta]')} instala o design system (default: ./design-system)`);
83
- console.log(` ${c.green('help')} mostra esta ajuda`);
84
- console.log(` ${c.green('version')} mostra a versão\n`);
85
- console.log(` ${c.bold('Uso:')}`);
86
- console.log(` npx zark-design init ${c.dim('# instala em ./design-system')}`);
87
- console.log(` npx zark-design init ./ui ${c.dim('# instala em ./ui')}`);
88
- console.log(` npx zark-design init --force ${c.dim('# sobrescreve se existe')}\n`);
89
- console.log(` ${c.bold('Filosofia:')}`);
90
- console.log(` Cria pasta com 2 implementações lado a lado: ${c.bold('html/')} e ${c.bold('jsx/')}.`);
91
- console.log(` Ambas consomem os MESMOS tokens.css + components.css.`);
92
- console.log(` A IA README.md, escolhe a versão certa pra stack do projeto`);
93
- console.log(` (React→jsx · Blade/Django/HTML→html · Vue/Svelte→adapta classes).\n`);
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 force = args.includes('--force');
98
- const positional = args.filter((a) => !a.startsWith('--'));
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 projectRoot = path.dirname(target);
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
- console.log(c.dim(` origem: ${TEMPLATES_DIR}`));
116
- console.log(c.dim(` destino: ${target}\n`));
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
- await copyDir(TEMPLATES_DIR, target);
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
- const fileList = [];
121
- async function walk(dir, base = '') {
122
- for (const e of await fs.readdir(dir, { withFileTypes: true })) {
123
- const sub = path.join(base, e.name);
124
- if (e.isDirectory()) await walk(path.join(dir, e.name), sub);
125
- else fileList.push(sub);
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
- for (const f of fileList) {
131
- console.log(` ${c.green('✓')} ${f}`);
132
- }
358
+ const written = [];
359
+ const copyOpts = { targetRoot: target, written };
133
360
 
134
- console.log('');
135
- const noteResult = await appendNoteToContextFiles(projectRoot, designDirName);
136
- if (noteResult) {
137
- console.log(` ${c.green('✓')} ${noteResult}`);
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
- console.log(c.dim(` (sem .ai-context/CONTEXT.md ou CLAUDE.md no projeto — sem auto-nota)`));
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
- console.log(c.green(`✓ Design system instalado em ${designDirName}/`));
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('•')} Ou abra ${designDirName}/html/showcase.html no browser pra ver tudo renderizado.`);
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] ?? 'help';
436
+ const cmd = argv[0] || 'help';
153
437
  const version = await getVersion();
154
-
155
438
  try {
156
439
  switch (cmd) {
157
- case 'init':
158
- await runInit(argv.slice(1));
159
- return;
160
- case 'version':
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": "2.0.0",
4
- "description": "ZARK SAAS Design System (Apps)install via `npx zark-design init` to drop the design system in any project. Ships HTML + JSX side-by-side, both consuming the same tokens.css + components.css, with App Patterns derived from the ZARK CRM.",
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
- # ZARK Design System — Apps
1
+ # {{name}} Design System — Apps
2
2
 
3
- Sistema de design para apps e sistemas internos da ZARK. Disponível em **HTML puro** e **React (JSX)**, ambos consumindo as mesmas folhas de estilo (`tokens.css` + `components.css`).
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 · 2026** · App Patterns derivados do CRM real.
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
- - `--ember-500` (#f56f10 — Liquid Lava) é a única cor de marca. Use com **moderação** em CTAs, focus, brand moments.
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 (`--ember-500`) **NUNCA muda no dark mode** — é assinatura ZARK. Apenas neutros, semânticos suaves e sombras ajustam.
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 `--ember-500` por outra cor.
243
- - ❌ Hardcode de hex (`#f56f10` no código). SEMPRE use `var(--ember-500)`.
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.