martinbonan 4.0.0 → 4.1.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/index.js CHANGED
@@ -17,6 +17,20 @@ if (cols < 60 || rows < 16) {
17
17
  process.exit(1);
18
18
  }
19
19
 
20
- render(React.createElement(App), {
20
+ // Enter alternate screen buffer (prevents scroll-back pollution)
21
+ process.stdout.write('\x1b[?1049h');
22
+ process.stdout.write('\x1b[2J\x1b[H');
23
+
24
+ const instance = render(React.createElement(App), {
21
25
  exitOnCtrlC: true,
22
26
  });
27
+
28
+ // Leave alternate screen on exit
29
+ instance.waitUntilExit().then(() => {
30
+ process.stdout.write('\x1b[?1049l');
31
+ });
32
+
33
+ // Also handle unexpected exits
34
+ process.on('exit', () => {
35
+ process.stdout.write('\x1b[?1049l');
36
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "martinbonan",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Martin Music -- interactive CLI portfolio with AI twin",
5
5
  "type": "module",
6
6
  "bin": {
package/src/App.js CHANGED
@@ -2,25 +2,20 @@ import React, { useState, useCallback } from 'react';
2
2
  import { Box } from 'ink';
3
3
  import BootSequence from './screens/BootSequence.js';
4
4
  import Dashboard from './screens/Dashboard.js';
5
- import { ThemeContext, THEMES, makeChalkColors, getNextThemeName } from './theme.js';
5
+ import { ThemeContext, THEMES, makeChalkColors } from './theme.js';
6
6
 
7
7
  const e = React.createElement;
8
8
 
9
9
  export default function App() {
10
10
  const [screen, setScreen] = useState('boot');
11
- const [themeName, setThemeName] = useState('clean');
12
- const theme = THEMES[themeName];
11
+ const theme = THEMES.sunset;
13
12
  const c = makeChalkColors(theme);
14
13
 
15
14
  const handleBootComplete = useCallback(() => {
16
15
  setScreen('dashboard');
17
16
  }, []);
18
17
 
19
- const nextTheme = useCallback(() => {
20
- setThemeName(name => getNextThemeName(name));
21
- }, []);
22
-
23
- const themeValue = { theme, themeName, nextTheme, c };
18
+ const themeValue = { theme, themeName: 'sunset', nextTheme: () => {}, c };
24
19
 
25
20
  return e(ThemeContext.Provider, { value: themeValue },
26
21
  e(Box, { flexDirection: 'column', minHeight: process.stdout.rows || 24 },
@@ -21,6 +21,8 @@ const TAB_COMPONENTS = {
21
21
  'Ask AI': AskAi,
22
22
  };
23
23
 
24
+ const GLITCH_CHARS = '\u2591\u2592\u2593\u2588\u2580\u2584\u258C\u2590\u2500\u2502';
25
+
24
26
  export default function ContentPanel({
25
27
  activeTab,
26
28
  isFocused,
@@ -28,6 +30,7 @@ export default function ContentPanel({
28
30
  selectedIndex,
29
31
  expandedProject,
30
32
  onCollapseProject,
33
+ transitioning,
31
34
  }) {
32
35
  const { theme } = React.useContext(ThemeContext);
33
36
  const Component = TAB_COMPONENTS[activeTab];
@@ -36,8 +39,27 @@ export default function ContentPanel({
36
39
  const cols = process.stdout.columns || 80;
37
40
  const separator = '\u2500'.repeat(Math.max(cols - 2, 20));
38
41
 
39
- const props = {};
42
+ // Glitch transition effect
43
+ if (transitioning) {
44
+ const glitchLines = [];
45
+ for (let i = 0; i < 4; i++) {
46
+ let line = '';
47
+ for (let j = 0; j < Math.min(cols - 4, 70); j++) {
48
+ line += Math.random() < 0.3
49
+ ? GLITCH_CHARS[Math.floor(Math.random() * GLITCH_CHARS.length)]
50
+ : ' ';
51
+ }
52
+ glitchLines.push(line);
53
+ }
54
+ return e(Box, { flexDirection: 'column', flexGrow: 1 },
55
+ e(Text, { dimColor: true, color: theme.dim }, separator),
56
+ ...glitchLines.map((l, i) =>
57
+ e(Text, { key: i, color: theme.accent, dimColor: true }, l)
58
+ ),
59
+ );
60
+ }
40
61
 
62
+ const props = {};
41
63
  switch (activeTab) {
42
64
  case 'Projects':
43
65
  props.selectedIndex = selectedIndex;
@@ -57,6 +79,6 @@ export default function ContentPanel({
57
79
 
58
80
  return e(Box, { flexDirection: 'column', flexGrow: 1 },
59
81
  e(Text, { dimColor: true, color: theme.dim }, separator),
60
- e(Component, props)
82
+ e(Component, props),
61
83
  );
62
84
  }
@@ -8,39 +8,43 @@ import { ThemeContext } from '../theme.js';
8
8
  const e = React.createElement;
9
9
 
10
10
  function makeGradient(theme) {
11
- // Build gradient from theme colors
12
11
  return gradient([theme.primary, theme.secondary || theme.primary]);
13
12
  }
14
13
 
15
14
  export default function Header({ tagline, fading }) {
16
15
  const { theme } = React.useContext(ThemeContext);
17
16
  const cols = process.stdout.columns || 80;
18
- const showPortrait = cols >= 80;
17
+ const showPortrait = cols >= 75;
19
18
  const grad = makeGradient(theme);
20
19
 
21
- let line1, line2;
20
+ let figletText;
22
21
  try {
23
- line1 = figlet.textSync('Martin', { font: 'Small' });
24
- line2 = figlet.textSync('Bonan', { font: 'Small' });
22
+ const line1 = figlet.textSync('MARTIN', { font: 'ANSI Shadow' });
23
+ const line2 = figlet.textSync('BONAN', { font: 'ANSI Shadow' });
24
+ figletText = line1 + '\n' + line2;
25
25
  } catch {
26
- line1 = 'MARTIN';
27
- line2 = 'BONAN';
26
+ try {
27
+ const line1 = figlet.textSync('MARTIN', { font: 'Standard' });
28
+ const line2 = figlet.textSync('BONAN', { font: 'Standard' });
29
+ figletText = line1 + '\n' + line2;
30
+ } catch {
31
+ figletText = 'MARTIN\nBONAN';
32
+ }
28
33
  }
29
34
 
30
- const figletText = line1 + '\n' + line2;
31
35
  const gradientName = grad(figletText);
32
36
 
33
37
  return e(Box, { flexDirection: 'column' },
34
38
  e(Box, { flexDirection: 'row' },
35
39
  showPortrait && e(Box, { marginRight: 2 },
36
- e(Portrait, null)
40
+ e(Portrait, null),
37
41
  ),
38
42
  e(Box, { flexDirection: 'column', justifyContent: 'center' },
39
- e(Text, null, gradientName)
40
- )
43
+ e(Text, null, gradientName),
44
+ ),
41
45
  ),
42
46
  e(Box, { marginTop: 0 },
43
- e(Text, { italic: true, color: theme.text, dimColor: fading || false }, tagline || '')
44
- )
47
+ e(Text, { italic: true, color: theme.text, dimColor: fading || false }, tagline || ''),
48
+ ),
45
49
  );
46
50
  }
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { ThemeContext } from '../theme.js';
4
+
5
+ const e = React.createElement;
6
+
7
+ const SHORTCUTS = [
8
+ ['\u2190 \u2192', 'Navigate tabs'],
9
+ ['1-7', 'Jump to tab directly'],
10
+ ['\u2191 \u2193', 'Scroll / select items'],
11
+ ['Enter', 'Expand project / focus chat'],
12
+ ['Esc', 'Go back / collapse'],
13
+ ['?', 'Toggle this help screen'],
14
+ ['q', 'Quit martin-cli'],
15
+ ];
16
+
17
+ const TIPS = [
18
+ 'Type "neofetch" or "sudo hire martin" in AI chat',
19
+ 'Type "help" in AI chat for all commands',
20
+ ];
21
+
22
+ export default function HelpOverlay() {
23
+ const { theme } = React.useContext(ThemeContext);
24
+
25
+ const w = 50;
26
+ const top = '\u256D\u2500\u2500\u2500 Help ' + '\u2500'.repeat(w - 7) + '\u256E';
27
+ const bot = '\u2570' + '\u2500'.repeat(w) + '\u256F';
28
+ const emp = '\u2502' + ' '.repeat(w) + '\u2502';
29
+
30
+ const padRight = (str, total) => str + ' '.repeat(Math.max(total - str.length, 0));
31
+
32
+ return e(Box, { flexDirection: 'column', paddingLeft: 2, marginTop: 1 },
33
+ e(Text, { color: theme.border }, top),
34
+ e(Text, { color: theme.border }, emp),
35
+ e(Text, { color: theme.border }, '\u2502 ',
36
+ e(Text, { color: theme.primary, bold: true }, padRight('KEYBOARD SHORTCUTS', w - 2)),
37
+ '\u2502',
38
+ ),
39
+ e(Text, { color: theme.border }, emp),
40
+ ...SHORTCUTS.map((s, i) => {
41
+ const content = s[0].padEnd(12) + s[1];
42
+ const pad = Math.max(w - content.length - 2, 0);
43
+ return e(Text, { key: `s-${i}`, color: theme.border }, '\u2502 ',
44
+ e(Text, { color: theme.accent, bold: true }, s[0].padEnd(12)),
45
+ e(Text, { color: theme.text }, s[1]),
46
+ ' '.repeat(pad), '\u2502',
47
+ );
48
+ }),
49
+ e(Text, { color: theme.border }, emp),
50
+ e(Text, { color: theme.border }, '\u2502 ',
51
+ e(Text, { color: theme.primary, bold: true }, padRight('TIPS', w - 2)),
52
+ '\u2502',
53
+ ),
54
+ e(Text, { color: theme.border }, emp),
55
+ ...TIPS.map((t, i) => {
56
+ const content = '\u2022 ' + t;
57
+ const pad = Math.max(w - content.length - 2, 0);
58
+ return e(Text, { key: `t-${i}`, color: theme.border }, '\u2502 ',
59
+ e(Text, { color: theme.dim }, content),
60
+ ' '.repeat(pad), '\u2502',
61
+ );
62
+ }),
63
+ e(Text, { color: theme.border }, emp),
64
+ e(Text, { color: theme.border }, bot),
65
+ );
66
+ }
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Text } from 'ink';
2
+ import { Box, Text } from 'ink';
3
3
  import { PORTRAIT_ANSI, PORTRAIT_BRAILLE } from '../data/portrait.js';
4
4
  import { ThemeContext, supportsTrueColor } from '../theme.js';
5
5
 
@@ -8,10 +8,19 @@ const e = React.createElement;
8
8
  export default function Portrait() {
9
9
  const { theme } = React.useContext(ThemeContext);
10
10
 
11
+ // True-color ANSI portrait (no animations — avoids flickering)
11
12
  if (supportsTrueColor && PORTRAIT_ANSI.length > 0) {
12
- return e(Text, null, PORTRAIT_ANSI.join('\n'));
13
+ return e(Box, { flexDirection: 'column' },
14
+ ...PORTRAIT_ANSI.map((line, i) =>
15
+ e(Text, { key: i }, line),
16
+ ),
17
+ );
13
18
  }
14
19
 
15
- // Use theme primary color for braille portrait
16
- return e(Text, { color: theme.dim }, PORTRAIT_BRAILLE.join('\n'));
20
+ // Braille fallback
21
+ return e(Box, { flexDirection: 'column' },
22
+ ...PORTRAIT_BRAILLE.map((line, i) =>
23
+ e(Text, { key: i, color: theme.primary }, line),
24
+ ),
25
+ );
17
26
  }
@@ -5,12 +5,13 @@ import { ThemeContext } from '../theme.js';
5
5
  const e = React.createElement;
6
6
 
7
7
  const KEYBINDS = {
8
- normal: '<- -> tabs | up/dn scroll | enter expand | t theme | q quit',
9
- expanded: 'esc back | up/dn scroll',
10
- chat: 'type to chat | enter send | esc back to tabs',
8
+ normal: '\u2190 \u2192 tabs \u2502 \u2191\u2193 scroll \u2502 enter expand \u2502 ? help \u2502 q quit',
9
+ expanded: 'esc back \u2502 \u2191\u2193 scroll',
10
+ chat: 'type to chat \u2502 enter send \u2502 esc back',
11
+ help: '? or esc to close help',
11
12
  };
12
13
 
13
- export default function StatusBar({ mode, themeName }) {
14
+ export default function StatusBar({ mode, exploredCount = 0, totalTabs = 7 }) {
14
15
  const { theme } = React.useContext(ThemeContext);
15
16
  const [clock, setClock] = React.useState('');
16
17
 
@@ -22,15 +23,16 @@ export default function StatusBar({ mode, themeName }) {
22
23
  setClock(`${hh}:${mm}`);
23
24
  };
24
25
  update();
25
- const id = setInterval(update, 30000);
26
+ const id = setInterval(update, 60000);
26
27
  return () => clearInterval(id);
27
28
  }, []);
28
29
 
29
30
  const keys = KEYBINDS[mode] || KEYBINDS.normal;
30
- const right = `${themeName || 'cyberpunk'} | ${clock}`;
31
+ const explored = '\u2588'.repeat(exploredCount) + '\u2591'.repeat(totalTabs - exploredCount);
32
+ const right = `${explored} ${exploredCount}/${totalTabs} \u2502 ${clock}`;
31
33
 
32
34
  return e(Box, { width: '100%', justifyContent: 'space-between', marginTop: 1 },
33
35
  e(Text, { dimColor: true, color: theme.dim }, keys),
34
- e(Text, { dimColor: true, color: theme.dim }, right)
36
+ e(Text, { dimColor: true, color: theme.dim }, right),
35
37
  );
36
38
  }
@@ -8,22 +8,23 @@ export default function TabBar({ tabs, activeIndex }) {
8
8
  const { theme } = React.useContext(ThemeContext);
9
9
 
10
10
  return e(Box, { flexDirection: 'row', marginTop: 1 },
11
- tabs.map((label, i) => {
11
+ ...tabs.map((label, i) => {
12
12
  const isActive = i === activeIndex;
13
+ const num = String(i + 1);
13
14
 
14
15
  if (isActive) {
15
16
  return e(Text, {
16
17
  key: label,
17
18
  bold: true,
18
- backgroundColor: theme.primary,
19
- color: 'black',
20
- }, `[ ${label} ]`);
19
+ backgroundColor: theme.accent,
20
+ color: theme.bg,
21
+ }, `[${num}:${label}]`);
21
22
  }
22
23
 
23
- return e(Text, {
24
- key: label,
25
- dimColor: true,
26
- }, ` ${label} `);
27
- })
24
+ return e(Box, { key: label },
25
+ e(Text, { dimColor: true, color: theme.dim }, ` ${num}:`),
26
+ e(Text, { dimColor: true }, `${label} `),
27
+ );
28
+ }),
28
29
  );
29
30
  }
@@ -256,7 +256,7 @@ export const CONTACT = [
256
256
  { label: 'X', value: 'x.com/martinbonan', url: 'https://x.com/martinbonan' },
257
257
  ];
258
258
 
259
- export const TABS = ['About', 'Projects', 'Stack', 'History', 'Education', 'Contact'];
259
+ export const TABS = ['About', 'Projects', 'Stack', 'History', 'Education', 'Contact', 'Ask AI'];
260
260
 
261
261
  export const EASTER_EGGS = {
262
262
  hack: true,
@@ -1,20 +1,32 @@
1
- // True-color ANSI portrait data placeholder
2
- // Replace with generated ANSI half-block portrait lines
3
- // Each line uses \x1b[38;2;R;G;Bm (fg) and \x1b[48;2;R;G;Bm (bg) with ▀ chars
4
- export const PORTRAIT_ANSI = [];
1
+ // ANSI half-block portrait — converted from ASCII art
2
+ // Transparent background, ~20 wide x 12 half-block rows
3
+ export const PORTRAIT_ANSI = [
4
+ "\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m",
5
+ "\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[38;2;78;55;36m\u001b[49m▄\u001b[38;2;42;28;18m\u001b[49m▄\u001b[38;2;78;55;36m\u001b[48;2;42;28;18m▀\u001b[38;2;58;40;26m\u001b[48;2;42;28;18m▀\u001b[38;2;58;40;26m\u001b[48;2;42;28;18m▀\u001b[38;2;78;55;36m\u001b[48;2;42;28;18m▀\u001b[38;2;42;28;18m\u001b[49m▄\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m",
6
+ "\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[38;2;95;68;45m\u001b[49m▄\u001b[38;2;95;68;45m\u001b[48;2;42;28;18m▀\u001b[38;2;42;28;18m\u001b[48;2;42;28;18m▀\u001b[38;2;42;28;18m\u001b[48;2;42;28;18m▀\u001b[38;2;42;28;18m\u001b[48;2;42;28;18m▀\u001b[38;2;42;28;18m\u001b[48;2;42;28;18m▀\u001b[38;2;42;28;18m\u001b[48;2;42;28;18m▀\u001b[38;2;42;28;18m\u001b[48;2;42;28;18m▀\u001b[38;2;42;28;18m\u001b[48;2;42;28;18m▀\u001b[38;2;42;28;18m\u001b[48;2;42;28;18m▀\u001b[38;2;95;68;45m\u001b[48;2;42;28;18m▀\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m",
7
+ "\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[38;2;42;28;18m\u001b[48;2;58;40;26m▀\u001b[38;2;42;28;18m\u001b[48;2;42;28;18m▀\u001b[38;2;42;28;18m\u001b[48;2;42;28;18m▀\u001b[38;2;42;28;18m\u001b[48;2;58;40;26m▀\u001b[38;2;42;28;18m\u001b[48;2;58;40;26m▀\u001b[38;2;42;28;18m\u001b[48;2;42;28;18m▀\u001b[38;2;42;28;18m\u001b[48;2;42;28;18m▀\u001b[38;2;42;28;18m\u001b[48;2;42;28;18m▀\u001b[38;2;42;28;18m\u001b[48;2;42;28;18m▀\u001b[38;2;42;28;18m\u001b[48;2;42;28;18m▀\u001b[38;2;42;28;18m\u001b[48;2;78;55;36m▀\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m",
8
+ "\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[38;2;42;28;18m\u001b[48;2;42;28;18m▀\u001b[38;2;215;180;150m\u001b[48;2;230;195;165m▀\u001b[38;2;230;195;165m\u001b[48;2;230;195;165m▀\u001b[38;2;240;210;180m\u001b[48;2;240;210;180m▀\u001b[38;2;240;210;180m\u001b[48;2;230;195;165m▀\u001b[38;2;230;195;165m\u001b[48;2;55;38;25m▀\u001b[38;2;230;195;165m\u001b[48;2;215;180;150m▀\u001b[38;2;215;180;150m\u001b[48;2;215;180;150m▀\u001b[38;2;55;38;25m\u001b[48;2;55;38;25m▀\u001b[38;2;240;210;180m\u001b[48;2;240;210;180m▀\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m",
9
+ "\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[38;2;185;150;122m\u001b[48;2;228;195;165m▀\u001b[38;2;185;150;122m\u001b[48;2;210;178;150m▀\u001b[38;2;228;195;165m\u001b[48;2;228;195;165m▀\u001b[38;2;228;195;165m\u001b[48;2;228;195;165m▀\u001b[38;2;185;150;122m\u001b[48;2;228;195;165m▀\u001b[38;2;185;150;122m\u001b[48;2;228;195;165m▀\u001b[38;2;210;178;150m\u001b[48;2;228;195;165m▀\u001b[38;2;185;150;122m\u001b[48;2;228;195;165m▀\u001b[38;2;228;195;165m\u001b[48;2;238;208;178m▀\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m",
10
+ "\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[38;2;228;195;165m\u001b[48;2;228;195;165m▀\u001b[38;2;228;195;165m\u001b[48;2;238;208;178m▀\u001b[38;2;228;195;165m\u001b[48;2;228;195;165m▀\u001b[38;2;228;195;165m\u001b[48;2;228;195;165m▀\u001b[38;2;228;195;165m\u001b[48;2;228;195;165m▀\u001b[38;2;238;208;178m\u001b[48;2;228;195;165m▀\u001b[38;2;228;195;165m\u001b[48;2;228;195;165m▀\u001b[38;2;210;178;150m\u001b[48;2;228;195;165m▀\u001b[38;2;238;208;178m\u001b[49m▀\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m",
11
+ "\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[38;2;228;195;165m\u001b[48;2;228;195;165m▀\u001b[38;2;210;178;150m\u001b[48;2;228;195;165m▀\u001b[38;2;185;150;122m\u001b[48;2;228;195;165m▀\u001b[38;2;185;150;122m\u001b[48;2;228;195;165m▀\u001b[38;2;228;195;165m\u001b[48;2;228;195;165m▀\u001b[38;2;228;195;165m\u001b[48;2;210;178;150m▀\u001b[38;2;238;208;178m\u001b[48;2;238;208;178m▀\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m",
12
+ "\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[38;2;228;195;165m\u001b[48;2;228;195;165m▀\u001b[38;2;228;195;165m\u001b[48;2;228;195;165m▀\u001b[38;2;228;195;165m\u001b[48;2;215;180;152m▀\u001b[38;2;228;195;165m\u001b[48;2;215;180;152m▀\u001b[38;2;210;178;150m\u001b[48;2;215;180;152m▀\u001b[38;2;238;208;178m\u001b[48;2;215;180;152m▀\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m",
13
+ "\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[38;2;18;18;22m\u001b[49m▄\u001b[38;2;228;195;165m\u001b[48;2;48;44;50m▀\u001b[38;2;215;180;152m\u001b[48;2;48;44;50m▀\u001b[38;2;215;180;152m\u001b[48;2;48;44;50m▀\u001b[38;2;215;180;152m\u001b[48;2;48;44;50m▀\u001b[38;2;228;195;165m\u001b[48;2;48;44;50m▀\u001b[38;2;215;180;152m\u001b[48;2;48;44;50m▀\u001b[38;2;228;195;165m\u001b[48;2;32;30;35m▀\u001b[38;2;215;180;152m\u001b[48;2;18;18;22m▀\u001b[38;2;228;195;165m\u001b[48;2;18;18;22m▀\u001b[38;2;32;30;35m\u001b[49m▄\u001b[0m \u001b[0m \u001b[0m",
14
+ "\u001b[0m \u001b[0m \u001b[0m \u001b[38;2;48;44;50m\u001b[49m▄\u001b[38;2;18;18;22m\u001b[49m▄\u001b[38;2;18;18;22m\u001b[49m▄\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;32;30;35m\u001b[48;2;18;18;22m▀\u001b[38;2;48;44;50m\u001b[48;2;18;18;22m▀\u001b[38;2;48;44;50m\u001b[48;2;18;18;22m▀\u001b[38;2;48;44;50m\u001b[48;2;18;18;22m▀\u001b[38;2;48;44;50m\u001b[48;2;18;18;22m▀\u001b[38;2;32;30;35m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[49m▄\u001b[0m",
15
+ "\u001b[0m \u001b[0m \u001b[38;2;48;44;50m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[38;2;18;18;22m\u001b[48;2;18;18;22m▀\u001b[0m",
16
+ ];
5
17
 
6
- // Braille fallback portrait
18
+ // Braille fallback (not used when true-color is available)
7
19
  export const PORTRAIT_BRAILLE = [
8
- '⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀',
9
- '⠀⠀⠀⠀⠀⠀⠀⢀⣀⣤⣤⣤⣶⣶⣶⣾⣿⣿⣿⣿⣶⣶⣦⣄⠀',
10
- '⠀⠀⠀⠀⢀⣤⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶',
11
- '⠀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠇⠀',
12
- '⠀⠀⠀⠀⠈⠙⠻⣿⣿⠟⠁⠀⠈⠉⠉⢉⣉⣉⣁⡘⢿⣿⡿⠁⠀',
13
- '⠀⠀⠀⠀⠀⠀⠀⠀⢻⠒⢛⡫⢭⣿⣧⠂⠾⠿⠶⠀⠀⠻⡟⡁⠀',
14
- '⠀⠀⠀⠀⠀⠀⠀⠀⠁⠀⠀⠀⠀⠀⠀⠀⣀⡀⠀⠀⠀⠀⠀⠀⠀',
15
- '⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⡀⠠⣤⠤⠄⠀⠀⠀⠀',
16
- '⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠤⠄⠀⠀⠀',
17
- '⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠈⠉⠀⠀⠀⣀⡀⠀',
18
- '⠀⠀⠀⠀⠀⠀⠀⣀⣀⣤⣤⣶⣂⠀⠀⠀⠀⠀⣀⣤⣴⣾⣿⣿⣷',
19
- '⠀⠀⠀⢀⣤⣶⣾⣿⣿⣿⣿⣿⣿⣶⣦⣤⣶⣿⣿⣿⣿⣿⣿⣿⣿',
20
+ ' ',
21
+ ' ⠀⠏⠟⠿⡿⠟⠇ ',
22
+ ' ⠏⣿⣿⣿⣿⣿⣿⣿⣿⠇ ',
23
+ ' ⠟⣿⡿⡿⠿⡿⡿⡿⡿⣿⠟ ',
24
+ ' ⠿⠇⠁⠀⠀⠇⠏⠏⣿⠀ ',
25
+ ' ⠇⠏⠃⠁⠇⠃⠁⠇⠁ ',
26
+ ' ⠁⠀⠁⠁⠁⠀⠁⠃ ',
27
+ ' ⠁⠃⠇⠇⠁⠃⠀ ',
28
+ ' ⠁⠁⠁⠃⠇⠁ ',
29
+ ' ⠁⠁⠁⠁⠁⠁⠁⠃⠟⠏⠁ ',
30
+ ' ⠁⠇⡿⣿⠟⠇⠃⠇⠏⠿⣿⣿⣿⣿⡿⠏',
31
+ ' ⠏⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿',
20
32
  ];
@@ -1,11 +1,24 @@
1
- export const BOOT_LINES = [
2
- { prefix: 'INIT', text: 'Loading martin-cli v3.0.0...' },
3
- { prefix: 'LOAD', text: 'Initializing neural interface', dots: 10 },
4
- { prefix: 'LOAD', text: 'Loading personality matrix', dots: 10 },
5
- { prefix: 'LOAD', text: 'Connecting to AI twin', dots: 10 },
6
- { prefix: 'LOAD', text: 'Rendering portrait', dots: 10 },
1
+ export const SSH_LINES = [
2
+ { text: '$ ssh martin@portfolio.dev -p 2026', delay: 600, style: 'cmd' },
3
+ { text: 'Connecting to portfolio.dev...', delay: 500 },
4
+ { text: '', delay: 150 },
5
+ { text: 'ED25519 key fingerprint: SHA256:xK3d...mB7qZ9', delay: 350 },
6
+ { text: 'Verifying identity...', delay: 400 },
7
+ { text: 'Authenticated ✓', delay: 250, style: 'ok' },
8
+ ];
9
+
10
+ export const BANNER = [
11
+ '╭─────────────────────────────────────────╮',
12
+ '│ Welcome to martin-cli v4.0.0 │',
13
+ '│ Last login: from curious.visitor │',
14
+ '│ "Sell before you build." │',
15
+ '╰─────────────────────────────────────────╯',
16
+ ];
17
+
18
+ export const INIT_LINES = [
19
+ { prefix: 'SYS ', text: 'Loading personality matrix', dots: 10 },
20
+ { prefix: 'NET ', text: 'Connecting to AI twin', dots: 12 },
21
+ { prefix: 'LOAD', text: 'Mounting component tree', dots: 6 },
7
22
  { prefix: 'LOAD', text: 'Calibrating vibes', dots: 10 },
8
23
  { prefix: 'SYS ', text: 'All systems operational.' },
9
24
  ];
10
-
11
- export const BOOT_FINAL = '> Entering interactive mode...';
@@ -1,61 +1,78 @@
1
1
  import React, { useState, useEffect, useRef } from 'react';
2
2
  import { Box, Text } from 'ink';
3
- import { BOOT_LINES, BOOT_FINAL } from '../effects/boot.js';
4
- import { ThemeContext } from '../theme.js';
3
+ import { SSH_LINES, BANNER, INIT_LINES } from '../effects/boot.js';
5
4
 
6
5
  const e = React.createElement;
7
6
 
7
+ // Sunset theme colors for boot
8
+ const primary = '#FF6B35';
9
+ const secondary = '#F7C59F';
10
+ const accent = '#FFC857';
11
+
8
12
  export default function BootSequence({ onComplete }) {
9
- const { theme } = React.useContext(ThemeContext);
10
- const [lines, setLines] = useState([]);
11
- const [showFinal, setShowFinal] = useState(false);
13
+ const [sshLines, setSshLines] = useState([]);
14
+ const [showBanner, setShowBanner] = useState(false);
15
+ const [initLines, setInitLines] = useState([]);
16
+ const [progress, setProgress] = useState(-1);
17
+ const [showPrompt, setShowPrompt] = useState(false);
12
18
  const started = useRef(false);
13
19
 
14
- const bootColor = theme.name === 'retro' ? '#33FF33' : '#33AA66';
20
+ const barWidth = 36;
15
21
 
16
22
  useEffect(() => {
17
23
  if (started.current) return;
18
24
  started.current = true;
19
-
20
25
  let cancelled = false;
21
- const output = [];
22
26
 
23
27
  async function run() {
24
- for (const item of BOOT_LINES) {
28
+ for (const line of SSH_LINES) {
25
29
  if (cancelled) return;
30
+ await delay(line.delay);
31
+ setSshLines(prev => [...prev, line]);
32
+ }
26
33
 
34
+ if (cancelled) return;
35
+ await delay(350);
36
+ setShowBanner(true);
37
+ await delay(700);
38
+
39
+ const output = [];
40
+ for (const item of INIT_LINES) {
41
+ if (cancelled) return;
27
42
  if (!item.dots) {
28
- // Simple line show immediately
29
- output.push({ prefix: item.prefix, text: item.text, dotStr: '', ok: false, simple: true });
30
- setLines([...output]);
31
- await delay(200);
43
+ output.push({ ...item, dotStr: '', ok: false, simple: true });
44
+ setInitLines([...output]);
45
+ await delay(180);
32
46
  continue;
33
47
  }
34
-
35
- // Line with dots animation
36
- output.push({ prefix: item.prefix, text: item.text, dotStr: '', ok: false, simple: false });
37
- setLines([...output]);
38
-
39
- // Animate dots
48
+ output.push({ ...item, dotStr: '', ok: false, simple: false });
49
+ setInitLines([...output]);
40
50
  for (let d = 1; d <= item.dots; d++) {
41
51
  if (cancelled) return;
42
- await delay(40);
43
- output[output.length - 1] = { prefix: item.prefix, text: item.text, dotStr: '.'.repeat(d), ok: false, simple: false };
44
- setLines([...output]);
52
+ await delay(28);
53
+ output[output.length - 1] = { ...item, dotStr: '.'.repeat(d), ok: false, simple: false };
54
+ setInitLines([...output]);
45
55
  }
46
-
47
- // Show OK
48
- await delay(80);
56
+ await delay(50);
49
57
  output[output.length - 1] = { ...output[output.length - 1], ok: true };
50
- setLines([...output]);
51
- await delay(80);
58
+ setInitLines([...output]);
59
+ await delay(50);
52
60
  }
53
61
 
54
- // Final line
55
62
  if (!cancelled) {
56
- await delay(300);
57
- setShowFinal(true);
58
- await delay(800);
63
+ setProgress(0);
64
+ for (let p = 0; p <= 100; p += 4) {
65
+ if (cancelled) return;
66
+ await delay(14);
67
+ setProgress(Math.min(p, 100));
68
+ }
69
+ setProgress(100);
70
+ await delay(200);
71
+ }
72
+
73
+ if (!cancelled) {
74
+ setShowPrompt(true);
75
+ await delay(500);
59
76
  if (!cancelled && onComplete) onComplete();
60
77
  }
61
78
  }
@@ -64,17 +81,45 @@ export default function BootSequence({ onComplete }) {
64
81
  return () => { cancelled = true; };
65
82
  }, [onComplete]);
66
83
 
84
+ const filled = progress >= 0 ? Math.round((Math.min(progress, 100) / 100) * barWidth) : 0;
85
+ const empty = barWidth - filled;
86
+
67
87
  return e(Box, { flexDirection: 'column', paddingLeft: 2, paddingTop: 1 },
68
- ...lines.map((line, i) => {
69
- const prefix = `[${line.prefix}]`;
70
- const dotsDisplay = line.simple ? '' : line.dotStr + (line.ok ? '' : '.'.repeat(Math.max(0, 10 - line.dotStr.length)));
71
- const okDisplay = line.ok ? ' OK' : '';
88
+ ...sshLines.map((line, i) =>
89
+ e(Text, {
90
+ key: `ssh-${i}`,
91
+ color: line.style === 'ok' ? accent : line.style === 'cmd' ? primary : secondary,
92
+ bold: line.style === 'cmd' || line.style === 'ok',
93
+ dimColor: !line.style,
94
+ }, line.text)
95
+ ),
72
96
 
73
- return e(Text, { key: i, color: bootColor, dimColor: !line.ok && !line.simple },
74
- prefix + ' ' + line.text + dotsDisplay + okDisplay
97
+ showBanner ? e(Box, { key: 'banner', flexDirection: 'column', marginTop: 1, marginBottom: 1 },
98
+ ...BANNER.map((line, i) =>
99
+ e(Text, { key: `b-${i}`, color: primary }, line)
100
+ ),
101
+ ) : null,
102
+
103
+ ...initLines.map((line, i) => {
104
+ const prefix = `[${line.prefix}]`;
105
+ const dots = line.simple ? '' : line.dotStr + (line.ok ? '' : '.'.repeat(Math.max(0, 10 - line.dotStr.length)));
106
+ const ok = line.ok ? ' OK' : '';
107
+ return e(Text, { key: `init-${i}`, color: line.ok || line.simple ? primary : secondary, dimColor: !line.ok && !line.simple },
108
+ prefix + ' ' + line.text + dots + ok
75
109
  );
76
110
  }),
77
- showFinal ? e(Text, { key: 'final', color: bootColor, bold: true }, '\n' + BOOT_FINAL) : null,
111
+
112
+ progress >= 0 ? e(Box, { key: 'progress', marginTop: 1 },
113
+ e(Text, { color: primary }, '['),
114
+ e(Text, { color: primary }, '\u2588'.repeat(filled)),
115
+ e(Text, { color: secondary, dimColor: true }, '\u2591'.repeat(empty)),
116
+ e(Text, { color: primary }, '] '),
117
+ e(Text, { color: accent, bold: true }, `${Math.min(progress, 100)}%`),
118
+ ) : null,
119
+
120
+ showPrompt ? e(Text, { key: 'prompt', color: accent, bold: true },
121
+ '\nmartin@portfolio:~$ entering interactive mode...'
122
+ ) : null,
78
123
  );
79
124
  }
80
125