whale-igniter 1.3.4 → 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.
@@ -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("grid", `${grid}px`, { keyWidth: 8 })}`);
54
- console.log(` ${ui.kv("radius", `${controlRadius}px controls, ${containerRadius}px containers`, { keyWidth: 8 })}`);
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(args) {
218
- const { grid, controlRadius, containerRadius, includeProof, includeContact } = args;
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
- --space-grid: ${grid}px;
221
- --color-bg: #f7fafc;
222
- --color-text: #111827;
223
- --color-muted: #4b5563;
224
- --color-surface: #ffffff;
225
- --color-accent: #0891b2;
226
- --color-accent-strong: #0e7490;
227
- --color-border: #d1d5db;
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: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
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: ${grid * 8}px ${grid * 3}px;
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: ${grid * 140}px;
283
+ max-width: calc(var(--space-grid) * 140);
252
284
  margin: 0 auto;
253
285
  }
254
286
 
255
287
  .hero {
256
- min-height: ${grid * 80}px;
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(${grid * 36}px, 0.8fr);
266
- gap: ${grid * 6}px;
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 ${grid * 2}px;
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: ${grid * 88}px;
287
- margin-bottom: ${grid * 3}px;
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: ${grid * 80}px;
294
- margin-bottom: ${grid * 4}px;
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: ${grid}px;
334
+ margin-bottom: var(--space-grid);
301
335
  font-size: 1.125rem;
302
336
  }
303
337
 
304
338
  .lede {
305
- max-width: ${grid * 72}px;
306
- margin-bottom: ${grid * 4}px;
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: ${grid * 6}px;
348
+ min-height: calc(var(--space-grid) * 6);
315
349
  align-items: center;
316
350
  justify-content: center;
317
- padding: ${grid}px ${grid * 3}px;
351
+ padding: var(--space-grid) calc(var(--space-grid) * 3);
318
352
  border: 0;
319
- border-radius: ${controlRadius}px;
353
+ border-radius: var(--radius-control);
320
354
  background: var(--color-accent);
321
- color: var(--color-surface);
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
- outline: ${controlRadius}px solid var(--color-accent);
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: 1px solid var(--color-border);
344
- border-radius: ${containerRadius}px;
377
+ border: var(--border-width) solid var(--color-border);
378
+ border-radius: var(--radius-container);
345
379
  background: var(--color-surface);
346
- box-shadow: 0 ${grid}px ${grid * 3}px rgba(17, 24, 39, 0.08);
380
+ box-shadow: var(--shadow);
347
381
  }
348
382
 
349
383
  .hero__panel {
350
384
  display: grid;
351
- gap: ${grid * 2}px;
352
- padding: ${grid * 4}px;
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: ${grid * 3}px;
396
+ gap: calc(var(--space-grid) * 3);
363
397
  }
364
398
 
365
399
  .card {
366
- padding: ${grid * 3}px;
400
+ padding: calc(var(--space-grid) * 3);
367
401
  }
368
- ${includeProof ? proofCss(grid) : ""}
369
- ${includeContact ? contactCss(grid) : ""}
370
- @media (max-width: ${grid * 96}px) {
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: ${grid * 6}px ${grid * 2}px;
413
+ padding: calc(var(--space-grid) * 6) calc(var(--space-grid) * 2);
380
414
  }
381
415
  }
382
416
  `;
383
417
  }
384
- function proofCss(grid) {
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: ${grid * 3}px;
426
+ padding: calc(var(--space-grid) * 3);
393
427
  }
394
428
 
395
429
  blockquote p {
396
- margin-bottom: ${grid * 2}px;
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(grid) {
441
+ function contactCss() {
408
442
  return `
409
443
  .contact-form {
410
444
  display: grid;
411
- gap: ${grid * 2}px;
412
- padding: ${grid * 3}px;
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: ${grid}px;
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: ${grid}px ${grid * 2}px;
425
- border: 1px solid var(--color-border);
426
- border-radius: 2px;
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
  `;
@@ -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(ui.header("Whale Igniter", `ignite • ${mode}`));
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
+ }
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ 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";
18
19
  import { updateTokensCommand } from "./commands/updateTokens.js";
19
20
  import { seleneDescribeCommand, seleneAuditCommand, seleneSuggestCommand, seleneApplyCommand, seleneStatusCommand, seleneCacheClearCommand } from "./commands/selene.js";
20
21
  import { mcpServeCommand, mcpConfigCommand } from "./commands/mcp.js";
@@ -182,6 +183,7 @@ create
182
183
  .option("--force", "Overwrite generated files if they already exist")
183
184
  .option("--no-register", "Don't add generated sections to intelligence/components.json")
184
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`)")
185
187
  .action((opts) => createLandingCommand(opts));
186
188
  const adopt = program
187
189
  .command("adopt [target]")
@@ -294,4 +296,11 @@ program
294
296
  .description("Show what changed in intelligence stores since a git ref.")
295
297
  .option("--since <ref>", "Git ref to compare against (default: HEAD~1)", "HEAD~1")
296
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());
297
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.4";
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.4",
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
+ }