shivenverma 1.0.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/LICENSE +21 -0
- package/README.md +119 -0
- package/bin/cli.js +6 -0
- package/dist/cli.js +1103 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shiven Verma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# shivenverma
|
|
2
|
+
|
|
3
|
+
An interactive terminal portfolio for **Shiven Verma** — a small, keyboard-driven
|
|
4
|
+
TUI that runs in your terminal. Built with Node.js, React, and [Ink](https://github.com/vadimdemedes/ink).
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
_____ __ _______ _________ __ _ ____________ __ ______
|
|
8
|
+
/ ___// / / / _/ | / / ____/ | / / | | / / ____/ __ \/ |/ / |
|
|
9
|
+
\__ \/ /_/ // / | | / / __/ / |/ / | | / / __/ / /_/ / /|_/ / /| |
|
|
10
|
+
___/ / __ // / | |/ / /___/ /| / | |/ / /___/ _, _/ / / / ___ |
|
|
11
|
+
/____/_/ /_/___/ |___/_____/_/ |_/ |___/_____/_/ |_/_/ /_/_/ |_|
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Run it
|
|
15
|
+
|
|
16
|
+
Once published to npm, it runs with no install step:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx shivenverma
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Until then (or to run from a clone), see [Development](#development) below.
|
|
23
|
+
|
|
24
|
+
> Requires **Node.js 18+**.
|
|
25
|
+
|
|
26
|
+
## Using it
|
|
27
|
+
|
|
28
|
+
The interface is keyboard-first.
|
|
29
|
+
|
|
30
|
+
| Key | Action |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| `↑` `↓` / `k` `j` | Move the selection |
|
|
33
|
+
| `⏎` Enter | Open the selected item |
|
|
34
|
+
| `Esc` | Go back |
|
|
35
|
+
| `:` | Open the command bar |
|
|
36
|
+
| `q` | Quit |
|
|
37
|
+
|
|
38
|
+
### Command bar
|
|
39
|
+
|
|
40
|
+
Press `:` from anywhere and type a command:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
about skills projects github contact resume home clear help exit
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`help` lists everything. Unknown commands report back instead of failing.
|
|
47
|
+
|
|
48
|
+
### Sections
|
|
49
|
+
|
|
50
|
+
- **About** — a short intro and current areas of focus
|
|
51
|
+
- **Skills** — languages, frameworks, and tools (no made-up proficiency bars)
|
|
52
|
+
- **Projects** — a selectable list; open one for details and links
|
|
53
|
+
- **GitHub** — profile link, opens in your browser
|
|
54
|
+
- **Contact** — configured contact channels
|
|
55
|
+
- **Resume** — opens your resume link when configured
|
|
56
|
+
|
|
57
|
+
Links open in your default browser via the OS handler (`open` / `start` /
|
|
58
|
+
`xdg-open`). If no browser can be launched, the URL is shown so you can copy it —
|
|
59
|
+
the app never crashes on a failed open.
|
|
60
|
+
|
|
61
|
+
## Make it yours
|
|
62
|
+
|
|
63
|
+
All content lives in one file: [`src/data/portfolio.js`](src/data/portfolio.js).
|
|
64
|
+
Edit values there and rebuild — no component code needs to change.
|
|
65
|
+
|
|
66
|
+
Values written as `REPLACE_WITH_*` (or left empty) are treated as **not
|
|
67
|
+
configured**: the UI shows a tasteful fallback instead of a broken link. Search
|
|
68
|
+
the file for `REPLACE_WITH` to find everything still to fill in. At the time of
|
|
69
|
+
writing that includes:
|
|
70
|
+
|
|
71
|
+
- `links.linkedin`, `links.email`, `links.resume`
|
|
72
|
+
- `location` (optional)
|
|
73
|
+
- Each project's `links` (repo / docs / live URLs) and, for the placeholder
|
|
74
|
+
projects, their `description` and `tech`
|
|
75
|
+
|
|
76
|
+
The GitHub username is already set to `shivenverma`.
|
|
77
|
+
|
|
78
|
+
## Development
|
|
79
|
+
|
|
80
|
+
From a clone of the repository:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm install # install dependencies
|
|
84
|
+
npm run dev # build, then launch the portfolio
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Other scripts:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npm run build # bundle src/ -> dist/cli.js with esbuild
|
|
91
|
+
npm start # run the built CLI (bin/cli.js)
|
|
92
|
+
npm test # build, then run the smoke test
|
|
93
|
+
npm run banner # regenerate the ASCII wordmark (src/data/banner.js)
|
|
94
|
+
npm run package # build and create a local .tgz tarball
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### How it's built
|
|
98
|
+
|
|
99
|
+
- **[Ink](https://github.com/vadimdemedes/ink) + React** render the UI to the terminal.
|
|
100
|
+
- **esbuild** bundles `src/` into a single `dist/cli.js`. React and Ink stay as
|
|
101
|
+
runtime dependencies (installed by npm); only the app's own code is bundled.
|
|
102
|
+
- The ASCII wordmark is generated once at build time with
|
|
103
|
+
[figlet](https://github.com/patorjk/figlet.js) and committed as plain data
|
|
104
|
+
([`src/data/banner.js`](src/data/banner.js)), so there's no font loading at runtime.
|
|
105
|
+
- The layout is responsive: the wordmark steps down through smaller fonts (and a
|
|
106
|
+
plain-text fallback) as the terminal narrows, and dense sections compact on
|
|
107
|
+
short terminals.
|
|
108
|
+
|
|
109
|
+
### Flags
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
shivenverma --help show help
|
|
113
|
+
shivenverma --version print the version
|
|
114
|
+
shivenverma --no-startup skip the boot animation
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
[MIT](LICENSE) © Shiven Verma
|
package/bin/cli.js
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1103 @@
|
|
|
1
|
+
// src/index.jsx
|
|
2
|
+
import React21 from "react";
|
|
3
|
+
import { render } from "ink";
|
|
4
|
+
|
|
5
|
+
// src/App.jsx
|
|
6
|
+
import React20, { useState as useState5 } from "react";
|
|
7
|
+
import { Box as Box18, Text as Text18, useApp } from "ink";
|
|
8
|
+
|
|
9
|
+
// src/theme/theme.js
|
|
10
|
+
var theme = {
|
|
11
|
+
colors: {
|
|
12
|
+
// Text
|
|
13
|
+
primary: "#e6edf3",
|
|
14
|
+
// main foreground
|
|
15
|
+
secondary: "#8b949e",
|
|
16
|
+
// muted / supporting text
|
|
17
|
+
faint: "#6e7681",
|
|
18
|
+
// hints, separators, disabled
|
|
19
|
+
// Accents
|
|
20
|
+
accent: "#3fb950",
|
|
21
|
+
// green — selection, success, brand
|
|
22
|
+
accentDim: "#2ea043",
|
|
23
|
+
// green, slightly deeper
|
|
24
|
+
link: "#58a6ff",
|
|
25
|
+
// blue — links only
|
|
26
|
+
warning: "#d29922",
|
|
27
|
+
// muted amber — warnings / not-configured
|
|
28
|
+
// Surfaces
|
|
29
|
+
border: "#30363d"
|
|
30
|
+
// subtle rules and panel edges
|
|
31
|
+
},
|
|
32
|
+
// Glyphs kept to a small, widely-supported set.
|
|
33
|
+
glyphs: {
|
|
34
|
+
pointer: "\u203A",
|
|
35
|
+
bullet: "\u2022",
|
|
36
|
+
arrow: "\u2192",
|
|
37
|
+
check: "\u2713",
|
|
38
|
+
dot: "\xB7",
|
|
39
|
+
corner: "\u2514",
|
|
40
|
+
vbar: "\u2502"
|
|
41
|
+
},
|
|
42
|
+
// Layout constants (in columns).
|
|
43
|
+
layout: {
|
|
44
|
+
indent: 3,
|
|
45
|
+
// left gutter for content
|
|
46
|
+
maxWidth: 76,
|
|
47
|
+
// content never grows wider than this
|
|
48
|
+
minWidth: 44
|
|
49
|
+
// below this we drop into a compact layout
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var theme_default = theme;
|
|
53
|
+
|
|
54
|
+
// src/data/portfolio.js
|
|
55
|
+
var GITHUB_USERNAME = "shivenverma";
|
|
56
|
+
var portfolio = {
|
|
57
|
+
name: "Shiven Verma",
|
|
58
|
+
role: "Developer \u2022 Builder \u2022 Problem Solver",
|
|
59
|
+
tagline: "I build software, experiment with technology, and solve interesting problems.",
|
|
60
|
+
location: "Noida, India",
|
|
61
|
+
// e.g. "Delhi, India" — optional
|
|
62
|
+
// About screen. Each string is rendered as its own paragraph.
|
|
63
|
+
about: [
|
|
64
|
+
"I'm Shiven \u2014 a developer who likes building things, taking them apart, and understanding how they work.",
|
|
65
|
+
"I care about software that feels considered: tools that are pleasant to use and code that is a pleasure to read.",
|
|
66
|
+
"This portfolio is itself a small project. Everything you see lives in one data file, so it grows as I do."
|
|
67
|
+
],
|
|
68
|
+
// Short list of what you're currently into. Keep these honest and editable.
|
|
69
|
+
focus: [
|
|
70
|
+
"Developer tooling and productivity",
|
|
71
|
+
"Full-stack web development",
|
|
72
|
+
"Automation and small, sharp utilities",
|
|
73
|
+
"AI-assisted software"
|
|
74
|
+
],
|
|
75
|
+
// Skills grouped by category. Add or remove categories freely — the Skills
|
|
76
|
+
// screen renders whatever is here. No fake proficiency percentages.
|
|
77
|
+
skills: {
|
|
78
|
+
Languages: ["Java", "JavaScript", "Python", "C++", "SQL"],
|
|
79
|
+
Frontend: ["React", "HTML", "CSS"],
|
|
80
|
+
Backend: ["Node.js"],
|
|
81
|
+
Tools: ["Git", "GitHub", "VS Code"]
|
|
82
|
+
},
|
|
83
|
+
// Projects. Each entry becomes a selectable card in the Projects screen.
|
|
84
|
+
// links: any value that is empty or REPLACE_WITH_* is shown as "not linked".
|
|
85
|
+
projects: [
|
|
86
|
+
{
|
|
87
|
+
id: "gfghub",
|
|
88
|
+
name: "GFGHub",
|
|
89
|
+
summary: "Organize coding problems solved across GeeksforGeeks.",
|
|
90
|
+
description: "A developer productivity extension designed to organize coding problems solved across GeeksforGeeks, keeping your practice history in one place.",
|
|
91
|
+
tech: ["JavaScript", "Chrome Extensions", "GitHub API"],
|
|
92
|
+
status: "In Development",
|
|
93
|
+
links: {
|
|
94
|
+
github: "https://github.com/harshitcodes1308/gfg_hub_extension.git",
|
|
95
|
+
docs: "REPLACE_WITH_GFGHUB_DOCS_URL",
|
|
96
|
+
live: ""
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "batcave-experience",
|
|
101
|
+
name: "Batcave Experience",
|
|
102
|
+
summary: "An immersive personal music room where a curated YouTube playlist plays inside a painted 2D version of my flat.",
|
|
103
|
+
description: "Batcave Music is an immersive music experience inspired by themed web radio sites. It recreates my flat as a painted 2D environment, plays a curated YouTube playlist, and lets visitors manually change the room lighting between purple, crimson, and amber moods. I built it to explore immersive web experiences, custom music-player interactions, responsive UI, and realtime presence.",
|
|
104
|
+
tech: [
|
|
105
|
+
"React",
|
|
106
|
+
"TypeScript",
|
|
107
|
+
"Vite",
|
|
108
|
+
"YouTube IFrame Player API",
|
|
109
|
+
"Supabase Realtime"
|
|
110
|
+
],
|
|
111
|
+
status: "Deployed",
|
|
112
|
+
links: {
|
|
113
|
+
github: "https://github.com/shivenverma/Batcave-Experience.git",
|
|
114
|
+
live: "https://www.batcaveexperience.studio/"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
],
|
|
118
|
+
// GitHub section. The username is known; stats can be wired to the live API
|
|
119
|
+
// later (see src/utils/github.js) without changing this file.
|
|
120
|
+
github: {
|
|
121
|
+
username: GITHUB_USERNAME,
|
|
122
|
+
url: `https://github.com/shivenverma`,
|
|
123
|
+
blurb: "Open-source experiments, side projects, and the occasional useful tool."
|
|
124
|
+
},
|
|
125
|
+
// Contact + external links. mailto: is added automatically for email.
|
|
126
|
+
links: {
|
|
127
|
+
github: `https://github.com/shivenverma`,
|
|
128
|
+
linkedin: "https://www.linkedin.com/in/shiven-verma-24b5862a2/",
|
|
129
|
+
email: "shivenverma022@gmail.com",
|
|
130
|
+
resume: ""
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
var portfolio_default = portfolio;
|
|
134
|
+
|
|
135
|
+
// src/utils/commands.js
|
|
136
|
+
var COMMANDS = [
|
|
137
|
+
{ name: "about", screen: "about", description: "About me" },
|
|
138
|
+
{ name: "skills", screen: "skills", description: "Technical skills" },
|
|
139
|
+
{ name: "projects", screen: "projects", description: "View projects" },
|
|
140
|
+
{ name: "github", screen: "github", description: "GitHub profile" },
|
|
141
|
+
{ name: "contact", screen: "contact", description: "Contact information" },
|
|
142
|
+
{ name: "resume", screen: "resume", description: "Resume" },
|
|
143
|
+
{ name: "home", screen: "home", description: "Main menu" },
|
|
144
|
+
{ name: "clear", action: "clear", description: "Clear the screen" },
|
|
145
|
+
{ name: "help", screen: "help", description: "Show commands" },
|
|
146
|
+
{ name: "exit", action: "exit", description: "Exit portfolio" }
|
|
147
|
+
];
|
|
148
|
+
var ALIASES = {
|
|
149
|
+
quit: "exit",
|
|
150
|
+
q: "exit",
|
|
151
|
+
back: "home",
|
|
152
|
+
ls: "projects",
|
|
153
|
+
"?": "help",
|
|
154
|
+
man: "help"
|
|
155
|
+
};
|
|
156
|
+
var EASTER_EGGS = /* @__PURE__ */ new Set(["sudo shiven", "sudo make me a sandwich", "sudo", "whoami"]);
|
|
157
|
+
function matchCommand(rawInput) {
|
|
158
|
+
const input = rawInput.trim().toLowerCase().replace(/\s+/g, " ");
|
|
159
|
+
if (input.length === 0) return { type: "noop" };
|
|
160
|
+
if (EASTER_EGGS.has(input)) return { type: "action", action: "easter-egg" };
|
|
161
|
+
const name = ALIASES[input] ?? input;
|
|
162
|
+
const command = COMMANDS.find((c) => c.name === name);
|
|
163
|
+
if (!command) return { type: "unknown", input: rawInput.trim() };
|
|
164
|
+
if (command.screen) return { type: "screen", screen: command.screen };
|
|
165
|
+
return { type: "action", action: command.action };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/utils/browser.js
|
|
169
|
+
import { spawn } from "node:child_process";
|
|
170
|
+
function openUrl(url) {
|
|
171
|
+
return new Promise((resolve) => {
|
|
172
|
+
let settled = false;
|
|
173
|
+
const finish = (result) => {
|
|
174
|
+
if (!settled) {
|
|
175
|
+
settled = true;
|
|
176
|
+
resolve(result);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
if (typeof url !== "string" || url.trim().length === 0) {
|
|
180
|
+
finish({ ok: false, error: "No URL to open." });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const child = spawnHandler(url.trim());
|
|
185
|
+
child.once("error", (err) => finish({ ok: false, error: err.message }));
|
|
186
|
+
child.unref();
|
|
187
|
+
setTimeout(() => finish({ ok: true }), 150);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
finish({ ok: false, error: err.message });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function spawnHandler(url) {
|
|
194
|
+
const options = { stdio: "ignore", detached: true };
|
|
195
|
+
if (process.platform === "darwin") {
|
|
196
|
+
return spawn("open", [url], options);
|
|
197
|
+
}
|
|
198
|
+
if (process.platform === "win32") {
|
|
199
|
+
const escaped = url.replace(/[&|<>^]/g, "^$&");
|
|
200
|
+
return spawn("cmd", ["/c", "start", '""', escaped], {
|
|
201
|
+
...options,
|
|
202
|
+
windowsVerbatimArguments: true,
|
|
203
|
+
windowsHide: true
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return spawn("xdg-open", [url], options);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/hooks/useSafeInput.js
|
|
210
|
+
import { useInput, useStdin } from "ink";
|
|
211
|
+
function useSafeInput(handler, options = {}) {
|
|
212
|
+
const { isRawModeSupported } = useStdin();
|
|
213
|
+
const isActive = Boolean((options.isActive ?? true) && isRawModeSupported);
|
|
214
|
+
useInput(handler, { ...options, isActive });
|
|
215
|
+
}
|
|
216
|
+
var useSafeInput_default = useSafeInput;
|
|
217
|
+
|
|
218
|
+
// src/utils/terminal.js
|
|
219
|
+
var PLACEHOLDER_PREFIX = "REPLACE_WITH";
|
|
220
|
+
function isConfigured(value) {
|
|
221
|
+
if (typeof value !== "string") return Boolean(value);
|
|
222
|
+
const trimmed = value.trim();
|
|
223
|
+
if (trimmed.length === 0) return false;
|
|
224
|
+
return !trimmed.toUpperCase().startsWith(PLACEHOLDER_PREFIX);
|
|
225
|
+
}
|
|
226
|
+
function configuredList(list) {
|
|
227
|
+
if (!Array.isArray(list)) return [];
|
|
228
|
+
return list.filter(isConfigured);
|
|
229
|
+
}
|
|
230
|
+
function toMailto(email) {
|
|
231
|
+
if (!email) return "";
|
|
232
|
+
return email.startsWith("mailto:") ? email : `mailto:${email}`;
|
|
233
|
+
}
|
|
234
|
+
function clamp(value, min, max) {
|
|
235
|
+
return Math.max(min, Math.min(max, value));
|
|
236
|
+
}
|
|
237
|
+
function wrapWords(text, width) {
|
|
238
|
+
const safeWidth = Math.max(1, Math.floor(width) || 1);
|
|
239
|
+
const lines = [];
|
|
240
|
+
let line = "";
|
|
241
|
+
const flush = () => {
|
|
242
|
+
lines.push(line);
|
|
243
|
+
line = "";
|
|
244
|
+
};
|
|
245
|
+
for (const raw of String(text).split(/\s+/).filter(Boolean)) {
|
|
246
|
+
let word = raw;
|
|
247
|
+
while (word.length > safeWidth) {
|
|
248
|
+
if (line) flush();
|
|
249
|
+
lines.push(word.slice(0, safeWidth));
|
|
250
|
+
word = word.slice(safeWidth);
|
|
251
|
+
}
|
|
252
|
+
if (!line) line = word;
|
|
253
|
+
else if (line.length + 1 + word.length <= safeWidth) line += ` ${word}`;
|
|
254
|
+
else {
|
|
255
|
+
flush();
|
|
256
|
+
line = word;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (line) flush();
|
|
260
|
+
return lines.length > 0 ? lines : [""];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/hooks/useTerminalSize.js
|
|
264
|
+
import { useState, useEffect } from "react";
|
|
265
|
+
function useTerminalSize() {
|
|
266
|
+
const [size, setSize] = useState(() => read());
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
const onResize = () => setSize(read());
|
|
269
|
+
process.stdout.on("resize", onResize);
|
|
270
|
+
return () => process.stdout.off("resize", onResize);
|
|
271
|
+
}, []);
|
|
272
|
+
return size;
|
|
273
|
+
}
|
|
274
|
+
function read() {
|
|
275
|
+
return {
|
|
276
|
+
columns: process.stdout.columns || 80,
|
|
277
|
+
rows: process.stdout.rows || 24
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/hooks/useLayout.js
|
|
282
|
+
function useLayout() {
|
|
283
|
+
const { columns, rows } = useTerminalSize();
|
|
284
|
+
const indent = columns < 40 ? 1 : theme_default.layout.indent;
|
|
285
|
+
const width = clamp(columns - indent * 2, 16, theme_default.layout.maxWidth);
|
|
286
|
+
const compact = columns < theme_default.layout.minWidth || rows < 20;
|
|
287
|
+
return { columns, rows, indent, width, compact };
|
|
288
|
+
}
|
|
289
|
+
var useLayout_default = useLayout;
|
|
290
|
+
|
|
291
|
+
// src/components/Startup.jsx
|
|
292
|
+
import React, { useEffect as useEffect2, useRef, useState as useState2 } from "react";
|
|
293
|
+
import { Box, Text } from "ink";
|
|
294
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
295
|
+
var STEPS = ["Loading profile", "Loading projects", "Loading skills", "Connecting to portfolio"];
|
|
296
|
+
var SPINNER = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F".split("");
|
|
297
|
+
var STEP_MS = 120;
|
|
298
|
+
var READY_MS = 320;
|
|
299
|
+
function Startup({ onComplete }) {
|
|
300
|
+
const { indent } = useLayout_default();
|
|
301
|
+
const [step, setStep] = useState2(0);
|
|
302
|
+
const [frame, setFrame] = useState2(0);
|
|
303
|
+
const done = useRef(false);
|
|
304
|
+
const finish = () => {
|
|
305
|
+
if (!done.current) {
|
|
306
|
+
done.current = true;
|
|
307
|
+
onComplete();
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
useEffect2(() => {
|
|
311
|
+
if (step >= STEPS.length) {
|
|
312
|
+
const t2 = setTimeout(finish, READY_MS);
|
|
313
|
+
return () => clearTimeout(t2);
|
|
314
|
+
}
|
|
315
|
+
const t = setTimeout(() => setStep((s) => s + 1), step === 0 ? 240 : STEP_MS);
|
|
316
|
+
return () => clearTimeout(t);
|
|
317
|
+
}, [step]);
|
|
318
|
+
useEffect2(() => {
|
|
319
|
+
const t = setInterval(() => setFrame((f) => (f + 1) % SPINNER.length), 80);
|
|
320
|
+
return () => clearInterval(t);
|
|
321
|
+
}, []);
|
|
322
|
+
useSafeInput_default(() => finish());
|
|
323
|
+
const complete = step >= STEPS.length;
|
|
324
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: indent, paddingY: 1, children: [
|
|
325
|
+
/* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
|
|
326
|
+
/* @__PURE__ */ jsx(Text, { color: theme_default.colors.primary, bold: true, children: "Initializing ShivenOS" }),
|
|
327
|
+
/* @__PURE__ */ jsx(Text, { color: theme_default.colors.faint, children: "\u2026" })
|
|
328
|
+
] }),
|
|
329
|
+
STEPS.map((label, i) => {
|
|
330
|
+
const isDone = i < step;
|
|
331
|
+
const isCurrent = i === step && !complete;
|
|
332
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
333
|
+
/* @__PURE__ */ jsx(Text, { color: isDone ? theme_default.colors.accent : theme_default.colors.faint, children: isDone ? `${theme_default.glyphs.check} ` : isCurrent ? `${SPINNER[frame]} ` : " " }),
|
|
334
|
+
/* @__PURE__ */ jsx(
|
|
335
|
+
Text,
|
|
336
|
+
{
|
|
337
|
+
color: isDone ? theme_default.colors.secondary : isCurrent ? theme_default.colors.primary : theme_default.colors.faint,
|
|
338
|
+
children: label
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
] }, label);
|
|
342
|
+
}),
|
|
343
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: complete ? /* @__PURE__ */ jsx(Text, { color: theme_default.colors.accent, children: "System ready." }) : /* @__PURE__ */ jsx(Text, { color: theme_default.colors.faint, children: "press any key to skip" }) })
|
|
344
|
+
] });
|
|
345
|
+
}
|
|
346
|
+
var Startup_default = Startup;
|
|
347
|
+
|
|
348
|
+
// src/components/Home.jsx
|
|
349
|
+
import React7 from "react";
|
|
350
|
+
import { Box as Box6 } from "ink";
|
|
351
|
+
|
|
352
|
+
// src/components/Header.jsx
|
|
353
|
+
import React2 from "react";
|
|
354
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
355
|
+
|
|
356
|
+
// src/data/banner.js
|
|
357
|
+
var BANNER_TIERS = [
|
|
358
|
+
{ minColumns: 74, text: " _____ __ _______ _________ __ _ ____________ __ ______\n / ___// / / / _/ | / / ____/ | / / | | / / ____/ __ \\/ |/ / |\n \\__ \\/ /_/ // / | | / / __/ / |/ / | | / / __/ / /_/ / /|_/ / /| |\n ___/ / __ // / | |/ / /___/ /| / | |/ / /___/ _, _/ / / / ___ |\n/____/_/ /_/___/ |___/_____/_/ |_/ |___/_____/_/ |_/_/ /_/_/ |_|" },
|
|
359
|
+
{ minColumns: 62, text: " ______ _______ _______ __ _ _________ __ ______\n / __/ // / _/ | / / __/ |/ / | | / / __/ _ \\/ |/ / _ |\n _\\ \\/ _ // / | |/ / _// / | |/ / _// , _/ /|_/ / __ |\n/___/_//_/___/ |___/___/_/|_/ |___/___/_/|_/_/ /_/_/ |_|" },
|
|
360
|
+
{ minColumns: 34, text: " ___ _ _ _____ _____ _ _\n / __| || |_ _\\ \\ / / __| \\| |\n \\__ \\ __ || | \\ V /| _|| .` |\n |___/_||_|___| \\_/ |___|_|\\_|\n \\ \\ / / __| _ \\ \\/ | /_\\\n \\ V /| _|| / |\\/| |/ _ \\\n \\_/ |___|_|_\\_| |_/_/ \\_\\" }
|
|
361
|
+
];
|
|
362
|
+
function selectBanner(columns) {
|
|
363
|
+
const tier = BANNER_TIERS.find((t) => columns >= t.minColumns);
|
|
364
|
+
return tier ? tier.text : null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/components/Header.jsx
|
|
368
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
369
|
+
function Header({ columns, width, compact }) {
|
|
370
|
+
const banner = selectBanner(columns);
|
|
371
|
+
const roleParts = portfolio_default.role.split(" \u2022 ");
|
|
372
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
373
|
+
banner ? /* @__PURE__ */ jsx2(Text2, { color: theme_default.colors.primary, children: banner }) : /* @__PURE__ */ jsx2(Text2, { color: theme_default.colors.primary, bold: true, children: portfolio_default.name.toUpperCase() }),
|
|
374
|
+
/* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { children: roleParts.map((part, i) => /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
375
|
+
i > 0 ? /* @__PURE__ */ jsxs2(Text2, { color: theme_default.colors.accent, children: [
|
|
376
|
+
" ",
|
|
377
|
+
theme_default.glyphs.bullet,
|
|
378
|
+
" "
|
|
379
|
+
] }) : null,
|
|
380
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme_default.colors.secondary, children: part })
|
|
381
|
+
] }, part)) }) }),
|
|
382
|
+
!compact ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, width, children: /* @__PURE__ */ jsx2(Text2, { color: theme_default.colors.faint, wrap: "wrap", children: portfolio_default.tagline }) }) : null
|
|
383
|
+
] });
|
|
384
|
+
}
|
|
385
|
+
var Header_default = Header;
|
|
386
|
+
|
|
387
|
+
// src/components/Navigation.jsx
|
|
388
|
+
import React4 from "react";
|
|
389
|
+
import { Box as Box4 } from "ink";
|
|
390
|
+
|
|
391
|
+
// src/hooks/useSelection.js
|
|
392
|
+
import { useState as useState3, useEffect as useEffect3, useCallback } from "react";
|
|
393
|
+
function useSelection(length, { initial = 0, wrap = true } = {}) {
|
|
394
|
+
const [index, setIndex] = useState3(initial);
|
|
395
|
+
useEffect3(() => {
|
|
396
|
+
setIndex((current) => current >= length ? Math.max(0, length - 1) : current);
|
|
397
|
+
}, [length]);
|
|
398
|
+
const move = useCallback(
|
|
399
|
+
(delta) => {
|
|
400
|
+
setIndex((current) => {
|
|
401
|
+
if (length <= 0) return 0;
|
|
402
|
+
const next = current + delta;
|
|
403
|
+
if (wrap) return (next + length) % length;
|
|
404
|
+
return Math.max(0, Math.min(length - 1, next));
|
|
405
|
+
});
|
|
406
|
+
},
|
|
407
|
+
[length, wrap]
|
|
408
|
+
);
|
|
409
|
+
return { index, setIndex, move };
|
|
410
|
+
}
|
|
411
|
+
var useSelection_default = useSelection;
|
|
412
|
+
|
|
413
|
+
// src/components/SelectableList.jsx
|
|
414
|
+
import React3 from "react";
|
|
415
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
416
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
417
|
+
function SelectableList({ items, index, inline = false }) {
|
|
418
|
+
const labelWidth = inline ? 0 : items.reduce((max, item) => Math.max(max, item.label.length), 0);
|
|
419
|
+
return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: items.map((item, i) => {
|
|
420
|
+
const selected = i === index;
|
|
421
|
+
const pointer = selected ? `${theme_default.glyphs.pointer} ` : " ";
|
|
422
|
+
const labelColor = item.muted ? theme_default.colors.faint : selected ? theme_default.colors.primary : theme_default.colors.secondary;
|
|
423
|
+
const label = inline || !item.hint ? item.label : item.label.padEnd(labelWidth);
|
|
424
|
+
const hintText = item.hint ? inline ? ` ${theme_default.glyphs.dot} ${item.hint}` : ` ${item.hint}` : "";
|
|
425
|
+
return /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate-end", children: [
|
|
426
|
+
/* @__PURE__ */ jsx3(Text3, { color: selected ? theme_default.colors.accent : theme_default.colors.faint, children: pointer }),
|
|
427
|
+
/* @__PURE__ */ jsx3(Text3, { bold: selected, color: labelColor, children: label }),
|
|
428
|
+
hintText ? /* @__PURE__ */ jsx3(Text3, { color: theme_default.colors.faint, children: hintText }) : null
|
|
429
|
+
] }, item.key ?? item.label);
|
|
430
|
+
}) });
|
|
431
|
+
}
|
|
432
|
+
var SelectableList_default = SelectableList;
|
|
433
|
+
|
|
434
|
+
// src/components/Navigation.jsx
|
|
435
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
436
|
+
var MENU = [
|
|
437
|
+
{ label: "About", hint: "Who I am", screen: "about" },
|
|
438
|
+
{ label: "Skills", hint: "Languages, frameworks & tools", screen: "skills" },
|
|
439
|
+
{ label: "Projects", hint: "Things I've built", screen: "projects" },
|
|
440
|
+
{ label: "GitHub", hint: `github.com/${portfolio_default.github.username}`, screen: "github" },
|
|
441
|
+
{ label: "Contact", hint: "Get in touch", screen: "contact" },
|
|
442
|
+
{ label: "Resume", hint: "View my resume", screen: "resume" },
|
|
443
|
+
{ label: "Exit", hint: "Close the portfolio", action: "exit" }
|
|
444
|
+
];
|
|
445
|
+
function Navigation({ isActive, onNavigate, onQuit }) {
|
|
446
|
+
const { index, move } = useSelection_default(MENU.length);
|
|
447
|
+
useSafeInput_default(
|
|
448
|
+
(input, key) => {
|
|
449
|
+
if (key.upArrow || input === "k") move(-1);
|
|
450
|
+
else if (key.downArrow || input === "j") move(1);
|
|
451
|
+
else if (key.return) {
|
|
452
|
+
const item = MENU[index];
|
|
453
|
+
if (item.action === "exit") onQuit();
|
|
454
|
+
else onNavigate(item.screen);
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
{ isActive }
|
|
458
|
+
);
|
|
459
|
+
return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: /* @__PURE__ */ jsx4(SelectableList_default, { items: MENU, index }) });
|
|
460
|
+
}
|
|
461
|
+
var Navigation_default = Navigation;
|
|
462
|
+
|
|
463
|
+
// src/components/Rule.jsx
|
|
464
|
+
import React5 from "react";
|
|
465
|
+
import { Text as Text4 } from "ink";
|
|
466
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
467
|
+
function Rule({ width, color = theme_default.colors.border, char = "\u2500" }) {
|
|
468
|
+
return /* @__PURE__ */ jsx5(Text4, { color, children: char.repeat(Math.max(0, width)) });
|
|
469
|
+
}
|
|
470
|
+
var Rule_default = Rule;
|
|
471
|
+
|
|
472
|
+
// src/components/Footer.jsx
|
|
473
|
+
import React6 from "react";
|
|
474
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
475
|
+
import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
476
|
+
function Footer({ hints }) {
|
|
477
|
+
return /* @__PURE__ */ jsx6(Box5, { flexWrap: "wrap", children: hints.map((hint, i) => /* @__PURE__ */ jsxs4(Box5, { children: [
|
|
478
|
+
i > 0 ? /* @__PURE__ */ jsxs4(Text5, { color: theme_default.colors.border, children: [
|
|
479
|
+
" ",
|
|
480
|
+
theme_default.glyphs.dot,
|
|
481
|
+
" "
|
|
482
|
+
] }) : null,
|
|
483
|
+
/* @__PURE__ */ jsxs4(Text5, { color: theme_default.colors.secondary, children: [
|
|
484
|
+
hint.key,
|
|
485
|
+
" "
|
|
486
|
+
] }),
|
|
487
|
+
/* @__PURE__ */ jsx6(Text5, { color: theme_default.colors.faint, children: hint.label })
|
|
488
|
+
] }, hint.key)) });
|
|
489
|
+
}
|
|
490
|
+
var Footer_default = Footer;
|
|
491
|
+
|
|
492
|
+
// src/components/Home.jsx
|
|
493
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
494
|
+
var HOME_HINTS = [
|
|
495
|
+
{ key: "\u2191\u2193", label: "navigate" },
|
|
496
|
+
{ key: "\u23CE", label: "select" },
|
|
497
|
+
{ key: ":", label: "command" },
|
|
498
|
+
{ key: "q", label: "quit" }
|
|
499
|
+
];
|
|
500
|
+
function Home({ isActive, onNavigate, onQuit }) {
|
|
501
|
+
const { indent, width, columns, compact } = useLayout_default();
|
|
502
|
+
return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", paddingX: indent, paddingY: 1, width: width + indent * 2, children: [
|
|
503
|
+
/* @__PURE__ */ jsx7(Header_default, { columns, width, compact }),
|
|
504
|
+
/* @__PURE__ */ jsx7(Box6, { marginY: 1, width, children: /* @__PURE__ */ jsx7(Rule_default, { width }) }),
|
|
505
|
+
/* @__PURE__ */ jsx7(Navigation_default, { isActive, onNavigate, onQuit }),
|
|
506
|
+
/* @__PURE__ */ jsx7(Box6, { marginTop: 1, width, children: /* @__PURE__ */ jsx7(Rule_default, { width }) }),
|
|
507
|
+
/* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx7(Footer_default, { hints: HOME_HINTS }) })
|
|
508
|
+
] });
|
|
509
|
+
}
|
|
510
|
+
var Home_default = Home;
|
|
511
|
+
|
|
512
|
+
// src/components/About.jsx
|
|
513
|
+
import React10 from "react";
|
|
514
|
+
import { Box as Box8, Text as Text8 } from "ink";
|
|
515
|
+
|
|
516
|
+
// src/components/Screen.jsx
|
|
517
|
+
import React8 from "react";
|
|
518
|
+
import { Box as Box7, Text as Text6 } from "ink";
|
|
519
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
520
|
+
function Screen({ title, breadcrumb, hints, children }) {
|
|
521
|
+
const { indent, width } = useLayout_default();
|
|
522
|
+
return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", paddingX: indent, paddingY: 1, width: width + indent * 2, children: [
|
|
523
|
+
/* @__PURE__ */ jsxs6(Box7, { marginBottom: 1, children: [
|
|
524
|
+
/* @__PURE__ */ jsx8(Text6, { color: theme_default.colors.accent, children: portfolio_default.name }),
|
|
525
|
+
breadcrumb ? /* @__PURE__ */ jsxs6(Text6, { color: theme_default.colors.faint, children: [
|
|
526
|
+
" ",
|
|
527
|
+
theme_default.glyphs.pointer,
|
|
528
|
+
" ",
|
|
529
|
+
breadcrumb
|
|
530
|
+
] }) : null
|
|
531
|
+
] }),
|
|
532
|
+
title ? /* @__PURE__ */ jsx8(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx8(Text6, { color: theme_default.colors.accent, bold: true, children: title }) }) : null,
|
|
533
|
+
/* @__PURE__ */ jsx8(Box7, { flexDirection: "column", width, children }),
|
|
534
|
+
/* @__PURE__ */ jsx8(Box7, { marginTop: 1, flexDirection: "column", width, children: /* @__PURE__ */ jsx8(Rule_default, { width }) }),
|
|
535
|
+
/* @__PURE__ */ jsx8(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx8(Footer_default, { hints }) })
|
|
536
|
+
] });
|
|
537
|
+
}
|
|
538
|
+
var Screen_default = Screen;
|
|
539
|
+
|
|
540
|
+
// src/components/Paragraph.jsx
|
|
541
|
+
import React9 from "react";
|
|
542
|
+
import { Text as Text7 } from "ink";
|
|
543
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
544
|
+
function Paragraph({ children, color = theme_default.colors.secondary, width }) {
|
|
545
|
+
const layout = useLayout_default();
|
|
546
|
+
const text = Array.isArray(children) ? children.join("") : String(children ?? "");
|
|
547
|
+
const lines = wrapWords(text, width ?? layout.width);
|
|
548
|
+
return /* @__PURE__ */ jsx9(Text7, { color, children: lines.join("\n") });
|
|
549
|
+
}
|
|
550
|
+
var Paragraph_default = Paragraph;
|
|
551
|
+
|
|
552
|
+
// src/components/hints.js
|
|
553
|
+
var BACK_HINTS = [
|
|
554
|
+
{ key: "esc", label: "back" },
|
|
555
|
+
{ key: ":", label: "command" },
|
|
556
|
+
{ key: "q", label: "quit" }
|
|
557
|
+
];
|
|
558
|
+
var LIST_HINTS = [
|
|
559
|
+
{ key: "\u2191\u2193", label: "navigate" },
|
|
560
|
+
{ key: "\u23CE", label: "open" },
|
|
561
|
+
{ key: "esc", label: "back" },
|
|
562
|
+
{ key: "q", label: "quit" }
|
|
563
|
+
];
|
|
564
|
+
var OPEN_HINTS = [
|
|
565
|
+
{ key: "\u23CE", label: "open" },
|
|
566
|
+
{ key: "esc", label: "back" },
|
|
567
|
+
{ key: "q", label: "quit" }
|
|
568
|
+
];
|
|
569
|
+
|
|
570
|
+
// src/components/About.jsx
|
|
571
|
+
import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
572
|
+
function About() {
|
|
573
|
+
return /* @__PURE__ */ jsxs7(Screen_default, { title: "ABOUT", breadcrumb: "about", hints: BACK_HINTS, children: [
|
|
574
|
+
portfolio_default.about.map((paragraph, i) => /* @__PURE__ */ jsx10(Box8, { marginBottom: 1, children: /* @__PURE__ */ jsx10(Paragraph_default, { children: paragraph }) }, i)),
|
|
575
|
+
/* @__PURE__ */ jsx10(Box8, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text8, { color: theme_default.colors.accent, children: "CURRENTLY EXPLORING" }) }),
|
|
576
|
+
/* @__PURE__ */ jsx10(Box8, { flexDirection: "column", children: portfolio_default.focus.map((item) => /* @__PURE__ */ jsxs7(Box8, { children: [
|
|
577
|
+
/* @__PURE__ */ jsxs7(Text8, { color: theme_default.colors.faint, children: [
|
|
578
|
+
theme_default.glyphs.bullet,
|
|
579
|
+
" "
|
|
580
|
+
] }),
|
|
581
|
+
/* @__PURE__ */ jsx10(Text8, { color: theme_default.colors.secondary, children: item })
|
|
582
|
+
] }, item)) })
|
|
583
|
+
] });
|
|
584
|
+
}
|
|
585
|
+
var About_default = About;
|
|
586
|
+
|
|
587
|
+
// src/components/Skills.jsx
|
|
588
|
+
import React11 from "react";
|
|
589
|
+
import { Box as Box9, Text as Text9 } from "ink";
|
|
590
|
+
import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
591
|
+
function Skills() {
|
|
592
|
+
const { compact } = useLayout_default();
|
|
593
|
+
const categories = Object.entries(portfolio_default.skills);
|
|
594
|
+
const labelWidth = categories.reduce((max, [name]) => Math.max(max, name.length), 0);
|
|
595
|
+
return /* @__PURE__ */ jsx11(Screen_default, { title: "SKILLS", breadcrumb: "skills", hints: BACK_HINTS, children: /* @__PURE__ */ jsx11(Box9, { flexDirection: "column", children: categories.map(([name, items]) => /* @__PURE__ */ jsxs8(Box9, { flexDirection: compact ? "column" : "row", marginBottom: 1, children: [
|
|
596
|
+
/* @__PURE__ */ jsx11(Box9, { width: compact ? void 0 : labelWidth + 4, children: /* @__PURE__ */ jsx11(Text9, { color: theme_default.colors.accent, children: name.toUpperCase() }) }),
|
|
597
|
+
/* @__PURE__ */ jsx11(Text9, { color: theme_default.colors.secondary, children: items.map((item, i) => /* @__PURE__ */ jsxs8(Text9, { children: [
|
|
598
|
+
i > 0 ? /* @__PURE__ */ jsxs8(Text9, { color: theme_default.colors.border, children: [
|
|
599
|
+
" ",
|
|
600
|
+
theme_default.glyphs.dot,
|
|
601
|
+
" "
|
|
602
|
+
] }) : null,
|
|
603
|
+
item
|
|
604
|
+
] }, item)) })
|
|
605
|
+
] }, name)) }) });
|
|
606
|
+
}
|
|
607
|
+
var Skills_default = Skills;
|
|
608
|
+
|
|
609
|
+
// src/components/Projects.jsx
|
|
610
|
+
import React12 from "react";
|
|
611
|
+
import { Box as Box10, Text as Text10 } from "ink";
|
|
612
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
613
|
+
function Projects({ isActive, onOpen }) {
|
|
614
|
+
const projects = portfolio_default.projects;
|
|
615
|
+
const { index, move } = useSelection_default(projects.length);
|
|
616
|
+
useSafeInput_default(
|
|
617
|
+
(input, key) => {
|
|
618
|
+
if (key.upArrow || input === "k") move(-1);
|
|
619
|
+
else if (key.downArrow || input === "j") move(1);
|
|
620
|
+
else if (key.return && projects[index]) onOpen(projects[index].id);
|
|
621
|
+
},
|
|
622
|
+
{ isActive }
|
|
623
|
+
);
|
|
624
|
+
const items = projects.map((project) => ({
|
|
625
|
+
key: project.id,
|
|
626
|
+
label: project.name,
|
|
627
|
+
hint: project.summary
|
|
628
|
+
}));
|
|
629
|
+
return /* @__PURE__ */ jsx12(Screen_default, { title: "PROJECTS", breadcrumb: "projects", hints: LIST_HINTS, children: projects.length === 0 ? /* @__PURE__ */ jsx12(Text10, { color: theme_default.colors.faint, children: "No projects yet." }) : /* @__PURE__ */ jsx12(SelectableList_default, { items, index, inline: true }) });
|
|
630
|
+
}
|
|
631
|
+
var Projects_default = Projects;
|
|
632
|
+
|
|
633
|
+
// src/components/ProjectDetail.jsx
|
|
634
|
+
import React13 from "react";
|
|
635
|
+
import { Box as Box11, Text as Text11 } from "ink";
|
|
636
|
+
import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
637
|
+
var LINK_LABELS = { github: "GitHub", docs: "Documentation", live: "Live demo" };
|
|
638
|
+
function ProjectDetail({ isActive, projectId, onOpenLink }) {
|
|
639
|
+
const project = portfolio_default.projects.find((p) => p.id === projectId);
|
|
640
|
+
const links = project ? Object.entries(project.links).filter(([, url]) => isConfigured(url)).map(([kind, url]) => ({ key: kind, label: LINK_LABELS[kind] ?? kind, url })) : [];
|
|
641
|
+
const { index, move } = useSelection_default(links.length);
|
|
642
|
+
useSafeInput_default(
|
|
643
|
+
(input, key) => {
|
|
644
|
+
if (key.upArrow || input === "k") move(-1);
|
|
645
|
+
else if (key.downArrow || input === "j") move(1);
|
|
646
|
+
else if (key.return && links[index]) onOpenLink(links[index].url, links[index].label);
|
|
647
|
+
},
|
|
648
|
+
{ isActive }
|
|
649
|
+
);
|
|
650
|
+
if (!project) {
|
|
651
|
+
return /* @__PURE__ */ jsx13(Screen_default, { title: "PROJECT", breadcrumb: "projects", hints: BACK_HINTS, children: /* @__PURE__ */ jsx13(Text11, { color: theme_default.colors.warning, children: "That project could not be found." }) });
|
|
652
|
+
}
|
|
653
|
+
const tech = configuredList(project.tech);
|
|
654
|
+
return /* @__PURE__ */ jsxs9(
|
|
655
|
+
Screen_default,
|
|
656
|
+
{
|
|
657
|
+
title: project.name,
|
|
658
|
+
breadcrumb: `projects ${theme_default.glyphs.pointer} ${project.name.toLowerCase()}`,
|
|
659
|
+
hints: links.length > 0 ? LIST_HINTS : BACK_HINTS,
|
|
660
|
+
children: [
|
|
661
|
+
/* @__PURE__ */ jsx13(Box11, { marginBottom: 1, children: /* @__PURE__ */ jsx13(Paragraph_default, { children: project.description }) }),
|
|
662
|
+
/* @__PURE__ */ jsx13(Section, { label: "TECHNOLOGY", children: tech.length > 0 ? /* @__PURE__ */ jsx13(Text11, { color: theme_default.colors.secondary, children: tech.map((item, i) => /* @__PURE__ */ jsxs9(Text11, { children: [
|
|
663
|
+
i > 0 ? /* @__PURE__ */ jsxs9(Text11, { color: theme_default.colors.border, children: [
|
|
664
|
+
" ",
|
|
665
|
+
theme_default.glyphs.dot,
|
|
666
|
+
" "
|
|
667
|
+
] }) : null,
|
|
668
|
+
item
|
|
669
|
+
] }, item)) }) : /* @__PURE__ */ jsx13(Text11, { color: theme_default.colors.faint, children: "Not listed yet." }) }),
|
|
670
|
+
/* @__PURE__ */ jsx13(Section, { label: "STATUS", children: /* @__PURE__ */ jsx13(Text11, { color: theme_default.colors.accent, children: project.status }) }),
|
|
671
|
+
/* @__PURE__ */ jsx13(Section, { label: "LINKS", children: links.length > 0 ? /* @__PURE__ */ jsx13(SelectableList_default, { items: links.map((l) => ({ key: l.key, label: l.label, hint: l.url })), index }) : /* @__PURE__ */ jsx13(Text11, { color: theme_default.colors.faint, children: "No links yet \u2014 add them in portfolio.js." }) })
|
|
672
|
+
]
|
|
673
|
+
}
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
function Section({ label, children }) {
|
|
677
|
+
return /* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", marginBottom: 1, children: [
|
|
678
|
+
/* @__PURE__ */ jsx13(Text11, { color: theme_default.colors.faint, children: label }),
|
|
679
|
+
/* @__PURE__ */ jsx13(Box11, { children })
|
|
680
|
+
] });
|
|
681
|
+
}
|
|
682
|
+
var ProjectDetail_default = ProjectDetail;
|
|
683
|
+
|
|
684
|
+
// src/components/Github.jsx
|
|
685
|
+
import React14, { useEffect as useEffect4, useState as useState4 } from "react";
|
|
686
|
+
import { Box as Box12, Text as Text12 } from "ink";
|
|
687
|
+
|
|
688
|
+
// src/utils/github.js
|
|
689
|
+
async function getGithubProfile(github) {
|
|
690
|
+
return {
|
|
691
|
+
data: {
|
|
692
|
+
username: github.username,
|
|
693
|
+
url: github.url,
|
|
694
|
+
blurb: github.blurb
|
|
695
|
+
// The following are intentionally omitted until wired to the live API,
|
|
696
|
+
// so the UI never shows fabricated numbers:
|
|
697
|
+
// repos, followers, stars, contributions.
|
|
698
|
+
},
|
|
699
|
+
error: null
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/components/Github.jsx
|
|
704
|
+
import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
705
|
+
function Github({ isActive, onOpenLink }) {
|
|
706
|
+
const [state, setState] = useState4({ loading: true, data: null, error: null });
|
|
707
|
+
useEffect4(() => {
|
|
708
|
+
let alive = true;
|
|
709
|
+
getGithubProfile(portfolio_default.github).then((result) => {
|
|
710
|
+
if (alive) setState({ loading: false, data: result.data, error: result.error });
|
|
711
|
+
});
|
|
712
|
+
return () => {
|
|
713
|
+
alive = false;
|
|
714
|
+
};
|
|
715
|
+
}, []);
|
|
716
|
+
useSafeInput_default(
|
|
717
|
+
(input, key) => {
|
|
718
|
+
if (key.return) onOpenLink(portfolio_default.github.url, "GitHub profile");
|
|
719
|
+
},
|
|
720
|
+
{ isActive }
|
|
721
|
+
);
|
|
722
|
+
const profile = state.data ?? portfolio_default.github;
|
|
723
|
+
return /* @__PURE__ */ jsxs10(Screen_default, { title: "GITHUB", breadcrumb: "github", hints: OPEN_HINTS, children: [
|
|
724
|
+
/* @__PURE__ */ jsxs10(Box12, { children: [
|
|
725
|
+
/* @__PURE__ */ jsx14(Text12, { color: theme_default.colors.secondary, children: "@" }),
|
|
726
|
+
/* @__PURE__ */ jsx14(Text12, { color: theme_default.colors.primary, bold: true, children: profile.username })
|
|
727
|
+
] }),
|
|
728
|
+
/* @__PURE__ */ jsx14(Box12, { marginTop: 0, children: /* @__PURE__ */ jsx14(Text12, { color: theme_default.colors.link, underline: true, children: profile.url }) }),
|
|
729
|
+
/* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text12, { color: theme_default.colors.secondary, wrap: "wrap", children: profile.blurb }) }),
|
|
730
|
+
state.error ? /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsxs10(Text12, { color: theme_default.colors.warning, children: [
|
|
731
|
+
state.error,
|
|
732
|
+
" \u2014 showing offline info."
|
|
733
|
+
] }) }) : null,
|
|
734
|
+
/* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsxs10(Text12, { color: theme_default.colors.faint, children: [
|
|
735
|
+
"Press ",
|
|
736
|
+
/* @__PURE__ */ jsx14(Text12, { color: theme_default.colors.accent, children: "\u23CE" }),
|
|
737
|
+
" to open the profile in your browser."
|
|
738
|
+
] }) })
|
|
739
|
+
] });
|
|
740
|
+
}
|
|
741
|
+
var Github_default = Github;
|
|
742
|
+
|
|
743
|
+
// src/components/Contact.jsx
|
|
744
|
+
import React15 from "react";
|
|
745
|
+
import { Box as Box13, Text as Text13 } from "ink";
|
|
746
|
+
import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
747
|
+
function Contact({ isActive, onOpenLink }) {
|
|
748
|
+
const { links } = portfolio_default;
|
|
749
|
+
const channels = [
|
|
750
|
+
{ key: "github", label: "GitHub", value: links.github, url: links.github },
|
|
751
|
+
{ key: "linkedin", label: "LinkedIn", value: links.linkedin, url: links.linkedin },
|
|
752
|
+
{ key: "email", label: "Email", value: links.email, url: toMailto(links.email) }
|
|
753
|
+
];
|
|
754
|
+
const configured = channels.filter((c) => isConfigured(c.value));
|
|
755
|
+
const missing = channels.filter((c) => !isConfigured(c.value)).map((c) => c.label);
|
|
756
|
+
const { index, move } = useSelection_default(configured.length);
|
|
757
|
+
useSafeInput_default(
|
|
758
|
+
(input, key) => {
|
|
759
|
+
if (key.upArrow || input === "k") move(-1);
|
|
760
|
+
else if (key.downArrow || input === "j") move(1);
|
|
761
|
+
else if (key.return && configured[index]) {
|
|
762
|
+
onOpenLink(configured[index].url, configured[index].label);
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
{ isActive }
|
|
766
|
+
);
|
|
767
|
+
return /* @__PURE__ */ jsxs11(
|
|
768
|
+
Screen_default,
|
|
769
|
+
{
|
|
770
|
+
title: "CONTACT",
|
|
771
|
+
breadcrumb: "contact",
|
|
772
|
+
hints: configured.length > 0 ? LIST_HINTS : BACK_HINTS,
|
|
773
|
+
children: [
|
|
774
|
+
configured.length > 0 ? /* @__PURE__ */ jsx15(
|
|
775
|
+
SelectableList_default,
|
|
776
|
+
{
|
|
777
|
+
items: configured.map((c) => ({ key: c.key, label: c.label, hint: c.value })),
|
|
778
|
+
index
|
|
779
|
+
}
|
|
780
|
+
) : /* @__PURE__ */ jsx15(Text13, { color: theme_default.colors.faint, children: "No contact channels configured yet." }),
|
|
781
|
+
missing.length > 0 ? /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsxs11(Text13, { color: theme_default.colors.faint, children: [
|
|
782
|
+
"Not set up yet: ",
|
|
783
|
+
missing.join(", "),
|
|
784
|
+
" \u2014 add them in portfolio.js."
|
|
785
|
+
] }) }) : null
|
|
786
|
+
]
|
|
787
|
+
}
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
var Contact_default = Contact;
|
|
791
|
+
|
|
792
|
+
// src/components/Resume.jsx
|
|
793
|
+
import React16 from "react";
|
|
794
|
+
import { Box as Box14, Text as Text14 } from "ink";
|
|
795
|
+
import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
796
|
+
function Resume({ isActive, onOpenLink }) {
|
|
797
|
+
const url = portfolio_default.links.resume;
|
|
798
|
+
const configured = isConfigured(url);
|
|
799
|
+
useSafeInput_default(
|
|
800
|
+
(input, key) => {
|
|
801
|
+
if (key.return && configured) onOpenLink(url, "Resume");
|
|
802
|
+
},
|
|
803
|
+
{ isActive }
|
|
804
|
+
);
|
|
805
|
+
return /* @__PURE__ */ jsx16(Screen_default, { title: "RESUME", breadcrumb: "resume", hints: configured ? OPEN_HINTS : BACK_HINTS, children: configured ? /* @__PURE__ */ jsxs12(Box14, { flexDirection: "column", children: [
|
|
806
|
+
/* @__PURE__ */ jsx16(Text14, { color: theme_default.colors.link, underline: true, children: url }),
|
|
807
|
+
/* @__PURE__ */ jsx16(Box14, { marginTop: 1, children: /* @__PURE__ */ jsxs12(Text14, { color: theme_default.colors.faint, children: [
|
|
808
|
+
"Press ",
|
|
809
|
+
/* @__PURE__ */ jsx16(Text14, { color: theme_default.colors.accent, children: "\u23CE" }),
|
|
810
|
+
" to open the resume."
|
|
811
|
+
] }) })
|
|
812
|
+
] }) : /* @__PURE__ */ jsxs12(Box14, { flexDirection: "column", children: [
|
|
813
|
+
/* @__PURE__ */ jsx16(Text14, { color: theme_default.colors.warning, children: "Resume link has not been configured yet." }),
|
|
814
|
+
/* @__PURE__ */ jsx16(Box14, { marginTop: 1, children: /* @__PURE__ */ jsx16(Text14, { color: theme_default.colors.faint, children: "Set links.resume in portfolio.js to enable it." }) })
|
|
815
|
+
] }) });
|
|
816
|
+
}
|
|
817
|
+
var Resume_default = Resume;
|
|
818
|
+
|
|
819
|
+
// src/components/Help.jsx
|
|
820
|
+
import React17 from "react";
|
|
821
|
+
import { Box as Box15, Text as Text15 } from "ink";
|
|
822
|
+
import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
823
|
+
function Help() {
|
|
824
|
+
const nameWidth = COMMANDS.reduce((max, c) => Math.max(max, c.name.length), 0);
|
|
825
|
+
return /* @__PURE__ */ jsxs13(Screen_default, { title: "HELP", breadcrumb: "help", hints: BACK_HINTS, children: [
|
|
826
|
+
/* @__PURE__ */ jsx17(Box15, { marginBottom: 1, children: /* @__PURE__ */ jsxs13(Text15, { color: theme_default.colors.secondary, children: [
|
|
827
|
+
"Press ",
|
|
828
|
+
/* @__PURE__ */ jsx17(Text15, { color: theme_default.colors.accent, children: ":" }),
|
|
829
|
+
" anywhere to open the command bar, then type a command."
|
|
830
|
+
] }) }),
|
|
831
|
+
/* @__PURE__ */ jsx17(Box15, { flexDirection: "column", children: COMMANDS.map((command) => /* @__PURE__ */ jsxs13(Box15, { children: [
|
|
832
|
+
/* @__PURE__ */ jsx17(Text15, { color: theme_default.colors.accent, children: command.name.padEnd(nameWidth + 4) }),
|
|
833
|
+
/* @__PURE__ */ jsx17(Text15, { color: theme_default.colors.secondary, children: command.description })
|
|
834
|
+
] }, command.name)) })
|
|
835
|
+
] });
|
|
836
|
+
}
|
|
837
|
+
var Help_default = Help;
|
|
838
|
+
|
|
839
|
+
// src/components/EasterEgg.jsx
|
|
840
|
+
import React18 from "react";
|
|
841
|
+
import { Box as Box16, Text as Text16 } from "ink";
|
|
842
|
+
import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
843
|
+
var ART = ["\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510", "\u2502 > _ \u2502", "\u2502 \u2502", "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"];
|
|
844
|
+
var SWATCHES = [
|
|
845
|
+
theme_default.colors.accent,
|
|
846
|
+
theme_default.colors.link,
|
|
847
|
+
theme_default.colors.warning,
|
|
848
|
+
theme_default.colors.primary,
|
|
849
|
+
theme_default.colors.secondary,
|
|
850
|
+
theme_default.colors.faint
|
|
851
|
+
];
|
|
852
|
+
function EasterEgg() {
|
|
853
|
+
const { compact } = useLayout_default();
|
|
854
|
+
const title = `${portfolio_default.github.username}@portfolio`;
|
|
855
|
+
const languages = (portfolio_default.skills.Languages ?? []).join(" \xB7 ");
|
|
856
|
+
const info = [
|
|
857
|
+
["OS", "ShivenOS \xB7 terminal edition"],
|
|
858
|
+
["Host", "npx shivenverma"],
|
|
859
|
+
["Shell", "your terminal, thankfully"],
|
|
860
|
+
["Uptime", "since you ran the command"],
|
|
861
|
+
["Languages", languages || "\u2014"],
|
|
862
|
+
["Motto", "build \xB7 break \xB7 learn"]
|
|
863
|
+
];
|
|
864
|
+
return /* @__PURE__ */ jsxs14(Screen_default, { title: "WHOAMI", breadcrumb: "easter egg", hints: [{ key: "esc", label: "back" }], children: [
|
|
865
|
+
/* @__PURE__ */ jsxs14(Box16, { flexDirection: compact ? "column" : "row", children: [
|
|
866
|
+
/* @__PURE__ */ jsx18(Box16, { flexDirection: "column", marginRight: compact ? 0 : 3, children: ART.map((line, i) => /* @__PURE__ */ jsx18(Text16, { color: theme_default.colors.accent, children: line }, i)) }),
|
|
867
|
+
/* @__PURE__ */ jsxs14(Box16, { flexDirection: "column", children: [
|
|
868
|
+
/* @__PURE__ */ jsx18(Text16, { color: theme_default.colors.accent, bold: true, children: title }),
|
|
869
|
+
/* @__PURE__ */ jsx18(Text16, { color: theme_default.colors.border, children: "\u2500".repeat(title.length) }),
|
|
870
|
+
info.map(([label, value]) => /* @__PURE__ */ jsxs14(Box16, { children: [
|
|
871
|
+
/* @__PURE__ */ jsx18(Text16, { color: theme_default.colors.accent, children: label }),
|
|
872
|
+
/* @__PURE__ */ jsxs14(Text16, { color: theme_default.colors.secondary, children: [
|
|
873
|
+
": ",
|
|
874
|
+
value
|
|
875
|
+
] })
|
|
876
|
+
] }, label)),
|
|
877
|
+
/* @__PURE__ */ jsx18(Box16, { marginTop: 1, children: SWATCHES.map((color, i) => /* @__PURE__ */ jsx18(Text16, { color, children: "\u2588\u2588" }, i)) })
|
|
878
|
+
] })
|
|
879
|
+
] }),
|
|
880
|
+
/* @__PURE__ */ jsx18(Box16, { marginTop: 1, children: /* @__PURE__ */ jsx18(Text16, { color: theme_default.colors.faint, children: "we trust you have received the usual lecture from the local system administrator." }) })
|
|
881
|
+
] });
|
|
882
|
+
}
|
|
883
|
+
var EasterEgg_default = EasterEgg;
|
|
884
|
+
|
|
885
|
+
// src/components/CommandBar.jsx
|
|
886
|
+
import React19 from "react";
|
|
887
|
+
import { Box as Box17, Text as Text17 } from "ink";
|
|
888
|
+
import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
889
|
+
var STATUS_COLORS = {
|
|
890
|
+
success: theme_default.colors.accent,
|
|
891
|
+
warn: theme_default.colors.warning,
|
|
892
|
+
error: theme_default.colors.warning,
|
|
893
|
+
info: theme_default.colors.secondary
|
|
894
|
+
};
|
|
895
|
+
function CommandBar({ active, buffer }) {
|
|
896
|
+
const { indent } = useLayout_default();
|
|
897
|
+
if (!active) return null;
|
|
898
|
+
return /* @__PURE__ */ jsxs15(Box17, { paddingX: indent, children: [
|
|
899
|
+
/* @__PURE__ */ jsx19(Text17, { color: theme_default.colors.accent, children: ": " }),
|
|
900
|
+
/* @__PURE__ */ jsx19(Text17, { color: theme_default.colors.primary, children: buffer }),
|
|
901
|
+
/* @__PURE__ */ jsx19(Text17, { color: theme_default.colors.accent, children: "\u258B" })
|
|
902
|
+
] });
|
|
903
|
+
}
|
|
904
|
+
function StatusLine({ status }) {
|
|
905
|
+
const { indent } = useLayout_default();
|
|
906
|
+
if (!status) return null;
|
|
907
|
+
return /* @__PURE__ */ jsxs15(Box17, { paddingX: indent, children: [
|
|
908
|
+
/* @__PURE__ */ jsxs15(Text17, { color: theme_default.colors.faint, children: [
|
|
909
|
+
theme_default.glyphs.corner,
|
|
910
|
+
" "
|
|
911
|
+
] }),
|
|
912
|
+
/* @__PURE__ */ jsx19(Text17, { color: STATUS_COLORS[status.type] ?? theme_default.colors.secondary, wrap: "truncate-end", children: status.text })
|
|
913
|
+
] });
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// src/App.jsx
|
|
917
|
+
import { jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
918
|
+
function App({ interactive: interactive2 = true, skipStartup: skipStartup2 = false }) {
|
|
919
|
+
const { exit } = useApp();
|
|
920
|
+
const [booting, setBooting] = useState5(interactive2 && !skipStartup2);
|
|
921
|
+
const [stack, setStack] = useState5(["home"]);
|
|
922
|
+
const [projectId, setProjectId] = useState5(null);
|
|
923
|
+
const [command, setCommand] = useState5({ active: false, buffer: "" });
|
|
924
|
+
const [status, setStatus] = useState5(null);
|
|
925
|
+
const screen = stack[stack.length - 1];
|
|
926
|
+
const push = (next) => {
|
|
927
|
+
setStatus(null);
|
|
928
|
+
setStack((st) => [...st, next]);
|
|
929
|
+
};
|
|
930
|
+
const back = () => {
|
|
931
|
+
setStatus(null);
|
|
932
|
+
setStack((st) => st.length > 1 ? st.slice(0, -1) : st);
|
|
933
|
+
};
|
|
934
|
+
const goHome = () => {
|
|
935
|
+
setStatus(null);
|
|
936
|
+
setStack(["home"]);
|
|
937
|
+
};
|
|
938
|
+
const jumpTo = (next) => {
|
|
939
|
+
setStatus(null);
|
|
940
|
+
setStack(next === "home" ? ["home"] : ["home", next]);
|
|
941
|
+
};
|
|
942
|
+
const openProject = (id) => {
|
|
943
|
+
setProjectId(id);
|
|
944
|
+
push("project");
|
|
945
|
+
};
|
|
946
|
+
const quit = () => {
|
|
947
|
+
setStatus(null);
|
|
948
|
+
setStack(["bye"]);
|
|
949
|
+
setTimeout(() => exit(), 60);
|
|
950
|
+
};
|
|
951
|
+
const openLink = async (url, label) => {
|
|
952
|
+
setStatus({ type: "info", text: `Opening ${label}\u2026` });
|
|
953
|
+
const result = await openUrl(url);
|
|
954
|
+
if (result.ok) {
|
|
955
|
+
setStatus({ type: "success", text: `Opened ${label} in your browser.` });
|
|
956
|
+
} else {
|
|
957
|
+
setStatus({ type: "warn", text: `Couldn't open a browser. Copy this: ${url}` });
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
const runCommand = (raw) => {
|
|
961
|
+
const result = matchCommand(raw);
|
|
962
|
+
setCommand({ active: false, buffer: "" });
|
|
963
|
+
if (result.type === "screen") return jumpTo(result.screen);
|
|
964
|
+
if (result.type === "action") {
|
|
965
|
+
if (result.action === "exit") return quit();
|
|
966
|
+
if (result.action === "clear") return goHome();
|
|
967
|
+
if (result.action === "easter-egg") return jumpTo("egg");
|
|
968
|
+
}
|
|
969
|
+
if (result.type === "unknown") {
|
|
970
|
+
setStatus({
|
|
971
|
+
type: "warn",
|
|
972
|
+
text: `command not found: ${result.input} \u2014 type "help" to list commands.`
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
useSafeInput_default(
|
|
977
|
+
(input, key) => {
|
|
978
|
+
if (key.escape) return back();
|
|
979
|
+
if (input === "q") return quit();
|
|
980
|
+
if (input === ":" || input === "/") setCommand({ active: true, buffer: "" });
|
|
981
|
+
else if (input === "?") jumpTo("help");
|
|
982
|
+
},
|
|
983
|
+
{ isActive: !command.active && !booting && screen !== "bye" }
|
|
984
|
+
);
|
|
985
|
+
useSafeInput_default(
|
|
986
|
+
(input, key) => {
|
|
987
|
+
if (key.escape) return setCommand({ active: false, buffer: "" });
|
|
988
|
+
if (key.return) return runCommand(command.buffer);
|
|
989
|
+
if (key.backspace || key.delete) {
|
|
990
|
+
return setCommand((c) => ({ ...c, buffer: c.buffer.slice(0, -1) }));
|
|
991
|
+
}
|
|
992
|
+
if (key.ctrl && input === "u") return setCommand((c) => ({ ...c, buffer: "" }));
|
|
993
|
+
if (input && !key.ctrl && !key.meta && !key.tab) {
|
|
994
|
+
setCommand((c) => ({ ...c, buffer: c.buffer + input }));
|
|
995
|
+
}
|
|
996
|
+
},
|
|
997
|
+
{ isActive: command.active && !booting }
|
|
998
|
+
);
|
|
999
|
+
if (booting) {
|
|
1000
|
+
return /* @__PURE__ */ jsx20(Startup_default, { onComplete: () => setBooting(false) });
|
|
1001
|
+
}
|
|
1002
|
+
const inputActive = !command.active;
|
|
1003
|
+
return /* @__PURE__ */ jsxs16(Box18, { flexDirection: "column", children: [
|
|
1004
|
+
renderScreen({ screen, inputActive, projectId, push, openProject, openLink, quit }),
|
|
1005
|
+
/* @__PURE__ */ jsx20(StatusLine, { status }),
|
|
1006
|
+
/* @__PURE__ */ jsx20(CommandBar, { active: command.active, buffer: command.buffer })
|
|
1007
|
+
] });
|
|
1008
|
+
}
|
|
1009
|
+
function renderScreen({ screen, inputActive, projectId, push, openProject, openLink, quit }) {
|
|
1010
|
+
switch (screen) {
|
|
1011
|
+
case "about":
|
|
1012
|
+
return /* @__PURE__ */ jsx20(About_default, {});
|
|
1013
|
+
case "skills":
|
|
1014
|
+
return /* @__PURE__ */ jsx20(Skills_default, {});
|
|
1015
|
+
case "projects":
|
|
1016
|
+
return /* @__PURE__ */ jsx20(Projects_default, { isActive: inputActive, onOpen: openProject });
|
|
1017
|
+
case "project":
|
|
1018
|
+
return /* @__PURE__ */ jsx20(ProjectDetail_default, { isActive: inputActive, projectId, onOpenLink: openLink });
|
|
1019
|
+
case "github":
|
|
1020
|
+
return /* @__PURE__ */ jsx20(Github_default, { isActive: inputActive, onOpenLink: openLink });
|
|
1021
|
+
case "contact":
|
|
1022
|
+
return /* @__PURE__ */ jsx20(Contact_default, { isActive: inputActive, onOpenLink: openLink });
|
|
1023
|
+
case "resume":
|
|
1024
|
+
return /* @__PURE__ */ jsx20(Resume_default, { isActive: inputActive, onOpenLink: openLink });
|
|
1025
|
+
case "help":
|
|
1026
|
+
return /* @__PURE__ */ jsx20(Help_default, {});
|
|
1027
|
+
case "egg":
|
|
1028
|
+
return /* @__PURE__ */ jsx20(EasterEgg_default, {});
|
|
1029
|
+
case "bye":
|
|
1030
|
+
return /* @__PURE__ */ jsx20(Farewell, {});
|
|
1031
|
+
case "home":
|
|
1032
|
+
default:
|
|
1033
|
+
return /* @__PURE__ */ jsx20(Home_default, { isActive: inputActive, onNavigate: push, onQuit: quit });
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
function Farewell() {
|
|
1037
|
+
const { indent } = useLayout_default();
|
|
1038
|
+
return /* @__PURE__ */ jsxs16(Box18, { flexDirection: "column", paddingX: indent, paddingY: 1, children: [
|
|
1039
|
+
/* @__PURE__ */ jsx20(Text18, { color: theme_default.colors.accent, children: "Thanks for stopping by." }),
|
|
1040
|
+
/* @__PURE__ */ jsxs16(Text18, { color: theme_default.colors.faint, children: [
|
|
1041
|
+
"github.com/",
|
|
1042
|
+
portfolio_default.github.username,
|
|
1043
|
+
" ",
|
|
1044
|
+
theme_default.glyphs.dot,
|
|
1045
|
+
" npx shivenverma"
|
|
1046
|
+
] })
|
|
1047
|
+
] });
|
|
1048
|
+
}
|
|
1049
|
+
var App_default = App;
|
|
1050
|
+
|
|
1051
|
+
// src/index.jsx
|
|
1052
|
+
import { jsx as jsx21 } from "react/jsx-runtime";
|
|
1053
|
+
var VERSION = true ? "1.0.0" : "0.0.0";
|
|
1054
|
+
var args = process.argv.slice(2);
|
|
1055
|
+
var has = (...names) => names.some((name) => args.includes(name));
|
|
1056
|
+
if (has("-v", "--version")) {
|
|
1057
|
+
console.log(VERSION);
|
|
1058
|
+
process.exit(0);
|
|
1059
|
+
}
|
|
1060
|
+
if (has("-h", "--help")) {
|
|
1061
|
+
printHelp();
|
|
1062
|
+
process.exit(0);
|
|
1063
|
+
}
|
|
1064
|
+
var interactive = Boolean(process.stdout.isTTY && process.stdin.isTTY) && !has("--ci");
|
|
1065
|
+
var skipStartup = has("--no-startup", "--no-anim", "--skip") || !interactive;
|
|
1066
|
+
try {
|
|
1067
|
+
const instance = render(/* @__PURE__ */ jsx21(App_default, { interactive, skipStartup }), {
|
|
1068
|
+
exitOnCtrlC: true,
|
|
1069
|
+
// In one-shot / piped mode we don't want Ink buffering console output; this
|
|
1070
|
+
// also surfaces render errors on real stderr instead of swallowing them.
|
|
1071
|
+
patchConsole: interactive
|
|
1072
|
+
});
|
|
1073
|
+
if (!interactive) {
|
|
1074
|
+
setTimeout(() => instance.unmount(), 200);
|
|
1075
|
+
setTimeout(() => process.exit(0), 900).unref();
|
|
1076
|
+
}
|
|
1077
|
+
instance.waitUntilExit().catch(() => {
|
|
1078
|
+
});
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
console.error("shivenverma failed to start:", error?.message ?? error);
|
|
1081
|
+
process.exit(1);
|
|
1082
|
+
}
|
|
1083
|
+
function printHelp() {
|
|
1084
|
+
const lines = [
|
|
1085
|
+
"",
|
|
1086
|
+
" shivenverma \u2014 an interactive terminal portfolio",
|
|
1087
|
+
"",
|
|
1088
|
+
" Usage",
|
|
1089
|
+
" npx shivenverma launch the portfolio",
|
|
1090
|
+
" shivenverma [options]",
|
|
1091
|
+
"",
|
|
1092
|
+
" Options",
|
|
1093
|
+
" -h, --help show this help",
|
|
1094
|
+
" -v, --version print the version",
|
|
1095
|
+
" --no-startup skip the boot animation",
|
|
1096
|
+
"",
|
|
1097
|
+
" Once inside",
|
|
1098
|
+
" \u2191 \u2193 / j k navigate \u23CE select esc back",
|
|
1099
|
+
" : command mode q quit",
|
|
1100
|
+
""
|
|
1101
|
+
];
|
|
1102
|
+
console.log(lines.join("\n"));
|
|
1103
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shivenverma",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "An interactive terminal portfolio for Shiven Verma. Run it with: npx shivenverma",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"shivenverma": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "node scripts/build.mjs",
|
|
17
|
+
"banner": "node scripts/gen-banner.mjs",
|
|
18
|
+
"dev": "node scripts/build.mjs && node bin/cli.js",
|
|
19
|
+
"start": "node bin/cli.js",
|
|
20
|
+
"test": "node scripts/build.mjs && node scripts/smoke-test.mjs",
|
|
21
|
+
"package": "node scripts/build.mjs && npm pack",
|
|
22
|
+
"prepublishOnly": "node scripts/build.mjs"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"portfolio",
|
|
29
|
+
"cli",
|
|
30
|
+
"terminal",
|
|
31
|
+
"resume",
|
|
32
|
+
"developer",
|
|
33
|
+
"ink",
|
|
34
|
+
"react",
|
|
35
|
+
"npx",
|
|
36
|
+
"interactive",
|
|
37
|
+
"tui",
|
|
38
|
+
"shiven-verma"
|
|
39
|
+
],
|
|
40
|
+
"author": "Shiven Verma",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/shivenverma/shivenverma.git"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/shivenverma/shivenverma#readme",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/shivenverma/shivenverma/issues"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"ink": "^5.0.1",
|
|
52
|
+
"react": "^18.3.1"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"esbuild": "^0.23.1",
|
|
56
|
+
"figlet": "^1.7.0"
|
|
57
|
+
}
|
|
58
|
+
}
|