@kitlangton/ghui 0.1.14 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitlangton/ghui",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Terminal UI for GitHub pull requests",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/App.tsx CHANGED
@@ -11,7 +11,7 @@ import { formatShortDate, formatTimestamp } from "./date.js"
11
11
  import { availableMergeActions, mergeInfoFromPullRequest } from "./mergeActions.js"
12
12
  import { Observability } from "./observability.js"
13
13
  import { GitHubService } from "./services/GitHubService.js"
14
- import { colors, setActiveTheme, themeDefinitions, type ThemeId } from "./ui/colors.js"
14
+ import { colors, filterThemeDefinitions, setActiveTheme, themeDefinitions, type ThemeId } from "./ui/colors.js"
15
15
  import { pullRequestDiffKey, splitPatchFiles, type PullRequestDiffState } from "./ui/diff.js"
16
16
  import { DetailBody, DetailHeader, DetailPlaceholder, DetailsPane, getDetailBodyHeight, getDetailHeaderHeight, getDetailJunctionRows, getDetailsPaneHeight, LoadingPane, type DetailPlaceholderContent } from "./ui/DetailsPane.js"
17
17
  import { FooterHints, type RetryProgress } from "./ui/FooterHints.js"
@@ -264,6 +264,10 @@ export const App = () => {
264
264
  const [themeId, setThemeId] = useAtom(themeIdAtom)
265
265
  const [themeModal, setThemeModal] = useAtom(themeModalAtom)
266
266
  setActiveTheme(themeId)
267
+ const themeIdRef = useRef(themeId)
268
+ const themeModalRef = useRef(themeModal)
269
+ themeIdRef.current = themeId
270
+ themeModalRef.current = themeModal
267
271
  const [labelCache, setLabelCache] = useAtom(labelCacheAtom)
268
272
  const [pullRequestOverrides, setPullRequestOverrides] = useAtom(pullRequestOverridesAtom)
269
273
  const retryProgress = useAtomValue(retryProgressAtom)
@@ -559,12 +563,14 @@ export const App = () => {
559
563
  setMergeModal(initialMergeModalState)
560
564
  setThemeModal({
561
565
  open: true,
566
+ query: "",
567
+ filterMode: false,
562
568
  initialThemeId: themeId,
563
569
  })
564
570
  }
565
571
 
566
572
  const closeThemeModal = (confirm: boolean) => {
567
- const selectedTheme = themeDefinitions.find((theme) => theme.id === themeId)
573
+ const selectedTheme = themeDefinitions.find((theme) => theme.id === themeIdRef.current)
568
574
  if (!confirm) {
569
575
  setThemeId(themeModal.initialThemeId)
570
576
  } else if (selectedTheme) {
@@ -573,12 +579,42 @@ export const App = () => {
573
579
  setThemeModal(initialThemeModalState)
574
580
  }
575
581
 
582
+ const previewTheme = (id: ThemeId) => {
583
+ if (id === themeIdRef.current) return
584
+ themeIdRef.current = id
585
+ setThemeId(id)
586
+ }
587
+
576
588
  const moveThemeSelection = (delta: number) => {
577
- const currentIndex = Math.max(0, themeDefinitions.findIndex((theme) => theme.id === themeId))
578
- const selectedIndex = Math.max(0, Math.min(themeDefinitions.length - 1, currentIndex + delta))
589
+ const filteredThemes = filterThemeDefinitions(themeModalRef.current.query)
590
+ if (filteredThemes.length === 0) return
591
+ const currentIndex = Math.max(0, filteredThemes.findIndex((theme) => theme.id === themeIdRef.current))
592
+ const selectedIndex = Math.max(0, Math.min(filteredThemes.length - 1, currentIndex + delta))
579
593
  if (selectedIndex === currentIndex) return
580
- const theme = themeDefinitions[selectedIndex]
581
- if (theme && theme.id !== themeId) setThemeId(theme.id)
594
+ const theme = filteredThemes[selectedIndex]
595
+ if (theme) previewTheme(theme.id)
596
+ }
597
+
598
+ const updateThemeQuery = (query: string, options: { readonly previewFirst?: boolean; readonly filterMode?: boolean } = {}) => {
599
+ const current = themeModalRef.current
600
+ const next = {
601
+ ...current,
602
+ query,
603
+ filterMode: options.filterMode ?? current.filterMode,
604
+ }
605
+ if (next.query === current.query && next.filterMode === current.filterMode) return
606
+
607
+ themeModalRef.current = next
608
+ setThemeModal(next)
609
+
610
+ if (options.previewFirst && query.trim().length > 0) {
611
+ const firstTheme = filterThemeDefinitions(query)[0]
612
+ if (firstTheme) previewTheme(firstTheme.id)
613
+ }
614
+ }
615
+
616
+ const editThemeQuery = (transform: (query: string) => string) => {
617
+ updateThemeQuery(transform(themeModalRef.current.query), { previewFirst: true })
582
618
  }
583
619
 
584
620
  const openLabelModal = () => {
@@ -714,7 +750,7 @@ export const App = () => {
714
750
  }
715
751
 
716
752
  useKeyboard((key) => {
717
- if (key.name === "q" || (key.ctrl && key.name === "c")) {
753
+ if ((key.name === "q" && !(themeModal.open && themeModal.filterMode)) || (key.ctrl && key.name === "c")) {
718
754
  if (themeModal.open) {
719
755
  closeThemeModal(false)
720
756
  return
@@ -733,21 +769,42 @@ export const App = () => {
733
769
 
734
770
  if (themeModal.open) {
735
771
  if (key.name === "escape") {
772
+ if (themeModal.filterMode) {
773
+ updateThemeQuery("", { filterMode: false })
774
+ return
775
+ }
736
776
  closeThemeModal(false)
737
777
  return
738
778
  }
779
+ if (key.name === "/") {
780
+ updateThemeQuery("", { filterMode: true })
781
+ return
782
+ }
739
783
  if (key.name === "return" || key.name === "enter") {
784
+ if (themeModal.filterMode && filterThemeDefinitions(themeModal.query).length === 0) return
740
785
  closeThemeModal(true)
741
786
  return
742
787
  }
743
- if (key.name === "up" || key.name === "k") {
788
+ if (key.name === "up" || (!themeModal.filterMode && key.name === "k")) {
744
789
  moveThemeSelection(-1)
745
790
  return
746
791
  }
747
- if (key.name === "down" || key.name === "j") {
792
+ if (key.name === "down" || (!themeModal.filterMode && key.name === "j")) {
748
793
  moveThemeSelection(1)
749
794
  return
750
795
  }
796
+ if (themeModal.filterMode && key.name === "backspace") {
797
+ editThemeQuery((query) => query.slice(0, -1))
798
+ return
799
+ }
800
+ if (themeModal.filterMode && key.ctrl && key.name === "u") {
801
+ updateThemeQuery("")
802
+ return
803
+ }
804
+ if (themeModal.filterMode && !key.ctrl && !key.meta && key.sequence.length === 1 && key.name !== "return") {
805
+ editThemeQuery((query) => query + key.sequence)
806
+ return
807
+ }
751
808
  return
752
809
  }
753
810
 
@@ -1229,7 +1286,7 @@ export const App = () => {
1229
1286
 
1230
1287
  return (
1231
1288
  <box width={terminalWidth} height={terminalHeight} flexDirection="column" backgroundColor={colors.background}>
1232
- <box paddingLeft={1} paddingRight={1} flexDirection="column" backgroundColor={colors.panel}>
1289
+ <box paddingLeft={1} paddingRight={1} flexDirection="column" backgroundColor={colors.background}>
1233
1290
  <PlainLine text={headerLine} fg={colors.muted} bold />
1234
1291
  </box>
1235
1292
  {isWideLayout && !detailFullView && !diffFullView && !isInitialLoading ? (
@@ -1321,7 +1378,7 @@ export const App = () => {
1321
1378
  ) : (
1322
1379
  <Divider width={contentWidth} />
1323
1380
  )}
1324
- <box paddingLeft={1} paddingRight={1} backgroundColor={colors.footer}>
1381
+ <box paddingLeft={1} paddingRight={1} backgroundColor={colors.background}>
1325
1382
  {footerNotice ? (
1326
1383
  <PlainLine text={footerNotice} fg={colors.count} />
1327
1384
  ) : (
package/src/ui/colors.ts CHANGED
@@ -1,9 +1,20 @@
1
- export type ThemeId = "ghui" | "tokyo-night" | "catppuccin" | "rose-pine" | "gruvbox" | "nord" | "dracula" | "opencode"
1
+ export type ThemeId =
2
+ | "ghui"
3
+ | "tokyo-night"
4
+ | "catppuccin"
5
+ | "rose-pine"
6
+ | "gruvbox"
7
+ | "nord"
8
+ | "dracula"
9
+ | "kanagawa"
10
+ | "one-dark"
11
+ | "monokai"
12
+ | "solarized-dark"
13
+ | "everforest"
14
+ | "opencode"
2
15
 
3
16
  export interface ColorPalette {
4
17
  readonly background: string
5
- readonly panel: string
6
- readonly footer: string
7
18
  readonly modalBackground: string
8
19
  readonly text: string
9
20
  readonly muted: string
@@ -50,8 +61,6 @@ export interface ThemeDefinition {
50
61
 
51
62
  const ghuiColors: ColorPalette = {
52
63
  background: "#111018",
53
- panel: "#161923",
54
- footer: "#1d2430",
55
64
  modalBackground: "#1a1a2e",
56
65
  text: "#ede7da",
57
66
  muted: "#9f9788",
@@ -91,8 +100,6 @@ const ghuiColors: ColorPalette = {
91
100
 
92
101
  const tokyoNightColors: ColorPalette = {
93
102
  background: "#1a1b26",
94
- panel: "#16161e",
95
- footer: "#24283b",
96
103
  modalBackground: "#24283b",
97
104
  text: "#c0caf5",
98
105
  muted: "#787c99",
@@ -132,8 +139,6 @@ const tokyoNightColors: ColorPalette = {
132
139
 
133
140
  const opencodeColors: ColorPalette = {
134
141
  background: "#0a0a0a",
135
- panel: "#141414",
136
- footer: "#1e1e1e",
137
142
  modalBackground: "#1e1e1e",
138
143
  text: "#eeeeee",
139
144
  muted: "#808080",
@@ -173,8 +178,6 @@ const opencodeColors: ColorPalette = {
173
178
 
174
179
  const catppuccinColors: ColorPalette = {
175
180
  background: "#1e1e2e",
176
- panel: "#181825",
177
- footer: "#313244",
178
181
  modalBackground: "#313244",
179
182
  text: "#cdd6f4",
180
183
  muted: "#7f849c",
@@ -214,8 +217,6 @@ const catppuccinColors: ColorPalette = {
214
217
 
215
218
  const rosePineColors: ColorPalette = {
216
219
  background: "#191724",
217
- panel: "#1f1d2e",
218
- footer: "#26233a",
219
220
  modalBackground: "#26233a",
220
221
  text: "#e0def4",
221
222
  muted: "#908caa",
@@ -255,8 +256,6 @@ const rosePineColors: ColorPalette = {
255
256
 
256
257
  const gruvboxColors: ColorPalette = {
257
258
  background: "#282828",
258
- panel: "#1d2021",
259
- footer: "#3c3836",
260
259
  modalBackground: "#3c3836",
261
260
  text: "#ebdbb2",
262
261
  muted: "#928374",
@@ -296,8 +295,6 @@ const gruvboxColors: ColorPalette = {
296
295
 
297
296
  const nordColors: ColorPalette = {
298
297
  background: "#2e3440",
299
- panel: "#242933",
300
- footer: "#3b4252",
301
298
  modalBackground: "#3b4252",
302
299
  text: "#eceff4",
303
300
  muted: "#8892a7",
@@ -337,8 +334,6 @@ const nordColors: ColorPalette = {
337
334
 
338
335
  const draculaColors: ColorPalette = {
339
336
  background: "#282a36",
340
- panel: "#21222c",
341
- footer: "#343746",
342
337
  modalBackground: "#343746",
343
338
  text: "#f8f8f2",
344
339
  muted: "#8f94b8",
@@ -376,6 +371,201 @@ const draculaColors: ColorPalette = {
376
371
  },
377
372
  }
378
373
 
374
+ const kanagawaColors: ColorPalette = {
375
+ background: "#1f1f28",
376
+ modalBackground: "#2a2a37",
377
+ text: "#dcd7ba",
378
+ muted: "#727169",
379
+ separator: "#54546d",
380
+ accent: "#7e9cd8",
381
+ inlineCode: "#d27e99",
382
+ error: "#c34043",
383
+ selectedBg: "#363646",
384
+ selectedText: "#fff7d6",
385
+ count: "#ffa066",
386
+ status: {
387
+ draft: "#c0a36e",
388
+ approved: "#76946a",
389
+ changes: "#c34043",
390
+ review: "#7e9cd8",
391
+ none: "#727169",
392
+ passing: "#76946a",
393
+ pending: "#c0a36e",
394
+ failing: "#c34043",
395
+ },
396
+ repos: {
397
+ opencode: "#7e9cd8",
398
+ "effect-smol": "#98bb6c",
399
+ "opencode-console": "#957fb8",
400
+ opencontrol: "#ffa066",
401
+ default: "#7fb4ca",
402
+ },
403
+ diff: {
404
+ addedBg: "#253326",
405
+ removedBg: "#3a2528",
406
+ contextBg: "transparent",
407
+ lineNumberBg: "#16161d",
408
+ addedLineNumberBg: "#223025",
409
+ removedLineNumberBg: "#352326",
410
+ },
411
+ }
412
+
413
+ const oneDarkColors: ColorPalette = {
414
+ background: "#282c34",
415
+ modalBackground: "#2c313c",
416
+ text: "#abb2bf",
417
+ muted: "#7f848e",
418
+ separator: "#4b5263",
419
+ accent: "#61afef",
420
+ inlineCode: "#c678dd",
421
+ error: "#e06c75",
422
+ selectedBg: "#3e4451",
423
+ selectedText: "#ffffff",
424
+ count: "#d19a66",
425
+ status: {
426
+ draft: "#e5c07b",
427
+ approved: "#98c379",
428
+ changes: "#e06c75",
429
+ review: "#61afef",
430
+ none: "#7f848e",
431
+ passing: "#98c379",
432
+ pending: "#e5c07b",
433
+ failing: "#e06c75",
434
+ },
435
+ repos: {
436
+ opencode: "#61afef",
437
+ "effect-smol": "#98c379",
438
+ "opencode-console": "#c678dd",
439
+ opencontrol: "#d19a66",
440
+ default: "#56b6c2",
441
+ },
442
+ diff: {
443
+ addedBg: "#27362b",
444
+ removedBg: "#3a282c",
445
+ contextBg: "transparent",
446
+ lineNumberBg: "#21252b",
447
+ addedLineNumberBg: "#243228",
448
+ removedLineNumberBg: "#35262a",
449
+ },
450
+ }
451
+
452
+ const monokaiColors: ColorPalette = {
453
+ background: "#272822",
454
+ modalBackground: "#383830",
455
+ text: "#f8f8f2",
456
+ muted: "#90908a",
457
+ separator: "#5b5b50",
458
+ accent: "#66d9ef",
459
+ inlineCode: "#ae81ff",
460
+ error: "#f92672",
461
+ selectedBg: "#49483e",
462
+ selectedText: "#ffffff",
463
+ count: "#fd971f",
464
+ status: {
465
+ draft: "#e6db74",
466
+ approved: "#a6e22e",
467
+ changes: "#f92672",
468
+ review: "#66d9ef",
469
+ none: "#90908a",
470
+ passing: "#a6e22e",
471
+ pending: "#e6db74",
472
+ failing: "#f92672",
473
+ },
474
+ repos: {
475
+ opencode: "#66d9ef",
476
+ "effect-smol": "#a6e22e",
477
+ "opencode-console": "#ae81ff",
478
+ opencontrol: "#fd971f",
479
+ default: "#a1efe4",
480
+ },
481
+ diff: {
482
+ addedBg: "#2f3a22",
483
+ removedBg: "#3d2430",
484
+ contextBg: "transparent",
485
+ lineNumberBg: "#1f201b",
486
+ addedLineNumberBg: "#2b3620",
487
+ removedLineNumberBg: "#38222d",
488
+ },
489
+ }
490
+
491
+ const solarizedDarkColors: ColorPalette = {
492
+ background: "#002b36",
493
+ modalBackground: "#123d48",
494
+ text: "#eee8d5",
495
+ muted: "#839496",
496
+ separator: "#586e75",
497
+ accent: "#268bd2",
498
+ inlineCode: "#2aa198",
499
+ error: "#dc322f",
500
+ selectedBg: "#174652",
501
+ selectedText: "#fdf6e3",
502
+ count: "#cb4b16",
503
+ status: {
504
+ draft: "#b58900",
505
+ approved: "#859900",
506
+ changes: "#dc322f",
507
+ review: "#268bd2",
508
+ none: "#839496",
509
+ passing: "#859900",
510
+ pending: "#b58900",
511
+ failing: "#dc322f",
512
+ },
513
+ repos: {
514
+ opencode: "#268bd2",
515
+ "effect-smol": "#859900",
516
+ "opencode-console": "#d33682",
517
+ opencontrol: "#cb4b16",
518
+ default: "#2aa198",
519
+ },
520
+ diff: {
521
+ addedBg: "#123c2e",
522
+ removedBg: "#3c262a",
523
+ contextBg: "transparent",
524
+ lineNumberBg: "#073642",
525
+ addedLineNumberBg: "#10372b",
526
+ removedLineNumberBg: "#362429",
527
+ },
528
+ }
529
+
530
+ const everforestColors: ColorPalette = {
531
+ background: "#2d353b",
532
+ modalBackground: "#343f44",
533
+ text: "#d3c6aa",
534
+ muted: "#859289",
535
+ separator: "#56635f",
536
+ accent: "#7fbbb3",
537
+ inlineCode: "#d699b6",
538
+ error: "#e67e80",
539
+ selectedBg: "#465258",
540
+ selectedText: "#fff4d6",
541
+ count: "#e69875",
542
+ status: {
543
+ draft: "#dbbc7f",
544
+ approved: "#a7c080",
545
+ changes: "#e67e80",
546
+ review: "#7fbbb3",
547
+ none: "#859289",
548
+ passing: "#a7c080",
549
+ pending: "#dbbc7f",
550
+ failing: "#e67e80",
551
+ },
552
+ repos: {
553
+ opencode: "#7fbbb3",
554
+ "effect-smol": "#a7c080",
555
+ "opencode-console": "#d699b6",
556
+ opencontrol: "#e69875",
557
+ default: "#83c092",
558
+ },
559
+ diff: {
560
+ addedBg: "#33422f",
561
+ removedBg: "#463333",
562
+ contextBg: "transparent",
563
+ lineNumberBg: "#232a2e",
564
+ addedLineNumberBg: "#303d2d",
565
+ removedLineNumberBg: "#403030",
566
+ },
567
+ }
568
+
379
569
  export const themeDefinitions: readonly ThemeDefinition[] = [
380
570
  { id: "ghui", name: "GHUI", description: "Warm parchment accents on a deep slate background", colors: ghuiColors },
381
571
  { id: "tokyo-night", name: "Tokyo Night", description: "Cool indigo surfaces with neon editor accents", colors: tokyoNightColors },
@@ -384,6 +574,11 @@ export const themeDefinitions: readonly ThemeDefinition[] = [
384
574
  { id: "gruvbox", name: "Gruvbox", description: "Retro warm earth tones with punchy semantic accents", colors: gruvboxColors },
385
575
  { id: "nord", name: "Nord", description: "Arctic blue-gray surfaces with frosty accents", colors: nordColors },
386
576
  { id: "dracula", name: "Dracula", description: "High-contrast purple, pink, cyan, and green", colors: draculaColors },
577
+ { id: "kanagawa", name: "Kanagawa", description: "Ink-wash indigo, wave blues, and autumn accents", colors: kanagawaColors },
578
+ { id: "one-dark", name: "One Dark", description: "Atom-style charcoal with clean blue and green accents", colors: oneDarkColors },
579
+ { id: "monokai", name: "Monokai", description: "Classic dark olive with electric syntax colors", colors: monokaiColors },
580
+ { id: "solarized-dark", name: "Solarized Dark", description: "Low-contrast blue-green base with calibrated accents", colors: solarizedDarkColors },
581
+ { id: "everforest", name: "Everforest", description: "Soft green-gray forest tones with warm highlights", colors: everforestColors },
387
582
  { id: "opencode", name: "OpenCode", description: "Charcoal panels with peach, violet, and blue highlights", colors: opencodeColors },
388
583
  ] as const
389
584
 
@@ -393,6 +588,16 @@ export const colors: ColorPalette = { ...ghuiColors }
393
588
 
394
589
  export const getThemeDefinition = (id: ThemeId) => themeDefinitions.find((theme) => theme.id === id) ?? themeDefinitions[0]!
395
590
 
591
+ export const filterThemeDefinitions = (query: string) => {
592
+ const normalized = query.trim().toLowerCase()
593
+ if (normalized.length === 0) return themeDefinitions
594
+ return themeDefinitions.filter((theme) =>
595
+ theme.id.includes(normalized) ||
596
+ theme.name.toLowerCase().includes(normalized) ||
597
+ theme.description.toLowerCase().includes(normalized),
598
+ )
599
+ }
600
+
396
601
  export const setActiveTheme = (id: ThemeId) => {
397
602
  if (activeTheme.id === id) return
398
603
  activeTheme = getThemeDefinition(id)
package/src/ui/modals.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import { TextAttributes } from "@opentui/core"
2
2
  import type { PullRequestLabel, PullRequestMergeInfo } from "../domain.js"
3
3
  import { availableMergeActions } from "../mergeActions.js"
4
- import { colors, themeDefinitions, type ThemeId } from "./colors.js"
4
+ import { colors, filterThemeDefinitions, themeDefinitions, type ThemeId } from "./colors.js"
5
5
  import { centerCell, Divider, fitCell, ModalFrame, PlainLine, TextLine } from "./primitives.js"
6
6
  import { labelColor, shortRepoName } from "./pullRequests.js"
7
7
 
@@ -27,6 +27,8 @@ export interface MergeModalState {
27
27
 
28
28
  export interface ThemeModalState {
29
29
  readonly open: boolean
30
+ readonly query: string
31
+ readonly filterMode: boolean
30
32
  readonly initialThemeId: ThemeId
31
33
  }
32
34
 
@@ -52,6 +54,8 @@ export const initialMergeModalState: MergeModalState = {
52
54
 
53
55
  export const initialThemeModalState: ThemeModalState = {
54
56
  open: false,
57
+ query: "",
58
+ filterMode: false,
55
59
  initialThemeId: "ghui",
56
60
  }
57
61
 
@@ -276,21 +280,27 @@ export const ThemeModal = ({
276
280
  const innerWidth = Math.max(16, modalWidth - 2)
277
281
  const contentWidth = Math.max(14, innerWidth - 2)
278
282
  const rowWidth = innerWidth
279
- const maxVisible = Math.max(1, modalHeight - 7)
280
- const activeIndex = themeDefinitions.findIndex((theme) => theme.id === activeThemeId)
283
+ const filteredThemes = filterThemeDefinitions(state.query)
284
+ const maxVisible = Math.max(1, modalHeight - 8)
285
+ const activeIndex = filteredThemes.findIndex((theme) => theme.id === activeThemeId)
281
286
  const selectedIndex = Math.max(0, activeIndex)
282
- const selectedTheme = themeDefinitions[selectedIndex]!
287
+ const selectedTheme = filteredThemes[selectedIndex] ?? themeDefinitions.find((theme) => theme.id === activeThemeId) ?? themeDefinitions[0]!
283
288
  const scrollStart = Math.min(
284
- Math.max(0, themeDefinitions.length - maxVisible),
289
+ Math.max(0, filteredThemes.length - maxVisible),
285
290
  Math.max(0, selectedIndex - maxVisible + 1),
286
291
  )
287
- const visibleThemes = themeDefinitions.slice(scrollStart, scrollStart + maxVisible)
288
- const countText = `${selectedIndex + 1}/${themeDefinitions.length}`
292
+ const visibleThemes = filteredThemes.slice(scrollStart, scrollStart + maxVisible)
293
+ const countText = `${filteredThemes.length === 0 ? 0 : selectedIndex + 1}/${filteredThemes.length}`
289
294
  const title = "Themes"
290
295
  const headerGap = Math.max(1, contentWidth - title.length - countText.length)
296
+ const queryText = state.query.length > 0 ? state.query : "type to filter themes"
297
+ const queryPrefix = "/ "
298
+ const queryWidth = Math.max(1, contentWidth - queryPrefix.length)
299
+ const messageTopRows = Math.max(0, Math.floor((maxVisible - 1) / 2))
300
+ const messageBottomRows = Math.max(0, maxVisible - messageTopRows - 1)
291
301
 
292
302
  return (
293
- <ModalFrame left={offsetLeft} top={offsetTop} width={modalWidth} height={modalHeight} junctionRows={[2, modalHeight - 4]}>
303
+ <ModalFrame left={offsetLeft} top={offsetTop} width={modalWidth} height={modalHeight} junctionRows={[3, modalHeight - 4]}>
294
304
  <box height={1} paddingLeft={1} paddingRight={1}>
295
305
  <TextLine>
296
306
  <span fg={colors.accent} attributes={TextAttributes.BOLD}>{title}</span>
@@ -301,9 +311,21 @@ export const ThemeModal = ({
301
311
  <box height={1} paddingLeft={1} paddingRight={1}>
302
312
  <PlainLine text={fitCell(selectedTheme.description, contentWidth)} fg={colors.muted} />
303
313
  </box>
314
+ <box height={1} paddingLeft={1} paddingRight={1}>
315
+ <TextLine>
316
+ <span fg={colors.count}>{queryPrefix}</span>
317
+ <span fg={state.query.length > 0 ? colors.text : colors.muted}>{fitCell(queryText, queryWidth)}</span>
318
+ </TextLine>
319
+ </box>
304
320
  <Divider width={innerWidth} />
305
321
  <box height={maxVisible} flexDirection="column">
306
- {visibleThemes.map((theme, index) => {
322
+ {visibleThemes.length === 0 ? (
323
+ <>
324
+ {Array.from({ length: messageTopRows }, (_, index) => <box key={`top-${index}`} height={1} />)}
325
+ <PlainLine text={centerCell("No matching themes", rowWidth)} fg={colors.muted} />
326
+ {Array.from({ length: messageBottomRows }, (_, index) => <box key={`bottom-${index}`} height={1} />)}
327
+ </>
328
+ ) : visibleThemes.map((theme, index) => {
307
329
  const actualIndex = scrollStart + index
308
330
  const isSelected = actualIndex === selectedIndex
309
331
  const isActive = theme.id === activeThemeId
@@ -317,7 +339,7 @@ export const ThemeModal = ({
317
339
  <span> </span>
318
340
  <span>{fitCell(theme.name, nameWidth)}</span>
319
341
  <span bg={theme.colors.background}> </span>
320
- <span bg={theme.colors.panel}> </span>
342
+ <span bg={theme.colors.modalBackground}> </span>
321
343
  <span bg={theme.colors.accent}> </span>
322
344
  <span bg={theme.colors.status.passing}> </span>
323
345
  <span bg={theme.colors.status.failing}> </span>
@@ -331,6 +353,8 @@ export const ThemeModal = ({
331
353
  <TextLine>
332
354
  <span fg={colors.count}>↑↓</span>
333
355
  <span fg={colors.muted}> preview </span>
356
+ <span fg={colors.count}>/</span>
357
+ <span fg={colors.muted}> filter </span>
334
358
  <span fg={colors.count}>enter</span>
335
359
  <span fg={colors.muted}> select </span>
336
360
  <span fg={colors.count}>esc</span>