onboarding-kit 0.1.0 → 0.2.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.
@@ -4,6 +4,7 @@ exports.escapeHtml = void 0;
4
4
  exports.renderMissingPage = renderMissingPage;
5
5
  exports.renderErrorPage = renderErrorPage;
6
6
  exports.renderOnboarding = renderOnboarding;
7
+ const brand_1 = require("../brand");
7
8
  const load_1 = require("../load");
8
9
  const template_1 = require("../template");
9
10
  const assets_1 = require("./assets");
@@ -14,39 +15,97 @@ var html_2 = require("./html");
14
15
  Object.defineProperty(exports, "escapeHtml", { enumerable: true, get: function () { return html_2.escapeHtml; } });
15
16
  const ICON_GRID = '<svg viewBox="0 0 20 20" aria-hidden="true"><path fill="currentColor" d="M3 3h5.5v5.5H3zM11.5 3H17v5.5h-5.5zM3 11.5h5.5V17H3zM11.5 11.5H17V17h-5.5z" opacity=".9"/></svg>';
16
17
  const ICON_THEME = '<svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="7" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M10 3a7 7 0 0 1 0 14z" fill="currentColor"/></svg>';
17
- function shell(lang, title, body, opts = {}) {
18
+ const HUE_OFFSETS = [0, 18, -18, 36, -36];
19
+ /**
20
+ * Tema a partir das cores da marca. Sem cores, mantém a paleta própria do kit.
21
+ * Com uma cor, os capítulos variam levemente em torno dela; com várias, se revezam entre elas.
22
+ * Marcas sem cor (preto/cinza) viram um tema monocromático.
23
+ */
24
+ function themeFor(brand) {
25
+ const chromatic = brand.colors.filter((c) => !(0, brand_1.isAchromatic)(c)).map((c) => ({ ...c, h: Math.round(c.h) }));
26
+ const ref = chromatic[0] ?? (brand.colors[0] && { ...brand.colors[0], h: Math.round(brand.colors[0].h) });
27
+ if (!ref)
28
+ return { brand, hues: copy_1.CHAPTER_HUES, hue: copy_1.CHAPTER_HUES.welcome, css: "", mono: false };
29
+ const mono = chromatic.length === 0;
30
+ const hues = { ...copy_1.CHAPTER_HUES };
31
+ hues.welcome = hues.finale = ref.h;
32
+ slides_1.CHAPTERS.forEach(([id], i) => {
33
+ hues[id] = chromatic.length > 1 ? chromatic[i % chromatic.length].h : (ref.h + HUE_OFFSETS[i % HUE_OFFSETS.length] + 360) % 360;
34
+ });
35
+ const k = mono ? 0 : Math.round(Math.min(1, ref.s / 55) * 100) / 100;
36
+ const h2 = chromatic[1]?.h ?? (ref.h + 28) % 360;
37
+ const css = `:root{--brand-h:${ref.h};--h2:${h2};--k:${k};--brand-c:hsl(${ref.h} calc(var(--as) * ${k}) var(--al));--brand-on:var(--on-accent)}`;
38
+ return { brand, hues, hue: ref.h, css, mono };
39
+ }
40
+ function shell(lang, title, body, theme, opts = {}) {
41
+ const palette = theme.brand.colors.map(brand_1.toHex).join(" ");
18
42
  return `<!doctype html>
19
- <html lang="${(0, html_1.escapeHtml)(lang)}">
43
+ <html lang="${(0, html_1.escapeHtml)(lang)}"${theme.mono ? " data-mono" : ""}${palette ? ` data-palette="${palette}"` : ""}>
20
44
  <head>
21
45
  <meta charset="utf-8">
22
46
  <meta name="viewport" content="width=device-width, initial-scale=1">
23
47
  <meta name="robots" content="noindex">
24
48
  <title>${(0, html_1.escapeHtml)(title)}</title>
49
+ ${theme.brand.favicon ? `<link rel="icon" href="${(0, html_1.escapeHtml)(theme.brand.favicon)}">` : ""}
50
+ ${theme.brand.colors[0] ? `<meta name="theme-color" content="${(0, brand_1.toHex)(theme.brand.colors[0])}">` : ""}
25
51
  ${assets_1.FONTS}
26
- <style>${assets_1.CSS}</style>
52
+ <style>${assets_1.CSS}${theme.css}</style>
27
53
  </head>
28
- <body${opts.solo ? ' class="solo"' : ""}>
54
+ <body${opts.solo ? ' class="solo"' : ""}${opts.state ? ` data-ok-state="${opts.state}"` : ""}>
55
+ <div class="backdrop" id="ok-backdrop" style="--bh:${theme.hue}" aria-hidden="true"><i></i><i></i><i></i></div>
29
56
  ${body}
30
57
  <script>${assets_1.JS}</script>
31
58
  ${opts.mermaid ? `<script src="${assets_1.MERMAID_URL}" onload="window.__okMermaid&&window.__okMermaid()"></script>` : ""}
32
59
  </body>
33
60
  </html>`;
34
61
  }
35
- function soloPage(lang, t, heading, body, code, isError = false) {
36
- const block = isError
37
- ? `<pre>${(0, html_1.escapeHtml)(code)}</pre>`
38
- : `<div class="prompt"><pre>${(0, html_1.escapeHtml)(code)}</pre><button type="button" class="copy" data-copy="${(0, html_1.escapeHtml)(code)}" data-done="${(0, html_1.escapeHtml)(t.setup.copied)}">${t.setup.copy}</button></div>`;
39
- return shell(lang, "Onboarding", `<div class="solo-wrap"><div class="solo-card"><p class="kicker"><span class="num">👋</span>onboarding-kit</p><h1>${(0, html_1.escapeHtml)(heading)}</h1>${body ? `<p>${(0, html_1.escapeHtml)(body)}</p>` : ""}${block}</div></div>`, { solo: true });
62
+ function logoImg(theme, cls) {
63
+ return theme.brand.logo ? `<span class="${cls}"><img src="${(0, html_1.escapeHtml)(theme.brand.logo)}" alt=""></span>` : "";
64
+ }
65
+ function soloHead(theme, name, heading, body) {
66
+ const kicker = `<p class="kicker"><span class="num">👋</span>${(0, html_1.escapeHtml)(name ?? "onboarding-kit")}</p>`;
67
+ return `${logoImg(theme, "solo-logo")}${kicker}<h1>${(0, html_1.escapeHtml)(heading)}</h1>${body ? `<p>${(0, html_1.escapeHtml)(body)}</p>` : ""}`;
68
+ }
69
+ function promptBlock(t, prompt) {
70
+ return `<ol class="solo-steps">${t.pending.steps.map((step, i) => `<li><b>${i + 1}</b><span>${(0, html_1.escapeHtml)(step)}</span></li>`).join("")}</ol>
71
+ <div class="prompt-box"><pre class="prompt-text">${(0, html_1.escapeHtml)(prompt)}</pre><button type="button" class="cta copy-cta" data-copy="${(0, html_1.escapeHtml)(prompt)}" data-done="✓ ${(0, html_1.escapeHtml)(t.pending.copied)}">${(0, html_1.escapeHtml)(t.pending.copy)}</button></div>`;
40
72
  }
41
- function renderMissingPage() {
73
+ function waiting(t) {
74
+ return `<p class="waiting" aria-live="polite"><span class="pulse" aria-hidden="true"></span>${(0, html_1.escapeHtml)(t.pending.waiting)}</p>`;
75
+ }
76
+ /** Seções do JSON acompanhadas na tela de espera enquanto a IA preenche. */
77
+ const PROGRESS_SECTIONS = ["project", "business", "features", "domain", "stack", "architecture", "gettingStarted", "conventions", "tours", "gotchas", "decisions", "firstTasks", "brand"];
78
+ function progressBlock(data, t) {
79
+ const items = PROGRESS_SECTIONS.map((key) => ({ key, done: !(0, load_1.isBlank)(data[key]) }));
80
+ const done = items.filter((i) => i.done).length;
81
+ return `<div class="progress" id="ok-progress"><p class="mini-label">${(0, html_1.escapeHtml)(t.pending.progress.replace("{done}", String(done)).replace("{total}", String(items.length)))}</p><div class="progress-bar"><span style="width:${Math.round((done / items.length) * 100)}%"></span></div><ul class="progress-list">${items
82
+ .map((i) => `<li data-key="${i.key}"${i.done ? ' class="is-done"' : ""}>${(0, html_1.escapeHtml)(t.pending.sections[i.key])}</li>`)
83
+ .join("")}</ul></div>`;
84
+ }
85
+ function soloPage(lang, title, inner, theme, state) {
86
+ return shell(lang, title, `<div class="solo-wrap" style="--h:${theme.hue}"><div class="solo-card">${inner}</div></div>`, theme, { solo: true, state });
87
+ }
88
+ function renderMissingPage(root) {
42
89
  const t = (0, copy_1.copyFor)("pt-BR");
43
- return soloPage("pt-BR", t, t.missing.title, t.missing.body, "npx onboarding-kit init");
90
+ const theme = themeFor((0, brand_1.resolveBrand)(undefined, root));
91
+ const name = (0, brand_1.packageName)(root);
92
+ const inner = `${soloHead(theme, name, t.missing.title, t.missing.body)}${promptBlock(t, (0, template_1.aiPrompt)(undefined, { init: true }))}${waiting(t)}<p class="manual">${(0, html_1.escapeHtml)(t.missing.manual)} <code>npx onboarding-kit init</code></p>`;
93
+ return soloPage("pt-BR", name ? `${name} · Onboarding` : "Onboarding", inner, theme, "missing");
44
94
  }
45
- function renderErrorPage(message) {
95
+ function renderErrorPage(message, root) {
46
96
  const t = (0, copy_1.copyFor)("pt-BR");
47
- return soloPage("pt-BR", t, t.invalid.title, "", message, true);
97
+ const theme = themeFor((0, brand_1.resolveBrand)(undefined, root));
98
+ const name = (0, brand_1.packageName)(root);
99
+ const inner = `${soloHead(theme, name, t.invalid.title, t.invalid.body)}<pre class="error-text">${(0, html_1.escapeHtml)(message)}</pre>${waiting(t)}`;
100
+ return soloPage("pt-BR", "Onboarding", inner, theme, "error");
101
+ }
102
+ function renderPendingPage(data, root, lang, t) {
103
+ const theme = themeFor((0, brand_1.resolveBrand)(data, root));
104
+ const name = data.project?.name?.trim() || (0, brand_1.packageName)(root);
105
+ const inner = `${soloHead(theme, name, t.pending.title, t.pending.body)}${promptBlock(t, (0, template_1.aiPrompt)())}${waiting(t)}${progressBlock(data, t)}`;
106
+ return soloPage(lang, name ? `${name} · Onboarding` : "Onboarding", inner, theme, "pending");
48
107
  }
49
- function welcomeSlide(data, t, chapters, minutes) {
108
+ function welcomeSlide(data, t, chapters, minutes, theme) {
50
109
  const p = data.project;
51
110
  const name = p?.name?.trim() || "Onboarding";
52
111
  const generated = data.meta?.generatedAt ? new Date(data.meta.generatedAt) : undefined;
@@ -61,13 +120,14 @@ function welcomeSlide(data, t, chapters, minutes) {
61
120
  ].filter(Boolean);
62
121
  const journey = chapters
63
122
  .filter((c) => c.id !== "welcome" && c.id !== "finale")
64
- .map((c) => `<li><button type="button" data-go="${c.start}" style="--ch:${copy_1.CHAPTER_HUES[c.id]}"><span class="n">${c.num}</span>${(0, html_1.escapeHtml)(c.name)}</button></li>`)
123
+ .map((c) => `<li><button type="button" data-go="${c.start}" style="--ch:${theme.hues[c.id]}"><span class="n">${c.num}</span>${(0, html_1.escapeHtml)(c.name)}</button></li>`)
65
124
  .join("");
66
125
  return {
67
126
  chapter: "welcome",
68
127
  kind: "welcome",
69
128
  title: t.chapters.welcome,
70
129
  html: `<div class="welcome">
130
+ ${logoImg(theme, "brand-logo")}
71
131
  <p class="hi"><span class="wave" aria-hidden="true">👋</span>${(0, html_1.escapeHtml)(t.welcome.hi)}</p>
72
132
  <h1 class="title">${(0, html_1.escapeHtml)(t.welcome.title("")).trim()} <span class="grad">${(0, html_1.escapeHtml)(name)}</span></h1>
73
133
  ${p?.tagline ? `<p class="tagline">${(0, html_1.text)(p.tagline)}</p>` : ""}
@@ -78,10 +138,10 @@ ${journey ? `<div class="journey-wrap"><p class="mini-label">${(0, html_1.escape
78
138
  </div>`,
79
139
  };
80
140
  }
81
- function finaleSlide(t, chapters, hasTasks) {
141
+ function finaleSlide(t, chapters, hasTasks, theme) {
82
142
  const recap = chapters
83
143
  .filter((c) => c.id !== "welcome" && c.id !== "finale")
84
- .map((c) => `<li style="--ch:${copy_1.CHAPTER_HUES[c.id]}">${(0, html_1.escapeHtml)(c.name)}</li>`)
144
+ .map((c) => `<li style="--ch:${theme.hues[c.id]}">${(0, html_1.escapeHtml)(c.name)}</li>`)
85
145
  .join("");
86
146
  return {
87
147
  chapter: "finale",
@@ -97,13 +157,14 @@ ${hasTasks ? `<p class="task-count" data-task-count data-template="${(0, html_1.
97
157
  </div>`,
98
158
  };
99
159
  }
100
- function renderOnboarding(data) {
160
+ function renderOnboarding(data, options = {}) {
101
161
  const lang = data.meta?.language || "pt-BR";
102
162
  const t = (0, copy_1.copyFor)(lang);
103
163
  const name = data.project?.name?.trim() || "Onboarding";
104
164
  if (data.meta?.status !== "filled") {
105
- return soloPage(lang, t, t.pending.title, t.pending.body, (0, template_1.aiPrompt)());
165
+ return renderPendingPage(data, options.root, lang, t);
106
166
  }
167
+ const theme = themeFor((0, brand_1.resolveBrand)(data, options.root));
107
168
  // 1. capítulos do meio, numerados só quando têm conteúdo
108
169
  const middle = [];
109
170
  let num = 1;
@@ -126,12 +187,13 @@ function renderOnboarding(data) {
126
187
  const total = cursor + 1;
127
188
  const minutes = Math.max(3, Math.round(total * 0.4));
128
189
  const hasTasks = middle.some((c) => c.id === "mission");
129
- const slides = [welcomeSlide(data, t, chapters, minutes), ...middle.flatMap((c) => c.slides), finaleSlide(t, chapters, hasTasks)];
190
+ const slides = [welcomeSlide(data, t, chapters, minutes, theme), ...middle.flatMap((c) => c.slides), finaleSlide(t, chapters, hasTasks, theme)];
191
+ const chapterNum = (id) => (id === "welcome" ? "👋" : id === "finale" ? "🎉" : String(chapters.find((c) => c.id === id)?.num ?? ""));
130
192
  const slideHtml = slides
131
- .map((s, i) => `<section class="slide" data-kind="${(0, html_1.escapeHtml)(s.kind ?? s.chapter)}" data-chapter="${s.chapter}" data-chapter-name="${(0, html_1.escapeHtml)(t.chapters[s.chapter])}" style="--h:${copy_1.CHAPTER_HUES[s.chapter]}" aria-roledescription="slide" aria-label="${i + 1} / ${total}" aria-hidden="true"><div class="slide-inner">${s.html}</div></section>`)
193
+ .map((s, i) => `<section class="slide" data-kind="${(0, html_1.escapeHtml)(s.kind ?? s.chapter)}" data-chapter="${s.chapter}" data-chapter-name="${(0, html_1.escapeHtml)(t.chapters[s.chapter])}" data-chapter-num="${chapterNum(s.chapter)}" style="--h:${theme.hues[s.chapter]}" aria-roledescription="slide" aria-label="${i + 1} / ${total}" aria-hidden="true"><div class="slide-inner">${s.html}</div></section>`)
132
194
  .join("\n");
133
195
  const segments = chapters
134
- .map((c) => `<button type="button" class="seg" data-go="${c.start}" data-start="${c.start}" data-count="${c.count}" style="--h:${copy_1.CHAPTER_HUES[c.id]};--w:${c.count}" title="${(0, html_1.escapeHtml)(c.name)}" aria-label="${(0, html_1.escapeHtml)(c.name)}"><span></span></button>`)
196
+ .map((c) => `<button type="button" class="seg" data-go="${c.start}" data-start="${c.start}" data-count="${c.count}" style="--h:${theme.hues[c.id]};--w:${c.count}" title="${(0, html_1.escapeHtml)(c.name)}" aria-label="${(0, html_1.escapeHtml)(c.name)}"><span></span></button>`)
135
197
  .join("");
136
198
  let index = 0;
137
199
  const overview = chapters
@@ -140,18 +202,19 @@ function renderOnboarding(data) {
140
202
  .slice(c.start, c.start + c.count)
141
203
  .map((s) => `<button type="button" data-go="${index++}">${(0, html_1.escapeHtml)(s.title)}</button>`)
142
204
  .join("");
143
- return `<div class="ov-chapter" style="--ch:${copy_1.CHAPTER_HUES[c.id]}"><h3><span class="n">${c.id === "welcome" ? "👋" : c.id === "finale" ? "🎉" : c.num}</span>${(0, html_1.escapeHtml)(c.name)}</h3>${items}</div>`;
205
+ return `<div class="ov-chapter" style="--ch:${theme.hues[c.id]}"><h3><span class="n">${chapterNum(c.id)}</span>${(0, html_1.escapeHtml)(c.name)}</h3>${items}</div>`;
144
206
  })
145
207
  .join("");
146
208
  const body = `<div class="deck">
147
209
  <header class="topbar">
148
- <span class="brand">${(0, html_1.escapeHtml)(name)}</span>
210
+ <span class="brand">${logoImg(theme, "brand-mark")}<span class="brand-name">${(0, html_1.escapeHtml)(name)}</span></span>
149
211
  <nav class="segments" aria-label="${(0, html_1.escapeHtml)(t.ui.chapters)}">${segments}</nav>
150
212
  <button type="button" class="icon-btn" data-overview title="${(0, html_1.escapeHtml)(t.ui.overview)} (O)" aria-label="${(0, html_1.escapeHtml)(t.ui.overview)}">${ICON_GRID}</button>
151
213
  <button type="button" class="icon-btn" data-theme-toggle title="${(0, html_1.escapeHtml)(t.ui.theme)}" aria-label="${(0, html_1.escapeHtml)(t.ui.theme)}">${ICON_THEME}</button>
152
214
  </header>
153
215
  <main class="stage" id="ok-stage">
154
216
  ${slideHtml}
217
+ <div class="wipe" id="ok-wipe" aria-hidden="true"><span></span><strong></strong></div>
155
218
  </main>
156
219
  <footer class="controls">
157
220
  <button type="button" class="nav-btn" data-prev aria-label="${(0, html_1.escapeHtml)(t.ui.prev)}">← <span class="txt">${(0, html_1.escapeHtml)(t.ui.prev)}</span></button>
@@ -163,5 +226,5 @@ ${slideHtml}
163
226
  <div class="overview-inner"><div class="overview-head"><h2>${(0, html_1.escapeHtml)(t.ui.overview)}</h2><button type="button" class="icon-btn" data-close aria-label="${(0, html_1.escapeHtml)(t.ui.close)}">✕</button></div><div class="ov-grid">${overview}</div></div>
164
227
  </div>
165
228
  <div class="toast" id="ok-toast" data-template="${(0, html_1.escapeHtml)(t.ui.resume)}" role="status"><span></span><button type="button" class="yes" data-resume="0">${(0, html_1.escapeHtml)(t.ui.resumeYes)}</button><button type="button" data-dismiss>${(0, html_1.escapeHtml)(t.ui.resumeNo)}</button></div>`;
166
- return shell(lang, `${name} · Onboarding`, body, { mermaid: slides.some((s) => s.kind === "diagram") && !(0, load_1.isBlank)(data.architecture?.diagram) });
229
+ return shell(lang, `${name} · Onboarding`, body, theme, { mermaid: slides.some((s) => s.kind === "diagram") && !(0, load_1.isBlank)(data.architecture?.diagram) });
167
230
  }
@@ -5,5 +5,10 @@ export interface TemplateOptions {
5
5
  language?: string;
6
6
  }
7
7
  export declare function createTemplate({ framework, language }: TemplateOptions): OnboardingData;
8
- /** Frase para colar na IA local depois do `init`. */
9
- export declare function aiPrompt(file?: string): string;
8
+ /**
9
+ * Frase para colar na IA local. Com `init`, pede para a IA criar o arquivo antes
10
+ * (usado quando o onboarding.json ainda não existe).
11
+ */
12
+ export declare function aiPrompt(file?: string, options?: {
13
+ init?: boolean;
14
+ }): string;
package/dist/template.js CHANGED
@@ -21,6 +21,7 @@ const SECTION_INSTRUCTIONS = {
21
21
  tours: "1 a 3 tours guiados pelo código, ex.: 'Como uma requisição é atendida', 'Ciclo de vida de um pedido'. Cada step aponta um arquivo (com linha quando ajudar) e explica o que olhar ali. Ordene os steps na sequência de leitura.",
22
22
  gotchas: "Armadilhas e cuidados: comportamentos não óbvios, partes frágeis, workarounds, TODOs/FIXMEs importantes, coisas que costumam quebrar. Só inclua o que tiver evidence.",
23
23
  decisions: "Decisões de arquitetura perceptíveis no código ou na documentação (ex.: por que tal ORM, monorepo, cache, autenticação). context: o problema. decision: o que foi escolhido. Não invente motivos: se o motivo não está documentado, descreva só a decisão.",
24
+ brand: "Identidade visual, usada para a apresentação ter a cara da plataforma. primaryColor: a cor principal da marca em hex (#RRGGBB). Procure em tailwind.config (theme.extend.colors: brand, primary), no @theme do Tailwind v4 (--color-primary, --color-brand), em variáveis CSS de globals.css/index.css (--primary, --brand; converta HSL/OKLCH para hex), em temas de MUI/Chakra/styled-components (palette.primary.main) e no theme_color do manifest. palette: 1 a 4 outras cores da marca em hex (secundária, destaque). Ignore branco, preto e cinzas quando houver uma cor de verdade. logo: caminho do logo do projeto (procure em public/, assets/, src/assets/, app/icon.*); prefira SVG e a versão para fundo claro; ignore os arquivos de exemplo do framework (next.svg, vercel.svg, vite.svg, react.svg). favicon: caminho do favicon (public/favicon.ico, app/favicon.ico, app/icon.png). evidence: arquivos de onde tirou as cores. Deixe vazio o que não encontrar: o onboarding-kit tenta detectar sozinho.",
24
25
  firstTasks: "3 a 5 tarefas pequenas e seguras para alguém novo começar (ex.: um teste faltando, um TODO simples, uma melhoria de mensagem de erro), com os arquivos envolvidos.",
25
26
  };
26
27
  function createTemplate({ framework, language = "pt-BR" }) {
@@ -80,9 +81,16 @@ function createTemplate({ framework, language = "pt-BR" }) {
80
81
  gotchas: [{ title: "", description: "", evidence: [""] }],
81
82
  decisions: [{ title: "", context: "", decision: "", evidence: [""] }],
82
83
  firstTasks: [{ title: "", description: "", files: [""] }],
84
+ brand: { primaryColor: "", palette: [""], logo: "", favicon: "", evidence: [""] },
83
85
  };
84
86
  }
85
- /** Frase para colar na IA local depois do `init`. */
86
- function aiPrompt(file = "onboarding.json") {
87
+ /**
88
+ * Frase para colar na IA local. Com `init`, pede para a IA criar o arquivo antes
89
+ * (usado quando o onboarding.json ainda não existe).
90
+ */
91
+ function aiPrompt(file = "onboarding.json", options = {}) {
92
+ if (options.init) {
93
+ return `Rode "npx onboarding-kit init" na raiz deste projeto para criar o ${file}. Depois leia o arquivo e siga as instruções em "_instructions" para preenchê-lo com base neste repositório. No final, rode "npx onboarding-kit validate" e corrija o que ele apontar.`;
94
+ }
87
95
  return `Leia o arquivo ${file} e siga as instruções em "_instructions" para preenchê-lo com base neste repositório. No final, rode "npx onboarding-kit validate" e corrija o que ele apontar.`;
88
96
  }
package/dist/types.d.ts CHANGED
@@ -121,6 +121,17 @@ export interface FirstTask {
121
121
  description: string;
122
122
  files: Evidence[];
123
123
  }
124
+ export interface BrandInfo {
125
+ /** Cor principal da marca em hex (#RRGGBB). */
126
+ primaryColor: string;
127
+ /** Outras cores da paleta em hex. */
128
+ palette: string[];
129
+ /** Caminho relativo do logo (ex.: public/logo.svg). */
130
+ logo: string;
131
+ /** Caminho relativo do favicon (ex.: public/favicon.ico). */
132
+ favicon: string;
133
+ evidence: Evidence[];
134
+ }
124
135
  export interface OnboardingData {
125
136
  $schema?: string;
126
137
  _instructions?: unknown;
@@ -141,6 +152,8 @@ export interface OnboardingData {
141
152
  gotchas: Gotcha[];
142
153
  decisions: Decision[];
143
154
  firstTasks: FirstTask[];
155
+ /** Cores e logo do projeto, usados para dar a cara da marca à apresentação. Opcional. */
156
+ brand?: BrandInfo;
144
157
  }
145
158
  export interface OnboardingOptions {
146
159
  /** Caminho do onboarding.json. Padrão: "<cwd>/onboarding.json". */
package/dist/validate.js CHANGED
@@ -4,6 +4,7 @@ exports.parseEvidence = parseEvidence;
4
4
  exports.validateOnboarding = validateOnboarding;
5
5
  const node_fs_1 = require("node:fs");
6
6
  const node_path_1 = require("node:path");
7
+ const brand_1 = require("./brand");
7
8
  const load_1 = require("./load");
8
9
  /** Campo que identifica cada item e precisa estar preenchido. */
9
10
  const ARRAY_SECTIONS = [
@@ -85,7 +86,7 @@ function walkEvidence(value, at, root, issues) {
85
86
  if (EVIDENCE_KEYS.has(key) && Array.isArray(v)) {
86
87
  v.forEach((ref, i) => checkEvidence(ref, `${here}[${i}]`, root, issues));
87
88
  }
88
- else if (key === "file" && typeof v === "string") {
89
+ else if ((key === "file" || key === "logo" || key === "favicon") && typeof v === "string") {
89
90
  checkEvidence(v, here, root, issues);
90
91
  }
91
92
  else {
@@ -169,6 +170,17 @@ function validateOnboarding(data, options) {
169
170
  });
170
171
  }
171
172
  }
173
+ const brand = data.brand;
174
+ if (brand !== undefined && !isObj(brand)) {
175
+ err("brand", "precisa ser um objeto");
176
+ }
177
+ else if (isObj(brand)) {
178
+ const colors = [["brand.primaryColor", brand.primaryColor], ...(Array.isArray(brand.palette) ? brand.palette.map((c, i) => [`brand.palette[${i}]`, c]) : [])];
179
+ for (const [path, value] of colors) {
180
+ if (typeof value === "string" && !(0, load_1.isBlank)(value) && !(0, brand_1.parseColor)(value))
181
+ warn(String(path), `cor não reconhecida: "${value}" (use hex, ex.: #4f46e5)`);
182
+ }
183
+ }
172
184
  if (!isObj(data.gettingStarted))
173
185
  err("gettingStarted", "seção ausente");
174
186
  if (!isObj(data.domain))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onboarding-kit",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Onboarding de projetos gerado pela sua IA local: um JSON descreve o projeto e o onboarding-kit o apresenta em /onboarding.",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -29,9 +29,15 @@
29
29
  },
30
30
  "typesVersions": {
31
31
  "*": {
32
- "node": ["./dist/adapters/node.d.ts"],
33
- "next": ["./dist/adapters/next.d.ts"],
34
- "nest": ["./dist/adapters/nest.d.ts"]
32
+ "node": [
33
+ "./dist/adapters/node.d.ts"
34
+ ],
35
+ "next": [
36
+ "./dist/adapters/next.d.ts"
37
+ ],
38
+ "nest": [
39
+ "./dist/adapters/nest.d.ts"
40
+ ]
35
41
  }
36
42
  },
37
43
  "files": [