drexler 0.2.13 → 0.2.15
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 +64 -13
- package/package.json +1 -1
- package/src/commands.ts +127 -20
- package/src/config.ts +141 -32
- package/src/conversation.ts +0 -4
- package/src/index.ts +69 -6
- package/src/pet/petState.ts +408 -0
- package/src/repl.ts +1 -1
- package/src/ui/App.tsx +557 -146
- package/src/ui/CommandPalette.tsx +2 -0
- package/src/ui/DealDeskHeader.tsx +245 -77
- package/src/ui/DeathScreen.tsx +110 -0
- package/src/ui/MarkdownBody.tsx +406 -0
- package/src/ui/MascotIntro.tsx +713 -111
- package/src/ui/Message.tsx +24 -114
- package/src/ui/PetPanel.tsx +537 -0
- package/src/ui/SynergyEvent.tsx +3 -2
- package/src/ui/TranscriptViewport.tsx +461 -70
- package/src/ui/displayContent.ts +117 -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,131 @@ 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
|
-
|
|
138
|
+
|
|
139
|
+
// Width breakpoints (terminal columns).
|
|
140
|
+
const TINY_BREAKPOINT = 21;
|
|
141
|
+
const NARROW_BREAKPOINT = 24;
|
|
142
|
+
const COMPACT_BREAKPOINT = 72;
|
|
143
|
+
const WIDE_BREAKPOINT = 112;
|
|
144
|
+
|
|
145
|
+
// Inner-panel sizing floors / glue.
|
|
146
|
+
const MIN_DASHBOARD_WIDTH = 28;
|
|
147
|
+
const MIN_INNER_WIDTH = 24;
|
|
148
|
+
const MIN_COPY_WIDTH = 18;
|
|
149
|
+
const MIN_RIGHT_COLUMN_WIDTH = 20;
|
|
150
|
+
const MIN_MOOD_PANEL_WIDTH = 18;
|
|
151
|
+
const MAX_MOOD_PANEL_WIDTH = 44;
|
|
152
|
+
const RIGHT_COLUMN_INSET = 1;
|
|
153
|
+
const RIGHT_COLUMN_PAD_RIGHT = 1;
|
|
154
|
+
const LEFT_PANEL_MIN_COPY = 24;
|
|
155
|
+
|
|
156
|
+
export type MascotLayoutMode = "tiny" | "compact" | "stacked" | "split";
|
|
157
|
+
|
|
158
|
+
export interface MascotPanelBox {
|
|
159
|
+
width: number;
|
|
160
|
+
inset: number;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface MascotLayout {
|
|
164
|
+
mode: MascotLayoutMode;
|
|
165
|
+
available: number;
|
|
166
|
+
innerWidth: number;
|
|
167
|
+
leftPanel: MascotPanelBox;
|
|
168
|
+
rightColumn: MascotPanelBox;
|
|
169
|
+
rightChildWidth: number;
|
|
170
|
+
copy: MascotPanelBox;
|
|
171
|
+
mood: MascotPanelBox;
|
|
172
|
+
tips: MascotPanelBox;
|
|
173
|
+
dealDesk: MascotPanelBox;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function computeMascotLayout(width: number): MascotLayout {
|
|
177
|
+
const safeWidth = Math.max(1, Math.floor(width));
|
|
178
|
+
if (safeWidth < TINY_BREAKPOINT) {
|
|
179
|
+
const w = safeWidth;
|
|
180
|
+
return {
|
|
181
|
+
mode: "tiny",
|
|
182
|
+
available: w,
|
|
183
|
+
innerWidth: w,
|
|
184
|
+
leftPanel: { width: w, inset: 0 },
|
|
185
|
+
rightColumn: { width: 0, inset: 0 },
|
|
186
|
+
rightChildWidth: 0,
|
|
187
|
+
copy: { width: w, inset: 0 },
|
|
188
|
+
mood: { width: w, inset: 0 },
|
|
189
|
+
tips: { width: w, inset: 0 },
|
|
190
|
+
dealDesk: { width: w, inset: 0 },
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (safeWidth < COMPACT_BREAKPOINT) {
|
|
194
|
+
const w = Math.max(1, safeWidth - 1);
|
|
195
|
+
return {
|
|
196
|
+
mode: "compact",
|
|
197
|
+
available: w,
|
|
198
|
+
innerWidth: w,
|
|
199
|
+
leftPanel: { width: w, inset: 1 },
|
|
200
|
+
rightColumn: { width: 0, inset: 0 },
|
|
201
|
+
rightChildWidth: 0,
|
|
202
|
+
copy: { width: w, inset: 0 },
|
|
203
|
+
mood: { width: w, inset: 0 },
|
|
204
|
+
tips: { width: w, inset: 0 },
|
|
205
|
+
dealDesk: { width: w, inset: 0 },
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const available = Math.max(MIN_DASHBOARD_WIDTH, safeWidth);
|
|
209
|
+
const innerWidth = Math.max(MIN_INNER_WIDTH, available - FRAME_CHROME_WIDTH);
|
|
210
|
+
if (safeWidth < WIDE_BREAKPOINT) {
|
|
211
|
+
return {
|
|
212
|
+
mode: "stacked",
|
|
213
|
+
available,
|
|
214
|
+
innerWidth,
|
|
215
|
+
leftPanel: { width: innerWidth, inset: 0 },
|
|
216
|
+
rightColumn: { width: 0, inset: 0 },
|
|
217
|
+
rightChildWidth: innerWidth,
|
|
218
|
+
copy: { width: innerWidth, inset: 0 },
|
|
219
|
+
mood: { width: innerWidth, inset: 0 },
|
|
220
|
+
tips: { width: innerWidth, inset: 0 },
|
|
221
|
+
dealDesk: { width: innerWidth, inset: 0 },
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const leftPanelWidth = Math.max(
|
|
225
|
+
MASCOT_WIDTH + GUTTER_WIDTH + LEFT_PANEL_MIN_COPY,
|
|
226
|
+
Math.floor((innerWidth - SPLIT_DIVIDER_WIDTH) / 2),
|
|
227
|
+
);
|
|
228
|
+
const rightColumnWidth = Math.max(
|
|
229
|
+
MIN_RIGHT_COLUMN_WIDTH,
|
|
230
|
+
innerWidth - leftPanelWidth - SPLIT_DIVIDER_WIDTH,
|
|
231
|
+
);
|
|
232
|
+
const rightInner = Math.max(1, rightColumnWidth - RIGHT_COLUMN_PAD_RIGHT);
|
|
233
|
+
const rightChildWidth = Math.max(1, rightInner - RIGHT_COLUMN_INSET);
|
|
234
|
+
const copyWidth = Math.max(
|
|
235
|
+
MIN_COPY_WIDTH,
|
|
236
|
+
leftPanelWidth - MASCOT_WIDTH - GUTTER_WIDTH - 1,
|
|
237
|
+
);
|
|
238
|
+
return {
|
|
239
|
+
mode: "split",
|
|
240
|
+
available,
|
|
241
|
+
innerWidth,
|
|
242
|
+
leftPanel: { width: leftPanelWidth, inset: 0 },
|
|
243
|
+
rightColumn: { width: rightColumnWidth, inset: 0 },
|
|
244
|
+
rightChildWidth,
|
|
245
|
+
copy: { width: copyWidth, inset: 0 },
|
|
246
|
+
mood: { width: copyWidth, inset: 0 },
|
|
247
|
+
tips: { width: rightChildWidth, inset: RIGHT_COLUMN_INSET },
|
|
248
|
+
dealDesk: { width: rightChildWidth, inset: RIGHT_COLUMN_INSET },
|
|
249
|
+
};
|
|
250
|
+
}
|
|
132
251
|
|
|
133
252
|
interface IntroProps {
|
|
134
253
|
greeting: string;
|
|
@@ -137,6 +256,8 @@ interface IntroProps {
|
|
|
137
256
|
interface MascotDashboardProps {
|
|
138
257
|
greeting: string;
|
|
139
258
|
width: number;
|
|
259
|
+
mood?: string;
|
|
260
|
+
bootProgress?: number;
|
|
140
261
|
state?: MascotState;
|
|
141
262
|
bar?: string;
|
|
142
263
|
barColor?: string;
|
|
@@ -144,7 +265,7 @@ interface MascotDashboardProps {
|
|
|
144
265
|
dealDesk?: (width: number) => ReactNode;
|
|
145
266
|
}
|
|
146
267
|
|
|
147
|
-
function
|
|
268
|
+
function introBootBar(frameIdx: number, total: number): string {
|
|
148
269
|
const active = Math.max(
|
|
149
270
|
1,
|
|
150
271
|
Math.ceil(((frameIdx + 1) / total) * BOOT_BAR_WIDTH),
|
|
@@ -152,22 +273,509 @@ function bootBar(frameIdx: number, total: number): string {
|
|
|
152
273
|
return " " + "▰".repeat(active) + "▱".repeat(BOOT_BAR_WIDTH - active);
|
|
153
274
|
}
|
|
154
275
|
|
|
276
|
+
function introBootProgress(frameIdx: number, total: number): number {
|
|
277
|
+
return Math.max(0, Math.min(1, (frameIdx + 1) / total));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function gaugeBar(progress: number, width: number): string {
|
|
281
|
+
const safeWidth = Math.max(1, width);
|
|
282
|
+
const filled = Math.max(
|
|
283
|
+
0,
|
|
284
|
+
Math.min(safeWidth, Math.round(progress * safeWidth)),
|
|
285
|
+
);
|
|
286
|
+
return `${"█".repeat(filled)}${"░".repeat(safeWidth - filled)}`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function titledPanelBottom(width: number): string {
|
|
290
|
+
return `╰${"─".repeat(Math.max(0, width - 2))}╯`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function fixedDisplayRows(
|
|
294
|
+
input: string,
|
|
295
|
+
width: number,
|
|
296
|
+
rowCount: number,
|
|
297
|
+
): string[] {
|
|
298
|
+
const safeWidth = Math.max(1, width);
|
|
299
|
+
const rows: string[] = [];
|
|
300
|
+
const words = input.replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
|
|
301
|
+
let cursor = 0;
|
|
302
|
+
|
|
303
|
+
while (rows.length < rowCount && cursor < words.length) {
|
|
304
|
+
const remaining = words.slice(cursor).join(" ");
|
|
305
|
+
if (rows.length === rowCount - 1 || displayWidth(remaining) <= safeWidth) {
|
|
306
|
+
rows.push(fitDisplayText(remaining, safeWidth));
|
|
307
|
+
cursor = words.length;
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
let row = words[cursor] ?? "";
|
|
312
|
+
cursor += 1;
|
|
313
|
+
if (displayWidth(row) > safeWidth) {
|
|
314
|
+
rows.push(fitDisplayText(row, safeWidth));
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
while (cursor < words.length) {
|
|
319
|
+
const next = words[cursor] ?? "";
|
|
320
|
+
const candidate = `${row} ${next}`;
|
|
321
|
+
if (displayWidth(candidate) > safeWidth) break;
|
|
322
|
+
row = candidate;
|
|
323
|
+
cursor += 1;
|
|
324
|
+
}
|
|
325
|
+
rows.push(row);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
while (rows.length < rowCount) rows.push("");
|
|
329
|
+
return rows;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
type IntroColorPhase = "early" | "middle" | "late";
|
|
333
|
+
|
|
334
|
+
function introTotalFrames(width: number): number {
|
|
335
|
+
return width < COMPACT_BREAKPOINT
|
|
336
|
+
? COMPACT_INTRO_NOTES.length
|
|
337
|
+
: INTRO_FRAMES.length;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function introFrameDelayMs(frameIdx: number, width: number): number {
|
|
341
|
+
if (width < COMPACT_BREAKPOINT) return COMPACT_INTRO_DELAY_MS;
|
|
342
|
+
return (
|
|
343
|
+
INTRO_FRAMES[frameIdx] ?? INTRO_FRAMES[INTRO_FRAMES.length - 1]!
|
|
344
|
+
).delayMs;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function introColorPhase(frameIdx: number, total: number): IntroColorPhase {
|
|
348
|
+
if (frameIdx < total / 3) return "early";
|
|
349
|
+
if (frameIdx < (total * 2) / 3) return "middle";
|
|
350
|
+
return "late";
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function introPhaseColor(
|
|
354
|
+
phase: IntroColorPhase,
|
|
355
|
+
colors: { error: string; warning: string; primaryLight: string },
|
|
356
|
+
): string {
|
|
357
|
+
return phase === "early"
|
|
358
|
+
? colors.error
|
|
359
|
+
: phase === "middle"
|
|
360
|
+
? colors.warning
|
|
361
|
+
: colors.primaryLight;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function introSnapshot(frameIdx: number, width: number) {
|
|
365
|
+
const compact = width < COMPACT_BREAKPOINT;
|
|
366
|
+
const total = introTotalFrames(width);
|
|
367
|
+
const boundedFrameIdx = Math.min(frameIdx, total - 1);
|
|
368
|
+
const state =
|
|
369
|
+
INTRO_FRAMES[boundedFrameIdx] ?? INTRO_FRAMES[INTRO_FRAMES.length - 1]!;
|
|
370
|
+
const note = compact
|
|
371
|
+
? COMPACT_INTRO_NOTES[
|
|
372
|
+
Math.min(boundedFrameIdx, COMPACT_INTRO_NOTES.length - 1)
|
|
373
|
+
]!
|
|
374
|
+
: state.note;
|
|
375
|
+
return {
|
|
376
|
+
bar: introBootBar(boundedFrameIdx, total),
|
|
377
|
+
colorPhase: introColorPhase(frameIdx, total),
|
|
378
|
+
frameIdx: boundedFrameIdx,
|
|
379
|
+
note,
|
|
380
|
+
progress: introBootProgress(boundedFrameIdx, total),
|
|
381
|
+
state,
|
|
382
|
+
status: `${INTRO_STATUS_PREFIX}${note}`,
|
|
383
|
+
total,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function useIntroAnimation(
|
|
388
|
+
width: number,
|
|
389
|
+
active: boolean,
|
|
390
|
+
onComplete?: () => void,
|
|
391
|
+
) {
|
|
392
|
+
const [frameIdx, setFrameIdx] = useState(0);
|
|
393
|
+
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
if (!active) {
|
|
396
|
+
setFrameIdx(0);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const total = introTotalFrames(width);
|
|
401
|
+
if (frameIdx >= total - 1) {
|
|
402
|
+
if (!onComplete) return;
|
|
403
|
+
const handle = setTimeout(onComplete, SETTLE_HOLD_MS);
|
|
404
|
+
return () => clearTimeout(handle);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const handle = setTimeout(() => {
|
|
408
|
+
setFrameIdx((idx) => Math.min(idx + 1, total - 1));
|
|
409
|
+
}, introFrameDelayMs(frameIdx, width));
|
|
410
|
+
return () => clearTimeout(handle);
|
|
411
|
+
}, [active, frameIdx, onComplete, width]);
|
|
412
|
+
|
|
413
|
+
return useMemo(() => introSnapshot(frameIdx, width), [frameIdx, width]);
|
|
414
|
+
}
|
|
415
|
+
|
|
155
416
|
function TipsPanel({ width }: { width: number }) {
|
|
156
417
|
const t = useTheme();
|
|
157
418
|
const textWidth = Math.max(1, width);
|
|
419
|
+
const innerWidth = Math.max(1, textWidth - 4);
|
|
420
|
+
const title = "Tips";
|
|
421
|
+
const titlePrefix = "╭─ ";
|
|
422
|
+
const titleSuffix = " ";
|
|
423
|
+
const titleRule = "─".repeat(
|
|
424
|
+
Math.max(
|
|
425
|
+
0,
|
|
426
|
+
textWidth -
|
|
427
|
+
displayWidth(titlePrefix) -
|
|
428
|
+
displayWidth(title) -
|
|
429
|
+
displayWidth(titleSuffix) -
|
|
430
|
+
displayWidth("╮"),
|
|
431
|
+
),
|
|
432
|
+
);
|
|
158
433
|
return (
|
|
159
434
|
<Box flexDirection="column" width={textWidth}>
|
|
160
|
-
<Text
|
|
161
|
-
|
|
435
|
+
<Text color={t.primary}>
|
|
436
|
+
{titlePrefix}
|
|
437
|
+
<Text bold color={t.primaryLight}>{title}</Text>
|
|
438
|
+
{titleSuffix}
|
|
439
|
+
{titleRule}
|
|
440
|
+
╮
|
|
162
441
|
</Text>
|
|
163
|
-
|
|
164
|
-
{
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
442
|
+
{STARTUP_TIPS.map((tip, idx) => {
|
|
443
|
+
const label = `${idx + 1}. `;
|
|
444
|
+
const tipWidth = Math.max(1, innerWidth - displayWidth(label));
|
|
445
|
+
const clippedTip = fitDisplayText(tip, tipWidth);
|
|
446
|
+
const content = `${label}${clippedTip}`;
|
|
447
|
+
return (
|
|
448
|
+
<Text key={tip}>
|
|
449
|
+
<Text color={t.primary}>│ </Text>
|
|
450
|
+
<Text color={t.primaryLight}>{label}</Text>
|
|
451
|
+
<Text color={t.dim}>{clippedTip}</Text>
|
|
452
|
+
<Text color={t.primary}>
|
|
453
|
+
{" ".repeat(Math.max(0, innerWidth - displayWidth(content)))} │
|
|
454
|
+
</Text>
|
|
168
455
|
</Text>
|
|
169
|
-
)
|
|
456
|
+
);
|
|
457
|
+
})}
|
|
458
|
+
<Text color={t.primary}>{titledPanelBottom(textWidth)}</Text>
|
|
459
|
+
</Box>
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
type MoodTone = "error" | "primaryLight" | "warning";
|
|
464
|
+
|
|
465
|
+
interface MoodPosture {
|
|
466
|
+
badge: string;
|
|
467
|
+
detail: string;
|
|
468
|
+
tone: MoodTone;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const NAMED_MOOD_POSTURES: Record<string, readonly MoodPosture[]> = {
|
|
472
|
+
angry: [
|
|
473
|
+
{
|
|
474
|
+
badge: "HOSTILE TENDER",
|
|
475
|
+
detail: "board patience: vaporized",
|
|
476
|
+
tone: "error",
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
badge: "REDLINE FEVER",
|
|
480
|
+
detail: "counsel posture: braced",
|
|
481
|
+
tone: "error",
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
badge: "FEE HAWK",
|
|
485
|
+
detail: "intern confidence: first pass only",
|
|
486
|
+
tone: "warning",
|
|
487
|
+
},
|
|
488
|
+
],
|
|
489
|
+
exhausted: [
|
|
490
|
+
{
|
|
491
|
+
badge: "COFFEE DEBT",
|
|
492
|
+
detail: "intern confidence: ceremonial",
|
|
493
|
+
tone: "warning",
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
badge: "QUORUM NAPPING",
|
|
497
|
+
detail: "board patience: on fumes",
|
|
498
|
+
tone: "primaryLight",
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
badge: "LATE CLOSE",
|
|
502
|
+
detail: "risk posture: blinking slowly",
|
|
503
|
+
tone: "warning",
|
|
504
|
+
},
|
|
505
|
+
],
|
|
506
|
+
generous: [
|
|
507
|
+
{
|
|
508
|
+
badge: "FEE HOLIDAY",
|
|
509
|
+
detail: "board patience: briefly subsidized",
|
|
510
|
+
tone: "primaryLight",
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
badge: "SOFT CLOSE",
|
|
514
|
+
detail: "risk posture: laminated optimism",
|
|
515
|
+
tone: "primaryLight",
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
badge: "COUNSEL NEAR",
|
|
519
|
+
detail: "intern confidence: first pass only",
|
|
520
|
+
tone: "error",
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
manic: [
|
|
524
|
+
{
|
|
525
|
+
badge: "DEAL SPIRAL",
|
|
526
|
+
detail: "committee pulse: overclocked",
|
|
527
|
+
tone: "warning",
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
badge: "CALENDAR HEAT",
|
|
531
|
+
detail: "board patience: rescheduled twice",
|
|
532
|
+
tone: "warning",
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
badge: "TERM SHEET TORNADO",
|
|
536
|
+
detail: "risk posture: wearing a helmet",
|
|
537
|
+
tone: "error",
|
|
538
|
+
},
|
|
539
|
+
],
|
|
540
|
+
paranoid: [
|
|
541
|
+
{
|
|
542
|
+
badge: "RISK BUNKER",
|
|
543
|
+
detail: "board patience: subpoena-ready",
|
|
544
|
+
tone: "error",
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
badge: "BURNER ROOM",
|
|
548
|
+
detail: "counsel posture: whispering",
|
|
549
|
+
tone: "warning",
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
badge: "BOARD LOCKED",
|
|
553
|
+
detail: "committee pulse: encrypted",
|
|
554
|
+
tone: "error",
|
|
555
|
+
},
|
|
556
|
+
],
|
|
557
|
+
ruthless: [
|
|
558
|
+
{
|
|
559
|
+
badge: "FEE HAWK",
|
|
560
|
+
detail: "risk posture: smiling through counsel",
|
|
561
|
+
tone: "primaryLight",
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
badge: "MANDATE CLAW",
|
|
565
|
+
detail: "board patience: non-appealable",
|
|
566
|
+
tone: "warning",
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
badge: "COVENANT TEETH",
|
|
570
|
+
detail: "intern confidence: collateralized",
|
|
571
|
+
tone: "error",
|
|
572
|
+
},
|
|
573
|
+
],
|
|
574
|
+
victorious: [
|
|
575
|
+
{
|
|
576
|
+
badge: "TAPE PARADE",
|
|
577
|
+
detail: "intern confidence: legally inadvisable",
|
|
578
|
+
tone: "warning",
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
badge: "BELL RUNG",
|
|
582
|
+
detail: "board patience: temporarily restored",
|
|
583
|
+
tone: "primaryLight",
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
badge: "TROPHY FILING",
|
|
587
|
+
detail: "counsel posture: drafting confetti",
|
|
588
|
+
tone: "warning",
|
|
589
|
+
},
|
|
590
|
+
],
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const FALLBACK_MOOD_POSTURES: readonly MoodPosture[] = [
|
|
594
|
+
{
|
|
595
|
+
badge: "CALENDAR HEAT",
|
|
596
|
+
detail: "board patience: rescheduled twice",
|
|
597
|
+
tone: "warning",
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
badge: "SOFT CLOSE",
|
|
601
|
+
detail: "risk posture: laminated optimism",
|
|
602
|
+
tone: "primaryLight",
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
badge: "COUNSEL NEAR",
|
|
606
|
+
detail: "intern confidence: first pass only",
|
|
607
|
+
tone: "error",
|
|
608
|
+
},
|
|
609
|
+
];
|
|
610
|
+
|
|
611
|
+
function hashText(input: string): number {
|
|
612
|
+
return Array.from(input).reduce(
|
|
613
|
+
(sum, char) => sum + (char.codePointAt(0) ?? 0),
|
|
614
|
+
0,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function moodPosture(mood: string, seed: number): MoodPosture {
|
|
619
|
+
const key = mood.trim().toLowerCase();
|
|
620
|
+
const pool = NAMED_MOOD_POSTURES[key] ?? FALLBACK_MOOD_POSTURES;
|
|
621
|
+
return pool[Math.abs(hashText(key) + seed) % pool.length] ?? pool[0]!;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function moodToneColor(t: ReturnType<typeof useTheme>, tone: MoodTone): string {
|
|
625
|
+
return tone === "error"
|
|
626
|
+
? t.error
|
|
627
|
+
: tone === "warning"
|
|
628
|
+
? t.warning
|
|
629
|
+
: t.primaryLight;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function bootPostureDetail(progress: number): string {
|
|
633
|
+
if (progress < 0.25) return "committee pulse: suspicious";
|
|
634
|
+
if (progress < 0.5) return "fee antenna: extending";
|
|
635
|
+
if (progress < 0.75) return "counsel posture: stiffening";
|
|
636
|
+
return "board patience: nearly loaded";
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function MoodReadout({
|
|
640
|
+
mood,
|
|
641
|
+
progress = 1,
|
|
642
|
+
progressColor,
|
|
643
|
+
width,
|
|
644
|
+
}: {
|
|
645
|
+
mood?: string;
|
|
646
|
+
progress?: number;
|
|
647
|
+
progressColor?: string;
|
|
648
|
+
width: number;
|
|
649
|
+
}) {
|
|
650
|
+
const t = useTheme();
|
|
651
|
+
if (!mood) return null;
|
|
652
|
+
|
|
653
|
+
const postureSeed = useMemo(() => Math.floor(Math.random() * 1_000_000_000), []);
|
|
654
|
+
const boundedProgress = Math.max(0, Math.min(1, progress));
|
|
655
|
+
const normalizedMood = mood.toUpperCase();
|
|
656
|
+
const posture = moodPosture(mood, postureSeed);
|
|
657
|
+
const postureColor = moodToneColor(t, posture.tone);
|
|
658
|
+
const moodPrefix = "";
|
|
659
|
+
const pct = `${Math.round(boundedProgress * 100)
|
|
660
|
+
.toString()
|
|
661
|
+
.padStart(3, " ")}%`;
|
|
662
|
+
|
|
663
|
+
if (width < NARROW_BREAKPOINT) {
|
|
664
|
+
const tinyText =
|
|
665
|
+
boundedProgress >= 1
|
|
666
|
+
? `${normalizedMood} / ${posture.badge}`
|
|
667
|
+
: pct;
|
|
668
|
+
return (
|
|
669
|
+
<Box width={Math.max(1, width)}>
|
|
670
|
+
<Text
|
|
671
|
+
color={
|
|
672
|
+
boundedProgress >= 1 ? postureColor : progressColor ?? t.primaryLight
|
|
673
|
+
}
|
|
674
|
+
>
|
|
675
|
+
{fitDisplayText(tinyText, Math.max(1, width))}
|
|
676
|
+
</Text>
|
|
170
677
|
</Box>
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const panelWidth = Math.max(MIN_MOOD_PANEL_WIDTH, Math.min(MAX_MOOD_PANEL_WIDTH, width));
|
|
682
|
+
const innerWidth = Math.max(1, panelWidth - 4);
|
|
683
|
+
const title = "Mood";
|
|
684
|
+
const isSettled = boundedProgress >= 1;
|
|
685
|
+
const topPrefix = "╭─ ";
|
|
686
|
+
const topSuffix = " ";
|
|
687
|
+
const topRule = "─".repeat(
|
|
688
|
+
Math.max(
|
|
689
|
+
0,
|
|
690
|
+
panelWidth -
|
|
691
|
+
displayWidth(topPrefix) -
|
|
692
|
+
displayWidth(title) -
|
|
693
|
+
displayWidth(topSuffix) -
|
|
694
|
+
displayWidth("╮"),
|
|
695
|
+
),
|
|
696
|
+
);
|
|
697
|
+
const settledSuffix = ` / ${posture.badge}`;
|
|
698
|
+
const compactSettledSuffix = ` / ${fitDisplayText(posture.badge, 8)}`;
|
|
699
|
+
const activeSettledSuffix =
|
|
700
|
+
displayWidth(moodPrefix) +
|
|
701
|
+
displayWidth(normalizedMood) +
|
|
702
|
+
displayWidth(settledSuffix) <=
|
|
703
|
+
innerWidth
|
|
704
|
+
? settledSuffix
|
|
705
|
+
: compactSettledSuffix;
|
|
706
|
+
const moodTextWidth = Math.max(
|
|
707
|
+
1,
|
|
708
|
+
innerWidth -
|
|
709
|
+
displayWidth(moodPrefix) -
|
|
710
|
+
(isSettled ? displayWidth(activeSettledSuffix) : 0),
|
|
711
|
+
);
|
|
712
|
+
const moodText = fitDisplayText(normalizedMood, moodTextWidth);
|
|
713
|
+
const settledContent = `${moodPrefix}${moodText}${activeSettledSuffix}`;
|
|
714
|
+
const detailText = fitDisplayText(
|
|
715
|
+
isSettled ? posture.detail : bootPostureDetail(boundedProgress),
|
|
716
|
+
innerWidth,
|
|
717
|
+
);
|
|
718
|
+
const pctWidth = displayWidth(pct);
|
|
719
|
+
const barWidth = Math.max(4, innerWidth - pctWidth - 4);
|
|
720
|
+
const bar = gaugeBar(boundedProgress, barWidth);
|
|
721
|
+
const gaugeContent = `[${bar}] ${pct}`;
|
|
722
|
+
|
|
723
|
+
return (
|
|
724
|
+
<Box flexDirection="column" width={panelWidth}>
|
|
725
|
+
<Text color={t.primaryDim}>
|
|
726
|
+
{topPrefix}
|
|
727
|
+
<Text bold color={t.warning}>
|
|
728
|
+
{title}
|
|
729
|
+
</Text>
|
|
730
|
+
{topSuffix}
|
|
731
|
+
{topRule}
|
|
732
|
+
╮
|
|
733
|
+
</Text>
|
|
734
|
+
{isSettled ? (
|
|
735
|
+
<>
|
|
736
|
+
<Text>
|
|
737
|
+
<Text color={t.primaryDim}>│ </Text>
|
|
738
|
+
<Text bold color={postureColor}>
|
|
739
|
+
{moodText}
|
|
740
|
+
</Text>
|
|
741
|
+
<Text color={t.primaryDim}>{activeSettledSuffix}</Text>
|
|
742
|
+
<Text color={t.primaryDim}>
|
|
743
|
+
{" ".repeat(
|
|
744
|
+
Math.max(0, innerWidth - displayWidth(settledContent)),
|
|
745
|
+
)}
|
|
746
|
+
{" │"}
|
|
747
|
+
</Text>
|
|
748
|
+
</Text>
|
|
749
|
+
<Text>
|
|
750
|
+
<Text color={t.primaryDim}>│ </Text>
|
|
751
|
+
<Text color={t.dim}>{detailText}</Text>
|
|
752
|
+
<Text color={t.primaryDim}>
|
|
753
|
+
{" ".repeat(Math.max(0, innerWidth - displayWidth(detailText)))} │
|
|
754
|
+
</Text>
|
|
755
|
+
</Text>
|
|
756
|
+
</>
|
|
757
|
+
) : (
|
|
758
|
+
<>
|
|
759
|
+
<Text>
|
|
760
|
+
<Text color={t.primaryDim}>│ </Text>
|
|
761
|
+
<Text color={t.primaryDim}>[</Text>
|
|
762
|
+
<Text color={progressColor ?? t.primaryLight}>{bar}</Text>
|
|
763
|
+
<Text color={t.primaryDim}>] </Text>
|
|
764
|
+
<Text color={t.primaryLight}>{pct}</Text>
|
|
765
|
+
<Text color={t.primaryDim}>
|
|
766
|
+
{" ".repeat(Math.max(0, innerWidth - displayWidth(gaugeContent)))} │
|
|
767
|
+
</Text>
|
|
768
|
+
</Text>
|
|
769
|
+
<Text>
|
|
770
|
+
<Text color={t.primaryDim}>│ </Text>
|
|
771
|
+
<Text color={t.dim}>{detailText}</Text>
|
|
772
|
+
<Text color={t.primaryDim}>
|
|
773
|
+
{" ".repeat(Math.max(0, innerWidth - displayWidth(detailText)))} │
|
|
774
|
+
</Text>
|
|
775
|
+
</Text>
|
|
776
|
+
</>
|
|
777
|
+
)}
|
|
778
|
+
<Text color={t.primaryDim}>{titledPanelBottom(panelWidth)}</Text>
|
|
171
779
|
</Box>
|
|
172
780
|
);
|
|
173
781
|
}
|
|
@@ -175,82 +783,87 @@ function TipsPanel({ width }: { width: number }) {
|
|
|
175
783
|
export function MascotDashboard({
|
|
176
784
|
greeting,
|
|
177
785
|
width,
|
|
178
|
-
|
|
179
|
-
|
|
786
|
+
mood,
|
|
787
|
+
bootProgress = 1,
|
|
788
|
+
state = INTRO_FRAMES[INTRO_FRAMES.length - 1]!,
|
|
789
|
+
bar = introBootBar(INTRO_FRAMES.length - 1, INTRO_FRAMES.length),
|
|
180
790
|
barColor,
|
|
181
791
|
mascotStatus = `${INTRO_STATUS_PREFIX}${INTRO_BOOT_NOTES[INTRO_BOOT_NOTES.length - 1]}`,
|
|
182
792
|
dealDesk,
|
|
183
793
|
}: MascotDashboardProps) {
|
|
184
794
|
const t = useTheme();
|
|
185
795
|
const resolvedBarColor = barColor ?? t.primaryLight;
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const leftPanelWidth = compact
|
|
194
|
-
? available
|
|
195
|
-
: sideBySide
|
|
196
|
-
? Math.max(
|
|
197
|
-
MASCOT_WIDTH + GUTTER_WIDTH + 24,
|
|
198
|
-
Math.floor((innerWidth - RIGHT_COLUMN_BORDER_WIDTH) / 2),
|
|
199
|
-
)
|
|
200
|
-
: innerWidth;
|
|
201
|
-
const rightColumnWidth = sideBySide
|
|
202
|
-
? Math.max(20, innerWidth - leftPanelWidth)
|
|
203
|
-
: innerWidth;
|
|
204
|
-
const rightInnerWidth = sideBySide
|
|
205
|
-
? Math.max(1, rightColumnWidth - RIGHT_COLUMN_BORDER_WIDTH)
|
|
206
|
-
: rightColumnWidth;
|
|
207
|
-
const tipsWidth = sideBySide
|
|
208
|
-
? rightInnerWidth
|
|
209
|
-
: innerWidth;
|
|
210
|
-
const copyWidth = compact
|
|
211
|
-
? available
|
|
212
|
-
: sideBySide
|
|
213
|
-
? Math.max(18, leftPanelWidth - MASCOT_WIDTH - GUTTER_WIDTH)
|
|
214
|
-
: innerWidth;
|
|
215
|
-
|
|
216
|
-
if (tinyTerminal) {
|
|
796
|
+
const layout = computeMascotLayout(width);
|
|
797
|
+
const sideBySide = layout.mode === "split";
|
|
798
|
+
const wideGreetingRows = sideBySide
|
|
799
|
+
? fixedDisplayRows(greeting, layout.copy.width, 2)
|
|
800
|
+
: [];
|
|
801
|
+
|
|
802
|
+
if (layout.mode === "tiny") {
|
|
217
803
|
return (
|
|
218
|
-
<Box width={available} flexDirection="column">
|
|
804
|
+
<Box width={layout.available} flexDirection="column">
|
|
219
805
|
<Text color={resolvedBarColor}>{mascotStatus}</Text>
|
|
220
806
|
<Text bold color={t.primaryLight}>
|
|
221
807
|
Drexler™
|
|
222
808
|
</Text>
|
|
223
809
|
<Text color={t.primaryLight}>{greeting}</Text>
|
|
224
|
-
|
|
810
|
+
<Box marginTop={1}>
|
|
811
|
+
<MoodReadout
|
|
812
|
+
mood={mood}
|
|
813
|
+
progress={bootProgress}
|
|
814
|
+
progressColor={resolvedBarColor}
|
|
815
|
+
width={layout.mood.width}
|
|
816
|
+
/>
|
|
817
|
+
</Box>
|
|
818
|
+
{dealDesk ? (
|
|
819
|
+
<Box marginTop={1}>{dealDesk(layout.dealDesk.width)}</Box>
|
|
820
|
+
) : null}
|
|
225
821
|
</Box>
|
|
226
822
|
);
|
|
227
823
|
}
|
|
228
824
|
|
|
229
|
-
if (compact) {
|
|
825
|
+
if (layout.mode === "compact") {
|
|
230
826
|
return (
|
|
231
|
-
<Box
|
|
827
|
+
<Box
|
|
828
|
+
marginLeft={layout.leftPanel.inset}
|
|
829
|
+
width={layout.available}
|
|
830
|
+
flexDirection="column"
|
|
831
|
+
>
|
|
232
832
|
<Text color={resolvedBarColor}>{bar}</Text>
|
|
233
833
|
<Text color={resolvedBarColor}>{mascotStatus}</Text>
|
|
234
834
|
<Text bold color={t.primaryLight}>
|
|
235
835
|
Drexler International™
|
|
236
836
|
</Text>
|
|
237
837
|
<Text color={t.primaryLight}>{greeting}</Text>
|
|
238
|
-
|
|
838
|
+
<Box marginTop={1}>
|
|
839
|
+
<MoodReadout
|
|
840
|
+
mood={mood}
|
|
841
|
+
progress={bootProgress}
|
|
842
|
+
progressColor={resolvedBarColor}
|
|
843
|
+
width={layout.mood.width}
|
|
844
|
+
/>
|
|
845
|
+
</Box>
|
|
846
|
+
{dealDesk ? (
|
|
847
|
+
<Box marginTop={1}>{dealDesk(layout.dealDesk.width)}</Box>
|
|
848
|
+
) : null}
|
|
239
849
|
</Box>
|
|
240
850
|
);
|
|
241
851
|
}
|
|
242
852
|
|
|
243
853
|
return (
|
|
244
|
-
<Box width={available}>
|
|
854
|
+
<Box width={layout.available}>
|
|
245
855
|
<Box
|
|
246
|
-
width={available}
|
|
856
|
+
width={layout.available}
|
|
247
857
|
borderStyle="round"
|
|
248
858
|
borderColor={t.primary}
|
|
249
859
|
paddingX={1}
|
|
250
860
|
flexDirection={sideBySide ? "row" : "column"}
|
|
251
861
|
alignItems={sideBySide ? "flex-start" : "center"}
|
|
252
862
|
>
|
|
253
|
-
<Box
|
|
863
|
+
<Box
|
|
864
|
+
flexDirection={sideBySide ? "row" : "column"}
|
|
865
|
+
width={layout.leftPanel.width}
|
|
866
|
+
>
|
|
254
867
|
<Box
|
|
255
868
|
width={MASCOT_WIDTH}
|
|
256
869
|
flexShrink={0}
|
|
@@ -264,32 +877,61 @@ export function MascotDashboard({
|
|
|
264
877
|
<Box
|
|
265
878
|
flexDirection="column"
|
|
266
879
|
justifyContent="center"
|
|
267
|
-
width={
|
|
880
|
+
width={layout.copy.width}
|
|
268
881
|
marginTop={sideBySide ? 1 : 0}
|
|
269
882
|
>
|
|
270
883
|
<Text bold color={t.primaryLight}>
|
|
271
884
|
Drexler International™
|
|
272
885
|
</Text>
|
|
273
886
|
<Box height={1} />
|
|
274
|
-
|
|
887
|
+
{sideBySide ? (
|
|
888
|
+
wideGreetingRows.map((row, idx) => (
|
|
889
|
+
<Text key={idx} color={t.primaryLight}>
|
|
890
|
+
{row || " "}
|
|
891
|
+
</Text>
|
|
892
|
+
))
|
|
893
|
+
) : (
|
|
894
|
+
<Text color={t.primaryLight}>{greeting}</Text>
|
|
895
|
+
)}
|
|
275
896
|
<Box height={1} />
|
|
897
|
+
<MoodReadout
|
|
898
|
+
mood={mood}
|
|
899
|
+
progress={bootProgress}
|
|
900
|
+
progressColor={resolvedBarColor}
|
|
901
|
+
width={layout.mood.width}
|
|
902
|
+
/>
|
|
276
903
|
</Box>
|
|
277
904
|
</Box>
|
|
278
905
|
{sideBySide ? (
|
|
906
|
+
<>
|
|
907
|
+
<Box flexDirection="column" width={SPLIT_DIVIDER_WIDTH} flexShrink={0}>
|
|
908
|
+
{SPLIT_DIVIDER_ROWS.map((idx) => (
|
|
909
|
+
<Text key={idx} color={t.primaryDim}>
|
|
910
|
+
{" │ "}
|
|
911
|
+
</Text>
|
|
912
|
+
))}
|
|
913
|
+
</Box>
|
|
279
914
|
<Box
|
|
280
915
|
flexDirection="column"
|
|
281
|
-
width={
|
|
282
|
-
|
|
283
|
-
borderColor={t.primaryDim}
|
|
284
|
-
paddingLeft={1}
|
|
916
|
+
width={layout.rightColumn.width}
|
|
917
|
+
paddingRight={RIGHT_COLUMN_PAD_RIGHT}
|
|
285
918
|
>
|
|
286
|
-
<
|
|
287
|
-
|
|
919
|
+
<Box marginLeft={layout.tips.inset}>
|
|
920
|
+
<TipsPanel width={layout.tips.width} />
|
|
921
|
+
</Box>
|
|
922
|
+
{dealDesk ? (
|
|
923
|
+
<Box marginLeft={layout.dealDesk.inset}>
|
|
924
|
+
{dealDesk(layout.dealDesk.width)}
|
|
925
|
+
</Box>
|
|
926
|
+
) : null}
|
|
288
927
|
</Box>
|
|
928
|
+
</>
|
|
289
929
|
) : (
|
|
290
|
-
<Box marginTop={1} width={
|
|
291
|
-
<TipsPanel width={
|
|
292
|
-
{dealDesk ?
|
|
930
|
+
<Box marginTop={1} width={layout.tips.width} flexDirection="column">
|
|
931
|
+
<TipsPanel width={layout.tips.width} />
|
|
932
|
+
{dealDesk ? (
|
|
933
|
+
<Box marginTop={1}>{dealDesk(layout.dealDesk.width)}</Box>
|
|
934
|
+
) : null}
|
|
293
935
|
</Box>
|
|
294
936
|
)}
|
|
295
937
|
</Box>
|
|
@@ -302,7 +944,7 @@ export function MascotIntro({ greeting }: IntroProps) {
|
|
|
302
944
|
const { exit } = useApp();
|
|
303
945
|
const { stdout } = useStdout();
|
|
304
946
|
const [cols, setCols] = useState(stdout?.columns ?? 80);
|
|
305
|
-
const
|
|
947
|
+
const intro = useIntroAnimation(cols, true, exit);
|
|
306
948
|
|
|
307
949
|
useEffect(() => {
|
|
308
950
|
if (!stdout) return;
|
|
@@ -317,56 +959,16 @@ export function MascotIntro({ greeting }: IntroProps) {
|
|
|
317
959
|
if (key.escape || key.return || (key.ctrl && _input === "c")) exit();
|
|
318
960
|
});
|
|
319
961
|
|
|
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}`;
|
|
962
|
+
const barColor = introPhaseColor(intro.colorPhase, t);
|
|
361
963
|
|
|
362
964
|
return (
|
|
363
965
|
<MascotDashboard
|
|
364
966
|
greeting={greeting}
|
|
365
967
|
width={cols}
|
|
366
|
-
state={state}
|
|
367
|
-
bar={bar}
|
|
968
|
+
state={intro.state}
|
|
969
|
+
bar={intro.bar}
|
|
368
970
|
barColor={barColor}
|
|
369
|
-
mascotStatus={
|
|
971
|
+
mascotStatus={intro.status}
|
|
370
972
|
/>
|
|
371
973
|
);
|
|
372
974
|
}
|