@siegesailor/terminal-sigma 1.2.0 → 1.4.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/build/app.js CHANGED
@@ -3,18 +3,20 @@ import zod from "zod";
3
3
  import React from "react";
4
4
  import { ThemeProvider } from "@inkjs/ui";
5
5
  import { Box, Text, useApp, useInput, useStdout, useWindowSize } from "ink";
6
+ import AutocompleteInput from "./components/autocomplete-input.js";
6
7
  import Character from "./components/character.js";
7
8
  import Choice from "./components/choice.js";
8
9
  import EntryForm from "./components/entry-form.js";
9
10
  import Footer from "./components/footer.js";
10
11
  import LogsView, { LogRow } from "./components/logs-view.js";
11
- import MenuGrid from "./components/menu-grid.js";
12
+ import MultiChoice from "./components/multi-choice.js";
12
13
  import TodayPanel from "./components/today-panel.js";
13
- import { characterWidth, dietFieldsOf, layoutOf, menuItemsOf, menuOrder, profileFieldsOf, sectionsOf, workoutFieldsOf, } from "./content.js";
14
+ import { categoryOptionsOf, characterWidth, dietFieldsOf, intervalOptionsOf, layoutOf, menuEntriesOf, menuListWidth, menuOrder, petStateOf, profileFieldsOf, quoteOptionsOf, sectionsOf, workoutFieldsOf, } from "./content.js";
14
15
  import { adviceOf, targetsOf } from "./health.js";
15
16
  import { formatNumber, genders, healthGoals, intensities, languages, messagesOf, workoutActivities, workStyles, } from "./i18n.js";
17
+ import { lookOf } from "./pixel-art.js";
16
18
  import { experienceOf, experiencePerMeal, levelOf, logsOf, saveProgress, todayOf, workoutExperience, } from "./progress.js";
17
- import { fetchQuote, hasQuoteApi, localizedQuote, quoteRefreshMinutes, randomBundledQuote, } from "./quotes.js";
19
+ import { bundledQuotes, defaultQuoteSettings, fetchQuote, hasQuoteApi, localizedQuote, pickBundled, quoteCategories, quoteIntervals, } from "./quotes.js";
18
20
  import { paletteContext, themeNames, themes, uiThemeOf, } from "./theme.js";
19
21
  import { advanceTimer, formatClock, idleTimer, remainingOf, toggleTimer, } from "./timer.js";
20
22
  export const optionsSchema = zod.object({
@@ -35,6 +37,29 @@ const languageOptions = languages.map((language) => ({
35
37
  value: language,
36
38
  }));
37
39
  const pick = (options, value, fallback) => options.find((option) => option === value) ?? fallback;
40
+ // Appends an event, so every action lands in the logs with the change it made.
41
+ const withEvent = (progress, action, detail) => ({
42
+ ...progress,
43
+ events: [
44
+ ...progress.events,
45
+ {
46
+ at: new Date().toISOString(),
47
+ action,
48
+ ...(detail === undefined ? {} : { detail }),
49
+ },
50
+ ],
51
+ });
52
+ // Names to suggest for the author and work filters.
53
+ const quoteNames = (field) => [
54
+ ...new Set(bundledQuotes.flatMap((quote) => field === "author"
55
+ ? [
56
+ quote.author,
57
+ ...Object.values(quote.translations ?? {}).map((text) => text.author),
58
+ ]
59
+ : quote.work
60
+ ? [quote.work]
61
+ : [])),
62
+ ];
38
63
  function Titled({ title, children, }) {
39
64
  return (React.createElement(paletteContext.Consumer, null, (palette) => (React.createElement(Box, { flexDirection: "column" },
40
65
  React.createElement(Text, { bold: true, color: palette.accent }, title),
@@ -51,14 +76,20 @@ export default function App({ options, file, initialProgress }) {
51
76
  const [now, setNow] = React.useState(Date.now);
52
77
  const durations = React.useMemo(() => ({ focus: options.focus, break: options.break }), [options.focus, options.break]);
53
78
  const [timer, setTimer] = React.useState(() => idleTimer(durations));
54
- const [quote, setQuote] = React.useState(randomBundledQuote);
79
+ const quoteSettings = progress.quotes ?? defaultQuoteSettings;
80
+ const [quote, setQuote] = React.useState(() => pickBundled(quoteSettings, Date.now()));
55
81
  const [view, setView] = React.useState("menu");
82
+ const [focusedMenu, setFocusedMenu] = React.useState("quotes");
83
+ const [quoteEditor, setQuoteEditor] = React.useState();
84
+ const [quoteOption, setQuoteOption] = React.useState("draw");
56
85
  const [flash, setFlash] = React.useState();
57
86
  const [draftActivity, setDraftActivity] = React.useState();
58
87
  const language = progress.language ?? "en";
59
88
  const messages = messagesOf(language);
60
89
  const themeName = progress.theme ?? "ember";
61
- const palette = themes[themeName];
90
+ // While the theme picker is open, the focused theme previews live.
91
+ const [previewTheme, setPreviewTheme] = React.useState();
92
+ const palette = themes[previewTheme ?? themeName];
62
93
  const uiTheme = React.useMemo(() => uiThemeOf(palette), [palette]);
63
94
  const name = options.name ?? progress.profile?.name ?? "Rook Sigma";
64
95
  const [message, setMessage] = React.useState(() => ({
@@ -70,10 +101,9 @@ export default function App({ options, file, initialProgress }) {
70
101
  };
71
102
  // Saves first and only then shows the change, so the screen never claims
72
103
  // something the file does not hold.
73
- const save = React.useCallback((next) => {
104
+ const commit = React.useCallback((next, text, tone = "info") => {
74
105
  try {
75
106
  saveProgress(file, next);
76
- return true;
77
107
  }
78
108
  catch (error) {
79
109
  setMessage({
@@ -82,26 +112,27 @@ export default function App({ options, file, initialProgress }) {
82
112
  });
83
113
  return false;
84
114
  }
115
+ setProgress(next);
116
+ if (text) {
117
+ setMessage({ text, tone });
118
+ }
119
+ return true;
85
120
  }, [file]);
86
121
  const record = React.useCallback((update, text, activity) => {
87
- const next = update(progress);
88
- if (!save(next)) {
89
- return;
90
- }
122
+ let next = update(progress);
91
123
  const after = levelOf(experienceOf(next));
92
124
  const leveledUp = after > levelOf(experienceOf(progress));
93
- setProgress(next);
94
- setMessage({
95
- text: leveledUp
96
- ? `${text} ${messagesOf(next.language ?? "en").form.levelUp(after)}`
97
- : text,
98
- tone: "success",
99
- });
100
- setFlash({
101
- activity: leveledUp ? "levelUp" : activity,
102
- until: Date.now() + flashMilliseconds,
103
- });
104
- }, [progress, save]);
125
+ if (leveledUp) {
126
+ next = withEvent(next, "levelUp", String(after));
127
+ }
128
+ const { form } = messagesOf(next.language ?? "en");
129
+ if (commit(next, leveledUp ? `${text} ${form.levelUp(after)}` : text, "success")) {
130
+ setFlash({
131
+ activity: leveledUp ? "levelUp" : activity,
132
+ until: Date.now() + flashMilliseconds,
133
+ });
134
+ }
135
+ }, [progress, commit]);
105
136
  React.useEffect(() => {
106
137
  const interval = setInterval(() => {
107
138
  setNow(Date.now());
@@ -110,6 +141,14 @@ export default function App({ options, file, initialProgress }) {
110
141
  clearInterval(interval);
111
142
  };
112
143
  }, []);
144
+ // Opening the app is an action too.
145
+ const hasOpened = React.useRef(false);
146
+ React.useEffect(() => {
147
+ if (!hasOpened.current) {
148
+ hasOpened.current = true;
149
+ commit(withEvent(progress, "appOpened"));
150
+ }
151
+ }, [commit, progress]);
113
152
  React.useEffect(() => {
114
153
  const { timer: next, finished } = advanceTimer(timer, now, durations);
115
154
  if (!finished) {
@@ -119,7 +158,7 @@ export default function App({ options, file, initialProgress }) {
119
158
  setTimer(next);
120
159
  stdout.write("\u0007");
121
160
  if (finished === "break") {
122
- setMessage({ text: words.breakOver, tone: "info" });
161
+ commit(withEvent(progress, "breakOver"), words.breakOver);
123
162
  return;
124
163
  }
125
164
  record((current) => ({
@@ -129,11 +168,11 @@ export default function App({ options, file, initialProgress }) {
129
168
  { at: new Date(now).toISOString(), minutes: durations.focus },
130
169
  ],
131
170
  }), words.focusComplete(durations.focus, durations.break), "idle");
132
- }, [now, timer, durations, record, stdout, language]);
171
+ }, [now, timer, durations, record, commit, progress, stdout, language]);
133
172
  React.useEffect(() => {
134
173
  let isMounted = true;
135
174
  const refresh = async () => {
136
- const next = await fetchQuote();
175
+ const next = await fetchQuote(quoteSettings);
137
176
  if (isMounted) {
138
177
  setQuote(next);
139
178
  }
@@ -142,17 +181,24 @@ export default function App({ options, file, initialProgress }) {
142
181
  if (hasQuoteApi()) {
143
182
  void refresh();
144
183
  }
184
+ if (!quoteSettings.autoRefresh) {
185
+ return () => {
186
+ isMounted = false;
187
+ };
188
+ }
145
189
  const interval = setInterval(() => {
146
190
  void refresh();
147
- }, quoteRefreshMinutes * 60_000);
191
+ }, quoteSettings.interval * 1000);
148
192
  return () => {
149
193
  isMounted = false;
150
194
  clearInterval(interval);
151
195
  };
152
- }, []);
196
+ }, [quoteSettings]);
153
197
  const backToMenu = (text) => {
154
198
  setView("menu");
199
+ setQuoteEditor(undefined);
155
200
  setDraftActivity(undefined);
201
+ setPreviewTheme(undefined);
156
202
  if (text) {
157
203
  say(text);
158
204
  }
@@ -161,20 +207,19 @@ export default function App({ options, file, initialProgress }) {
161
207
  if (view === "menu" && input === "q") {
162
208
  exit();
163
209
  }
210
+ else if (view === "quotes" && quoteEditor && key.escape) {
211
+ setQuoteEditor(undefined);
212
+ }
164
213
  else if (view !== "menu" && key.escape) {
165
214
  const isForm = ["diet", "workout", "profile"].includes(view);
166
215
  backToMenu(isForm ? messages.form.cancelled : undefined);
167
216
  }
168
217
  }, { isActive: progress.language !== undefined });
169
- const saveSetting = (next, text) => {
170
- if (save(next)) {
171
- setProgress(next);
172
- backToMenu(text);
173
- }
174
- };
175
218
  const chooseLanguage = (value) => {
176
219
  const next = pick(languages, value, "en");
177
- saveSetting({ ...progress, language: next }, messagesOf(next).languageChanged);
220
+ if (commit(withEvent({ ...progress, language: next }, "languageChanged", next))) {
221
+ backToMenu(messagesOf(next).languageChanged);
222
+ }
178
223
  };
179
224
  const providers = (children) => (React.createElement(paletteContext.Provider, { value: palette },
180
225
  React.createElement(ThemeProvider, { theme: uiTheme }, children)));
@@ -191,27 +236,20 @@ export default function App({ options, file, initialProgress }) {
191
236
  timer.remaining === durations.focus * 60_000;
192
237
  const clock = formatClock(remainingOf(timer, now));
193
238
  const phaseLabel = messages.timer[timer.phase];
194
- const selectMenu = (index) => {
195
- const item = menuOrder[index];
196
- if (item === "timer" && isIdle) {
197
- setTimer(toggleTimer(timer, Date.now()));
198
- say(messages.timer.started);
199
- }
200
- else if (item === "quotes") {
201
- void fetchQuote().then(setQuote);
202
- say(messages.quoteDrawn);
203
- }
204
- else if (item) {
205
- setView(item);
206
- }
207
- };
208
239
  const chooseTimerAction = (action) => {
209
- if (action === "toggle") {
210
- setTimer(toggleTimer(timer, Date.now()));
211
- backToMenu(isRunning ? messages.timer.paused : messages.timer.resumed);
240
+ if (action === "start" || action === "toggle") {
241
+ const [event, text, detail] = action === "start"
242
+ ? ["timerStarted", messages.timer.started, String(durations.focus)]
243
+ : isRunning
244
+ ? ["timerPaused", messages.timer.paused, clock]
245
+ : ["timerResumed", messages.timer.resumed];
246
+ setTimer(toggleTimer(timer, now));
247
+ commit(withEvent(progress, event, detail));
248
+ backToMenu(text);
212
249
  }
213
250
  else if (action === "stop") {
214
251
  setTimer(idleTimer(durations));
252
+ commit(withEvent(progress, "timerStopped", clock));
215
253
  backToMenu(messages.timer.stopped);
216
254
  }
217
255
  else {
@@ -253,7 +291,7 @@ export default function App({ options, file, initialProgress }) {
253
291
  record((current) => ({ ...current, workouts: [...current.workouts, workout] }), messages.form.workoutLogged(messages.workoutActivity[workout.activity], workout.minutes, workoutExperience(workout)), workoutAnimation(workout.activity));
254
292
  };
255
293
  const submitProfile = (values) => {
256
- saveSetting({
294
+ const next = withEvent({
257
295
  ...progress,
258
296
  profile: {
259
297
  name: values.name ?? name,
@@ -264,11 +302,64 @@ export default function App({ options, file, initialProgress }) {
264
302
  workStyle: pick(workStyles, values.workStyle, "desk"),
265
303
  goal: pick(healthGoals, values.goal, "maintain"),
266
304
  },
267
- }, messages.form.profileSaved);
305
+ }, "profileSaved");
306
+ if (commit(next)) {
307
+ backToMenu(messages.form.profileSaved);
308
+ }
268
309
  };
269
310
  const chooseTheme = (value) => {
270
311
  const next = pick(themeNames, value, "ember");
271
- saveSetting({ ...progress, theme: next }, messages.themes.changed(messages.themes.names[next]));
312
+ if (commit(withEvent({ ...progress, theme: next }, "themeChanged", next))) {
313
+ backToMenu(messages.themes.changed(messages.themes.names[next]));
314
+ }
315
+ };
316
+ const drawQuote = (settings, base) => {
317
+ void fetchQuote(settings).then(setQuote);
318
+ return withEvent(base, "quoteDrawn");
319
+ };
320
+ // Saves 1 quote setting, logs it as "setting:value", and draws a quote that
321
+ // follows the new filters.
322
+ const updateQuotes = (patch, detail) => {
323
+ const settings = { ...quoteSettings, ...patch };
324
+ const next = withEvent({ ...progress, quotes: settings }, "quoteSettingsChanged", detail);
325
+ if (commit(next, messages.quotes.changed)) {
326
+ setQuoteEditor(undefined);
327
+ setQuote(pickBundled(settings, Date.now()));
328
+ }
329
+ };
330
+ const chooseQuoteOption = (value) => {
331
+ setQuoteOption(value);
332
+ switch (value) {
333
+ case "draw": {
334
+ commit(drawQuote(quoteSettings, progress), messages.quoteDrawn);
335
+ break;
336
+ }
337
+ case "autoRefresh": {
338
+ const isOn = !quoteSettings.autoRefresh;
339
+ updateQuotes({ autoRefresh: isOn }, `autoRefresh:${isOn ? "on" : "off"}`);
340
+ break;
341
+ }
342
+ case "mode": {
343
+ const mode = quoteSettings.mode === "random" ? "daily" : "random";
344
+ updateQuotes({ mode }, `mode:${mode}`);
345
+ break;
346
+ }
347
+ case "back": {
348
+ backToMenu();
349
+ break;
350
+ }
351
+ default: {
352
+ setQuoteEditor(pick(["interval", "categories", "excluded", "author", "work"], value, "interval"));
353
+ }
354
+ }
355
+ };
356
+ const openMenu = (value) => {
357
+ const item = pick(menuOrder, value, "quotes");
358
+ setFocusedMenu(item);
359
+ setView(item);
360
+ if (item === "theme") {
361
+ setPreviewTheme(themeName);
362
+ }
272
363
  };
273
364
  const experience = experienceOf(progress);
274
365
  const level = levelOf(experience);
@@ -277,6 +368,7 @@ export default function App({ options, file, initialProgress }) {
277
368
  const columns = size.columns || 100;
278
369
  const rows = size.rows || 40;
279
370
  const layout = layoutOf(columns, rows);
371
+ const pet = petStateOf(progress, now);
280
372
  const activity = flash && flash.until > now
281
373
  ? flash.activity
282
374
  : view === "diet"
@@ -286,48 +378,92 @@ export default function App({ options, file, initialProgress }) {
286
378
  : isRunning
287
379
  ? { focus: "focus", break: "break" }[timer.phase]
288
380
  : "idle";
381
+ let activityLabel = messages.activity[activity];
382
+ if (activity === "idle") {
383
+ activityLabel = messages.mood[pet.mood];
384
+ }
385
+ else if (activity === "levelUp") {
386
+ activityLabel = `${messages.activity.levelUp} ${messages.level(level)}`;
387
+ }
289
388
  let timerSignal = isRunning ? clock : messages.signal.paused;
290
389
  if (isIdle) {
291
390
  timerSignal = messages.signal.ready;
292
391
  }
293
- const hints = {
294
- menu: messages.hints.menu,
295
- logs: messages.hints.logs,
296
- diet: messages.hints.form,
297
- workout: messages.hints.form,
298
- profile: messages.hints.form,
299
- timer: messages.hints.select,
300
- health: messages.hints.select,
301
- theme: messages.hints.select,
302
- language: messages.hints.select,
392
+ const entries = menuEntriesOf(messages, {
393
+ timerSignal,
394
+ progress,
395
+ quotes: quoteSettings,
396
+ now,
397
+ logs: logs.length,
398
+ theme: themeName,
399
+ focus: durations.focus,
400
+ rest: durations.break,
401
+ });
402
+ const focusedEntry = entries.find((entry) => entry.key === focusedMenu) ?? entries[0];
403
+ const isTyping = view === "diet" ||
404
+ (view === "quotes" && (quoteEditor === "author" || quoteEditor === "work"));
405
+ let hint = messages.hints.select;
406
+ if (view === "menu") {
407
+ hint = messages.hints.menu;
408
+ }
409
+ else if (view === "logs") {
410
+ hint = messages.hints.logs;
411
+ }
412
+ else if (isTyping) {
413
+ hint = messages.hints.suggest;
414
+ }
415
+ else if (view === "workout" || view === "profile") {
416
+ hint = messages.hints.form;
417
+ }
418
+ const shownQuote = localizedQuote(quote, language);
419
+ const quoteEditors = {
420
+ interval: (React.createElement(Choice, { initialValue: String(quoteSettings.interval), options: intervalOptionsOf(messages), onSelect: (value) => {
421
+ const interval = quoteIntervals.find((option) => String(option) === value) ?? 1500;
422
+ updateQuotes({ interval }, `interval:${interval}`);
423
+ } })),
424
+ categories: (React.createElement(MultiChoice, { doneLabel: messages.quotes.done, initial: quoteSettings.categories, options: categoryOptionsOf(messages), onDone: (selected) => {
425
+ const categories = quoteCategories.filter((category) => selected.includes(category));
426
+ updateQuotes({ categories }, `categories:${categories.join(",")}`);
427
+ } })),
428
+ excluded: (React.createElement(MultiChoice, { doneLabel: messages.quotes.done, initial: quoteSettings.excluded, options: categoryOptionsOf(messages), onDone: (selected) => {
429
+ const excluded = quoteCategories.filter((category) => selected.includes(category));
430
+ updateQuotes({ excluded }, `excluded:${excluded.join(",")}`);
431
+ } })),
432
+ author: (React.createElement(AutocompleteInput, { initialValue: quoteSettings.author, placeholder: messages.quotes.authorPrompt, suggest: (text) => quoteNames("author")
433
+ .filter((author) => author.toLowerCase().includes(text.trim().toLowerCase()))
434
+ .slice(0, 5)
435
+ .map((label) => ({ label })), onSubmit: (text) => {
436
+ updateQuotes({ author: text.trim() }, `author:${text.trim()}`);
437
+ } })),
438
+ work: (React.createElement(AutocompleteInput, { initialValue: quoteSettings.work, placeholder: messages.quotes.workPrompt, suggest: (text) => quoteNames("work")
439
+ .filter((work) => work.toLowerCase().includes(text.trim().toLowerCase()))
440
+ .slice(0, 5)
441
+ .map((label) => ({ label })), onSubmit: (text) => {
442
+ updateQuotes({ work: text.trim() }, `work:${text.trim()}`);
443
+ } })),
303
444
  };
445
+ const timerOptions = isIdle
446
+ ? [{ label: messages.timer.start, value: "start" }]
447
+ : [
448
+ {
449
+ label: isRunning ? messages.timer.pause : messages.timer.resume,
450
+ value: "toggle",
451
+ },
452
+ { label: messages.timer.stop, value: "stop" },
453
+ ];
304
454
  const panels = {
305
- menu: (React.createElement(Box, { flexDirection: "column" },
306
- React.createElement(MenuGrid, { columns: layout.menuColumns, isActive: view === "menu", items: menuItemsOf(messages, language, {
307
- timerSignal,
308
- progress,
309
- now,
310
- logs: logs.length,
311
- theme: themeName,
312
- focus: durations.focus,
313
- rest: durations.break,
314
- }), onSelect: selectMenu }),
315
- React.createElement(Box, { flexDirection: "column", marginTop: 1 },
316
- React.createElement(Text, { bold: true, color: palette.soft }, messages.recent.title),
317
- logs.length === 0 ? (React.createElement(Text, { dimColor: true }, messages.recent.empty)) : null,
318
- logs.slice(0, recentCount).map((log) => (React.createElement(LogRow, { key: `${log.kind}-${log.at}`, language: language, log: log, messages: messages })))))),
455
+ quotes: (React.createElement(Titled, { title: messages.menu.quotes },
456
+ React.createElement(Text, { dimColor: true, italic: true, wrap: "truncate-end" }, `“${shownQuote.quote}” — ${shownQuote.author}`),
457
+ React.createElement(Box, { marginTop: 1 }, quoteEditor ? (React.createElement(Box, { key: quoteEditor }, quoteEditors[quoteEditor])) : (React.createElement(Choice, { key: "options", initialValue: quoteOption, options: quoteOptionsOf(messages, quoteSettings), onSelect: chooseQuoteOption }))))),
319
458
  timer: (React.createElement(Titled, { title: messages.menu.timer },
320
459
  React.createElement(Choice, { options: [
321
- {
322
- label: isRunning ? messages.timer.pause : messages.timer.resume,
323
- value: "toggle",
324
- },
325
- { label: messages.timer.stop, value: "stop" },
460
+ ...timerOptions,
326
461
  { label: messages.timer.back, value: "back" },
327
462
  ], onSelect: chooseTimerAction }))),
328
463
  health: (React.createElement(HealthPanel, { language: language, messages: messages, palette: palette, progress: progress, today: today, onSelect: chooseHealthAction })),
329
- diet: (React.createElement(EntryForm, { fields: dietFieldsOf(messages), messages: messages, title: messages.form.dietTitle, onSubmit: submitMeal })),
330
- workout: (React.createElement(EntryForm, { fields: workoutFieldsOf(messages), messages: messages, title: messages.form.workoutTitle, onSubmit: submitWorkout, onValues: (values) => {
464
+ logs: (React.createElement(LogsView, { height: Math.max(rows - 9, 6), language: language, logs: logs, messages: messages })),
465
+ diet: (React.createElement(EntryForm, { fields: dietFieldsOf(messages, language, progress.meals), messages: messages, title: messages.form.dietTitle, onSubmit: submitMeal })),
466
+ workout: (React.createElement(EntryForm, { fields: workoutFieldsOf(messages, progress.workouts), messages: messages, title: messages.form.workoutTitle, onSubmit: submitWorkout, onValues: (values) => {
331
467
  setDraftActivity(values.activity);
332
468
  } })),
333
469
  profile: (React.createElement(EntryForm, { fields: profileFieldsOf(messages, progress.profile, name), messages: messages, title: messages.form.profileTitle, onSubmit: submitProfile })),
@@ -336,28 +472,45 @@ export default function App({ options, file, initialProgress }) {
336
472
  label: messages.themes.names[option].padEnd(8),
337
473
  value: option,
338
474
  hint: React.createElement(Swatches, { palette: themes[option] }),
339
- })), onSelect: chooseTheme }))),
475
+ })), onFocus: (value) => {
476
+ setPreviewTheme(pick(themeNames, value, themeName));
477
+ }, onSelect: chooseTheme }))),
340
478
  language: (React.createElement(Titled, { title: messages.menu.language },
341
479
  React.createElement(Choice, { initialValue: language, options: languageOptions, onSelect: chooseLanguage }))),
342
480
  };
343
- const shownQuote = localizedQuote(quote, language);
344
- const todayPanel = (React.createElement(TodayPanel, { sections: sectionsOf(language, palette, progress, now), title: messages.today.title, width: layout.todayWidth }));
481
+ const preview = focusedEntry ? (React.createElement(Box, { flexDirection: "column" },
482
+ React.createElement(Text, { bold: true, color: palette.soft }, focusedEntry.status),
483
+ React.createElement(Box, { marginTop: 1 },
484
+ React.createElement(Text, { dimColor: true }, focusedEntry.description)),
485
+ React.createElement(Box, { marginTop: 1 },
486
+ React.createElement(Text, { dimColor: true, italic: true }, messages.describe.open)))) : null;
487
+ const todayPanel = (React.createElement(TodayPanel, { isCompact: layout.isTodayCompact, sections: sectionsOf(language, palette, progress, now), title: messages.today.title, width: layout.todayWidth }));
488
+ const recentBlock = (React.createElement(Box, { borderColor: palette.border, borderStyle: "round", flexDirection: "column", flexShrink: 0, paddingX: 1 },
489
+ React.createElement(Text, { bold: true, color: palette.soft }, messages.recent.title),
490
+ logs.length === 0 ? React.createElement(Text, { dimColor: true }, messages.recent.empty) : null,
491
+ logs.slice(0, recentCount).map((log) => (React.createElement(LogRow, { key: `${log.kind}-${log.at}`, language: language, log: log, messages: messages })))));
345
492
  return providers(React.createElement(Box, { flexDirection: "column", height: size.rows ? rows : undefined, overflow: "hidden", width: columns },
346
493
  React.createElement(Box, { borderColor: palette.accent, borderStyle: "round", flexDirection: "column", flexShrink: 0, paddingX: 1 },
347
494
  React.createElement(Box, { justifyContent: "space-between" },
348
495
  React.createElement(Text, { bold: true, color: palette.accent }, "\u273B Terminal Sigma"),
349
496
  React.createElement(Text, { dimColor: true }, new Date(now).toTimeString().slice(0, 8))),
350
497
  React.createElement(Text, { dimColor: true, italic: true, wrap: "truncate-end" }, `“${shownQuote.quote}” — ${shownQuote.author}`)),
351
- view === "logs" ? (React.createElement(Box, { borderColor: palette.accent, borderStyle: "round", flexGrow: 1, paddingX: 1 },
352
- React.createElement(LogsView, { height: rows - 7, language: language, logs: logs, messages: messages }))) : (React.createElement(React.Fragment, null,
353
- React.createElement(Box, { flexDirection: layout.stacked ? "column-reverse" : "row", flexGrow: 1 },
354
- layout.showCharacter ? (React.createElement(Character, { activity: activity, label: activity === "levelUp"
355
- ? `${messages.activity.levelUp} ${messages.level(level)}`
356
- : messages.activity[activity], level: messages.level(level), levelNumber: level, name: name, width: characterWidth })) : null,
357
- layout.showToday && layout.todayBeside ? todayPanel : null,
358
- React.createElement(Box, { borderColor: view === "menu" ? palette.border : palette.accent, borderStyle: "round", flexDirection: "column", paddingX: 1, width: layout.panelWidth }, view === "menu" ? panels.menu : panels[view])),
359
- layout.showToday && !layout.todayBeside ? todayPanel : null)),
360
- React.createElement(Footer, { hint: hints[view], message: isIdle || isRunning
498
+ React.createElement(Box, { flexDirection: layout.stacked ? "column-reverse" : "row", flexGrow: 1 },
499
+ layout.showCharacter ? (React.createElement(Character, { activity: activity, badges: pet.badges, label: activityLabel, level: messages.level(level), levelNumber: level, look: lookOf(progress.profile), mood: pet.mood, name: name, width: characterWidth })) : null,
500
+ layout.showToday && layout.todayBeside ? todayPanel : null,
501
+ React.createElement(Box, { flexDirection: "column", width: layout.panelWidth },
502
+ React.createElement(Box, { flexDirection: layout.isMenuSplit ? "row" : "column", flexGrow: 1 },
503
+ React.createElement(Box, { borderColor: view === "menu" ? palette.accent : palette.border, borderStyle: "round", display: view === "logs" ? "none" : "flex", flexShrink: 0, paddingX: 1, width: layout.isMenuSplit ? menuListWidth : undefined },
504
+ React.createElement(Choice, { isNumbered: true, initialValue: focusedMenu, isActive: view === "menu", options: entries.map((entry) => ({
505
+ label: entry.label,
506
+ value: entry.key,
507
+ })), onFocus: (value) => {
508
+ setFocusedMenu(pick(menuOrder, value, "quotes"));
509
+ }, onSelect: openMenu })),
510
+ React.createElement(Box, { borderColor: view === "menu" ? palette.border : palette.accent, borderStyle: "round", flexDirection: "column", flexGrow: 1, overflow: "hidden", paddingX: 1 }, view === "menu" ? preview : panels[view])),
511
+ layout.showRecent && view !== "logs" ? recentBlock : null)),
512
+ layout.showToday && !layout.todayBeside ? todayPanel : null,
513
+ React.createElement(Footer, { hint: hint, message: isIdle || isRunning
361
514
  ? message.text
362
515
  : `${messages.timer.pausedAt(phaseLabel, clock)} · ${message.text}`, status: isRunning ? messages.timer.left(phaseLabel, clock) : undefined, tone: message.tone })));
363
516
  }