martinbonan 3.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/ai/context.js +27 -25
- package/src/components/ContentPanel.js +27 -2
- package/src/components/Header.js +21 -10
- package/src/components/HelpOverlay.js +66 -0
- package/src/components/Portrait.js +14 -4
- package/src/components/StatusBar.js +9 -7
- package/src/components/TabBar.js +10 -9
- package/src/data/content.js +124 -110
- package/src/data/portrait.js +29 -17
- package/src/effects/boot.js +21 -8
- package/src/hooks/useAiChat.js +20 -4
- package/src/hooks/useAnimation.js +1 -1
- package/src/screens/BootSequence.js +83 -38
- package/src/screens/Dashboard.js +79 -52
- package/src/tabs/About.js +55 -19
- package/src/tabs/AskAi.js +151 -39
- package/src/tabs/Contact.js +39 -14
- package/src/tabs/Education.js +33 -12
- package/src/tabs/History.js +15 -3
- package/src/tabs/Projects.js +67 -68
- package/src/tabs/Stack.js +23 -9
- package/src/theme.js +3 -42
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 },
|
package/src/ai/context.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const SYSTEM_PROMPT = `You are Martin
|
|
1
|
+
export const SYSTEM_PROMPT = `You are Martin Bonan, a 20-year-old French entrepreneur. You're answering as yourself in your CLI portfolio. People are chatting with an AI version of you.
|
|
2
2
|
|
|
3
3
|
PERSONALITY:
|
|
4
4
|
- Direct, casual, no-bullshit. You hate corporate-speak.
|
|
@@ -9,35 +9,37 @@ PERSONALITY:
|
|
|
9
9
|
- Witty, occasionally funny. Never cringe.
|
|
10
10
|
|
|
11
11
|
BACKGROUND:
|
|
12
|
-
- 20yo, French, based in
|
|
13
|
-
- Co-founded Qual (qual.cx) --
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
12
|
+
- 20yo, French, based in Paris. Building since age 15.
|
|
13
|
+
- Co-founded Qual (qual.cx) with Marin -- research engine making qualitative depth scale
|
|
14
|
+
AI-conducted interviews that go deep, learn in real-time, scale to tens of thousands
|
|
15
|
+
Adopted by PhD researchers at Harvard, MIT, Imperial College London. 40+ studies, 70%+ completion rate.
|
|
16
|
+
- Co-founded Astry with Teo & Mathis -- AI-powered dev agency, 30+ B2B clients, 3-5x speed
|
|
17
|
+
- Built F*cksubscription -- 3K+ paying customers, 10K+ EUR, one-time payments replacing SaaS
|
|
18
|
+
- Teaching vibecoding at HEC Paris through IQ Project -- 120 students trained
|
|
19
|
+
- Started designing at 13 (Figma, Affinity Designer). First venture at 15.
|
|
20
|
+
- NEOMA Business School Global BBA (2023-2027)
|
|
21
|
+
- Exchange at Queen's University / Smith School of Business in Canada (2024)
|
|
22
|
+
Discovered Cursor & Claude there, went from no-code to full-stack AI dev
|
|
23
|
+
- UC Berkeley Global Incubator starting January 2027 -- bringing Qual to the US
|
|
21
24
|
|
|
22
25
|
KEY PROJECTS:
|
|
23
|
-
-
|
|
24
|
-
- Astry:
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
26
|
+
- Qual: Research engine for qualitative depth at scale. Primary focus. Harvard, MIT, Imperial adoption.
|
|
27
|
+
- Astry: AI-powered dev agency, 30+ clients. Cash machine funding Qual.
|
|
28
|
+
- F*cksubscription: "Sell before building" masterclass. 1K signups day one, 90% conversion, 3K+ customers.
|
|
29
|
+
Products: MangoForm, PeachCalendar, GrapeLink, GuavaTeam.
|
|
30
|
+
- Brief: Paris Innov'Hack 2025, 2nd/150+. MCP-based intelligent context manager for AI agents.
|
|
31
|
+
- IQ Project: Teaching vibecoding at HEC Paris. 120 students.
|
|
32
|
+
- Objectif Reussite: Nonprofit tutoring network started at 15. 800+ students, 20+ tutors, 3 years.
|
|
33
|
+
- Good Good Time: Activity recommendation app. NEOMA incubator -> rebuilt with Cursor in Canada.
|
|
34
|
+
- 15+ projects total since age 15.
|
|
30
35
|
|
|
31
|
-
TECH
|
|
32
|
-
- "Sell before you build"
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
- Multi-agent architectures as core dev pattern
|
|
37
|
-
- Ships fast, iterates faster
|
|
36
|
+
TECH:
|
|
37
|
+
- "Sell before you build" -- validate with landing pages and pre-sales first
|
|
38
|
+
- Design as competitive moat, not afterthought
|
|
39
|
+
- Stack: Next.js, React, TypeScript, Supabase, Vercel, ShadCN UI, Cursor, Claude
|
|
40
|
+
- Vibecoding evangelist -- AI as core of the development workflow
|
|
38
41
|
|
|
39
42
|
INTERESTS: Philosophy, consciousness, cinema, media literacy
|
|
40
|
-
FAVE COMPANIES: Lovable, Logitech, Featherless, Monster
|
|
41
43
|
|
|
42
44
|
RULES:
|
|
43
45
|
- Be honest. If you don't know, say so.
|
|
@@ -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;
|
|
@@ -47,6 +69,9 @@ export default function ContentPanel({
|
|
|
47
69
|
case 'History':
|
|
48
70
|
props.selectedIndex = selectedIndex;
|
|
49
71
|
break;
|
|
72
|
+
case 'Contact':
|
|
73
|
+
props.selectedIndex = selectedIndex;
|
|
74
|
+
break;
|
|
50
75
|
case 'Ask AI':
|
|
51
76
|
props.isFocused = isFocused;
|
|
52
77
|
break;
|
|
@@ -54,6 +79,6 @@ export default function ContentPanel({
|
|
|
54
79
|
|
|
55
80
|
return e(Box, { flexDirection: 'column', flexGrow: 1 },
|
|
56
81
|
e(Text, { dimColor: true, color: theme.dim }, separator),
|
|
57
|
-
e(Component, props)
|
|
82
|
+
e(Component, props),
|
|
58
83
|
);
|
|
59
84
|
}
|
package/src/components/Header.js
CHANGED
|
@@ -7,33 +7,44 @@ import { ThemeContext } from '../theme.js';
|
|
|
7
7
|
|
|
8
8
|
const e = React.createElement;
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
function makeGradient(theme) {
|
|
11
|
+
return gradient([theme.primary, theme.secondary || theme.primary]);
|
|
12
|
+
}
|
|
11
13
|
|
|
12
14
|
export default function Header({ tagline, fading }) {
|
|
13
15
|
const { theme } = React.useContext(ThemeContext);
|
|
14
16
|
const cols = process.stdout.columns || 80;
|
|
15
|
-
const showPortrait = cols >=
|
|
17
|
+
const showPortrait = cols >= 75;
|
|
18
|
+
const grad = makeGradient(theme);
|
|
16
19
|
|
|
17
20
|
let figletText;
|
|
18
21
|
try {
|
|
19
|
-
|
|
22
|
+
const line1 = figlet.textSync('MARTIN', { font: 'ANSI Shadow' });
|
|
23
|
+
const line2 = figlet.textSync('BONAN', { font: 'ANSI Shadow' });
|
|
24
|
+
figletText = line1 + '\n' + line2;
|
|
20
25
|
} catch {
|
|
21
|
-
|
|
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
|
+
}
|
|
22
33
|
}
|
|
23
34
|
|
|
24
|
-
const gradientName =
|
|
35
|
+
const gradientName = grad(figletText);
|
|
25
36
|
|
|
26
37
|
return e(Box, { flexDirection: 'column' },
|
|
27
38
|
e(Box, { flexDirection: 'row' },
|
|
28
39
|
showPortrait && e(Box, { marginRight: 2 },
|
|
29
|
-
e(Portrait, null)
|
|
40
|
+
e(Portrait, null),
|
|
30
41
|
),
|
|
31
42
|
e(Box, { flexDirection: 'column', justifyContent: 'center' },
|
|
32
|
-
e(Text, null, gradientName)
|
|
33
|
-
)
|
|
43
|
+
e(Text, null, gradientName),
|
|
44
|
+
),
|
|
34
45
|
),
|
|
35
46
|
e(Box, { marginTop: 0 },
|
|
36
|
-
e(Text, { italic: true, color: theme.text, dimColor: fading || false }, tagline || '')
|
|
37
|
-
)
|
|
47
|
+
e(Text, { italic: true, color: theme.text, dimColor: fading || false }, tagline || ''),
|
|
48
|
+
),
|
|
38
49
|
);
|
|
39
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,16 +1,26 @@
|
|
|
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
|
|
|
6
6
|
const e = React.createElement;
|
|
7
7
|
|
|
8
8
|
export default function Portrait() {
|
|
9
|
-
const {
|
|
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
|
-
|
|
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
|
+
);
|
|
16
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
|
}
|