drexler 0.2.13 → 0.2.14
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/CHANGELOG.md +8 -0
- package/README.md +58 -13
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/ui/App.tsx +16 -4
- package/src/ui/DealDeskHeader.tsx +245 -72
- package/src/ui/MarkdownBody.tsx +382 -0
- package/src/ui/MascotIntro.tsx +573 -72
- package/src/ui/Message.tsx +28 -15
- package/src/ui/SynergyEvent.tsx +3 -2
- package/src/ui/TranscriptViewport.tsx +258 -25
- package/src/ui/displayContent.ts +114 -0
- package/src/ui/graphemes.ts +1 -0
package/src/ui/MascotIntro.tsx
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Box, Text, useApp, useInput, useStdout } from "ink";
|
|
2
|
-
import { useEffect,
|
|
2
|
+
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
|
3
3
|
import { STARTUP_TIPS } from "../startupTips.ts";
|
|
4
4
|
import {
|
|
5
5
|
MascotFrame,
|
|
6
6
|
MASCOT_WIDTH,
|
|
7
7
|
type MascotState,
|
|
8
8
|
} from "./MascotFrame.tsx";
|
|
9
|
+
import { displayWidth, fitDisplayText } from "./graphemes.ts";
|
|
9
10
|
import { useTheme } from "./ThemeContext.tsx";
|
|
10
11
|
|
|
11
12
|
interface IntroFrame extends MascotState {
|
|
@@ -29,7 +30,7 @@ export const INTRO_BOOT_NOTES = [
|
|
|
29
30
|
|
|
30
31
|
type IntroBootNote = (typeof INTRO_BOOT_NOTES)[number];
|
|
31
32
|
|
|
32
|
-
const
|
|
33
|
+
const INTRO_FRAMES: IntroFrame[] = [
|
|
33
34
|
{
|
|
34
35
|
walls: "dim",
|
|
35
36
|
brows: "hidden",
|
|
@@ -122,13 +123,18 @@ const FRAMES: IntroFrame[] = [
|
|
|
122
123
|
},
|
|
123
124
|
];
|
|
124
125
|
|
|
125
|
-
const
|
|
126
|
-
const
|
|
126
|
+
const COMPACT_INTRO_NOTES = ["Booting", "Scanning", "Online"];
|
|
127
|
+
const COMPACT_INTRO_DELAY_MS = 850;
|
|
127
128
|
const SETTLE_HOLD_MS = 1200;
|
|
128
129
|
const FRAME_CHROME_WIDTH = 4;
|
|
129
130
|
const GUTTER_WIDTH = 4;
|
|
131
|
+
const SPLIT_DIVIDER_WIDTH = 3;
|
|
132
|
+
const SPLIT_DIVIDER_HEIGHT = 10;
|
|
133
|
+
const SPLIT_DIVIDER_ROWS: number[] = Array.from(
|
|
134
|
+
{ length: SPLIT_DIVIDER_HEIGHT },
|
|
135
|
+
(_, i) => i,
|
|
136
|
+
);
|
|
130
137
|
const BOOT_BAR_WIDTH = MASCOT_WIDTH - 1;
|
|
131
|
-
const RIGHT_COLUMN_BORDER_WIDTH = 2;
|
|
132
138
|
|
|
133
139
|
interface IntroProps {
|
|
134
140
|
greeting: string;
|
|
@@ -137,6 +143,8 @@ interface IntroProps {
|
|
|
137
143
|
interface MascotDashboardProps {
|
|
138
144
|
greeting: string;
|
|
139
145
|
width: number;
|
|
146
|
+
mood?: string;
|
|
147
|
+
bootProgress?: number;
|
|
140
148
|
state?: MascotState;
|
|
141
149
|
bar?: string;
|
|
142
150
|
barColor?: string;
|
|
@@ -144,7 +152,7 @@ interface MascotDashboardProps {
|
|
|
144
152
|
dealDesk?: (width: number) => ReactNode;
|
|
145
153
|
}
|
|
146
154
|
|
|
147
|
-
function
|
|
155
|
+
function introBootBar(frameIdx: number, total: number): string {
|
|
148
156
|
const active = Math.max(
|
|
149
157
|
1,
|
|
150
158
|
Math.ceil(((frameIdx + 1) / total) * BOOT_BAR_WIDTH),
|
|
@@ -152,22 +160,507 @@ function bootBar(frameIdx: number, total: number): string {
|
|
|
152
160
|
return " " + "▰".repeat(active) + "▱".repeat(BOOT_BAR_WIDTH - active);
|
|
153
161
|
}
|
|
154
162
|
|
|
163
|
+
function introBootProgress(frameIdx: number, total: number): number {
|
|
164
|
+
return Math.max(0, Math.min(1, (frameIdx + 1) / total));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function gaugeBar(progress: number, width: number): string {
|
|
168
|
+
const safeWidth = Math.max(1, width);
|
|
169
|
+
const filled = Math.max(
|
|
170
|
+
0,
|
|
171
|
+
Math.min(safeWidth, Math.round(progress * safeWidth)),
|
|
172
|
+
);
|
|
173
|
+
return `${"█".repeat(filled)}${"░".repeat(safeWidth - filled)}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function titledPanelBottom(width: number): string {
|
|
177
|
+
return `╰${"─".repeat(Math.max(0, width - 2))}╯`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function fixedDisplayRows(
|
|
181
|
+
input: string,
|
|
182
|
+
width: number,
|
|
183
|
+
rowCount: number,
|
|
184
|
+
): string[] {
|
|
185
|
+
const safeWidth = Math.max(1, width);
|
|
186
|
+
const rows: string[] = [];
|
|
187
|
+
const words = input.replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
|
|
188
|
+
let cursor = 0;
|
|
189
|
+
|
|
190
|
+
while (rows.length < rowCount && cursor < words.length) {
|
|
191
|
+
const remaining = words.slice(cursor).join(" ");
|
|
192
|
+
if (rows.length === rowCount - 1 || displayWidth(remaining) <= safeWidth) {
|
|
193
|
+
rows.push(fitDisplayText(remaining, safeWidth));
|
|
194
|
+
cursor = words.length;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let row = words[cursor] ?? "";
|
|
199
|
+
cursor += 1;
|
|
200
|
+
if (displayWidth(row) > safeWidth) {
|
|
201
|
+
rows.push(fitDisplayText(row, safeWidth));
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
while (cursor < words.length) {
|
|
206
|
+
const next = words[cursor] ?? "";
|
|
207
|
+
const candidate = `${row} ${next}`;
|
|
208
|
+
if (displayWidth(candidate) > safeWidth) break;
|
|
209
|
+
row = candidate;
|
|
210
|
+
cursor += 1;
|
|
211
|
+
}
|
|
212
|
+
rows.push(row);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
while (rows.length < rowCount) rows.push("");
|
|
216
|
+
return rows;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
type IntroColorPhase = "early" | "middle" | "late";
|
|
220
|
+
|
|
221
|
+
function introTotalFrames(width: number): number {
|
|
222
|
+
return width < 72 ? COMPACT_INTRO_NOTES.length : INTRO_FRAMES.length;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function introFrameDelayMs(frameIdx: number, width: number): number {
|
|
226
|
+
if (width < 72) return COMPACT_INTRO_DELAY_MS;
|
|
227
|
+
return (
|
|
228
|
+
INTRO_FRAMES[frameIdx] ?? INTRO_FRAMES[INTRO_FRAMES.length - 1]!
|
|
229
|
+
).delayMs;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function introColorPhase(frameIdx: number, total: number): IntroColorPhase {
|
|
233
|
+
if (frameIdx < total / 3) return "early";
|
|
234
|
+
if (frameIdx < (total * 2) / 3) return "middle";
|
|
235
|
+
return "late";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function introPhaseColor(
|
|
239
|
+
phase: IntroColorPhase,
|
|
240
|
+
colors: { error: string; warning: string; primaryLight: string },
|
|
241
|
+
): string {
|
|
242
|
+
return phase === "early"
|
|
243
|
+
? colors.error
|
|
244
|
+
: phase === "middle"
|
|
245
|
+
? colors.warning
|
|
246
|
+
: colors.primaryLight;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function introSnapshot(frameIdx: number, width: number) {
|
|
250
|
+
const compact = width < 72;
|
|
251
|
+
const total = introTotalFrames(width);
|
|
252
|
+
const boundedFrameIdx = Math.min(frameIdx, total - 1);
|
|
253
|
+
const state =
|
|
254
|
+
INTRO_FRAMES[boundedFrameIdx] ?? INTRO_FRAMES[INTRO_FRAMES.length - 1]!;
|
|
255
|
+
const note = compact
|
|
256
|
+
? COMPACT_INTRO_NOTES[
|
|
257
|
+
Math.min(boundedFrameIdx, COMPACT_INTRO_NOTES.length - 1)
|
|
258
|
+
]!
|
|
259
|
+
: state.note;
|
|
260
|
+
return {
|
|
261
|
+
bar: introBootBar(boundedFrameIdx, total),
|
|
262
|
+
colorPhase: introColorPhase(frameIdx, total),
|
|
263
|
+
frameIdx: boundedFrameIdx,
|
|
264
|
+
note,
|
|
265
|
+
progress: introBootProgress(boundedFrameIdx, total),
|
|
266
|
+
state,
|
|
267
|
+
status: `${INTRO_STATUS_PREFIX}${note}`,
|
|
268
|
+
total,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function useIntroAnimation(
|
|
273
|
+
width: number,
|
|
274
|
+
active: boolean,
|
|
275
|
+
onComplete?: () => void,
|
|
276
|
+
) {
|
|
277
|
+
const [frameIdx, setFrameIdx] = useState(0);
|
|
278
|
+
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
if (!active) {
|
|
281
|
+
setFrameIdx(0);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const total = introTotalFrames(width);
|
|
286
|
+
if (frameIdx >= total - 1) {
|
|
287
|
+
if (!onComplete) return;
|
|
288
|
+
const handle = setTimeout(onComplete, SETTLE_HOLD_MS);
|
|
289
|
+
return () => clearTimeout(handle);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const handle = setTimeout(() => {
|
|
293
|
+
setFrameIdx((idx) => Math.min(idx + 1, total - 1));
|
|
294
|
+
}, introFrameDelayMs(frameIdx, width));
|
|
295
|
+
return () => clearTimeout(handle);
|
|
296
|
+
}, [active, frameIdx, onComplete, width]);
|
|
297
|
+
|
|
298
|
+
return useMemo(() => introSnapshot(frameIdx, width), [frameIdx, width]);
|
|
299
|
+
}
|
|
300
|
+
|
|
155
301
|
function TipsPanel({ width }: { width: number }) {
|
|
156
302
|
const t = useTheme();
|
|
157
303
|
const textWidth = Math.max(1, width);
|
|
304
|
+
const innerWidth = Math.max(1, textWidth - 4);
|
|
305
|
+
const title = "Tips";
|
|
306
|
+
const titlePrefix = "╭─ ";
|
|
307
|
+
const titleSuffix = " ";
|
|
308
|
+
const titleRule = "─".repeat(
|
|
309
|
+
Math.max(
|
|
310
|
+
0,
|
|
311
|
+
textWidth -
|
|
312
|
+
displayWidth(titlePrefix) -
|
|
313
|
+
displayWidth(title) -
|
|
314
|
+
displayWidth(titleSuffix) -
|
|
315
|
+
displayWidth("╮"),
|
|
316
|
+
),
|
|
317
|
+
);
|
|
158
318
|
return (
|
|
159
319
|
<Box flexDirection="column" width={textWidth}>
|
|
160
|
-
<Text
|
|
161
|
-
|
|
320
|
+
<Text color={t.primary}>
|
|
321
|
+
{titlePrefix}
|
|
322
|
+
<Text bold color={t.primaryLight}>{title}</Text>
|
|
323
|
+
{titleSuffix}
|
|
324
|
+
{titleRule}
|
|
325
|
+
╮
|
|
162
326
|
</Text>
|
|
163
|
-
|
|
164
|
-
{
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
327
|
+
{STARTUP_TIPS.map((tip, idx) => {
|
|
328
|
+
const label = `${idx + 1}. `;
|
|
329
|
+
const tipWidth = Math.max(1, innerWidth - displayWidth(label));
|
|
330
|
+
const clippedTip = fitDisplayText(tip, tipWidth);
|
|
331
|
+
const content = `${label}${clippedTip}`;
|
|
332
|
+
return (
|
|
333
|
+
<Text key={tip}>
|
|
334
|
+
<Text color={t.primary}>│ </Text>
|
|
335
|
+
<Text color={t.primaryLight}>{label}</Text>
|
|
336
|
+
<Text color={t.dim}>{clippedTip}</Text>
|
|
337
|
+
<Text color={t.primary}>
|
|
338
|
+
{" ".repeat(Math.max(0, innerWidth - displayWidth(content)))} │
|
|
339
|
+
</Text>
|
|
168
340
|
</Text>
|
|
169
|
-
)
|
|
341
|
+
);
|
|
342
|
+
})}
|
|
343
|
+
<Text color={t.primary}>{titledPanelBottom(textWidth)}</Text>
|
|
344
|
+
</Box>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
type MoodTone = "error" | "primaryLight" | "warning";
|
|
349
|
+
|
|
350
|
+
interface MoodPosture {
|
|
351
|
+
badge: string;
|
|
352
|
+
detail: string;
|
|
353
|
+
tone: MoodTone;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const NAMED_MOOD_POSTURES: Record<string, readonly MoodPosture[]> = {
|
|
357
|
+
angry: [
|
|
358
|
+
{
|
|
359
|
+
badge: "HOSTILE TENDER",
|
|
360
|
+
detail: "board patience: vaporized",
|
|
361
|
+
tone: "error",
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
badge: "REDLINE FEVER",
|
|
365
|
+
detail: "counsel posture: braced",
|
|
366
|
+
tone: "error",
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
badge: "FEE HAWK",
|
|
370
|
+
detail: "intern confidence: first pass only",
|
|
371
|
+
tone: "warning",
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
exhausted: [
|
|
375
|
+
{
|
|
376
|
+
badge: "COFFEE DEBT",
|
|
377
|
+
detail: "intern confidence: ceremonial",
|
|
378
|
+
tone: "warning",
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
badge: "QUORUM NAPPING",
|
|
382
|
+
detail: "board patience: on fumes",
|
|
383
|
+
tone: "primaryLight",
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
badge: "LATE CLOSE",
|
|
387
|
+
detail: "risk posture: blinking slowly",
|
|
388
|
+
tone: "warning",
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
generous: [
|
|
392
|
+
{
|
|
393
|
+
badge: "FEE HOLIDAY",
|
|
394
|
+
detail: "board patience: briefly subsidized",
|
|
395
|
+
tone: "primaryLight",
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
badge: "SOFT CLOSE",
|
|
399
|
+
detail: "risk posture: laminated optimism",
|
|
400
|
+
tone: "primaryLight",
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
badge: "COUNSEL NEAR",
|
|
404
|
+
detail: "intern confidence: first pass only",
|
|
405
|
+
tone: "error",
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
manic: [
|
|
409
|
+
{
|
|
410
|
+
badge: "DEAL SPIRAL",
|
|
411
|
+
detail: "committee pulse: overclocked",
|
|
412
|
+
tone: "warning",
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
badge: "CALENDAR HEAT",
|
|
416
|
+
detail: "board patience: rescheduled twice",
|
|
417
|
+
tone: "warning",
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
badge: "TERM SHEET TORNADO",
|
|
421
|
+
detail: "risk posture: wearing a helmet",
|
|
422
|
+
tone: "error",
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
paranoid: [
|
|
426
|
+
{
|
|
427
|
+
badge: "RISK BUNKER",
|
|
428
|
+
detail: "board patience: subpoena-ready",
|
|
429
|
+
tone: "error",
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
badge: "BURNER ROOM",
|
|
433
|
+
detail: "counsel posture: whispering",
|
|
434
|
+
tone: "warning",
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
badge: "BOARD LOCKED",
|
|
438
|
+
detail: "committee pulse: encrypted",
|
|
439
|
+
tone: "error",
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
ruthless: [
|
|
443
|
+
{
|
|
444
|
+
badge: "FEE HAWK",
|
|
445
|
+
detail: "risk posture: smiling through counsel",
|
|
446
|
+
tone: "primaryLight",
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
badge: "MANDATE CLAW",
|
|
450
|
+
detail: "board patience: non-appealable",
|
|
451
|
+
tone: "warning",
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
badge: "COVENANT TEETH",
|
|
455
|
+
detail: "intern confidence: collateralized",
|
|
456
|
+
tone: "error",
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
victorious: [
|
|
460
|
+
{
|
|
461
|
+
badge: "TAPE PARADE",
|
|
462
|
+
detail: "intern confidence: legally inadvisable",
|
|
463
|
+
tone: "warning",
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
badge: "BELL RUNG",
|
|
467
|
+
detail: "board patience: temporarily restored",
|
|
468
|
+
tone: "primaryLight",
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
badge: "TROPHY FILING",
|
|
472
|
+
detail: "counsel posture: drafting confetti",
|
|
473
|
+
tone: "warning",
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const FALLBACK_MOOD_POSTURES: readonly MoodPosture[] = [
|
|
479
|
+
{
|
|
480
|
+
badge: "CALENDAR HEAT",
|
|
481
|
+
detail: "board patience: rescheduled twice",
|
|
482
|
+
tone: "warning",
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
badge: "SOFT CLOSE",
|
|
486
|
+
detail: "risk posture: laminated optimism",
|
|
487
|
+
tone: "primaryLight",
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
badge: "COUNSEL NEAR",
|
|
491
|
+
detail: "intern confidence: first pass only",
|
|
492
|
+
tone: "error",
|
|
493
|
+
},
|
|
494
|
+
];
|
|
495
|
+
|
|
496
|
+
function hashText(input: string): number {
|
|
497
|
+
return Array.from(input).reduce(
|
|
498
|
+
(sum, char) => sum + (char.codePointAt(0) ?? 0),
|
|
499
|
+
0,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function moodPosture(mood: string, seed: number): MoodPosture {
|
|
504
|
+
const key = mood.trim().toLowerCase();
|
|
505
|
+
const pool = NAMED_MOOD_POSTURES[key] ?? FALLBACK_MOOD_POSTURES;
|
|
506
|
+
return pool[Math.abs(hashText(key) + seed) % pool.length] ?? pool[0]!;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function moodToneColor(t: ReturnType<typeof useTheme>, tone: MoodTone): string {
|
|
510
|
+
return tone === "error"
|
|
511
|
+
? t.error
|
|
512
|
+
: tone === "warning"
|
|
513
|
+
? t.warning
|
|
514
|
+
: t.primaryLight;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function bootPostureDetail(progress: number): string {
|
|
518
|
+
if (progress < 0.25) return "committee pulse: suspicious";
|
|
519
|
+
if (progress < 0.5) return "fee antenna: extending";
|
|
520
|
+
if (progress < 0.75) return "counsel posture: stiffening";
|
|
521
|
+
return "board patience: nearly loaded";
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function MoodReadout({
|
|
525
|
+
mood,
|
|
526
|
+
progress = 1,
|
|
527
|
+
progressColor,
|
|
528
|
+
width,
|
|
529
|
+
}: {
|
|
530
|
+
mood?: string;
|
|
531
|
+
progress?: number;
|
|
532
|
+
progressColor?: string;
|
|
533
|
+
width: number;
|
|
534
|
+
}) {
|
|
535
|
+
const t = useTheme();
|
|
536
|
+
if (!mood) return null;
|
|
537
|
+
|
|
538
|
+
const postureSeed = useMemo(() => Math.floor(Math.random() * 1_000_000_000), []);
|
|
539
|
+
const boundedProgress = Math.max(0, Math.min(1, progress));
|
|
540
|
+
const normalizedMood = mood.toUpperCase();
|
|
541
|
+
const posture = moodPosture(mood, postureSeed);
|
|
542
|
+
const postureColor = moodToneColor(t, posture.tone);
|
|
543
|
+
const moodPrefix = "";
|
|
544
|
+
const pct = `${Math.round(boundedProgress * 100)
|
|
545
|
+
.toString()
|
|
546
|
+
.padStart(3, " ")}%`;
|
|
547
|
+
|
|
548
|
+
if (width < 24) {
|
|
549
|
+
const tinyText =
|
|
550
|
+
boundedProgress >= 1
|
|
551
|
+
? `${normalizedMood} / ${posture.badge}`
|
|
552
|
+
: pct;
|
|
553
|
+
return (
|
|
554
|
+
<Box width={Math.max(1, width)}>
|
|
555
|
+
<Text
|
|
556
|
+
color={
|
|
557
|
+
boundedProgress >= 1 ? postureColor : progressColor ?? t.primaryLight
|
|
558
|
+
}
|
|
559
|
+
>
|
|
560
|
+
{fitDisplayText(tinyText, Math.max(1, width))}
|
|
561
|
+
</Text>
|
|
170
562
|
</Box>
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const panelWidth = Math.max(18, Math.min(44, width));
|
|
567
|
+
const innerWidth = Math.max(1, panelWidth - 4);
|
|
568
|
+
const title = "Mood";
|
|
569
|
+
const isSettled = boundedProgress >= 1;
|
|
570
|
+
const topPrefix = "╭─ ";
|
|
571
|
+
const topSuffix = " ";
|
|
572
|
+
const topRule = "─".repeat(
|
|
573
|
+
Math.max(
|
|
574
|
+
0,
|
|
575
|
+
panelWidth -
|
|
576
|
+
displayWidth(topPrefix) -
|
|
577
|
+
displayWidth(title) -
|
|
578
|
+
displayWidth(topSuffix) -
|
|
579
|
+
displayWidth("╮"),
|
|
580
|
+
),
|
|
581
|
+
);
|
|
582
|
+
const settledSuffix = ` / ${posture.badge}`;
|
|
583
|
+
const compactSettledSuffix = ` / ${fitDisplayText(posture.badge, 8)}`;
|
|
584
|
+
const activeSettledSuffix =
|
|
585
|
+
displayWidth(moodPrefix) +
|
|
586
|
+
displayWidth(normalizedMood) +
|
|
587
|
+
displayWidth(settledSuffix) <=
|
|
588
|
+
innerWidth
|
|
589
|
+
? settledSuffix
|
|
590
|
+
: compactSettledSuffix;
|
|
591
|
+
const moodTextWidth = Math.max(
|
|
592
|
+
1,
|
|
593
|
+
innerWidth -
|
|
594
|
+
displayWidth(moodPrefix) -
|
|
595
|
+
(isSettled ? displayWidth(activeSettledSuffix) : 0),
|
|
596
|
+
);
|
|
597
|
+
const moodText = fitDisplayText(normalizedMood, moodTextWidth);
|
|
598
|
+
const settledContent = `${moodPrefix}${moodText}${activeSettledSuffix}`;
|
|
599
|
+
const detailText = fitDisplayText(
|
|
600
|
+
isSettled ? posture.detail : bootPostureDetail(boundedProgress),
|
|
601
|
+
innerWidth,
|
|
602
|
+
);
|
|
603
|
+
const pctWidth = displayWidth(pct);
|
|
604
|
+
const barWidth = Math.max(4, innerWidth - pctWidth - 4);
|
|
605
|
+
const bar = gaugeBar(boundedProgress, barWidth);
|
|
606
|
+
const gaugeContent = `[${bar}] ${pct}`;
|
|
607
|
+
|
|
608
|
+
return (
|
|
609
|
+
<Box flexDirection="column" width={panelWidth}>
|
|
610
|
+
<Text color={t.primaryDim}>
|
|
611
|
+
{topPrefix}
|
|
612
|
+
<Text bold color={t.warning}>
|
|
613
|
+
{title}
|
|
614
|
+
</Text>
|
|
615
|
+
{topSuffix}
|
|
616
|
+
{topRule}
|
|
617
|
+
╮
|
|
618
|
+
</Text>
|
|
619
|
+
{isSettled ? (
|
|
620
|
+
<>
|
|
621
|
+
<Text>
|
|
622
|
+
<Text color={t.primaryDim}>│ </Text>
|
|
623
|
+
<Text bold color={postureColor}>
|
|
624
|
+
{moodText}
|
|
625
|
+
</Text>
|
|
626
|
+
<Text color={t.primaryDim}>{activeSettledSuffix}</Text>
|
|
627
|
+
<Text color={t.primaryDim}>
|
|
628
|
+
{" ".repeat(
|
|
629
|
+
Math.max(0, innerWidth - displayWidth(settledContent)),
|
|
630
|
+
)}
|
|
631
|
+
{" │"}
|
|
632
|
+
</Text>
|
|
633
|
+
</Text>
|
|
634
|
+
<Text>
|
|
635
|
+
<Text color={t.primaryDim}>│ </Text>
|
|
636
|
+
<Text color={t.dim}>{detailText}</Text>
|
|
637
|
+
<Text color={t.primaryDim}>
|
|
638
|
+
{" ".repeat(Math.max(0, innerWidth - displayWidth(detailText)))} │
|
|
639
|
+
</Text>
|
|
640
|
+
</Text>
|
|
641
|
+
</>
|
|
642
|
+
) : (
|
|
643
|
+
<>
|
|
644
|
+
<Text>
|
|
645
|
+
<Text color={t.primaryDim}>│ </Text>
|
|
646
|
+
<Text color={t.primaryDim}>[</Text>
|
|
647
|
+
<Text color={progressColor ?? t.primaryLight}>{bar}</Text>
|
|
648
|
+
<Text color={t.primaryDim}>] </Text>
|
|
649
|
+
<Text color={t.primaryLight}>{pct}</Text>
|
|
650
|
+
<Text color={t.primaryDim}>
|
|
651
|
+
{" ".repeat(Math.max(0, innerWidth - displayWidth(gaugeContent)))} │
|
|
652
|
+
</Text>
|
|
653
|
+
</Text>
|
|
654
|
+
<Text>
|
|
655
|
+
<Text color={t.primaryDim}>│ </Text>
|
|
656
|
+
<Text color={t.dim}>{detailText}</Text>
|
|
657
|
+
<Text color={t.primaryDim}>
|
|
658
|
+
{" ".repeat(Math.max(0, innerWidth - displayWidth(detailText)))} │
|
|
659
|
+
</Text>
|
|
660
|
+
</Text>
|
|
661
|
+
</>
|
|
662
|
+
)}
|
|
663
|
+
<Text color={t.primaryDim}>{titledPanelBottom(panelWidth)}</Text>
|
|
171
664
|
</Box>
|
|
172
665
|
);
|
|
173
666
|
}
|
|
@@ -175,8 +668,10 @@ function TipsPanel({ width }: { width: number }) {
|
|
|
175
668
|
export function MascotDashboard({
|
|
176
669
|
greeting,
|
|
177
670
|
width,
|
|
178
|
-
|
|
179
|
-
|
|
671
|
+
mood,
|
|
672
|
+
bootProgress = 1,
|
|
673
|
+
state = INTRO_FRAMES[INTRO_FRAMES.length - 1]!,
|
|
674
|
+
bar = introBootBar(INTRO_FRAMES.length - 1, INTRO_FRAMES.length),
|
|
180
675
|
barColor,
|
|
181
676
|
mascotStatus = `${INTRO_STATUS_PREFIX}${INTRO_BOOT_NOTES[INTRO_BOOT_NOTES.length - 1]}`,
|
|
182
677
|
dealDesk,
|
|
@@ -186,7 +681,7 @@ export function MascotDashboard({
|
|
|
186
681
|
const tinyTerminal = width < 21;
|
|
187
682
|
const compact = width < 72;
|
|
188
683
|
const sideBySide = width >= 112;
|
|
189
|
-
const available = compact ? Math.max(1, width - 1) : Math.max(28, width
|
|
684
|
+
const available = compact ? Math.max(1, width - 1) : Math.max(28, width);
|
|
190
685
|
const innerWidth = compact
|
|
191
686
|
? available
|
|
192
687
|
: Math.max(24, available - FRAME_CHROME_WIDTH);
|
|
@@ -195,14 +690,14 @@ export function MascotDashboard({
|
|
|
195
690
|
: sideBySide
|
|
196
691
|
? Math.max(
|
|
197
692
|
MASCOT_WIDTH + GUTTER_WIDTH + 24,
|
|
198
|
-
Math.floor((innerWidth -
|
|
693
|
+
Math.floor((innerWidth - SPLIT_DIVIDER_WIDTH) / 2),
|
|
199
694
|
)
|
|
200
695
|
: innerWidth;
|
|
201
696
|
const rightColumnWidth = sideBySide
|
|
202
|
-
? Math.max(20, innerWidth - leftPanelWidth)
|
|
697
|
+
? Math.max(20, innerWidth - leftPanelWidth - SPLIT_DIVIDER_WIDTH)
|
|
203
698
|
: innerWidth;
|
|
204
699
|
const rightInnerWidth = sideBySide
|
|
205
|
-
? Math.max(1, rightColumnWidth -
|
|
700
|
+
? Math.max(1, rightColumnWidth - 1)
|
|
206
701
|
: rightColumnWidth;
|
|
207
702
|
const tipsWidth = sideBySide
|
|
208
703
|
? rightInnerWidth
|
|
@@ -210,8 +705,11 @@ export function MascotDashboard({
|
|
|
210
705
|
const copyWidth = compact
|
|
211
706
|
? available
|
|
212
707
|
: sideBySide
|
|
213
|
-
? Math.max(18, leftPanelWidth - MASCOT_WIDTH - GUTTER_WIDTH)
|
|
708
|
+
? Math.max(18, leftPanelWidth - MASCOT_WIDTH - GUTTER_WIDTH - 1)
|
|
214
709
|
: innerWidth;
|
|
710
|
+
const wideGreetingRows = sideBySide
|
|
711
|
+
? fixedDisplayRows(greeting, copyWidth, 2)
|
|
712
|
+
: [];
|
|
215
713
|
|
|
216
714
|
if (tinyTerminal) {
|
|
217
715
|
return (
|
|
@@ -221,6 +719,14 @@ export function MascotDashboard({
|
|
|
221
719
|
Drexler™
|
|
222
720
|
</Text>
|
|
223
721
|
<Text color={t.primaryLight}>{greeting}</Text>
|
|
722
|
+
<Box marginTop={1}>
|
|
723
|
+
<MoodReadout
|
|
724
|
+
mood={mood}
|
|
725
|
+
progress={bootProgress}
|
|
726
|
+
progressColor={resolvedBarColor}
|
|
727
|
+
width={available}
|
|
728
|
+
/>
|
|
729
|
+
</Box>
|
|
224
730
|
{dealDesk ? <Box marginTop={1}>{dealDesk(Math.max(1, available))}</Box> : null}
|
|
225
731
|
</Box>
|
|
226
732
|
);
|
|
@@ -235,6 +741,14 @@ export function MascotDashboard({
|
|
|
235
741
|
Drexler International™
|
|
236
742
|
</Text>
|
|
237
743
|
<Text color={t.primaryLight}>{greeting}</Text>
|
|
744
|
+
<Box marginTop={1}>
|
|
745
|
+
<MoodReadout
|
|
746
|
+
mood={mood}
|
|
747
|
+
progress={bootProgress}
|
|
748
|
+
progressColor={resolvedBarColor}
|
|
749
|
+
width={available}
|
|
750
|
+
/>
|
|
751
|
+
</Box>
|
|
238
752
|
{dealDesk ? <Box marginTop={1}>{dealDesk(Math.max(1, available))}</Box> : null}
|
|
239
753
|
</Box>
|
|
240
754
|
);
|
|
@@ -271,21 +785,48 @@ export function MascotDashboard({
|
|
|
271
785
|
Drexler International™
|
|
272
786
|
</Text>
|
|
273
787
|
<Box height={1} />
|
|
274
|
-
|
|
788
|
+
{sideBySide ? (
|
|
789
|
+
wideGreetingRows.map((row, idx) => (
|
|
790
|
+
<Text key={idx} color={t.primaryLight}>
|
|
791
|
+
{row || " "}
|
|
792
|
+
</Text>
|
|
793
|
+
))
|
|
794
|
+
) : (
|
|
795
|
+
<Text color={t.primaryLight}>{greeting}</Text>
|
|
796
|
+
)}
|
|
275
797
|
<Box height={1} />
|
|
798
|
+
<MoodReadout
|
|
799
|
+
mood={mood}
|
|
800
|
+
progress={bootProgress}
|
|
801
|
+
progressColor={resolvedBarColor}
|
|
802
|
+
width={copyWidth}
|
|
803
|
+
/>
|
|
276
804
|
</Box>
|
|
277
805
|
</Box>
|
|
278
806
|
{sideBySide ? (
|
|
807
|
+
<>
|
|
808
|
+
<Box flexDirection="column" width={SPLIT_DIVIDER_WIDTH} flexShrink={0}>
|
|
809
|
+
{SPLIT_DIVIDER_ROWS.map((idx) => (
|
|
810
|
+
<Text key={idx} color={t.primaryDim}>
|
|
811
|
+
{" │ "}
|
|
812
|
+
</Text>
|
|
813
|
+
))}
|
|
814
|
+
</Box>
|
|
279
815
|
<Box
|
|
280
816
|
flexDirection="column"
|
|
281
817
|
width={rightColumnWidth}
|
|
282
|
-
|
|
283
|
-
borderColor={t.primaryDim}
|
|
284
|
-
paddingLeft={1}
|
|
818
|
+
paddingRight={1}
|
|
285
819
|
>
|
|
286
|
-
<
|
|
287
|
-
|
|
820
|
+
<Box marginLeft={1}>
|
|
821
|
+
<TipsPanel width={Math.max(1, rightInnerWidth - 1)} />
|
|
822
|
+
</Box>
|
|
823
|
+
{dealDesk ? (
|
|
824
|
+
<Box marginLeft={1}>
|
|
825
|
+
{dealDesk(Math.max(1, rightInnerWidth - 1))}
|
|
826
|
+
</Box>
|
|
827
|
+
) : null}
|
|
288
828
|
</Box>
|
|
829
|
+
</>
|
|
289
830
|
) : (
|
|
290
831
|
<Box marginTop={1} width={tipsWidth} flexDirection="column">
|
|
291
832
|
<TipsPanel width={tipsWidth} />
|
|
@@ -302,7 +843,7 @@ export function MascotIntro({ greeting }: IntroProps) {
|
|
|
302
843
|
const { exit } = useApp();
|
|
303
844
|
const { stdout } = useStdout();
|
|
304
845
|
const [cols, setCols] = useState(stdout?.columns ?? 80);
|
|
305
|
-
const
|
|
846
|
+
const intro = useIntroAnimation(cols, true, exit);
|
|
306
847
|
|
|
307
848
|
useEffect(() => {
|
|
308
849
|
if (!stdout) return;
|
|
@@ -317,56 +858,16 @@ export function MascotIntro({ greeting }: IntroProps) {
|
|
|
317
858
|
if (key.escape || key.return || (key.ctrl && _input === "c")) exit();
|
|
318
859
|
});
|
|
319
860
|
|
|
320
|
-
const
|
|
321
|
-
useEffect(() => {
|
|
322
|
-
return () => {
|
|
323
|
-
mountedRef.current = false;
|
|
324
|
-
};
|
|
325
|
-
}, []);
|
|
326
|
-
|
|
327
|
-
useEffect(() => {
|
|
328
|
-
const compact = cols < 72;
|
|
329
|
-
const total = compact ? COMPACT_NOTES.length : FRAMES.length;
|
|
330
|
-
if (frameIdx >= total - 1) {
|
|
331
|
-
const handle = setTimeout(() => {
|
|
332
|
-
if (mountedRef.current) exit();
|
|
333
|
-
}, SETTLE_HOLD_MS);
|
|
334
|
-
return () => clearTimeout(handle);
|
|
335
|
-
}
|
|
336
|
-
const delay = compact
|
|
337
|
-
? COMPACT_DELAY_MS
|
|
338
|
-
: (FRAMES[frameIdx] ?? FRAMES[FRAMES.length - 1]!).delayMs;
|
|
339
|
-
const handle = setTimeout(() => {
|
|
340
|
-
if (mountedRef.current) setFrameIdx((i) => i + 1);
|
|
341
|
-
}, delay);
|
|
342
|
-
return () => clearTimeout(handle);
|
|
343
|
-
}, [cols, frameIdx, exit]);
|
|
344
|
-
|
|
345
|
-
const state = FRAMES[frameIdx] ?? FRAMES[FRAMES.length - 1]!;
|
|
346
|
-
const compact = cols < 72;
|
|
347
|
-
const bar = bootBar(
|
|
348
|
-
Math.min(frameIdx, compact ? COMPACT_NOTES.length - 1 : FRAMES.length - 1),
|
|
349
|
-
compact ? COMPACT_NOTES.length : FRAMES.length,
|
|
350
|
-
);
|
|
351
|
-
const barColor =
|
|
352
|
-
frameIdx < (compact ? COMPACT_NOTES.length : FRAMES.length) / 3
|
|
353
|
-
? t.error
|
|
354
|
-
: frameIdx < ((compact ? COMPACT_NOTES.length : FRAMES.length) * 2) / 3
|
|
355
|
-
? t.warning
|
|
356
|
-
: t.primaryLight;
|
|
357
|
-
const note = compact
|
|
358
|
-
? COMPACT_NOTES[Math.min(frameIdx, COMPACT_NOTES.length - 1)]!
|
|
359
|
-
: state.note;
|
|
360
|
-
const mascotStatus = `${INTRO_STATUS_PREFIX}${note}`;
|
|
861
|
+
const barColor = introPhaseColor(intro.colorPhase, t);
|
|
361
862
|
|
|
362
863
|
return (
|
|
363
864
|
<MascotDashboard
|
|
364
865
|
greeting={greeting}
|
|
365
866
|
width={cols}
|
|
366
|
-
state={state}
|
|
367
|
-
bar={bar}
|
|
867
|
+
state={intro.state}
|
|
868
|
+
bar={intro.bar}
|
|
368
869
|
barColor={barColor}
|
|
369
|
-
mascotStatus={
|
|
870
|
+
mascotStatus={intro.status}
|
|
370
871
|
/>
|
|
371
872
|
);
|
|
372
873
|
}
|