@carlesandres/house 0.4.6 → 0.4.8
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 +57 -5
- package/README.md +23 -11
- package/package.json +1 -1
- package/src/Browser.tsx +321 -184
- package/src/CommandPalette.tsx +78 -14
- package/src/Footer.tsx +65 -31
- package/src/Header.tsx +4 -1
- package/src/Spinner.tsx +39 -0
- package/src/StatusPopover.tsx +189 -0
- package/src/cli/argv.ts +19 -6
- package/src/commands/buildCommands.ts +22 -33
- package/src/commands/score.ts +1 -1
- package/src/config/load.ts +23 -1
- package/src/discovery/walk.ts +20 -3
- package/src/index.tsx +159 -155
- package/src/keymap/browser.ts +17 -23
- package/src/keymap/keymap.ts +4 -4
- package/src/layout/sidebarEmptyState.ts +7 -0
- package/src/markdown/frontmatter.ts +62 -0
- package/src/serve/server.ts +1 -1
- package/src/theme/resolve.ts +1 -1
- package/src/theme/themes/aura.json +1 -1
- package/src/theme/themes/carbonfox.json +1 -1
- package/src/theme/themes/lucent-orng.json +4 -4
- package/src/theme/themes/nightowl.json +2 -2
- package/src/theme/themes/orng.json +2 -2
- package/src/theme/themes/solarized.json +6 -2
- package/src/theme/themes/vesper.json +2 -2
- package/src/tips.ts +0 -5
- package/src/update/check.ts +1 -1
- package/src/update/runtime.ts +1 -1
- package/src/HelpOverlay.tsx +0 -148
package/src/Browser.tsx
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - q / ctrl+c quit.
|
|
8
8
|
*
|
|
9
9
|
* Deferred to next iteration: focus model, reader scrolling via j/k,
|
|
10
|
-
* sidebar collapse with
|
|
10
|
+
* sidebar collapse with `\`.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { SyntaxStyle } from "@opentui/core"
|
|
@@ -15,16 +15,16 @@ import type { BorderSides } from "@opentui/core"
|
|
|
15
15
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
|
|
16
16
|
import { useAtomValue, useAtomSet } from "@effect/atom-react"
|
|
17
17
|
import { Effect } from "effect"
|
|
18
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
18
|
+
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react"
|
|
19
19
|
import { buildCommands } from "./commands/buildCommands.ts"
|
|
20
20
|
import { clampSelectedIndex, filterCommands } from "./commands/score.ts"
|
|
21
|
-
import { CommandPalette } from "./CommandPalette.tsx"
|
|
21
|
+
import { CommandPalette, orderCommandsForPalette } from "./CommandPalette.tsx"
|
|
22
22
|
import { filterFiles } from "./discovery/filter.ts"
|
|
23
23
|
import { type FileEntry } from "./discovery/walk.ts"
|
|
24
|
+
import { parseFrontmatter } from "./markdown/frontmatter.ts"
|
|
24
25
|
import { BRAND, BRAND_NAME } from "./brand.ts"
|
|
25
|
-
import { Footer, FOOTER_HEIGHT } from "./Footer.tsx"
|
|
26
|
+
import { Footer, FOOTER_HEIGHT, type FooterProps } from "./Footer.tsx"
|
|
26
27
|
import { Header, HEADER_HEIGHT } from "./Header.tsx"
|
|
27
|
-
import { HelpOverlay } from "./HelpOverlay.tsx"
|
|
28
28
|
import { openInEditor, resolveEditor } from "./io/editor.ts"
|
|
29
29
|
import { readFileText } from "./io/readFile.ts"
|
|
30
30
|
import { browserBindings, type BrowserCtx } from "./keymap/browser.ts"
|
|
@@ -35,8 +35,10 @@ import {
|
|
|
35
35
|
initialShownForAuto,
|
|
36
36
|
resolveSidebarWidth,
|
|
37
37
|
} from "./layout/resolve.ts"
|
|
38
|
+
import { fitSidebarEmptyValue } from "./layout/sidebarEmptyState.ts"
|
|
38
39
|
import { formatSidebarRow } from "./layout/sidebarRow.ts"
|
|
39
40
|
import { PromptRow } from "./PromptRow.tsx"
|
|
41
|
+
import { StatusPopoverPanel } from "./StatusPopover.tsx"
|
|
40
42
|
import { buildReaderEmptyStateTips, pickTipByRotation } from "./tips.ts"
|
|
41
43
|
import { openInBrowser } from "./serve/openBrowser.ts"
|
|
42
44
|
import { startServer, type ServerHandle } from "./serve/server.ts"
|
|
@@ -50,11 +52,26 @@ export type StartupFocus = "sidebar" | "reader" | "filter"
|
|
|
50
52
|
export interface BrowserProps {
|
|
51
53
|
readonly files: readonly FileEntry[]
|
|
52
54
|
readonly initialIndex?: number
|
|
55
|
+
/** Initial applied filter query seeded from the CLI positional. */
|
|
56
|
+
readonly initialQuery?: string
|
|
53
57
|
/** Cap the rendered markdown's width at N columns. Null = fill the pane. */
|
|
54
58
|
readonly maxWidth?: number | null
|
|
59
|
+
/** Discovery root label used in the post-discovery empty-vault sidebar row. */
|
|
60
|
+
readonly emptyRootLabel?: string
|
|
55
61
|
/** Persistent footer indicator (e.g. "indexing… 42"). Pass null/undefined
|
|
56
62
|
* when discovery has finished; the indicator clears. */
|
|
57
63
|
readonly discoveryStatus?: string | null
|
|
64
|
+
/** Test seam: override the footer discovery spinner speed. */
|
|
65
|
+
readonly discoverySpinnerIntervalMs?: number
|
|
66
|
+
readonly discoverySpinnerInitialFrameIndex?: number
|
|
67
|
+
/** Test seam: deterministic footer spinner driver. */
|
|
68
|
+
readonly discoverySpinnerRegisterTick?: ((tick: () => void) => void) | null
|
|
69
|
+
/** Test seam: override filter debounce timing. */
|
|
70
|
+
readonly filterDebounceMs?: number
|
|
71
|
+
/** Test seam: override rendered-path debounce timing. */
|
|
72
|
+
readonly renderedPathDebounceMs?: number
|
|
73
|
+
/** Test seam: disable reader-empty-state tip rotation effect. */
|
|
74
|
+
readonly disableReaderEmptyStateRotation?: boolean
|
|
58
75
|
/** Initial sidebar visibility (`--sidebar` flag). `auto` consults the
|
|
59
76
|
* launch viewport bucket once; subsequent visibility goes through `s`. */
|
|
60
77
|
readonly sidebarMode?: SidebarMode
|
|
@@ -69,6 +86,8 @@ export interface BrowserProps {
|
|
|
69
86
|
/** TTL (ms) for the update-notice toast. Exposed so tests can use a small
|
|
70
87
|
* value instead of sleeping for the production 10s window. */
|
|
71
88
|
readonly updateNoticeTtlMs?: number
|
|
89
|
+
/** Test seam: disable footer-notice auto-clear timers. */
|
|
90
|
+
readonly disableFooterNoticeAutoClear?: boolean
|
|
72
91
|
/** Flip the parent's discovery vocabulary (#145). Browser doesn't need
|
|
73
92
|
* to know which categories are currently on — the toggle is opaque
|
|
74
93
|
* from this side; we just snapshot the selected path so it can be
|
|
@@ -83,18 +102,6 @@ const defaultReadFile = (path: string): Promise<string> => Effect.runPromise(rea
|
|
|
83
102
|
|
|
84
103
|
const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(max, n))
|
|
85
104
|
|
|
86
|
-
/** Bindings the help overlay lets through. Single source of truth for both
|
|
87
|
-
* the keyboard early-return and the footer hint filter. `palette.open`
|
|
88
|
-
* passes through so users can jump from help into the palette in one
|
|
89
|
-
* keystroke — `openPalette` closes help on its way in. */
|
|
90
|
-
const HELP_ALLOWED_IDS: ReadonlySet<string> = new Set([
|
|
91
|
-
"help.toggle",
|
|
92
|
-
"theme.next",
|
|
93
|
-
"theme.prev",
|
|
94
|
-
"theme.toneToggle",
|
|
95
|
-
"palette.open",
|
|
96
|
-
])
|
|
97
|
-
|
|
98
105
|
let nextReaderEmptyStateTipRotation = 0
|
|
99
106
|
|
|
100
107
|
export const resetReaderEmptyStateTipRotationForTests = () => {
|
|
@@ -105,16 +112,96 @@ export const setReaderEmptyStateTipRotationForTests = (next: number) => {
|
|
|
105
112
|
nextReaderEmptyStateTipRotation = next
|
|
106
113
|
}
|
|
107
114
|
|
|
115
|
+
const FILTER_DEBOUNCE_MS = 50
|
|
116
|
+
const RENDERED_PATH_DEBOUNCE_MS = 80
|
|
117
|
+
|
|
118
|
+
const isPartialDiscoveryWarning = (status: string | null | undefined): boolean =>
|
|
119
|
+
status?.trimStart().startsWith("scan incomplete:") ?? false
|
|
120
|
+
|
|
121
|
+
type FloatingOverlay =
|
|
122
|
+
| { readonly kind: "none" }
|
|
123
|
+
| { readonly kind: "command-palette" }
|
|
124
|
+
| { readonly kind: "status-popover"; readonly content: string }
|
|
125
|
+
|
|
126
|
+
type FloatingOverlayAction =
|
|
127
|
+
| { readonly type: "close" }
|
|
128
|
+
| { readonly type: "open-command-palette" }
|
|
129
|
+
| { readonly type: "toggle-status-popover"; readonly content: string }
|
|
130
|
+
| { readonly type: "update-status-popover"; readonly content: string }
|
|
131
|
+
|
|
132
|
+
const noFloatingOverlay: FloatingOverlay = { kind: "none" }
|
|
133
|
+
|
|
134
|
+
const floatingOverlayReducer = (
|
|
135
|
+
state: FloatingOverlay,
|
|
136
|
+
action: FloatingOverlayAction,
|
|
137
|
+
): FloatingOverlay => {
|
|
138
|
+
switch (action.type) {
|
|
139
|
+
case "close":
|
|
140
|
+
return noFloatingOverlay
|
|
141
|
+
case "open-command-palette":
|
|
142
|
+
return { kind: "command-palette" }
|
|
143
|
+
case "toggle-status-popover":
|
|
144
|
+
return state.kind === "status-popover"
|
|
145
|
+
? noFloatingOverlay
|
|
146
|
+
: { kind: "status-popover", content: action.content }
|
|
147
|
+
case "update-status-popover":
|
|
148
|
+
return state.kind === "status-popover"
|
|
149
|
+
? { kind: "status-popover", content: action.content }
|
|
150
|
+
: state
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const SidebarEmptyMessage = ({
|
|
155
|
+
label,
|
|
156
|
+
value,
|
|
157
|
+
width,
|
|
158
|
+
withTopSpacer,
|
|
159
|
+
}: {
|
|
160
|
+
readonly label: string
|
|
161
|
+
readonly value: string
|
|
162
|
+
readonly width: number
|
|
163
|
+
readonly withTopSpacer: boolean
|
|
164
|
+
}) => (
|
|
165
|
+
<>
|
|
166
|
+
{withTopSpacer && <text content="" />}
|
|
167
|
+
<box
|
|
168
|
+
style={{
|
|
169
|
+
width,
|
|
170
|
+
flexDirection: "row",
|
|
171
|
+
flexWrap: "wrap",
|
|
172
|
+
justifyContent: "center",
|
|
173
|
+
gap: 1,
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
<text wrapMode="none">
|
|
177
|
+
<span style={{ fg: colors.textMuted }}>{label}</span>
|
|
178
|
+
</text>
|
|
179
|
+
<text wrapMode="none">
|
|
180
|
+
<span style={{ fg: colors.textMuted }}>{`"${fitSidebarEmptyValue(value, width)}"`}</span>
|
|
181
|
+
</text>
|
|
182
|
+
</box>
|
|
183
|
+
</>
|
|
184
|
+
)
|
|
185
|
+
|
|
108
186
|
export const Browser = ({
|
|
109
187
|
files,
|
|
110
188
|
initialIndex = 0,
|
|
189
|
+
initialQuery = "",
|
|
111
190
|
maxWidth = null,
|
|
191
|
+
emptyRootLabel = "current root",
|
|
112
192
|
discoveryStatus = null,
|
|
193
|
+
discoverySpinnerIntervalMs,
|
|
194
|
+
discoverySpinnerInitialFrameIndex,
|
|
195
|
+
discoverySpinnerRegisterTick = null,
|
|
196
|
+
filterDebounceMs = FILTER_DEBOUNCE_MS,
|
|
197
|
+
renderedPathDebounceMs = RENDERED_PATH_DEBOUNCE_MS,
|
|
198
|
+
disableReaderEmptyStateRotation = false,
|
|
113
199
|
sidebarMode = "auto",
|
|
114
200
|
onQuit,
|
|
115
201
|
readFile = defaultReadFile,
|
|
116
202
|
updateNotice = null,
|
|
117
203
|
updateNoticeTtlMs = 10000,
|
|
204
|
+
disableFooterNoticeAutoClear = false,
|
|
118
205
|
onToggleAll,
|
|
119
206
|
startupFocus = null,
|
|
120
207
|
}: BrowserProps) => {
|
|
@@ -164,17 +251,29 @@ export const Browser = ({
|
|
|
164
251
|
shown || initialFocus === "sidebar" ? "sidebar" : "reader",
|
|
165
252
|
)
|
|
166
253
|
const [sidebarScroll, setSidebarScroll] = useState<number>(0)
|
|
167
|
-
const [helpVisible, setHelpVisible] = useState<boolean>(false)
|
|
168
254
|
const [filterOpen, setFilterOpen] = useState<boolean>(startInFilter)
|
|
169
|
-
const [
|
|
170
|
-
const [
|
|
255
|
+
const [filterInput, setFilterInput] = useState<string>(initialQuery)
|
|
256
|
+
const [filterApplied, setFilterApplied] = useState<string>(initialQuery)
|
|
257
|
+
const [floatingOverlay, dispatchFloatingOverlayState] = useReducer(
|
|
258
|
+
floatingOverlayReducer,
|
|
259
|
+
noFloatingOverlay,
|
|
260
|
+
)
|
|
261
|
+
const floatingOverlayRef = useRef<FloatingOverlay>(noFloatingOverlay)
|
|
262
|
+
const dispatchFloatingOverlay = (action: FloatingOverlayAction): void => {
|
|
263
|
+
const next = floatingOverlayReducer(floatingOverlayRef.current, action)
|
|
264
|
+
floatingOverlayRef.current = next
|
|
265
|
+
dispatchFloatingOverlayState(action)
|
|
266
|
+
}
|
|
267
|
+
const closeFloatingOverlay = (): void => dispatchFloatingOverlay({ type: "close" })
|
|
268
|
+
const paletteOpen = floatingOverlay.kind === "command-palette"
|
|
269
|
+
const activeStatusPopover = floatingOverlay.kind === "status-popover" ? floatingOverlay : null
|
|
270
|
+
const discoveryWarningStatus = isPartialDiscoveryWarning(discoveryStatus) ? discoveryStatus : null
|
|
171
271
|
const [paletteQuery, setPaletteQuery] = useState<string>("")
|
|
172
272
|
const [paletteIndex, setPaletteIndex] = useState<number>(0)
|
|
173
273
|
// Synchronous mirrors for the keyboard handler — same reason filterOpenRef
|
|
174
274
|
// exists. Modal input can arrive in one React batch (e.g. ctrl+p, Down,
|
|
175
275
|
// Return), so every palette field read by later keys must update its ref
|
|
176
276
|
// before React state commits.
|
|
177
|
-
const paletteOpenRef = useRef(false)
|
|
178
277
|
const paletteQueryRef = useRef("")
|
|
179
278
|
const paletteIndexRef = useRef(0)
|
|
180
279
|
const [readerEmptyStateTipRotation, setReaderEmptyStateTipRotation] = useState(
|
|
@@ -186,7 +285,9 @@ export const Browser = ({
|
|
|
186
285
|
// first key opens the filter; subsequent keys in the same tick would
|
|
187
286
|
// otherwise still observe filterOpen=false through closure).
|
|
188
287
|
const filterOpenRef = useRef(startInFilter)
|
|
189
|
-
const
|
|
288
|
+
const filterInputRef = useRef(initialQuery)
|
|
289
|
+
const filterAppliedRef = useRef(initialQuery)
|
|
290
|
+
const autoSelectForAppliedFilterRef = useRef(true)
|
|
190
291
|
const focusRef = useRef<"sidebar" | "reader">(focus)
|
|
191
292
|
const restoreFilterOnSidebarFocusRef = useRef(startInFilter)
|
|
192
293
|
const [footerNotice, setFooterNoticeState] = useState<{
|
|
@@ -220,9 +321,24 @@ export const Browser = ({
|
|
|
220
321
|
// the other's display window.
|
|
221
322
|
useEffect(() => {
|
|
222
323
|
if (footerNotice === null) return
|
|
324
|
+
closeFloatingOverlay()
|
|
325
|
+
if (disableFooterNoticeAutoClear) return
|
|
223
326
|
const timer = setTimeout(() => setFooterNoticeState(null), footerNotice.ttlMs)
|
|
224
327
|
return () => clearTimeout(timer)
|
|
225
|
-
}, [footerNotice])
|
|
328
|
+
}, [disableFooterNoticeAutoClear, footerNotice])
|
|
329
|
+
|
|
330
|
+
useEffect(() => {
|
|
331
|
+
if (discoveryWarningStatus === null) {
|
|
332
|
+
if (floatingOverlay.kind === "status-popover") closeFloatingOverlay()
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
if (
|
|
336
|
+
floatingOverlay.kind === "status-popover" &&
|
|
337
|
+
floatingOverlay.content !== discoveryWarningStatus
|
|
338
|
+
) {
|
|
339
|
+
dispatchFloatingOverlay({ type: "update-status-popover", content: discoveryWarningStatus })
|
|
340
|
+
}
|
|
341
|
+
}, [discoveryWarningStatus, floatingOverlay])
|
|
226
342
|
|
|
227
343
|
// Push the update-available nudge once, when it arrives from the parent
|
|
228
344
|
// (the registry probe resolves asynchronously after boot). 10s gives the
|
|
@@ -257,8 +373,29 @@ export const Browser = ({
|
|
|
257
373
|
pushFooterNotice(`tone: ${nextTone}`)
|
|
258
374
|
}
|
|
259
375
|
|
|
260
|
-
|
|
261
|
-
|
|
376
|
+
useEffect(() => {
|
|
377
|
+
if (filterInput === filterApplied) return
|
|
378
|
+
const timer = setTimeout(() => {
|
|
379
|
+
filterAppliedRef.current = filterInput
|
|
380
|
+
setFilterApplied(filterInput)
|
|
381
|
+
}, filterDebounceMs)
|
|
382
|
+
return () => clearTimeout(timer)
|
|
383
|
+
}, [filterApplied, filterDebounceMs, filterInput])
|
|
384
|
+
|
|
385
|
+
useEffect(() => {
|
|
386
|
+
autoSelectForAppliedFilterRef.current = true
|
|
387
|
+
}, [filterApplied])
|
|
388
|
+
|
|
389
|
+
const displayedFiles = useMemo(() => filterFiles(files, filterApplied), [files, filterApplied])
|
|
390
|
+
const filterHasNoMatches = filterInput.length > 0 && displayedFiles.length === 0
|
|
391
|
+
|
|
392
|
+
useEffect(() => {
|
|
393
|
+
if (filterApplied.length === 0) return
|
|
394
|
+
if (!autoSelectForAppliedFilterRef.current) return
|
|
395
|
+
if (displayedFiles.length === 0) return
|
|
396
|
+
setSelectedIndex(0)
|
|
397
|
+
autoSelectForAppliedFilterRef.current = false
|
|
398
|
+
}, [displayedFiles, filterApplied])
|
|
262
399
|
// When the filtered list shrinks, keep selectedIndex valid. The reset to 0
|
|
263
400
|
// on every query change happens in the keystroke handler, not here, so a
|
|
264
401
|
// no-op rerender doesn't snap the cursor back to the top.
|
|
@@ -296,9 +433,9 @@ export const Browser = ({
|
|
|
296
433
|
useEffect(() => {
|
|
297
434
|
const target = selected?.path ?? null
|
|
298
435
|
if (target === renderedPath) return
|
|
299
|
-
const timer = setTimeout(() => setRenderedPath(target),
|
|
436
|
+
const timer = setTimeout(() => setRenderedPath(target), renderedPathDebounceMs)
|
|
300
437
|
return () => clearTimeout(timer)
|
|
301
|
-
}, [selected?.path, renderedPath])
|
|
438
|
+
}, [selected?.path, renderedPath, renderedPathDebounceMs])
|
|
302
439
|
|
|
303
440
|
useEffect(() => {
|
|
304
441
|
if (!renderedPath) {
|
|
@@ -337,10 +474,9 @@ export const Browser = ({
|
|
|
337
474
|
hasSelected: selected != null,
|
|
338
475
|
focus,
|
|
339
476
|
sidebarShown: shown,
|
|
340
|
-
helpVisible,
|
|
341
477
|
filterOpen,
|
|
342
478
|
restoreFilterOnSidebarFocus: restoreFilterOnSidebarFocusRef.current,
|
|
343
|
-
filterQuery,
|
|
479
|
+
filterQuery: filterInput,
|
|
344
480
|
paletteOpen,
|
|
345
481
|
setFocus,
|
|
346
482
|
// Wrapped so any keymap-driven selection move (j/k/g/G/[/], reader
|
|
@@ -350,6 +486,7 @@ export const Browser = ({
|
|
|
350
486
|
// itself) deliberately use the raw `setSelectedIndex` setter.
|
|
351
487
|
setSelectedIndex: (updater) => {
|
|
352
488
|
pendingSelectionPathRef.current = null
|
|
489
|
+
autoSelectForAppliedFilterRef.current = false
|
|
353
490
|
setSelectedIndex(updater)
|
|
354
491
|
},
|
|
355
492
|
toggleShown: () => {
|
|
@@ -376,8 +513,8 @@ export const Browser = ({
|
|
|
376
513
|
if (focus === "reader") setFocus("sidebar")
|
|
377
514
|
}
|
|
378
515
|
},
|
|
379
|
-
setHelpVisible,
|
|
380
516
|
openFilter: () => {
|
|
517
|
+
closeFloatingOverlay()
|
|
381
518
|
// Focus the sidebar so the filter input has a home. In wide,
|
|
382
519
|
// §7.1's visibility rule (`shown || focus === "sidebar"`) brings
|
|
383
520
|
// the inline sidebar back on screen if it was hidden. In narrow,
|
|
@@ -390,11 +527,14 @@ export const Browser = ({
|
|
|
390
527
|
setFilterOpen(true)
|
|
391
528
|
},
|
|
392
529
|
clearAndOpenFilter: () => {
|
|
530
|
+
closeFloatingOverlay()
|
|
393
531
|
// Reset both the ref and the state so the freshly-opened modal
|
|
394
532
|
// shows an empty input and selection lands on the first file in
|
|
395
533
|
// the (now unfiltered) list.
|
|
396
|
-
|
|
397
|
-
|
|
534
|
+
filterInputRef.current = ""
|
|
535
|
+
filterAppliedRef.current = ""
|
|
536
|
+
setFilterInput("")
|
|
537
|
+
setFilterApplied("")
|
|
398
538
|
setSelectedIndex(() => 0)
|
|
399
539
|
focusRef.current = "sidebar"
|
|
400
540
|
if (focus !== "sidebar") setFocus("sidebar")
|
|
@@ -403,16 +543,13 @@ export const Browser = ({
|
|
|
403
543
|
setFilterOpen(true)
|
|
404
544
|
},
|
|
405
545
|
openPalette: () => {
|
|
406
|
-
// Close help if it was open — palette is the active modal now.
|
|
407
546
|
// Reset query/index so each open starts fresh (no stale state from
|
|
408
547
|
// the previous session).
|
|
409
|
-
if (helpVisible) setHelpVisible(() => false)
|
|
410
548
|
paletteQueryRef.current = ""
|
|
411
549
|
setPaletteQuery("")
|
|
412
550
|
paletteIndexRef.current = 0
|
|
413
551
|
setPaletteIndex(0)
|
|
414
|
-
|
|
415
|
-
setPaletteOpen(true)
|
|
552
|
+
dispatchFloatingOverlay({ type: "open-command-palette" })
|
|
416
553
|
},
|
|
417
554
|
cycleTheme,
|
|
418
555
|
toggleTone,
|
|
@@ -467,7 +604,7 @@ export const Browser = ({
|
|
|
467
604
|
if (!file) return
|
|
468
605
|
const editor = resolveEditor(process.env)
|
|
469
606
|
if (!editor) {
|
|
470
|
-
pushFooterNotice("set $EDITOR or $VISUAL to use
|
|
607
|
+
pushFooterNotice("set $EDITOR or $VISUAL to use E")
|
|
471
608
|
return
|
|
472
609
|
}
|
|
473
610
|
if (!renderer) {
|
|
@@ -526,6 +663,68 @@ export const Browser = ({
|
|
|
526
663
|
}
|
|
527
664
|
|
|
528
665
|
useKeyboard((key) => {
|
|
666
|
+
// Command palette modal: capture keystrokes for the query input and
|
|
667
|
+
// list navigation. This branch intentionally runs before the filter
|
|
668
|
+
// branch so ctrl+p can open the palette from filter mode while the
|
|
669
|
+
// palette still owns Esc/arrows/Return until it closes.
|
|
670
|
+
if (floatingOverlayRef.current.kind === "command-palette") {
|
|
671
|
+
const closePalette = () => {
|
|
672
|
+
paletteQueryRef.current = ""
|
|
673
|
+
paletteIndexRef.current = 0
|
|
674
|
+
closeFloatingOverlay()
|
|
675
|
+
setPaletteQuery("")
|
|
676
|
+
setPaletteIndex(0)
|
|
677
|
+
}
|
|
678
|
+
const setPaletteIndexSync = (next: number) => {
|
|
679
|
+
paletteIndexRef.current = next
|
|
680
|
+
setPaletteIndex(next)
|
|
681
|
+
}
|
|
682
|
+
const allCommands = buildCommands(ctx)
|
|
683
|
+
const filtered = orderCommandsForPalette(filterCommands(allCommands, paletteQueryRef.current))
|
|
684
|
+
if (key.name === "escape") {
|
|
685
|
+
closePalette()
|
|
686
|
+
return
|
|
687
|
+
}
|
|
688
|
+
if (key.name === "return") {
|
|
689
|
+
const picked = filtered[clampSelectedIndex(paletteIndexRef.current, filtered)]
|
|
690
|
+
closePalette()
|
|
691
|
+
picked?.run()
|
|
692
|
+
return
|
|
693
|
+
}
|
|
694
|
+
if (key.name === "up") {
|
|
695
|
+
setPaletteIndexSync(Math.max(0, paletteIndexRef.current - 1))
|
|
696
|
+
return
|
|
697
|
+
}
|
|
698
|
+
if (key.name === "down") {
|
|
699
|
+
setPaletteIndexSync(Math.min(Math.max(0, filtered.length - 1), paletteIndexRef.current + 1))
|
|
700
|
+
return
|
|
701
|
+
}
|
|
702
|
+
if (key.name === "backspace" || key.name === "delete") {
|
|
703
|
+
if (paletteQueryRef.current.length === 0) return
|
|
704
|
+
paletteQueryRef.current = paletteQueryRef.current.slice(0, -1)
|
|
705
|
+
setPaletteQuery(paletteQueryRef.current)
|
|
706
|
+
setPaletteIndexSync(0)
|
|
707
|
+
return
|
|
708
|
+
}
|
|
709
|
+
// ctrl+p again closes.
|
|
710
|
+
if (key.ctrl && !key.meta && key.name === "p") {
|
|
711
|
+
closePalette()
|
|
712
|
+
return
|
|
713
|
+
}
|
|
714
|
+
if (key.ctrl || key.meta) return
|
|
715
|
+
let char: string | null = null
|
|
716
|
+
if (key.name === "space") char = " "
|
|
717
|
+
else if (typeof key.name === "string" && key.name.length === 1) {
|
|
718
|
+
char = key.shift ? key.name.toUpperCase() : key.name
|
|
719
|
+
}
|
|
720
|
+
if (char !== null) {
|
|
721
|
+
paletteQueryRef.current = paletteQueryRef.current + char
|
|
722
|
+
setPaletteQuery(paletteQueryRef.current)
|
|
723
|
+
setPaletteIndexSync(0)
|
|
724
|
+
}
|
|
725
|
+
return
|
|
726
|
+
}
|
|
727
|
+
|
|
529
728
|
// Filter modal: capture keystrokes for the input. Esc closes,
|
|
530
729
|
// leaving the typed query applied as the active filter; Return
|
|
531
730
|
// closes and focuses the reader (open the match); Ctrl+\ clears
|
|
@@ -543,6 +742,8 @@ export const Browser = ({
|
|
|
543
742
|
// the Return semantic (open the match in the reader); false is
|
|
544
743
|
// Esc (stop typing, keep the applied filter, stay in sidebar).
|
|
545
744
|
const closeFilter = (commit: boolean) => {
|
|
745
|
+
filterAppliedRef.current = filterInputRef.current
|
|
746
|
+
setFilterApplied(filterInputRef.current)
|
|
546
747
|
const picked = displayedFiles[selectedIndex] ?? null
|
|
547
748
|
const effectiveCommit = commit && picked !== null
|
|
548
749
|
restoreFilterOnSidebarFocusRef.current = false
|
|
@@ -578,25 +779,31 @@ export const Browser = ({
|
|
|
578
779
|
setFocus("reader")
|
|
579
780
|
return
|
|
580
781
|
}
|
|
782
|
+
if (key.ctrl && !key.meta && key.name === "p") {
|
|
783
|
+
ctx.openPalette()
|
|
784
|
+
return
|
|
785
|
+
}
|
|
581
786
|
if (key.ctrl && key.name === "\\") {
|
|
582
787
|
// Same action as the `filter.clearOrOpen` binding fires from
|
|
583
788
|
// outside the modal: clear the query, reset selection. The
|
|
584
789
|
// keymap doesn't see keys in filter mode, so this branch is
|
|
585
790
|
// the in-modal half of that single chord.
|
|
586
|
-
|
|
587
|
-
|
|
791
|
+
filterInputRef.current = ""
|
|
792
|
+
filterAppliedRef.current = ""
|
|
793
|
+
setFilterInput("")
|
|
794
|
+
setFilterApplied("")
|
|
588
795
|
setSelectedIndex(() => 0)
|
|
589
796
|
return
|
|
590
797
|
}
|
|
591
798
|
if (key.name === "backspace" || key.name === "delete") {
|
|
592
799
|
// Backspace on empty input closes the modal — the leading `/`
|
|
593
800
|
// chevron is the last thing left to "delete."
|
|
594
|
-
if (
|
|
801
|
+
if (filterInputRef.current.length === 0) {
|
|
595
802
|
closeFilter(false)
|
|
596
803
|
return
|
|
597
804
|
}
|
|
598
|
-
|
|
599
|
-
|
|
805
|
+
filterInputRef.current = filterInputRef.current.slice(0, -1)
|
|
806
|
+
setFilterInput(filterInputRef.current)
|
|
600
807
|
setSelectedIndex(() => 0)
|
|
601
808
|
return
|
|
602
809
|
}
|
|
@@ -615,101 +822,12 @@ export const Browser = ({
|
|
|
615
822
|
char = key.shift ? key.name.toUpperCase() : key.name
|
|
616
823
|
}
|
|
617
824
|
if (char !== null) {
|
|
618
|
-
|
|
619
|
-
|
|
825
|
+
filterInputRef.current = filterInputRef.current + char
|
|
826
|
+
setFilterInput(filterInputRef.current)
|
|
620
827
|
setSelectedIndex(() => 0)
|
|
621
828
|
}
|
|
622
829
|
return
|
|
623
830
|
}
|
|
624
|
-
// Command palette modal: capture keystrokes for the query input and
|
|
625
|
-
// list navigation. Esc closes (single press, regardless of query —
|
|
626
|
-
// #70 Q7a). Return runs the selected command. Up/Down navigate.
|
|
627
|
-
// Backspace edits the query and is a no-op on empty (#70 Q7b —
|
|
628
|
-
// intentionally diverges from the filter modal, which closes on
|
|
629
|
-
// empty-backspace, because accidental close feels worse in the
|
|
630
|
-
// palette). Printable characters extend the query and snap selection
|
|
631
|
-
// to 0 (#70 Q7c). Ctrl/Meta-modified keys are swallowed except
|
|
632
|
-
// ctrl+p, which toggles the palette closed (matches help-toggle's
|
|
633
|
-
// re-press-to-close behavior).
|
|
634
|
-
if (paletteOpenRef.current) {
|
|
635
|
-
const closePalette = () => {
|
|
636
|
-
paletteOpenRef.current = false
|
|
637
|
-
paletteQueryRef.current = ""
|
|
638
|
-
paletteIndexRef.current = 0
|
|
639
|
-
setPaletteOpen(false)
|
|
640
|
-
setPaletteQuery("")
|
|
641
|
-
setPaletteIndex(0)
|
|
642
|
-
}
|
|
643
|
-
const setPaletteIndexSync = (next: number) => {
|
|
644
|
-
paletteIndexRef.current = next
|
|
645
|
-
setPaletteIndex(next)
|
|
646
|
-
}
|
|
647
|
-
const allCommands = buildCommands(ctx)
|
|
648
|
-
const filtered = filterCommands(allCommands, paletteQueryRef.current)
|
|
649
|
-
if (key.name === "escape") {
|
|
650
|
-
closePalette()
|
|
651
|
-
return
|
|
652
|
-
}
|
|
653
|
-
if (key.name === "return") {
|
|
654
|
-
const picked = filtered[clampSelectedIndex(paletteIndexRef.current, filtered)]
|
|
655
|
-
closePalette()
|
|
656
|
-
picked?.run()
|
|
657
|
-
return
|
|
658
|
-
}
|
|
659
|
-
if (key.name === "up") {
|
|
660
|
-
setPaletteIndexSync(Math.max(0, paletteIndexRef.current - 1))
|
|
661
|
-
return
|
|
662
|
-
}
|
|
663
|
-
if (key.name === "down") {
|
|
664
|
-
setPaletteIndexSync(Math.min(Math.max(0, filtered.length - 1), paletteIndexRef.current + 1))
|
|
665
|
-
return
|
|
666
|
-
}
|
|
667
|
-
if (key.name === "backspace" || key.name === "delete") {
|
|
668
|
-
if (paletteQueryRef.current.length === 0) return
|
|
669
|
-
paletteQueryRef.current = paletteQueryRef.current.slice(0, -1)
|
|
670
|
-
setPaletteQuery(paletteQueryRef.current)
|
|
671
|
-
setPaletteIndexSync(0)
|
|
672
|
-
return
|
|
673
|
-
}
|
|
674
|
-
// ctrl+p again closes — matches help-toggle behavior.
|
|
675
|
-
if (key.ctrl && !key.meta && key.name === "p") {
|
|
676
|
-
closePalette()
|
|
677
|
-
return
|
|
678
|
-
}
|
|
679
|
-
if (key.ctrl || key.meta) return
|
|
680
|
-
let char: string | null = null
|
|
681
|
-
if (key.name === "space") char = " "
|
|
682
|
-
else if (typeof key.name === "string" && key.name.length === 1) {
|
|
683
|
-
char = key.shift ? key.name.toUpperCase() : key.name
|
|
684
|
-
}
|
|
685
|
-
if (char !== null) {
|
|
686
|
-
paletteQueryRef.current = paletteQueryRef.current + char
|
|
687
|
-
setPaletteQuery(paletteQueryRef.current)
|
|
688
|
-
setPaletteIndexSync(0)
|
|
689
|
-
}
|
|
690
|
-
return
|
|
691
|
-
}
|
|
692
|
-
// While help is open, swallow most keys: only ? (toggle), esc
|
|
693
|
-
// (close), and the theme bindings pass through. Theme keys stay live
|
|
694
|
-
// so users can preview palette changes against the overlay itself —
|
|
695
|
-
// it is the largest theme-painted surface in the app. Everything else
|
|
696
|
-
// is suppressed so the user can read without driving the UI behind.
|
|
697
|
-
// This is the one place we step outside the data-driven keymap; the
|
|
698
|
-
// alternative — adding `when: !c.helpVisible` to every other binding
|
|
699
|
-
// — would clutter the array. See DESIGN.md §12 (keymap composition).
|
|
700
|
-
if (helpVisible) {
|
|
701
|
-
if (key.name === "escape") {
|
|
702
|
-
setHelpVisible(() => false)
|
|
703
|
-
return
|
|
704
|
-
}
|
|
705
|
-
const allowed = browserBindings.filter((b) => HELP_ALLOWED_IDS.has(b.id))
|
|
706
|
-
// Defensive: stub quit even though no allowed binding currently
|
|
707
|
-
// calls it. Keeps the invariant local to this branch instead of
|
|
708
|
-
// relying on a future maintainer remembering not to add quit-ish
|
|
709
|
-
// bindings to HELP_ALLOWED_IDS.
|
|
710
|
-
dispatch(allowed, { ...ctx, quit: () => {} }, key)
|
|
711
|
-
return
|
|
712
|
-
}
|
|
713
831
|
dispatch(browserBindings, ctx, key)
|
|
714
832
|
})
|
|
715
833
|
|
|
@@ -732,12 +850,14 @@ export const Browser = ({
|
|
|
732
850
|
// per-pane border title that used to carry this information).
|
|
733
851
|
const currentFile = selected?.relativePath ?? null
|
|
734
852
|
const content = loaded?.path === renderedPath ? loaded.content : ""
|
|
853
|
+
const parsedContent = useMemo(() => parseFrontmatter(content), [content])
|
|
735
854
|
const readerEmptyStateTitle = filterHasNoMatches
|
|
736
|
-
? `No files match: ${
|
|
855
|
+
? `No files match: ${filterInput}`
|
|
737
856
|
: `${BRAND} ${BRAND_NAME}`
|
|
738
857
|
const readerEmptyStateVisible = error == null && renderedPath == null
|
|
739
858
|
|
|
740
859
|
useEffect(() => {
|
|
860
|
+
if (disableReaderEmptyStateRotation) return
|
|
741
861
|
if (readerEmptyStateVisible) {
|
|
742
862
|
if (!readerEmptyStateVisibleRef.current) {
|
|
743
863
|
readerEmptyStateVisibleRef.current = true
|
|
@@ -746,7 +866,7 @@ export const Browser = ({
|
|
|
746
866
|
return
|
|
747
867
|
}
|
|
748
868
|
readerEmptyStateVisibleRef.current = false
|
|
749
|
-
}, [readerEmptyStateVisible])
|
|
869
|
+
}, [disableReaderEmptyStateRotation, readerEmptyStateVisible])
|
|
750
870
|
|
|
751
871
|
// Sidebar virtualization: render only the visible window. Without this,
|
|
752
872
|
// every keystroke re-renders all N file rows even though only the bg of
|
|
@@ -784,19 +904,27 @@ export const Browser = ({
|
|
|
784
904
|
[sidebarTextWidth],
|
|
785
905
|
)
|
|
786
906
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
907
|
+
const footerProps = {
|
|
908
|
+
bindings: browserBindings,
|
|
909
|
+
ctx,
|
|
910
|
+
width,
|
|
911
|
+
notice: footerNotice?.text ?? null,
|
|
912
|
+
discoveryStatus,
|
|
913
|
+
...(discoverySpinnerIntervalMs === undefined ? {} : { discoverySpinnerIntervalMs }),
|
|
914
|
+
...(discoverySpinnerInitialFrameIndex === undefined
|
|
915
|
+
? {}
|
|
916
|
+
: { discoverySpinnerInitialFrameIndex }),
|
|
917
|
+
...(discoverySpinnerRegisterTick === undefined ? {} : { discoverySpinnerRegisterTick }),
|
|
918
|
+
...(discoveryWarningStatus === null
|
|
919
|
+
? {}
|
|
920
|
+
: {
|
|
921
|
+
onDiscoveryWarningToggle: () =>
|
|
922
|
+
dispatchFloatingOverlay({
|
|
923
|
+
type: "toggle-status-popover",
|
|
924
|
+
content: discoveryWarningStatus,
|
|
925
|
+
}),
|
|
926
|
+
}),
|
|
927
|
+
} satisfies FooterProps<BrowserCtx>
|
|
800
928
|
const readerEmptyStateTips = useMemo(() => buildReaderEmptyStateTips(browserBindings, ctx), [ctx])
|
|
801
929
|
const readerEmptyStateTip = useMemo(
|
|
802
930
|
() => pickTipByRotation(readerEmptyStateTips, readerEmptyStateTipRotation),
|
|
@@ -809,40 +937,32 @@ export const Browser = ({
|
|
|
809
937
|
<>
|
|
810
938
|
{filterRowVisible && (
|
|
811
939
|
<PromptRow
|
|
812
|
-
query={
|
|
940
|
+
query={filterInput}
|
|
813
941
|
editing={filterOpen}
|
|
814
942
|
placeholder="/ to filter…"
|
|
815
943
|
width={sidebarTextWidth}
|
|
816
944
|
/>
|
|
817
945
|
)}
|
|
818
946
|
{displayedFiles.length === 0 ? (
|
|
819
|
-
<
|
|
820
|
-
|
|
947
|
+
<SidebarEmptyMessage
|
|
948
|
+
withTopSpacer={filterRowVisible}
|
|
949
|
+
width={sidebarTextWidth}
|
|
950
|
+
label={
|
|
821
951
|
files.length === 0
|
|
822
952
|
? discoveryActive
|
|
823
|
-
? "
|
|
824
|
-
: "
|
|
825
|
-
: "
|
|
953
|
+
? "Scanning"
|
|
954
|
+
: "No markdown files in"
|
|
955
|
+
: "No files match"
|
|
826
956
|
}
|
|
827
|
-
|
|
957
|
+
value={files.length === 0 ? (discoveryActive ? "…" : emptyRootLabel) : filterApplied}
|
|
828
958
|
/>
|
|
829
959
|
) : (
|
|
830
960
|
visibleFiles.map((file, idx) => {
|
|
831
961
|
const realIdx = desiredScroll + idx
|
|
832
962
|
const isSelected = realIdx === selectedIndex
|
|
833
963
|
const { basename, separator, parent } = layoutSidebarRow(file.relativePath)
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
? colors.primary
|
|
837
|
-
: colors.selectedListItemText
|
|
838
|
-
const basenameFg = isSelected
|
|
839
|
-
? sidebarActive
|
|
840
|
-
? selectedFg
|
|
841
|
-
: colors.primary
|
|
842
|
-
: colors.text
|
|
843
|
-
const rowStyle = isSelected
|
|
844
|
-
? { bg: sidebarActive ? colors.backgroundElement : colors.borderSubtle }
|
|
845
|
-
: {}
|
|
964
|
+
const basenameFg = isSelected ? colors.selectedListItemText : colors.text
|
|
965
|
+
const rowStyle = isSelected ? { bg: colors.backgroundElement } : {}
|
|
846
966
|
return (
|
|
847
967
|
<text key={file.path} wrapMode="none" style={rowStyle}>
|
|
848
968
|
<span style={{ fg: basenameFg }}>{basename}</span>
|
|
@@ -877,6 +997,7 @@ export const Browser = ({
|
|
|
877
997
|
rightT: "┤",
|
|
878
998
|
cross: "┼",
|
|
879
999
|
} as const
|
|
1000
|
+
const INACTIVE_PANE_OPACITY = 0.62
|
|
880
1001
|
|
|
881
1002
|
return (
|
|
882
1003
|
<box
|
|
@@ -897,7 +1018,7 @@ export const Browser = ({
|
|
|
897
1018
|
// Narrow mode runs single-pane: the sidebar fills the area
|
|
898
1019
|
// and drops its right divider (no neighbour to abut).
|
|
899
1020
|
border: isNarrow ? readerBorderSides : sidebarBorderSides,
|
|
900
|
-
borderColor: colors.
|
|
1021
|
+
borderColor: colors.border,
|
|
901
1022
|
...(isNarrow
|
|
902
1023
|
? { flexGrow: 1, flexShrink: 1 }
|
|
903
1024
|
: { width: sidebarWidth, flexShrink: 0 }),
|
|
@@ -916,6 +1037,7 @@ export const Browser = ({
|
|
|
916
1037
|
flexDirection: "column",
|
|
917
1038
|
paddingLeft: 1,
|
|
918
1039
|
backgroundColor: sidebarActive ? colors.background : colors.backgroundPanel,
|
|
1040
|
+
opacity: sidebarActive ? 1 : INACTIVE_PANE_OPACITY,
|
|
919
1041
|
}}
|
|
920
1042
|
>
|
|
921
1043
|
{sidebarBody}
|
|
@@ -926,7 +1048,7 @@ export const Browser = ({
|
|
|
926
1048
|
<box
|
|
927
1049
|
style={{
|
|
928
1050
|
border: readerBorderSides,
|
|
929
|
-
borderColor: colors.
|
|
1051
|
+
borderColor: colors.border,
|
|
930
1052
|
flexGrow: 1,
|
|
931
1053
|
flexShrink: 1,
|
|
932
1054
|
flexDirection: "column",
|
|
@@ -941,6 +1063,7 @@ export const Browser = ({
|
|
|
941
1063
|
flexDirection: "column",
|
|
942
1064
|
padding: 1,
|
|
943
1065
|
backgroundColor: readerActive ? colors.background : colors.backgroundPanel,
|
|
1066
|
+
opacity: readerActive ? 1 : INACTIVE_PANE_OPACITY,
|
|
944
1067
|
}}
|
|
945
1068
|
>
|
|
946
1069
|
{error ? (
|
|
@@ -984,17 +1107,38 @@ export const Browser = ({
|
|
|
984
1107
|
}}
|
|
985
1108
|
// opentui's scrollbox consumes arrow keys at the focused-element
|
|
986
1109
|
// level *before* useKeyboard fires, so a modal that handles
|
|
987
|
-
// arrow keys itself (palette nav
|
|
1110
|
+
// arrow keys itself (palette nav) would still
|
|
988
1111
|
// see the reader scroll alongside its own action. Unfocus the
|
|
989
1112
|
// scrollbox while any blocking modal is up — useKeyboard's
|
|
990
1113
|
// modal branches own the keys in that state. Filter is not
|
|
991
1114
|
// listed because it force-focuses the sidebar (readerActive
|
|
992
1115
|
// is already false).
|
|
993
|
-
focused={readerActive && !paletteOpen
|
|
1116
|
+
focused={readerActive && !paletteOpen}
|
|
994
1117
|
>
|
|
1118
|
+
{parsedContent.fields.length > 0 && (
|
|
1119
|
+
<box style={{ flexDirection: "column", marginBottom: 1 }}>
|
|
1120
|
+
{parsedContent.fields.map((field) => (
|
|
1121
|
+
<box
|
|
1122
|
+
key={field.key}
|
|
1123
|
+
style={{ flexDirection: "row", gap: 1, flexWrap: "wrap" }}
|
|
1124
|
+
>
|
|
1125
|
+
<text
|
|
1126
|
+
content={`${field.key}:`}
|
|
1127
|
+
wrapMode="word"
|
|
1128
|
+
style={{ fg: colors.secondary }}
|
|
1129
|
+
/>
|
|
1130
|
+
<text
|
|
1131
|
+
content={field.value}
|
|
1132
|
+
wrapMode="word"
|
|
1133
|
+
style={{ fg: colors.textMuted }}
|
|
1134
|
+
/>
|
|
1135
|
+
</box>
|
|
1136
|
+
))}
|
|
1137
|
+
</box>
|
|
1138
|
+
)}
|
|
995
1139
|
<markdown
|
|
996
1140
|
key={renderedPath ?? "empty"}
|
|
997
|
-
content={
|
|
1141
|
+
content={parsedContent.body}
|
|
998
1142
|
syntaxStyle={syntaxStyle}
|
|
999
1143
|
fg={colors.text}
|
|
1000
1144
|
bg={readerActive ? colors.background : colors.backgroundPanel}
|
|
@@ -1007,26 +1151,19 @@ export const Browser = ({
|
|
|
1007
1151
|
</box>
|
|
1008
1152
|
)}
|
|
1009
1153
|
</box>
|
|
1010
|
-
<Footer
|
|
1011
|
-
bindings={footerBindings}
|
|
1012
|
-
ctx={ctx}
|
|
1013
|
-
width={width}
|
|
1014
|
-
notice={footerNotice?.text ?? null}
|
|
1015
|
-
discoveryStatus={discoveryStatus}
|
|
1016
|
-
filterQuery={!filterOpen && filterQuery.length > 0 ? filterQuery : null}
|
|
1017
|
-
/>
|
|
1018
|
-
{helpVisible && (
|
|
1019
|
-
<HelpOverlay bindings={browserBindings} viewportWidth={width} viewportHeight={height} />
|
|
1020
|
-
)}
|
|
1154
|
+
<Footer {...footerProps} />
|
|
1021
1155
|
{paletteOpen && (
|
|
1022
1156
|
<CommandPalette
|
|
1023
|
-
commands={filterCommands(buildCommands(ctx), paletteQuery)}
|
|
1157
|
+
commands={orderCommandsForPalette(filterCommands(buildCommands(ctx), paletteQuery))}
|
|
1024
1158
|
query={paletteQuery}
|
|
1025
1159
|
selectedIndex={paletteIndex}
|
|
1026
1160
|
viewportWidth={width}
|
|
1027
1161
|
viewportHeight={height}
|
|
1028
1162
|
/>
|
|
1029
1163
|
)}
|
|
1164
|
+
{activeStatusPopover && (
|
|
1165
|
+
<StatusPopoverPanel content={activeStatusPopover.content} variant="warning" />
|
|
1166
|
+
)}
|
|
1030
1167
|
</box>
|
|
1031
1168
|
)
|
|
1032
1169
|
}
|