whale-igniter 1.3.3 → 1.3.5
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/dist/commands/createLanding.js +94 -60
- package/dist/commands/ignite.js +3 -1
- package/dist/commands/themes.js +20 -0
- package/dist/commands/updateTokens.js +86 -0
- package/dist/index.js +21 -0
- package/dist/themes/registry.js +49 -0
- package/dist/themes/types.js +1 -0
- package/dist/ui/splash.js +36 -0
- package/dist/version.js +1 -1
- package/package.json +2 -1
- package/themes/atlas.json +34 -0
- package/themes/aurora.json +34 -0
- package/themes/harbor.json +34 -0
|
@@ -5,6 +5,7 @@ import { loadConfig } from "../utils/config.js";
|
|
|
5
5
|
import { upsertComponent } from "../utils/components.js";
|
|
6
6
|
import { generateWiki } from "../generators/wikiGenerator.js";
|
|
7
7
|
import { ui } from "../ui/index.js";
|
|
8
|
+
import { loadUiTheme, DEFAULT_UI_THEME } from "../themes/registry.js";
|
|
8
9
|
const DEFAULT_SECTIONS = ["hero", "features", "proof", "contact"];
|
|
9
10
|
const VALID_SECTIONS = new Set(DEFAULT_SECTIONS);
|
|
10
11
|
export async function createLandingCommand(options = {}) {
|
|
@@ -35,23 +36,26 @@ export async function createLandingCommand(options = {}) {
|
|
|
35
36
|
console.log(ui.warn(`Project stack is ${ui.code(config.stack)}; generating portable HTML/CSS anyway.`));
|
|
36
37
|
}
|
|
37
38
|
await fs.ensureDir(path.dirname(htmlAbs));
|
|
38
|
-
const grid = config.foundations?.grid ?? 8;
|
|
39
|
-
const controlRadius = config.foundations?.radius?.control ?? 2;
|
|
40
|
-
const containerRadius = config.foundations?.radius?.container ?? 4;
|
|
41
39
|
const projectName = config.projectName ?? "Whale Landing";
|
|
40
|
+
// Resolve theme: CLI flag → config.theme → DEFAULT_UI_THEME
|
|
41
|
+
const themeName = options.theme ?? config.theme ?? DEFAULT_UI_THEME;
|
|
42
|
+
const theme = await loadUiTheme(themeName);
|
|
43
|
+
if (!theme) {
|
|
44
|
+
console.log(ui.fail(`Unknown theme '${themeName}'. Run \`whale themes list\`.`));
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
42
48
|
await fs.writeFile(htmlAbs, renderHtml(projectName, sections), "utf8");
|
|
43
|
-
await fs.writeFile(cssAbs, renderCss({
|
|
44
|
-
grid,
|
|
45
|
-
controlRadius,
|
|
46
|
-
containerRadius,
|
|
49
|
+
await fs.writeFile(cssAbs, renderCss(theme, {
|
|
47
50
|
includeProof: sections.includes("proof"),
|
|
48
51
|
includeContact: sections.includes("contact")
|
|
49
52
|
}), "utf8");
|
|
50
53
|
console.log(ui.ok(`Created ${ui.path(htmlRel)}`));
|
|
51
54
|
console.log(ui.ok(`Created ${ui.path(cssRel)}`));
|
|
52
55
|
console.log(` ${ui.kv("sections", sections.join(", "), { keyWidth: 8 })}`);
|
|
53
|
-
console.log(` ${ui.kv("
|
|
54
|
-
console.log(` ${ui.kv("
|
|
56
|
+
console.log(` ${ui.kv("theme", theme.label, { keyWidth: 8 })}`);
|
|
57
|
+
console.log(` ${ui.kv("grid", `${theme.structural.grid}px`, { keyWidth: 8 })}`);
|
|
58
|
+
console.log(` ${ui.kv("radius", `${theme.structural.radiusControl}px controls, ${theme.structural.radiusContainer}px containers`, { keyWidth: 8 })}`);
|
|
55
59
|
if (!options.noRegister) {
|
|
56
60
|
for (const section of sections) {
|
|
57
61
|
await upsertComponent(target, {
|
|
@@ -214,17 +218,45 @@ function renderSection(section, title, sections) {
|
|
|
214
218
|
</section>`;
|
|
215
219
|
}
|
|
216
220
|
}
|
|
217
|
-
function renderCss(
|
|
218
|
-
const {
|
|
221
|
+
function renderCss(theme, opts) {
|
|
222
|
+
const { includeProof, includeContact } = opts;
|
|
223
|
+
const { identity, typography, structural, interaction } = theme;
|
|
224
|
+
const g = structural.grid;
|
|
225
|
+
const focusStyles = interaction.focus === "ring"
|
|
226
|
+
? `box-shadow: 0 0 0 3px var(--color-accent);
|
|
227
|
+
outline: none;`
|
|
228
|
+
: `outline: var(--border-width) solid var(--color-accent);
|
|
229
|
+
outline-offset: ${g / 2}px;`;
|
|
219
230
|
return `:root {
|
|
220
|
-
|
|
221
|
-
--color-bg:
|
|
222
|
-
--color-
|
|
223
|
-
--color-
|
|
224
|
-
--color-
|
|
225
|
-
--color-
|
|
226
|
-
--color-accent
|
|
227
|
-
--color-
|
|
231
|
+
/* Identity */
|
|
232
|
+
--color-bg: ${identity.bg};
|
|
233
|
+
--color-surface: ${identity.surface};
|
|
234
|
+
--color-text: ${identity.text};
|
|
235
|
+
--color-muted: ${identity.muted};
|
|
236
|
+
--color-border: ${identity.border};
|
|
237
|
+
--color-accent: ${identity.accent};
|
|
238
|
+
--color-accent-strong: ${identity.accentStrong};
|
|
239
|
+
--color-on-accent: ${identity.onAccent};
|
|
240
|
+
|
|
241
|
+
/* Typography */
|
|
242
|
+
--font-display: ${typography.display};
|
|
243
|
+
--font-body: ${typography.body};
|
|
244
|
+
--font-mono: ${typography.mono};
|
|
245
|
+
|
|
246
|
+
/* Structure */
|
|
247
|
+
--space-grid: ${structural.grid}px;
|
|
248
|
+
--radius-control: ${structural.radiusControl}px;
|
|
249
|
+
--radius-container: ${structural.radiusContainer}px;
|
|
250
|
+
--border-width: ${structural.borderWidth}px;
|
|
251
|
+
--shadow: ${structural.shadow};
|
|
252
|
+
|
|
253
|
+
/* Motion */
|
|
254
|
+
--motion-duration: ${interaction.duration}ms;
|
|
255
|
+
--motion-ease: ${interaction.easing};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@media (prefers-reduced-motion: reduce) {
|
|
259
|
+
* { transition: none !important; }
|
|
228
260
|
}
|
|
229
261
|
|
|
230
262
|
* {
|
|
@@ -233,7 +265,7 @@ function renderCss(args) {
|
|
|
233
265
|
|
|
234
266
|
body {
|
|
235
267
|
margin: 0;
|
|
236
|
-
font-family:
|
|
268
|
+
font-family: var(--font-body);
|
|
237
269
|
color: var(--color-text);
|
|
238
270
|
background: var(--color-bg);
|
|
239
271
|
}
|
|
@@ -243,17 +275,17 @@ a {
|
|
|
243
275
|
}
|
|
244
276
|
|
|
245
277
|
.section {
|
|
246
|
-
padding:
|
|
278
|
+
padding: calc(var(--space-grid) * 8) calc(var(--space-grid) * 3);
|
|
247
279
|
}
|
|
248
280
|
|
|
249
281
|
.section__inner {
|
|
250
282
|
width: 100%;
|
|
251
|
-
max-width:
|
|
283
|
+
max-width: calc(var(--space-grid) * 140);
|
|
252
284
|
margin: 0 auto;
|
|
253
285
|
}
|
|
254
286
|
|
|
255
287
|
.hero {
|
|
256
|
-
min-height:
|
|
288
|
+
min-height: calc(var(--space-grid) * 80);
|
|
257
289
|
display: grid;
|
|
258
290
|
align-items: center;
|
|
259
291
|
}
|
|
@@ -262,13 +294,13 @@ a {
|
|
|
262
294
|
.contact__grid,
|
|
263
295
|
.proof__grid {
|
|
264
296
|
display: grid;
|
|
265
|
-
grid-template-columns: minmax(0, 1.2fr) minmax(
|
|
266
|
-
gap:
|
|
297
|
+
grid-template-columns: minmax(0, 1.2fr) minmax(calc(var(--space-grid) * 36), 0.8fr);
|
|
298
|
+
gap: calc(var(--space-grid) * 6);
|
|
267
299
|
align-items: center;
|
|
268
300
|
}
|
|
269
301
|
|
|
270
302
|
.eyebrow {
|
|
271
|
-
margin: 0 0
|
|
303
|
+
margin: 0 0 calc(var(--space-grid) * 2);
|
|
272
304
|
color: var(--color-accent-strong);
|
|
273
305
|
font-size: 0.875rem;
|
|
274
306
|
font-weight: 700;
|
|
@@ -283,27 +315,29 @@ p {
|
|
|
283
315
|
}
|
|
284
316
|
|
|
285
317
|
h1 {
|
|
286
|
-
max-width:
|
|
287
|
-
margin-bottom:
|
|
318
|
+
max-width: calc(var(--space-grid) * 88);
|
|
319
|
+
margin-bottom: calc(var(--space-grid) * 3);
|
|
320
|
+
font-family: var(--font-display);
|
|
288
321
|
font-size: clamp(2.5rem, 6vw, 4.5rem);
|
|
289
322
|
line-height: 1;
|
|
290
323
|
}
|
|
291
324
|
|
|
292
325
|
h2 {
|
|
293
|
-
max-width:
|
|
294
|
-
margin-bottom:
|
|
326
|
+
max-width: calc(var(--space-grid) * 80);
|
|
327
|
+
margin-bottom: calc(var(--space-grid) * 4);
|
|
328
|
+
font-family: var(--font-display);
|
|
295
329
|
font-size: clamp(2rem, 4vw, 3rem);
|
|
296
330
|
line-height: 1.1;
|
|
297
331
|
}
|
|
298
332
|
|
|
299
333
|
h3 {
|
|
300
|
-
margin-bottom:
|
|
334
|
+
margin-bottom: var(--space-grid);
|
|
301
335
|
font-size: 1.125rem;
|
|
302
336
|
}
|
|
303
337
|
|
|
304
338
|
.lede {
|
|
305
|
-
max-width:
|
|
306
|
-
margin-bottom:
|
|
339
|
+
max-width: calc(var(--space-grid) * 72);
|
|
340
|
+
margin-bottom: calc(var(--space-grid) * 4);
|
|
307
341
|
color: var(--color-muted);
|
|
308
342
|
font-size: 1.125rem;
|
|
309
343
|
line-height: 1.6;
|
|
@@ -311,18 +345,19 @@ h3 {
|
|
|
311
345
|
|
|
312
346
|
.button {
|
|
313
347
|
display: inline-flex;
|
|
314
|
-
min-height:
|
|
348
|
+
min-height: calc(var(--space-grid) * 6);
|
|
315
349
|
align-items: center;
|
|
316
350
|
justify-content: center;
|
|
317
|
-
padding:
|
|
351
|
+
padding: var(--space-grid) calc(var(--space-grid) * 3);
|
|
318
352
|
border: 0;
|
|
319
|
-
border-radius:
|
|
353
|
+
border-radius: var(--radius-control);
|
|
320
354
|
background: var(--color-accent);
|
|
321
|
-
color: var(--color-
|
|
355
|
+
color: var(--color-on-accent);
|
|
322
356
|
font: inherit;
|
|
323
357
|
font-weight: 700;
|
|
324
358
|
text-decoration: none;
|
|
325
359
|
cursor: pointer;
|
|
360
|
+
transition: background var(--motion-duration) var(--motion-ease);
|
|
326
361
|
}
|
|
327
362
|
|
|
328
363
|
.button:hover {
|
|
@@ -332,24 +367,23 @@ h3 {
|
|
|
332
367
|
.button:focus-visible,
|
|
333
368
|
input:focus-visible,
|
|
334
369
|
textarea:focus-visible {
|
|
335
|
-
|
|
336
|
-
outline-offset: ${grid / 2}px;
|
|
370
|
+
${focusStyles}
|
|
337
371
|
}
|
|
338
372
|
|
|
339
373
|
.hero__panel,
|
|
340
374
|
.card,
|
|
341
375
|
blockquote,
|
|
342
376
|
.contact-form {
|
|
343
|
-
border:
|
|
344
|
-
border-radius:
|
|
377
|
+
border: var(--border-width) solid var(--color-border);
|
|
378
|
+
border-radius: var(--radius-container);
|
|
345
379
|
background: var(--color-surface);
|
|
346
|
-
box-shadow:
|
|
380
|
+
box-shadow: var(--shadow);
|
|
347
381
|
}
|
|
348
382
|
|
|
349
383
|
.hero__panel {
|
|
350
384
|
display: grid;
|
|
351
|
-
gap:
|
|
352
|
-
padding:
|
|
385
|
+
gap: calc(var(--space-grid) * 2);
|
|
386
|
+
padding: calc(var(--space-grid) * 4);
|
|
353
387
|
}
|
|
354
388
|
|
|
355
389
|
.hero__panel strong {
|
|
@@ -359,15 +393,15 @@ blockquote,
|
|
|
359
393
|
.feature-grid {
|
|
360
394
|
display: grid;
|
|
361
395
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
362
|
-
gap:
|
|
396
|
+
gap: calc(var(--space-grid) * 3);
|
|
363
397
|
}
|
|
364
398
|
|
|
365
399
|
.card {
|
|
366
|
-
padding:
|
|
400
|
+
padding: calc(var(--space-grid) * 3);
|
|
367
401
|
}
|
|
368
|
-
${includeProof ? proofCss(
|
|
369
|
-
${includeContact ? contactCss(
|
|
370
|
-
@media (max-width:
|
|
402
|
+
${includeProof ? proofCss() : ""}
|
|
403
|
+
${includeContact ? contactCss() : ""}
|
|
404
|
+
@media (max-width: calc(var(--space-grid) * 96)) {
|
|
371
405
|
.hero__grid,
|
|
372
406
|
.contact__grid,
|
|
373
407
|
.proof__grid,
|
|
@@ -376,12 +410,12 @@ ${includeContact ? contactCss(grid) : ""}
|
|
|
376
410
|
}
|
|
377
411
|
|
|
378
412
|
.section {
|
|
379
|
-
padding:
|
|
413
|
+
padding: calc(var(--space-grid) * 6) calc(var(--space-grid) * 2);
|
|
380
414
|
}
|
|
381
415
|
}
|
|
382
416
|
`;
|
|
383
417
|
}
|
|
384
|
-
function proofCss(
|
|
418
|
+
function proofCss() {
|
|
385
419
|
return `
|
|
386
420
|
.proof {
|
|
387
421
|
background: var(--color-surface);
|
|
@@ -389,11 +423,11 @@ function proofCss(grid) {
|
|
|
389
423
|
|
|
390
424
|
blockquote {
|
|
391
425
|
margin: 0;
|
|
392
|
-
padding:
|
|
426
|
+
padding: calc(var(--space-grid) * 3);
|
|
393
427
|
}
|
|
394
428
|
|
|
395
429
|
blockquote p {
|
|
396
|
-
margin-bottom:
|
|
430
|
+
margin-bottom: calc(var(--space-grid) * 2);
|
|
397
431
|
font-size: 1.25rem;
|
|
398
432
|
line-height: 1.5;
|
|
399
433
|
}
|
|
@@ -404,26 +438,26 @@ cite {
|
|
|
404
438
|
}
|
|
405
439
|
`;
|
|
406
440
|
}
|
|
407
|
-
function contactCss(
|
|
441
|
+
function contactCss() {
|
|
408
442
|
return `
|
|
409
443
|
.contact-form {
|
|
410
444
|
display: grid;
|
|
411
|
-
gap:
|
|
412
|
-
padding:
|
|
445
|
+
gap: calc(var(--space-grid) * 2);
|
|
446
|
+
padding: calc(var(--space-grid) * 3);
|
|
413
447
|
}
|
|
414
448
|
|
|
415
449
|
label {
|
|
416
450
|
display: grid;
|
|
417
|
-
gap:
|
|
451
|
+
gap: var(--space-grid);
|
|
418
452
|
font-weight: 700;
|
|
419
453
|
}
|
|
420
454
|
|
|
421
455
|
input,
|
|
422
456
|
textarea {
|
|
423
457
|
width: 100%;
|
|
424
|
-
padding:
|
|
425
|
-
border:
|
|
426
|
-
border-radius:
|
|
458
|
+
padding: var(--space-grid) calc(var(--space-grid) * 2);
|
|
459
|
+
border: var(--border-width) solid var(--color-border);
|
|
460
|
+
border-radius: var(--radius-control);
|
|
427
461
|
font: inherit;
|
|
428
462
|
}
|
|
429
463
|
`;
|
package/dist/commands/ignite.js
CHANGED
|
@@ -10,6 +10,7 @@ import { getAiAvailability } from "../utils/aiAvailability.js";
|
|
|
10
10
|
import { mapWizardAnswers, suggestUiCategories } from "../utils/wizardMapping.js";
|
|
11
11
|
import { ui } from "../ui/index.js";
|
|
12
12
|
import { PACKAGE_VERSION } from "../version.js";
|
|
13
|
+
import { splash } from "../ui/splash.js";
|
|
13
14
|
const OPINIONATED_PACKS = ["lighthouse", "forge", "scribe"];
|
|
14
15
|
const MINIMAL_PACKS = ["lighthouse"];
|
|
15
16
|
export async function igniteCommand(projectName = "whale-project", options = {}) {
|
|
@@ -19,7 +20,8 @@ export async function igniteCommand(projectName = "whale-project", options = {})
|
|
|
19
20
|
? "minimal"
|
|
20
21
|
: "opinionated";
|
|
21
22
|
console.log();
|
|
22
|
-
console.log(
|
|
23
|
+
console.log(splash());
|
|
24
|
+
console.log(ui.muted(`ignite • ${mode}`));
|
|
23
25
|
console.log();
|
|
24
26
|
// ---- Resolve config based on mode -----------------------------------------
|
|
25
27
|
let config;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ui } from "../ui/index.js";
|
|
2
|
+
import { listUiThemes } from "../themes/registry.js";
|
|
3
|
+
export async function themesListCommand() {
|
|
4
|
+
const themes = await listUiThemes();
|
|
5
|
+
if (themes.length === 0) {
|
|
6
|
+
console.log(ui.warn("No UI themes found."));
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
for (const theme of themes) {
|
|
10
|
+
console.log();
|
|
11
|
+
console.log(ui.section(theme.label));
|
|
12
|
+
console.log(ui.muted(theme.description));
|
|
13
|
+
console.log(` ${ui.kv("accent", theme.identity.accent, { keyWidth: 10 })}`);
|
|
14
|
+
console.log(` ${ui.kv("radius", `${theme.structural.radiusControl}px / ${theme.structural.radiusContainer}px`, { keyWidth: 10 })}`);
|
|
15
|
+
console.log(` ${ui.kv("mode", theme.identity.mode, { keyWidth: 10 })}`);
|
|
16
|
+
console.log(` ${ui.kv("slug", theme.name, { keyWidth: 10 })}`);
|
|
17
|
+
}
|
|
18
|
+
console.log();
|
|
19
|
+
console.log(ui.muted(`Use --theme <slug> with \`whale create landing\` to apply a theme.`));
|
|
20
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { resolveTarget } from "../utils/paths.js";
|
|
4
|
+
import { generateWiki } from "../generators/wikiGenerator.js";
|
|
5
|
+
import { ui } from "../ui/index.js";
|
|
6
|
+
const COLOR_ALIASES = {
|
|
7
|
+
black: { base: "#111827", hover: "#000000" },
|
|
8
|
+
slate: { base: "#334155", hover: "#0f172a" },
|
|
9
|
+
cyan: { base: "#0891b2", hover: "#0e7490" },
|
|
10
|
+
blue: { base: "#2563eb", hover: "#1d4ed8" },
|
|
11
|
+
green: { base: "#16a34a", hover: "#15803d" },
|
|
12
|
+
red: { base: "#dc2626", hover: "#b91c1c" }
|
|
13
|
+
};
|
|
14
|
+
export async function updateTokensCommand(options = {}) {
|
|
15
|
+
const target = resolveTarget();
|
|
16
|
+
const fileRel = options.file ?? "src/styles.css";
|
|
17
|
+
const fileAbs = path.join(target, fileRel);
|
|
18
|
+
if (!options.buttonColor) {
|
|
19
|
+
console.log(ui.fail("Pass --button-color <name|hex>. Example: whale update tokens --button-color black"));
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (!(await fs.pathExists(fileAbs))) {
|
|
24
|
+
console.log(ui.fail(`${ui.path(fileRel)} was not found.`));
|
|
25
|
+
console.log(ui.muted("Run `whale create landing --sections hero,features,proof,contact` first, or pass --file <path>."));
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const colors = resolveColors(options.buttonColor, options.hoverColor);
|
|
30
|
+
if (!colors) {
|
|
31
|
+
console.log(ui.fail("Color must be a known name or a hex value like #111827."));
|
|
32
|
+
console.log(ui.muted(`Known names: ${Object.keys(COLOR_ALIASES).join(", ")}`));
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const before = await fs.readFile(fileAbs, "utf8");
|
|
37
|
+
let after = upsertCssCustomProperty(before, "--color-accent", colors.base);
|
|
38
|
+
after = upsertCssCustomProperty(after, "--color-accent-strong", colors.hover);
|
|
39
|
+
if (after === before) {
|
|
40
|
+
console.log(ui.muted("No token changes needed."));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
await fs.writeFile(fileAbs, after, "utf8");
|
|
44
|
+
console.log(ui.ok(`Updated button tokens in ${ui.path(fileRel)}`));
|
|
45
|
+
console.log(` ${ui.kv("--color-accent", colors.base, { keyWidth: 22 })}`);
|
|
46
|
+
console.log(` ${ui.kv("--color-accent-strong", colors.hover, { keyWidth: 22 })}`);
|
|
47
|
+
if (!options.noSync) {
|
|
48
|
+
await generateWiki(target);
|
|
49
|
+
console.log(ui.muted(`${ui.glyph.check} AI context updated`));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function resolveColors(buttonColor, hoverColor) {
|
|
53
|
+
const base = resolveColor(buttonColor);
|
|
54
|
+
const hover = hoverColor ? resolveColor(hoverColor) : undefined;
|
|
55
|
+
if (!base || (hoverColor && !hover))
|
|
56
|
+
return null;
|
|
57
|
+
const alias = COLOR_ALIASES[buttonColor.trim().toLowerCase()];
|
|
58
|
+
return {
|
|
59
|
+
base,
|
|
60
|
+
hover: hover ?? alias?.hover ?? base
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function resolveColor(input) {
|
|
64
|
+
const normalized = input.trim().toLowerCase();
|
|
65
|
+
const alias = COLOR_ALIASES[normalized];
|
|
66
|
+
if (alias)
|
|
67
|
+
return alias.base;
|
|
68
|
+
if (/^#[0-9a-f]{3}(?:[0-9a-f]{3})?(?:[0-9a-f]{2})?$/i.test(input.trim())) {
|
|
69
|
+
return input.trim();
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
function upsertCssCustomProperty(css, property, value) {
|
|
74
|
+
const propertyPattern = new RegExp(`(${escapeRegExp(property)}\\s*:\\s*)[^;]+;`);
|
|
75
|
+
if (propertyPattern.test(css)) {
|
|
76
|
+
return css.replace(propertyPattern, `$1${value};`);
|
|
77
|
+
}
|
|
78
|
+
const rootPattern = /:root\s*\{/;
|
|
79
|
+
if (rootPattern.test(css)) {
|
|
80
|
+
return css.replace(rootPattern, `:root {\n ${property}: ${value};`);
|
|
81
|
+
}
|
|
82
|
+
return `:root {\n ${property}: ${value};\n}\n\n${css}`;
|
|
83
|
+
}
|
|
84
|
+
function escapeRegExp(input) {
|
|
85
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
86
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,8 @@ import { adoptReviewCommand, adoptStatusCommand } from "./commands/adoptReview.j
|
|
|
15
15
|
import { insightsCommand, insightsDriftReviewCommand } from "./commands/insights.js";
|
|
16
16
|
import { createComponentCommand } from "./commands/createComponent.js";
|
|
17
17
|
import { createLandingCommand } from "./commands/createLanding.js";
|
|
18
|
+
import { themesListCommand } from "./commands/themes.js";
|
|
19
|
+
import { updateTokensCommand } from "./commands/updateTokens.js";
|
|
18
20
|
import { seleneDescribeCommand, seleneAuditCommand, seleneSuggestCommand, seleneApplyCommand, seleneStatusCommand, seleneCacheClearCommand } from "./commands/selene.js";
|
|
19
21
|
import { mcpServeCommand, mcpConfigCommand } from "./commands/mcp.js";
|
|
20
22
|
import { watchCommand } from "./commands/watch.js";
|
|
@@ -181,6 +183,7 @@ create
|
|
|
181
183
|
.option("--force", "Overwrite generated files if they already exist")
|
|
182
184
|
.option("--no-register", "Don't add generated sections to intelligence/components.json")
|
|
183
185
|
.option("--no-sync", "Don't regenerate CLAUDE.md / wiki after creating")
|
|
186
|
+
.option("--theme <name>", "Visual theme for the generated UI (see `whale themes list`)")
|
|
184
187
|
.action((opts) => createLandingCommand(opts));
|
|
185
188
|
const adopt = program
|
|
186
189
|
.command("adopt [target]")
|
|
@@ -226,6 +229,17 @@ component
|
|
|
226
229
|
.command("list")
|
|
227
230
|
.description("List components registered in the catalog.")
|
|
228
231
|
.action(componentListCommand);
|
|
232
|
+
const update = program
|
|
233
|
+
.command("update")
|
|
234
|
+
.description("Apply deterministic updates to project files.");
|
|
235
|
+
update
|
|
236
|
+
.command("tokens")
|
|
237
|
+
.description("Update generated CSS design tokens, such as button colors.")
|
|
238
|
+
.option("--button-color <nameOrHex>", "Button color name or hex (black, cyan, blue, green, red, slate, #111827)")
|
|
239
|
+
.option("--hover-color <nameOrHex>", "Optional hover color name or hex")
|
|
240
|
+
.option("--file <path>", "CSS file relative to the project root (default: src/styles.css)")
|
|
241
|
+
.option("--no-sync", "Don't regenerate CLAUDE.md / wiki after updating")
|
|
242
|
+
.action((opts) => updateTokensCommand(opts));
|
|
229
243
|
const team = program
|
|
230
244
|
.command("team")
|
|
231
245
|
.description("Manage your AI team (friendly layer for operational packs).");
|
|
@@ -282,4 +296,11 @@ program
|
|
|
282
296
|
.description("Show what changed in intelligence stores since a git ref.")
|
|
283
297
|
.option("--since <ref>", "Git ref to compare against (default: HEAD~1)", "HEAD~1")
|
|
284
298
|
.action((target, opts) => changesCommand({ since: opts.since, target }));
|
|
299
|
+
const themes = program
|
|
300
|
+
.command("themes")
|
|
301
|
+
.description("Browse local UI themes for generated output.");
|
|
302
|
+
themes
|
|
303
|
+
.command("list")
|
|
304
|
+
.description("List available UI themes.")
|
|
305
|
+
.action(() => themesListCommand());
|
|
285
306
|
program.parse();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = path.dirname(__filename);
|
|
6
|
+
// Resolve themes/ relative to compiled output (dist/themes/ → ../../themes)
|
|
7
|
+
const THEMES_DIR = path.resolve(__dirname, "../../themes");
|
|
8
|
+
export const DEFAULT_UI_THEME = "atlas";
|
|
9
|
+
function isValidUiTheme(obj) {
|
|
10
|
+
if (!obj || typeof obj !== "object")
|
|
11
|
+
return false;
|
|
12
|
+
const t = obj;
|
|
13
|
+
return (typeof t.name === "string" &&
|
|
14
|
+
typeof t.label === "string" &&
|
|
15
|
+
typeof t.description === "string" &&
|
|
16
|
+
typeof t.identity === "object" && t.identity !== null &&
|
|
17
|
+
typeof t.typography === "object" && t.typography !== null &&
|
|
18
|
+
typeof t.structural === "object" && t.structural !== null &&
|
|
19
|
+
typeof t.interaction === "object" && t.interaction !== null);
|
|
20
|
+
}
|
|
21
|
+
export async function listUiThemes() {
|
|
22
|
+
const files = await fs.readdir(THEMES_DIR).catch(() => []);
|
|
23
|
+
const themes = [];
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
if (!file.endsWith(".json"))
|
|
26
|
+
continue;
|
|
27
|
+
try {
|
|
28
|
+
const raw = await fs.readJson(path.join(THEMES_DIR, file));
|
|
29
|
+
if (isValidUiTheme(raw))
|
|
30
|
+
themes.push(raw);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// skip malformed packs
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return themes.sort((a, b) => a.label.localeCompare(b.label));
|
|
37
|
+
}
|
|
38
|
+
export async function loadUiTheme(name) {
|
|
39
|
+
const filePath = path.join(THEMES_DIR, `${name}.json`);
|
|
40
|
+
if (!(await fs.pathExists(filePath)))
|
|
41
|
+
return null;
|
|
42
|
+
try {
|
|
43
|
+
const raw = await fs.readJson(filePath);
|
|
44
|
+
return isValidUiTheme(raw) ? raw : null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* splash.ts — capability-gated welcome banner for `whale ignite`.
|
|
3
|
+
*
|
|
4
|
+
* Three tiers:
|
|
5
|
+
* 1. Full (color + unicode) — wordmark with accent color + glyphs
|
|
6
|
+
* 2. Unicode, no color — same layout, no color
|
|
7
|
+
* 3. Plain — single ASCII line, no glyphs
|
|
8
|
+
*
|
|
9
|
+
* Rules: no chalk imports, no direct env var reads, no "premium".
|
|
10
|
+
* Uses `capabilities()` from capabilities.js and `ui` atoms.
|
|
11
|
+
*/
|
|
12
|
+
import { capabilities } from "./capabilities.js";
|
|
13
|
+
import { accent, muted, emphasis } from "./atoms.js";
|
|
14
|
+
import { PACKAGE_VERSION } from "../version.js";
|
|
15
|
+
const TAGLINE = "map the project before the agent moves";
|
|
16
|
+
export function splash() {
|
|
17
|
+
const { color, unicode } = capabilities();
|
|
18
|
+
const isPlain = !unicode;
|
|
19
|
+
// Tier 3 — plain ASCII, single line
|
|
20
|
+
if (isPlain) {
|
|
21
|
+
return `WHALE IGNITER v${PACKAGE_VERSION} — ${TAGLINE}`;
|
|
22
|
+
}
|
|
23
|
+
// Tier 1 & 2 — unicode layout (color applied via atom functions when available)
|
|
24
|
+
const whale = "◆ ~";
|
|
25
|
+
const wordmarkWhale = color ? accent("WHALE") : "WHALE";
|
|
26
|
+
const wordmarkIgniter = color ? emphasis("IGNITER") : "IGNITER";
|
|
27
|
+
const version = color ? muted(`v${PACKAGE_VERSION}`) : `v${PACKAGE_VERSION}`;
|
|
28
|
+
const tag = color ? muted(TAGLINE) : TAGLINE;
|
|
29
|
+
const rule = "─".repeat(36);
|
|
30
|
+
return [
|
|
31
|
+
`${whale}`,
|
|
32
|
+
`${wordmarkWhale} ${wordmarkIgniter} ${version}`,
|
|
33
|
+
tag,
|
|
34
|
+
rule
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = "1.3.
|
|
1
|
+
export const PACKAGE_VERSION = "1.3.5";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whale-igniter",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"description": "CLI-first operational intelligence. Bootstraps and adopts AI-readable project context so agents like Claude Code, Codex and Cursor understand your design system from the first commit. Includes an MCP server, a file watcher, deterministic insights, and an opt-in AI bridge (Selene).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"dist",
|
|
12
12
|
"brand",
|
|
13
13
|
"packs",
|
|
14
|
+
"themes",
|
|
14
15
|
"ui-references",
|
|
15
16
|
"README.md",
|
|
16
17
|
"LICENSE",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "atlas",
|
|
3
|
+
"label": "Atlas",
|
|
4
|
+
"description": "Enterprise minimal: dense, neutral, hairline borders.",
|
|
5
|
+
"identity": {
|
|
6
|
+
"mode": "light",
|
|
7
|
+
"bg": "#FFFFFF",
|
|
8
|
+
"surface": "#FFFFFF",
|
|
9
|
+
"text": "#0F172A",
|
|
10
|
+
"muted": "#475569",
|
|
11
|
+
"border": "#E2E8F0",
|
|
12
|
+
"accent": "#334155",
|
|
13
|
+
"accentStrong": "#0F172A",
|
|
14
|
+
"onAccent": "#FFFFFF"
|
|
15
|
+
},
|
|
16
|
+
"typography": {
|
|
17
|
+
"display": "Inter, ui-sans-serif, system-ui, -apple-system, sans-serif",
|
|
18
|
+
"body": "Inter, ui-sans-serif, system-ui, -apple-system, sans-serif",
|
|
19
|
+
"mono": "ui-monospace, SFMono-Regular, monospace",
|
|
20
|
+
"scaleRatio": 1.2
|
|
21
|
+
},
|
|
22
|
+
"structural": {
|
|
23
|
+
"grid": 8,
|
|
24
|
+
"radiusControl": 2,
|
|
25
|
+
"radiusContainer": 4,
|
|
26
|
+
"borderWidth": 1,
|
|
27
|
+
"shadow": "none"
|
|
28
|
+
},
|
|
29
|
+
"interaction": {
|
|
30
|
+
"duration": 120,
|
|
31
|
+
"easing": "cubic-bezier(0.4, 0, 0.2, 1)",
|
|
32
|
+
"focus": "ring"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aurora",
|
|
3
|
+
"label": "Aurora",
|
|
4
|
+
"description": "Modern SaaS: soft, airy, friendly, generous whitespace.",
|
|
5
|
+
"identity": {
|
|
6
|
+
"mode": "light",
|
|
7
|
+
"bg": "#FBFAFF",
|
|
8
|
+
"surface": "#FFFFFF",
|
|
9
|
+
"text": "#26215C",
|
|
10
|
+
"muted": "#534AB7",
|
|
11
|
+
"border": "#E4DFFA",
|
|
12
|
+
"accent": "#6D5AE6",
|
|
13
|
+
"accentStrong": "#534AB7",
|
|
14
|
+
"onAccent": "#FFFFFF"
|
|
15
|
+
},
|
|
16
|
+
"typography": {
|
|
17
|
+
"display": "ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif",
|
|
18
|
+
"body": "ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif",
|
|
19
|
+
"mono": "ui-monospace, SFMono-Regular, monospace",
|
|
20
|
+
"scaleRatio": 1.25
|
|
21
|
+
},
|
|
22
|
+
"structural": {
|
|
23
|
+
"grid": 8,
|
|
24
|
+
"radiusControl": 6,
|
|
25
|
+
"radiusContainer": 8,
|
|
26
|
+
"borderWidth": 1,
|
|
27
|
+
"shadow": "0 8px 24px rgba(109, 90, 230, 0.12)"
|
|
28
|
+
},
|
|
29
|
+
"interaction": {
|
|
30
|
+
"duration": 200,
|
|
31
|
+
"easing": "cubic-bezier(0.16, 1, 0.3, 1)",
|
|
32
|
+
"focus": "ring"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "harbor",
|
|
3
|
+
"label": "Harbor",
|
|
4
|
+
"description": "The Whale brand: confident, editorial, royal blue on paper.",
|
|
5
|
+
"identity": {
|
|
6
|
+
"mode": "light",
|
|
7
|
+
"bg": "#F2ECE1",
|
|
8
|
+
"surface": "#FFFFFF",
|
|
9
|
+
"text": "#003087",
|
|
10
|
+
"muted": "#3A4D7A",
|
|
11
|
+
"border": "#C9D6F2",
|
|
12
|
+
"accent": "#0034D3",
|
|
13
|
+
"accentStrong": "#003087",
|
|
14
|
+
"onAccent": "#F2ECE1"
|
|
15
|
+
},
|
|
16
|
+
"typography": {
|
|
17
|
+
"display": "\"Special Elite\", ui-monospace, SFMono-Regular, monospace",
|
|
18
|
+
"body": "ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif",
|
|
19
|
+
"mono": "\"JetBrains Mono\", ui-monospace, monospace",
|
|
20
|
+
"scaleRatio": 1.25
|
|
21
|
+
},
|
|
22
|
+
"structural": {
|
|
23
|
+
"grid": 8,
|
|
24
|
+
"radiusControl": 2,
|
|
25
|
+
"radiusContainer": 4,
|
|
26
|
+
"borderWidth": 1,
|
|
27
|
+
"shadow": "0 8px 24px rgba(0, 48, 135, 0.10)"
|
|
28
|
+
},
|
|
29
|
+
"interaction": {
|
|
30
|
+
"duration": 160,
|
|
31
|
+
"easing": "cubic-bezier(0.2, 0, 0, 1)",
|
|
32
|
+
"focus": "outline"
|
|
33
|
+
}
|
|
34
|
+
}
|