@kitlangton/ghui 0.1.12 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitlangton/ghui",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Terminal UI for GitHub pull requests",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/App.tsx CHANGED
@@ -198,7 +198,7 @@ const copyPullRequestMetadata = async (pullRequest: PullRequestItem) => {
198
198
 
199
199
  const isShiftG = (key: { readonly name: string; readonly shift?: boolean }) => key.name === "G" || key.name === "g" && key.shift
200
200
 
201
- const isShiftT = (key: { readonly name: string; readonly shift?: boolean }) => key.name === "T" || key.name === "t" && key.shift
201
+ const isThemeKey = (key: { readonly name: string; readonly ctrl?: boolean; readonly meta?: boolean }) => !key.ctrl && !key.meta && key.name.toLowerCase() === "t"
202
202
 
203
203
  const getDetailPlaceholderContent = ({
204
204
  status,
@@ -278,8 +278,10 @@ export const App = () => {
278
278
  const getPullRequestDiff = useAtomSet(getPullRequestDiffAtom, { mode: "promise" })
279
279
  const getPullRequestMergeInfo = useAtomSet(getPullRequestMergeInfoAtom, { mode: "promise" })
280
280
  const mergePullRequest = useAtomSet(mergePullRequestAtom, { mode: "promise" })
281
- const contentWidth = Math.max(60, width ?? 100)
282
- const isWideLayout = (width ?? 100) >= 100
281
+ const terminalWidth = width ?? 100
282
+ const terminalHeight = height ?? 24
283
+ const contentWidth = Math.max(1, terminalWidth)
284
+ const isWideLayout = terminalWidth >= 100
283
285
  const splitGap = 1
284
286
  const sectionPadding = 1
285
287
  const leftPaneWidth = isWideLayout ? Math.max(44, Math.floor((contentWidth - splitGap) * 0.56)) : contentWidth
@@ -287,8 +289,8 @@ export const App = () => {
287
289
  const dividerJunctionAt = Math.max(1, leftPaneWidth)
288
290
  const leftContentWidth = isWideLayout ? Math.max(24, leftPaneWidth - 3) : Math.max(24, contentWidth - sectionPadding * 2)
289
291
  const rightContentWidth = isWideLayout ? Math.max(24, rightPaneWidth - sectionPadding * 2) : Math.max(24, contentWidth - sectionPadding * 2)
290
- const wideDetailLines = Math.max(8, (height ?? 24) - 8) // fill available vertical space
291
- const wideBodyHeight = Math.max(8, (height ?? 24) - 4)
292
+ const wideDetailLines = Math.max(8, terminalHeight - 8) // fill available vertical space
293
+ const wideBodyHeight = Math.max(8, terminalHeight - 4)
292
294
  const noticeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
293
295
  const pendingGTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
294
296
  const diffPrefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
@@ -557,13 +559,12 @@ export const App = () => {
557
559
  setMergeModal(initialMergeModalState)
558
560
  setThemeModal({
559
561
  open: true,
560
- selectedIndex: Math.max(0, themeDefinitions.findIndex((theme) => theme.id === themeId)),
561
562
  initialThemeId: themeId,
562
563
  })
563
564
  }
564
565
 
565
566
  const closeThemeModal = (confirm: boolean) => {
566
- const selectedTheme = themeDefinitions[themeModal.selectedIndex]
567
+ const selectedTheme = themeDefinitions.find((theme) => theme.id === themeId)
567
568
  if (!confirm) {
568
569
  setThemeId(themeModal.initialThemeId)
569
570
  } else if (selectedTheme) {
@@ -573,11 +574,11 @@ export const App = () => {
573
574
  }
574
575
 
575
576
  const moveThemeSelection = (delta: number) => {
576
- const selectedIndex = Math.max(0, Math.min(themeDefinitions.length - 1, themeModal.selectedIndex + delta))
577
- if (selectedIndex === themeModal.selectedIndex) return
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))
579
+ if (selectedIndex === currentIndex) return
578
580
  const theme = themeDefinitions[selectedIndex]
579
581
  if (theme && theme.id !== themeId) setThemeId(theme.id)
580
- setThemeModal((current) => ({ ...current, selectedIndex }))
581
582
  }
582
583
 
583
584
  const openLabelModal = () => {
@@ -1032,7 +1033,7 @@ export const App = () => {
1032
1033
  }
1033
1034
  }
1034
1035
 
1035
- if (isShiftT(key)) {
1036
+ if (isThemeKey(key)) {
1036
1037
  openThemeModal()
1037
1038
  return
1038
1039
  }
@@ -1183,7 +1184,7 @@ export const App = () => {
1183
1184
  })
1184
1185
 
1185
1186
  const fullscreenContentWidth = Math.max(24, contentWidth - 2)
1186
- const fullscreenBodyLines = Math.max(8, (height ?? 24) - 8)
1187
+ const fullscreenBodyLines = Math.max(8, terminalHeight - 8)
1187
1188
  const wideFullscreenDetailScrollable = getDetailsPaneHeight({
1188
1189
  pullRequest: selectedPullRequest,
1189
1190
  contentWidth: fullscreenContentWidth,
@@ -1214,20 +1215,20 @@ export const App = () => {
1214
1215
 
1215
1216
  const longestLabelName = labelModal.availableLabels.reduce((max, label) => Math.max(max, label.name.length), 0)
1216
1217
  const labelModalWidth = Math.min(Math.max(42, longestLabelName + 16), 56, contentWidth - 4)
1217
- const labelModalHeight = Math.min(20, (height ?? 24) - 4)
1218
+ const labelModalHeight = Math.min(20, terminalHeight - 4)
1218
1219
  const labelModalLeft = Math.floor((contentWidth - labelModalWidth) / 2)
1219
- const labelModalTop = Math.floor(((height ?? 24) - labelModalHeight) / 2)
1220
+ const labelModalTop = Math.floor((terminalHeight - labelModalHeight) / 2)
1220
1221
  const mergeModalWidth = Math.min(68, Math.max(46, contentWidth - 12))
1221
- const mergeModalHeight = Math.min(16, (height ?? 24) - 4)
1222
+ const mergeModalHeight = Math.min(16, terminalHeight - 4)
1222
1223
  const mergeModalLeft = Math.floor((contentWidth - mergeModalWidth) / 2)
1223
- const mergeModalTop = Math.floor(((height ?? 24) - mergeModalHeight) / 2)
1224
+ const mergeModalTop = Math.floor((terminalHeight - mergeModalHeight) / 2)
1224
1225
  const themeModalWidth = Math.min(58, Math.max(38, contentWidth - 12))
1225
- const themeModalHeight = Math.min(10, (height ?? 24) - 4)
1226
+ const themeModalHeight = Math.min(16, terminalHeight - 4)
1226
1227
  const themeModalLeft = Math.floor((contentWidth - themeModalWidth) / 2)
1227
- const themeModalTop = Math.floor(((height ?? 24) - themeModalHeight) / 2)
1228
+ const themeModalTop = Math.floor((terminalHeight - themeModalHeight) / 2)
1228
1229
 
1229
1230
  return (
1230
- <box flexGrow={1} flexDirection="column" backgroundColor={colors.background}>
1231
+ <box width={terminalWidth} height={terminalHeight} flexDirection="column" backgroundColor={colors.background}>
1231
1232
  <box paddingLeft={1} paddingRight={1} flexDirection="column" backgroundColor={colors.panel}>
1232
1233
  <PlainLine text={headerLine} fg={colors.muted} bold />
1233
1234
  </box>
@@ -1262,6 +1263,7 @@ export const App = () => {
1262
1263
  showChecks
1263
1264
  placeholderContent={detailPlaceholderContent}
1264
1265
  loadingIndicator={loadingIndicator}
1266
+ themeId={themeId}
1265
1267
  />
1266
1268
  </scrollbox>
1267
1269
  </box>
@@ -1278,7 +1280,7 @@ export const App = () => {
1278
1280
  <>
1279
1281
  <DetailHeader pullRequest={selectedPullRequest} contentWidth={rightContentWidth} paneWidth={rightPaneWidth} showChecks />
1280
1282
  <scrollbox flexGrow={1} verticalScrollbarOptions={{ visible: wideDetailBodyScrollable }}>
1281
- <DetailBody pullRequest={selectedPullRequest} contentWidth={rightContentWidth} bodyLines={wideDetailLines} loadingIndicator={loadingIndicator} />
1283
+ <DetailBody pullRequest={selectedPullRequest} contentWidth={rightContentWidth} bodyLines={wideDetailLines} loadingIndicator={loadingIndicator} themeId={themeId} />
1282
1284
  </scrollbox>
1283
1285
  </>
1284
1286
  ) : (
@@ -1296,12 +1298,13 @@ export const App = () => {
1296
1298
  paneWidth={contentWidth}
1297
1299
  placeholderContent={detailPlaceholderContent}
1298
1300
  loadingIndicator={loadingIndicator}
1301
+ themeId={themeId}
1299
1302
  />
1300
1303
  </scrollbox>
1301
1304
  </box>
1302
1305
  ) : (
1303
- <>
1304
- <DetailsPane pullRequest={selectedPullRequest} contentWidth={rightContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} />
1306
+ <box height={wideBodyHeight} flexDirection="column">
1307
+ <DetailsPane pullRequest={selectedPullRequest} contentWidth={rightContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
1305
1308
  <Divider width={contentWidth} />
1306
1309
  <box flexGrow={1} flexDirection="column">
1307
1310
  <scrollbox flexGrow={1}>
@@ -1310,7 +1313,7 @@ export const App = () => {
1310
1313
  </box>
1311
1314
  </scrollbox>
1312
1315
  </box>
1313
- </>
1316
+ </box>
1314
1317
  )}
1315
1318
 
1316
1319
  {isWideLayout && !detailFullView && !diffFullView && !isInitialLoading ? (
@@ -1318,7 +1321,7 @@ export const App = () => {
1318
1321
  ) : (
1319
1322
  <Divider width={contentWidth} />
1320
1323
  )}
1321
- <box paddingLeft={1} paddingRight={1} backgroundColor={colors.panel}>
1324
+ <box paddingLeft={1} paddingRight={1} backgroundColor={colors.footer}>
1322
1325
  {footerNotice ? (
1323
1326
  <PlainLine text={footerNotice} fg={colors.count} />
1324
1327
  ) : (
@@ -2,7 +2,7 @@ import { TextAttributes } from "@opentui/core"
2
2
  import { Fragment, useMemo } from "react"
3
3
  import { formatRelativeDate } from "../date.js"
4
4
  import type { CheckItem, PullRequestItem } from "../domain.js"
5
- import { colors } from "./colors.js"
5
+ import { colors, type ThemeId } from "./colors.js"
6
6
  import { diffStatText } from "./diff.js"
7
7
  import { centerCell, Divider, fitCell, PlainLine, TextLine } from "./primitives.js"
8
8
  import { labelColor, labelTextColor, reviewLabel, shortRepoName, statusColor } from "./pullRequests.js"
@@ -387,15 +387,17 @@ export const DetailBody = ({
387
387
  contentWidth,
388
388
  bodyLines = DETAIL_BODY_LINES,
389
389
  loadingIndicator,
390
+ themeId,
390
391
  }: {
391
392
  pullRequest: PullRequestItem
392
393
  contentWidth: number
393
394
  bodyLines?: number
394
395
  loadingIndicator: string
396
+ themeId: ThemeId
395
397
  }) => {
396
398
  const previewLines = useMemo(
397
399
  () => bodyPreview(pullRequest.body, contentWidth, bodyLines),
398
- [pullRequest.body, contentWidth, bodyLines],
400
+ [pullRequest.body, contentWidth, bodyLines, themeId],
399
401
  )
400
402
 
401
403
  if (!pullRequest.detailLoaded) {
@@ -486,6 +488,7 @@ export const DetailsPane = ({
486
488
  showChecks = false,
487
489
  placeholderContent,
488
490
  loadingIndicator,
491
+ themeId,
489
492
  }: {
490
493
  pullRequest: PullRequestItem | null
491
494
  contentWidth: number
@@ -494,6 +497,7 @@ export const DetailsPane = ({
494
497
  showChecks?: boolean
495
498
  placeholderContent: DetailPlaceholderContent
496
499
  loadingIndicator: string
500
+ themeId: ThemeId
497
501
  }) => {
498
502
  const contentHeight = getDetailsPaneHeight({ pullRequest, contentWidth, bodyLines, paneWidth, showChecks })
499
503
 
@@ -502,7 +506,7 @@ export const DetailsPane = ({
502
506
  {pullRequest ? (
503
507
  <>
504
508
  <DetailHeader pullRequest={pullRequest} contentWidth={contentWidth} paneWidth={paneWidth} showChecks={showChecks} />
505
- <DetailBody pullRequest={pullRequest} contentWidth={contentWidth} bodyLines={bodyLines} loadingIndicator={loadingIndicator} />
509
+ <DetailBody pullRequest={pullRequest} contentWidth={contentWidth} bodyLines={bodyLines} loadingIndicator={loadingIndicator} themeId={themeId} />
506
510
  </>
507
511
  ) : (
508
512
  <>
@@ -86,7 +86,7 @@ export const FooterHints = ({
86
86
  <TextLine>
87
87
  <span fg={colors.count}>/</span>
88
88
  <span fg={colors.muted}> filter </span>
89
- <span fg={colors.count}>T</span>
89
+ <span fg={colors.count}>t</span>
90
90
  <span fg={colors.muted}> theme </span>
91
91
  {showFilterClear ? (
92
92
  <>
package/src/ui/colors.ts CHANGED
@@ -1,8 +1,9 @@
1
- export type ThemeId = "ghui" | "tokyo-night" | "opencode"
1
+ export type ThemeId = "ghui" | "tokyo-night" | "catppuccin" | "rose-pine" | "gruvbox" | "nord" | "dracula" | "opencode"
2
2
 
3
3
  export interface ColorPalette {
4
4
  readonly background: string
5
5
  readonly panel: string
6
+ readonly footer: string
6
7
  readonly modalBackground: string
7
8
  readonly text: string
8
9
  readonly muted: string
@@ -50,6 +51,7 @@ export interface ThemeDefinition {
50
51
  const ghuiColors: ColorPalette = {
51
52
  background: "#111018",
52
53
  panel: "#161923",
54
+ footer: "#1d2430",
53
55
  modalBackground: "#1a1a2e",
54
56
  text: "#ede7da",
55
57
  muted: "#9f9788",
@@ -90,6 +92,7 @@ const ghuiColors: ColorPalette = {
90
92
  const tokyoNightColors: ColorPalette = {
91
93
  background: "#1a1b26",
92
94
  panel: "#16161e",
95
+ footer: "#24283b",
93
96
  modalBackground: "#24283b",
94
97
  text: "#c0caf5",
95
98
  muted: "#787c99",
@@ -130,6 +133,7 @@ const tokyoNightColors: ColorPalette = {
130
133
  const opencodeColors: ColorPalette = {
131
134
  background: "#0a0a0a",
132
135
  panel: "#141414",
136
+ footer: "#1e1e1e",
133
137
  modalBackground: "#1e1e1e",
134
138
  text: "#eeeeee",
135
139
  muted: "#808080",
@@ -167,9 +171,219 @@ const opencodeColors: ColorPalette = {
167
171
  },
168
172
  }
169
173
 
174
+ const catppuccinColors: ColorPalette = {
175
+ background: "#1e1e2e",
176
+ panel: "#181825",
177
+ footer: "#313244",
178
+ modalBackground: "#313244",
179
+ text: "#cdd6f4",
180
+ muted: "#7f849c",
181
+ separator: "#45475a",
182
+ accent: "#cba6f7",
183
+ inlineCode: "#f5c2e7",
184
+ error: "#f38ba8",
185
+ selectedBg: "#45475a",
186
+ selectedText: "#f5e0dc",
187
+ count: "#fab387",
188
+ status: {
189
+ draft: "#f9e2af",
190
+ approved: "#a6e3a1",
191
+ changes: "#f38ba8",
192
+ review: "#89b4fa",
193
+ none: "#7f849c",
194
+ passing: "#a6e3a1",
195
+ pending: "#f9e2af",
196
+ failing: "#f38ba8",
197
+ },
198
+ repos: {
199
+ opencode: "#89b4fa",
200
+ "effect-smol": "#a6e3a1",
201
+ "opencode-console": "#f5c2e7",
202
+ opencontrol: "#fab387",
203
+ default: "#74c7ec",
204
+ },
205
+ diff: {
206
+ addedBg: "#243927",
207
+ removedBg: "#3b2532",
208
+ contextBg: "transparent",
209
+ lineNumberBg: "#181825",
210
+ addedLineNumberBg: "#203524",
211
+ removedLineNumberBg: "#36232f",
212
+ },
213
+ }
214
+
215
+ const rosePineColors: ColorPalette = {
216
+ background: "#191724",
217
+ panel: "#1f1d2e",
218
+ footer: "#26233a",
219
+ modalBackground: "#26233a",
220
+ text: "#e0def4",
221
+ muted: "#908caa",
222
+ separator: "#524f67",
223
+ accent: "#c4a7e7",
224
+ inlineCode: "#f6c177",
225
+ error: "#eb6f92",
226
+ selectedBg: "#403d52",
227
+ selectedText: "#f6f1ff",
228
+ count: "#ebbcba",
229
+ status: {
230
+ draft: "#f6c177",
231
+ approved: "#9ccfd8",
232
+ changes: "#eb6f92",
233
+ review: "#31748f",
234
+ none: "#908caa",
235
+ passing: "#9ccfd8",
236
+ pending: "#f6c177",
237
+ failing: "#eb6f92",
238
+ },
239
+ repos: {
240
+ opencode: "#31748f",
241
+ "effect-smol": "#9ccfd8",
242
+ "opencode-console": "#c4a7e7",
243
+ opencontrol: "#f6c177",
244
+ default: "#ebbcba",
245
+ },
246
+ diff: {
247
+ addedBg: "#23343a",
248
+ removedBg: "#3a2534",
249
+ contextBg: "transparent",
250
+ lineNumberBg: "#1f1d2e",
251
+ addedLineNumberBg: "#203137",
252
+ removedLineNumberBg: "#352330",
253
+ },
254
+ }
255
+
256
+ const gruvboxColors: ColorPalette = {
257
+ background: "#282828",
258
+ panel: "#1d2021",
259
+ footer: "#3c3836",
260
+ modalBackground: "#3c3836",
261
+ text: "#ebdbb2",
262
+ muted: "#928374",
263
+ separator: "#665c54",
264
+ accent: "#fabd2f",
265
+ inlineCode: "#d3869b",
266
+ error: "#fb4934",
267
+ selectedBg: "#504945",
268
+ selectedText: "#fbf1c7",
269
+ count: "#fe8019",
270
+ status: {
271
+ draft: "#fabd2f",
272
+ approved: "#b8bb26",
273
+ changes: "#fb4934",
274
+ review: "#83a598",
275
+ none: "#928374",
276
+ passing: "#b8bb26",
277
+ pending: "#fabd2f",
278
+ failing: "#fb4934",
279
+ },
280
+ repos: {
281
+ opencode: "#83a598",
282
+ "effect-smol": "#b8bb26",
283
+ "opencode-console": "#d3869b",
284
+ opencontrol: "#fe8019",
285
+ default: "#8ec07c",
286
+ },
287
+ diff: {
288
+ addedBg: "#32361f",
289
+ removedBg: "#3c2927",
290
+ contextBg: "transparent",
291
+ lineNumberBg: "#1d2021",
292
+ addedLineNumberBg: "#2f331e",
293
+ removedLineNumberBg: "#382726",
294
+ },
295
+ }
296
+
297
+ const nordColors: ColorPalette = {
298
+ background: "#2e3440",
299
+ panel: "#242933",
300
+ footer: "#3b4252",
301
+ modalBackground: "#3b4252",
302
+ text: "#eceff4",
303
+ muted: "#8892a7",
304
+ separator: "#4c566a",
305
+ accent: "#88c0d0",
306
+ inlineCode: "#b48ead",
307
+ error: "#bf616a",
308
+ selectedBg: "#434c5e",
309
+ selectedText: "#eceff4",
310
+ count: "#ebcb8b",
311
+ status: {
312
+ draft: "#ebcb8b",
313
+ approved: "#a3be8c",
314
+ changes: "#bf616a",
315
+ review: "#81a1c1",
316
+ none: "#8892a7",
317
+ passing: "#a3be8c",
318
+ pending: "#ebcb8b",
319
+ failing: "#bf616a",
320
+ },
321
+ repos: {
322
+ opencode: "#81a1c1",
323
+ "effect-smol": "#a3be8c",
324
+ "opencode-console": "#b48ead",
325
+ opencontrol: "#d08770",
326
+ default: "#88c0d0",
327
+ },
328
+ diff: {
329
+ addedBg: "#334033",
330
+ removedBg: "#433238",
331
+ contextBg: "transparent",
332
+ lineNumberBg: "#242933",
333
+ addedLineNumberBg: "#303d31",
334
+ removedLineNumberBg: "#3f3036",
335
+ },
336
+ }
337
+
338
+ const draculaColors: ColorPalette = {
339
+ background: "#282a36",
340
+ panel: "#21222c",
341
+ footer: "#343746",
342
+ modalBackground: "#343746",
343
+ text: "#f8f8f2",
344
+ muted: "#8f94b8",
345
+ separator: "#4f5268",
346
+ accent: "#bd93f9",
347
+ inlineCode: "#ff79c6",
348
+ error: "#ff5555",
349
+ selectedBg: "#44475a",
350
+ selectedText: "#f8f8f2",
351
+ count: "#ffb86c",
352
+ status: {
353
+ draft: "#f1fa8c",
354
+ approved: "#50fa7b",
355
+ changes: "#ff5555",
356
+ review: "#8be9fd",
357
+ none: "#8f94b8",
358
+ passing: "#50fa7b",
359
+ pending: "#f1fa8c",
360
+ failing: "#ff5555",
361
+ },
362
+ repos: {
363
+ opencode: "#8be9fd",
364
+ "effect-smol": "#50fa7b",
365
+ "opencode-console": "#ff79c6",
366
+ opencontrol: "#ffb86c",
367
+ default: "#bd93f9",
368
+ },
369
+ diff: {
370
+ addedBg: "#203a29",
371
+ removedBg: "#43272f",
372
+ contextBg: "transparent",
373
+ lineNumberBg: "#21222c",
374
+ addedLineNumberBg: "#1d3627",
375
+ removedLineNumberBg: "#3d252c",
376
+ },
377
+ }
378
+
170
379
  export const themeDefinitions: readonly ThemeDefinition[] = [
171
380
  { id: "ghui", name: "GHUI", description: "Warm parchment accents on a deep slate background", colors: ghuiColors },
172
381
  { id: "tokyo-night", name: "Tokyo Night", description: "Cool indigo surfaces with neon editor accents", colors: tokyoNightColors },
382
+ { id: "catppuccin", name: "Catppuccin", description: "Mocha lavender, peach, and soft pastel contrast", colors: catppuccinColors },
383
+ { id: "rose-pine", name: "Rose Pine", description: "Muted rose, pine, and gold on dusky violet", colors: rosePineColors },
384
+ { id: "gruvbox", name: "Gruvbox", description: "Retro warm earth tones with punchy semantic accents", colors: gruvboxColors },
385
+ { id: "nord", name: "Nord", description: "Arctic blue-gray surfaces with frosty accents", colors: nordColors },
386
+ { id: "dracula", name: "Dracula", description: "High-contrast purple, pink, cyan, and green", colors: draculaColors },
173
387
  { id: "opencode", name: "OpenCode", description: "Charcoal panels with peach, violet, and blue highlights", colors: opencodeColors },
174
388
  ] as const
175
389
 
@@ -180,6 +394,7 @@ export const colors: ColorPalette = { ...ghuiColors }
180
394
  export const getThemeDefinition = (id: ThemeId) => themeDefinitions.find((theme) => theme.id === id) ?? themeDefinitions[0]!
181
395
 
182
396
  export const setActiveTheme = (id: ThemeId) => {
397
+ if (activeTheme.id === id) return
183
398
  activeTheme = getThemeDefinition(id)
184
399
  Object.assign(colors, activeTheme.colors)
185
400
  }
package/src/ui/modals.tsx CHANGED
@@ -27,7 +27,6 @@ export interface MergeModalState {
27
27
 
28
28
  export interface ThemeModalState {
29
29
  readonly open: boolean
30
- readonly selectedIndex: number
31
30
  readonly initialThemeId: ThemeId
32
31
  }
33
32
 
@@ -53,7 +52,6 @@ export const initialMergeModalState: MergeModalState = {
53
52
 
54
53
  export const initialThemeModalState: ThemeModalState = {
55
54
  open: false,
56
- selectedIndex: 0,
57
55
  initialThemeId: "ghui",
58
56
  }
59
57
 
@@ -279,7 +277,8 @@ export const ThemeModal = ({
279
277
  const contentWidth = Math.max(14, innerWidth - 2)
280
278
  const rowWidth = innerWidth
281
279
  const maxVisible = Math.max(1, modalHeight - 7)
282
- const selectedIndex = Math.max(0, Math.min(state.selectedIndex, themeDefinitions.length - 1))
280
+ const activeIndex = themeDefinitions.findIndex((theme) => theme.id === activeThemeId)
281
+ const selectedIndex = Math.max(0, activeIndex)
283
282
  const selectedTheme = themeDefinitions[selectedIndex]!
284
283
  const scrollStart = Math.min(
285
284
  Math.max(0, themeDefinitions.length - maxVisible),