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 +15 -1
- package/package.json +1 -1
- package/src/App.js +3 -8
- package/src/components/ContentPanel.js +24 -2
- package/src/components/Header.js +17 -13
- package/src/components/HelpOverlay.js +66 -0
- package/src/components/Portrait.js +13 -4
- package/src/components/StatusBar.js +9 -7
- package/src/components/TabBar.js +10 -9
- package/src/data/content.js +1 -1
- package/src/data/portrait.js +29 -17
- package/src/effects/boot.js +21 -8
- package/src/screens/BootSequence.js +83 -38
- package/src/screens/Dashboard.js +65 -50
- package/src/tabs/About.js +51 -20
- package/src/tabs/AskAi.js +141 -33
- package/src/tabs/Contact.js +38 -16
- package/src/tabs/Education.js +33 -12
- package/src/tabs/History.js +15 -3
- package/src/tabs/Projects.js +26 -19
- package/src/tabs/Stack.js +23 -9
- package/src/theme.js +3 -54
package/bin/index.js
CHANGED
|
@@ -17,6 +17,20 @@ if (cols < 60 || rows < 16) {
|
|
|
17
17
|
process.exit(1);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
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
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/src/components/Header.js
CHANGED
|
@@ -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 >=
|
|
17
|
+
const showPortrait = cols >= 75;
|
|
19
18
|
const grad = makeGradient(theme);
|
|
20
19
|
|
|
21
|
-
let
|
|
20
|
+
let figletText;
|
|
22
21
|
try {
|
|
23
|
-
line1 = figlet.textSync('
|
|
24
|
-
line2 = figlet.textSync('
|
|
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
|
-
|
|
27
|
-
|
|
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(
|
|
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
|
-
//
|
|
16
|
-
return e(
|
|
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: '
|
|
9
|
-
expanded: 'esc back
|
|
10
|
-
chat: 'type to chat
|
|
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,
|
|
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,
|
|
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
|
|
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
|
}
|
package/src/components/TabBar.js
CHANGED
|
@@ -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.
|
|
19
|
-
color:
|
|
20
|
-
}, `[
|
|
19
|
+
backgroundColor: theme.accent,
|
|
20
|
+
color: theme.bg,
|
|
21
|
+
}, `[${num}:${label}]`);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
return e(
|
|
24
|
-
|
|
25
|
-
dimColor: true,
|
|
26
|
-
|
|
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
|
}
|
package/src/data/content.js
CHANGED
|
@@ -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,
|
package/src/data/portrait.js
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
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
|
];
|
package/src/effects/boot.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
|
-
export const
|
|
2
|
-
{
|
|
3
|
-
{
|
|
4
|
-
{
|
|
5
|
-
{
|
|
6
|
-
{
|
|
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 {
|
|
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
|
|
10
|
-
const [
|
|
11
|
-
const [
|
|
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
|
|
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
|
|
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
|
-
|
|
29
|
-
output
|
|
30
|
-
|
|
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
|
-
|
|
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(
|
|
43
|
-
output[output.length - 1] = {
|
|
44
|
-
|
|
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
|
-
|
|
51
|
-
await delay(
|
|
58
|
+
setInitLines([...output]);
|
|
59
|
+
await delay(50);
|
|
52
60
|
}
|
|
53
61
|
|
|
54
|
-
// Final line
|
|
55
62
|
if (!cancelled) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
...
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
|