@brimveyn/aimux 1.23.6 → 1.24.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/package.json +2 -2
- package/src/app-runtime/tab-actions.ts +7 -1
- package/src/input/keymap/key-chord.ts +10 -0
- package/src/input/keymap/key-format.ts +14 -1
- package/src/input/modes/handlers/shared.ts +48 -0
- package/src/pty/command-registry.ts +35 -0
- package/src/state/actions.ts +2 -1
- package/src/state/project-persistence.ts +16 -1
- package/src/state/reducers/modal-state.ts +24 -28
- package/src/state/reducers/tab-state.ts +14 -3
- package/src/state/text-cursor.ts +117 -0
- package/src/ui/breaking-update-screen.tsx +7 -1
- package/src/ui/components/git/pane/pr-checks-panel.tsx +7 -2
- package/src/ui/components/layout/bar-footer.tsx +3 -5
- package/src/ui/components/layout/bar.tsx +41 -16
- package/src/ui/components/layout/sidebar/project-list.tsx +6 -16
- package/src/ui/components/layout/sidebar/workspace-row.tsx +6 -9
- package/src/ui/components/layout/status-bar.tsx +9 -3
- package/src/ui/components/layout/terminal-pane.tsx +18 -16
- package/src/ui/components/modals/app/help-modal.tsx +20 -15
- package/src/ui/components/modals/app/update-available-modal.tsx +3 -1
- package/src/ui/components/modals/git/git-commit-modal.tsx +2 -2
- package/src/ui/components/modals/projects/create-project-modal.tsx +9 -3
- package/src/ui/components/modals/projects/project-picker-modal.tsx +6 -6
- package/src/ui/components/modals/settings/settings-search-modal.tsx +19 -14
- package/src/ui/components/modals/shared/modal-shell.tsx +12 -2
- package/src/ui/components/modals/shared/picker.tsx +8 -4
- package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +7 -5
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +10 -12
- package/src/ui/components/modals/themes/theme-picker-modal.tsx +14 -10
- package/src/ui/components/modals/workspace/create-workspace-modal.tsx +19 -14
- package/src/ui/components/modals/workspace/workspace-move-modal.tsx +4 -2
- package/src/ui/components/overlays/context-menu/context-menu-overlay.tsx +10 -4
- package/src/ui/components/primitives/list-item.tsx +24 -2
- package/src/ui/components/primitives/surface.tsx +4 -1
- package/src/ui/components/settings/row-value.tsx +8 -1
- package/src/ui/components/settings/settings-footer.tsx +3 -2
- package/src/ui/components/settings/settings-row.tsx +3 -0
- package/src/ui/components/settings/settings-search-bar.tsx +4 -6
- package/src/ui/components/settings/settings-view.tsx +25 -21
- package/src/ui/components/stats/stats-view.tsx +4 -2
- package/src/ui/selection-ink.ts +20 -0
|
@@ -20,6 +20,12 @@ export interface BarBoundaryResizeInfo {
|
|
|
20
20
|
totalSize: number
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Columns between the widgets and the terminal: one to grab for a resize, one so
|
|
25
|
+
* the content is not flush against the edge. Both grab.
|
|
26
|
+
*/
|
|
27
|
+
const GUTTER = 2
|
|
28
|
+
|
|
23
29
|
interface BarProps {
|
|
24
30
|
side: BarSide
|
|
25
31
|
onResizeDrag?: (event: OtuiMouseEvent) => boolean
|
|
@@ -28,8 +34,6 @@ interface BarProps {
|
|
|
28
34
|
onBoundaryResizeStart?: (info: BarBoundaryResizeInfo) => void
|
|
29
35
|
}
|
|
30
36
|
|
|
31
|
-
const RESIZE_HANDLE = '─'
|
|
32
|
-
|
|
33
37
|
/**
|
|
34
38
|
* One edge bar hosting a vertical stack of widgets. Both bars are this
|
|
35
39
|
* component; the only asymmetry is which side the resize handle sits on.
|
|
@@ -78,11 +82,21 @@ export function Bar({
|
|
|
78
82
|
if (width === 0) return null
|
|
79
83
|
|
|
80
84
|
const visible = visibleWidgets(bar)
|
|
81
|
-
const contentWidth = Math.max(1, width -
|
|
82
|
-
|
|
83
|
-
//
|
|
85
|
+
const contentWidth = Math.max(1, width - GUTTER)
|
|
86
|
+
|
|
87
|
+
// The gutter on the side facing the terminal: the resize grip and the widgets'
|
|
88
|
+
// inset from the terminal are the same two columns — the padding is not dead
|
|
89
|
+
// space, both cells start a resize. Painted with the bar, not in the page
|
|
90
|
+
// colour: a page-coloured strip here left a seam between the bar and the tab
|
|
91
|
+
// bar above it, which are the same panel. What separates the bar from the
|
|
92
|
+
// terminal is the terminal's own background, nothing drawn.
|
|
84
93
|
const edge = (
|
|
85
|
-
<box
|
|
94
|
+
<box
|
|
95
|
+
width={GUTTER}
|
|
96
|
+
flexShrink={0}
|
|
97
|
+
backgroundColor={t.backgroundPanel}
|
|
98
|
+
onMouseDown={handleEdgeMouseDown}
|
|
99
|
+
/>
|
|
86
100
|
)
|
|
87
101
|
|
|
88
102
|
const body = (
|
|
@@ -93,7 +107,6 @@ export function Bar({
|
|
|
93
107
|
bodyRef={bodyRef}
|
|
94
108
|
contentWidth={contentWidth}
|
|
95
109
|
grow={widget.grow}
|
|
96
|
-
handleColor={t.border}
|
|
97
110
|
index={index}
|
|
98
111
|
isLast={index === visible.length - 1}
|
|
99
112
|
onBoundaryResizeStart={onBoundaryResizeStart}
|
|
@@ -109,7 +122,7 @@ export function Bar({
|
|
|
109
122
|
width={width}
|
|
110
123
|
padding={0}
|
|
111
124
|
flexDirection="row"
|
|
112
|
-
backgroundColor={t.
|
|
125
|
+
backgroundColor={t.backgroundPanel}
|
|
113
126
|
gap={0}
|
|
114
127
|
overflow="hidden"
|
|
115
128
|
rightClickMenu={buildBarContextMenu(bars, side)}
|
|
@@ -118,7 +131,17 @@ export function Bar({
|
|
|
118
131
|
onMouseUp={handleMouseUp}
|
|
119
132
|
>
|
|
120
133
|
{side === 'right' ? edge : null}
|
|
121
|
-
|
|
134
|
+
{/* The bar is one surface, top to bottom: widgets, the gaps between them,
|
|
135
|
+
the footer and the gutter all share it, and it runs straight into the
|
|
136
|
+
tab bar above. Painting each widget separately left the gaps and the
|
|
137
|
+
footer showing the page colour through, which read as seams. */}
|
|
138
|
+
<box
|
|
139
|
+
width={contentWidth}
|
|
140
|
+
flexGrow={1}
|
|
141
|
+
flexDirection="column"
|
|
142
|
+
overflow="hidden"
|
|
143
|
+
backgroundColor={t.backgroundPanel}
|
|
144
|
+
>
|
|
122
145
|
{body}
|
|
123
146
|
{side === 'left' ? <BarFooter contentWidth={contentWidth} /> : null}
|
|
124
147
|
</box>
|
|
@@ -131,7 +154,6 @@ function BarWidgetSlot({
|
|
|
131
154
|
bodyRef,
|
|
132
155
|
contentWidth,
|
|
133
156
|
grow,
|
|
134
|
-
handleColor,
|
|
135
157
|
index,
|
|
136
158
|
isLast,
|
|
137
159
|
onBoundaryResizeStart,
|
|
@@ -141,7 +163,6 @@ function BarWidgetSlot({
|
|
|
141
163
|
bodyRef: React.RefObject<BoxRenderable | null>
|
|
142
164
|
contentWidth: number
|
|
143
165
|
grow: number
|
|
144
|
-
handleColor: string
|
|
145
166
|
index: number
|
|
146
167
|
isLast: boolean
|
|
147
168
|
side: BarSide
|
|
@@ -181,12 +202,16 @@ function BarWidgetSlot({
|
|
|
181
202
|
>
|
|
182
203
|
{render(contentWidth)}
|
|
183
204
|
</ContextMenuBox>
|
|
205
|
+
{/* The boundary between two widgets: a blank grabbable row, no rule drawn
|
|
206
|
+
in it. Widgets read as separate because of the gap, the same way
|
|
207
|
+
opencode separates its surfaces. */}
|
|
184
208
|
{isLast ? null : (
|
|
185
|
-
<box
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
209
|
+
<box
|
|
210
|
+
minHeight={1}
|
|
211
|
+
width={contentWidth}
|
|
212
|
+
flexShrink={0}
|
|
213
|
+
onMouseDown={handleBoundaryMouseDown}
|
|
214
|
+
/>
|
|
190
215
|
)}
|
|
191
216
|
</>
|
|
192
217
|
)
|
|
@@ -234,7 +234,6 @@ export function ProjectList({ contentWidth }: ProjectListProps) {
|
|
|
234
234
|
<ProjectRow
|
|
235
235
|
key={`ws:${project.id}`}
|
|
236
236
|
project={project}
|
|
237
|
-
inCurrentGroup={isCurrentProject}
|
|
238
237
|
projectIndex={projectIndex}
|
|
239
238
|
dragging={draggingId === project.id}
|
|
240
239
|
contentWidth={contentWidth}
|
|
@@ -249,7 +248,6 @@ export function ProjectList({ contentWidth }: ProjectListProps) {
|
|
|
249
248
|
workspace={workspace}
|
|
250
249
|
projectIndex={projectIndex}
|
|
251
250
|
isActiveItem={isCurrentProject && workspace.id === activeWorkspaceId}
|
|
252
|
-
inCurrentGroup={isCurrentProject}
|
|
253
251
|
contentWidth={contentWidth}
|
|
254
252
|
/>
|
|
255
253
|
)
|
|
@@ -300,13 +298,6 @@ function DropGap({ active, contentWidth, index, setGapRef }: DropGapProps) {
|
|
|
300
298
|
|
|
301
299
|
interface ProjectRowProps {
|
|
302
300
|
project: ProjectRecord
|
|
303
|
-
/**
|
|
304
|
-
* True when this row belongs to the current project (selection scope).
|
|
305
|
-
*
|
|
306
|
-
* There is deliberately no `isActiveItem`: the cursor lives on workspace
|
|
307
|
-
* rows, and this row is the heading they sit under.
|
|
308
|
-
*/
|
|
309
|
-
inCurrentGroup: boolean
|
|
310
301
|
/** 1-based index in the visible order, so the "+" can switch projects first. */
|
|
311
302
|
projectIndex: number
|
|
312
303
|
dragging: boolean
|
|
@@ -318,7 +309,6 @@ interface ProjectRowProps {
|
|
|
318
309
|
const ProjectRow = memo(function ProjectRow({
|
|
319
310
|
contentWidth,
|
|
320
311
|
dragging,
|
|
321
|
-
inCurrentGroup,
|
|
322
312
|
onDragStart,
|
|
323
313
|
project,
|
|
324
314
|
projectIndex,
|
|
@@ -329,12 +319,12 @@ const ProjectRow = memo(function ProjectRow({
|
|
|
329
319
|
const base = useBaseTheme()
|
|
330
320
|
// Only the drag highlight is "selected"-strength here. A heading that lights
|
|
331
321
|
// up like a cursor row is what made the project look like a workspace.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
322
|
+
// No band for "this row's project is the current one": the bar is a single
|
|
323
|
+
// backgroundPanel surface now and backgroundElement is spoken for by the
|
|
324
|
+
// cursor row, which leaves no third tone that works in every theme. The
|
|
325
|
+
// cursor row — one step off the panel, plus its accent bar — is what says
|
|
326
|
+
// where you are; the group it sits in follows from that.
|
|
327
|
+
const bgColor = dragging ? base.backgroundElement : undefined
|
|
338
328
|
const currentProjectId = useAppStore((s) => s.currentProjectId)
|
|
339
329
|
|
|
340
330
|
const handleMouseDown = useCallback(
|
|
@@ -33,14 +33,11 @@ interface WorkspaceRowProps {
|
|
|
33
33
|
projectIndex: number
|
|
34
34
|
/** True when this row is the active cursor item. */
|
|
35
35
|
isActiveItem: boolean
|
|
36
|
-
/** True when this row's project is the current project (selection scope). */
|
|
37
|
-
inCurrentGroup: boolean
|
|
38
36
|
contentWidth: number
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
export const WorkspaceRow = memo(function WorkspaceRow({
|
|
42
40
|
contentWidth,
|
|
43
|
-
inCurrentGroup,
|
|
44
41
|
isActiveItem,
|
|
45
42
|
project,
|
|
46
43
|
projectIndex,
|
|
@@ -134,12 +131,12 @@ export const WorkspaceRow = memo(function WorkspaceRow({
|
|
|
134
131
|
return entries
|
|
135
132
|
}, [project.id, workspace.branch, workspace.id, workspace.name, workspace.source])
|
|
136
133
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
134
|
+
// No band for "this row's project is the current one": the bar is a single
|
|
135
|
+
// backgroundPanel surface now and backgroundElement is spoken for by the
|
|
136
|
+
// cursor row, which leaves no third tone that works in every theme. The
|
|
137
|
+
// cursor row — one step off the panel, plus its accent bar — is what says
|
|
138
|
+
// where you are; the group it sits in follows from that.
|
|
139
|
+
const bgColor = isActiveItem ? base.backgroundElement : undefined
|
|
143
140
|
// The cursor: a full-height accent bar down the left of the row, both lines
|
|
144
141
|
// of it. The background alone is one step of grey and gets lost among rows
|
|
145
142
|
// that carry colour of their own; a bar the height of the row is found
|
|
@@ -129,9 +129,15 @@ export function StatusBar() {
|
|
|
129
129
|
|
|
130
130
|
const glyphs = SEPARATOR_GLYPHS[getStatusBarSeparator()]
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
// The bar's own band is backgroundElement, not backgroundPanel: the chrome
|
|
133
|
+
// directly above it — the tab bar and both widget bars — is panel, and with no
|
|
134
|
+
// rules left anywhere the status bar merged into it. Its two mid tiles drop to
|
|
135
|
+
// panel to keep a step between tile and filler. Which of the three tones is
|
|
136
|
+
// lighter is a theme's business (catppuccin recesses, aimux lifts); all that
|
|
137
|
+
// matters is that they are three different tokens.
|
|
138
|
+
const tileB = t.backgroundPanel
|
|
139
|
+
const tileFiller = t.backgroundElement
|
|
140
|
+
const tileX = t.backgroundPanel
|
|
135
141
|
const tileY = modeColor
|
|
136
142
|
|
|
137
143
|
const hasB = model.projectSegments.length > 0
|
|
@@ -44,12 +44,6 @@ interface TerminalPaneProps {
|
|
|
44
44
|
junctionEdges?: JunctionEdges
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function getBorderColor(isActive: boolean, focusMode: TerminalPaneProps['focusMode']): string {
|
|
48
|
-
const t = getCurrentTheme()
|
|
49
|
-
if (!isActive) return t.border
|
|
50
|
-
return focusMode === 'terminal-input' ? t.accent : t.primary
|
|
51
|
-
}
|
|
52
|
-
|
|
53
47
|
function renderSpan(span: TerminalSpan, key: string, softCursor: boolean): ReactNode {
|
|
54
48
|
let node: ReactNode = span.text
|
|
55
49
|
|
|
@@ -177,15 +171,19 @@ const NOOP = (): void => {}
|
|
|
177
171
|
// wraps) a wheel event bumps _scrollY, and opentui's onResize never re-clamps it,
|
|
178
172
|
// so the offset sticks for the renderable's life: the viewport shifts up, dead
|
|
179
173
|
// rows appear at the bottom, and the prompt scrolls off-screen after `clear`.
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
174
|
+
//
|
|
175
|
+
// Kill the built-in wheel handler at its dispatch point: processMouseEvent calls
|
|
176
|
+
// onMouseEvent() unconditionally after any React mouse listener, and
|
|
177
|
+
// TextBufferRenderable.onMouseEvent() unconditionally calls handleScroll() for a
|
|
178
|
+
// scroll event. Overriding onMouseEvent (rather than only handleScroll) is a
|
|
179
|
+
// single deterministic no-op standing between every wheel step and _scrollY, so
|
|
180
|
+
// the drift can no longer slip in on a path the later reset misses. Selection is
|
|
181
|
+
// unaffected: it is driven by the renderer (startSelection/updateSelection), not
|
|
182
|
+
// by onMouseEvent, which exists only to scroll. Both are protected; reach them
|
|
183
|
+
// via Reflect, mirroring the scrollY reset already used elsewhere in the pane.
|
|
186
184
|
const pinTerminalScroll = (node: TextRenderable | null): void => {
|
|
187
185
|
if (!node) return
|
|
188
|
-
Reflect.set(node, '
|
|
186
|
+
Reflect.set(node, 'onMouseEvent', NOOP)
|
|
189
187
|
if (node.scrollY !== 0) node.scrollY = 0
|
|
190
188
|
}
|
|
191
189
|
|
|
@@ -498,9 +496,13 @@ export function TerminalPane({
|
|
|
498
496
|
return (
|
|
499
497
|
<box flexDirection="column" flexGrow={1} gap={0}>
|
|
500
498
|
<ContextMenuBox
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
padding
|
|
499
|
+
// No frame, in any state — not around the pane, not between two split
|
|
500
|
+
// panes, and nothing that colours itself by focus mode. What was the
|
|
501
|
+
// border is now plain padding: same one-cell inset, so the geometry
|
|
502
|
+
// PANE_BORDER describes and the resize/hit-test gutter both still hold,
|
|
503
|
+
// but the cells are painted in the editor background like everything
|
|
504
|
+
// else. Surfaces are told apart by their background alone.
|
|
505
|
+
padding={1}
|
|
504
506
|
flexDirection="column"
|
|
505
507
|
flexGrow={1}
|
|
506
508
|
backgroundColor={editorBg}
|
|
@@ -5,6 +5,7 @@ import { useCallback, useLayoutEffect, useMemo } from 'react'
|
|
|
5
5
|
import { collectHelpEntries, HELP_MODE_LABELS } from '../../../../input/keymap/help-entries'
|
|
6
6
|
import { dispatchGlobal } from '../../../../state/dispatch-ref'
|
|
7
7
|
import { useKeymap } from '../../../keymap-context'
|
|
8
|
+
import { useSelectionInk } from '../../../selection-ink'
|
|
8
9
|
import { useTheme } from '../../../theme'
|
|
9
10
|
import { uiTokens } from '../../../ui-tokens'
|
|
10
11
|
import { Picker, type PickerItem } from '../shared/picker'
|
|
@@ -24,6 +25,7 @@ function matchesFilter(needle: string, ...fields: (string | undefined)[]): boole
|
|
|
24
25
|
|
|
25
26
|
export function HelpModal({ cursorPos, filter, scope, selectedIndex }: HelpModalProps) {
|
|
26
27
|
const t = useTheme()
|
|
28
|
+
const ink = useSelectionInk()
|
|
27
29
|
const config = useKeymap()
|
|
28
30
|
const allEntries = useMemo(() => collectHelpEntries(config), [config])
|
|
29
31
|
const scoped = useMemo(
|
|
@@ -50,21 +52,24 @@ export function HelpModal({ cursorPos, filter, scope, selectedIndex }: HelpModal
|
|
|
50
52
|
|
|
51
53
|
const items = useMemo<PickerItem[]>(
|
|
52
54
|
() =>
|
|
53
|
-
filtered.map((entry, index) =>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
55
|
+
filtered.map((entry, index) => {
|
|
56
|
+
const active = index === selectedIndex
|
|
57
|
+
return {
|
|
58
|
+
group: entry.modeLabel,
|
|
59
|
+
key: `${entry.mode}::${entry.keysDisplay}::${entry.description ?? ''}::${index}`,
|
|
60
|
+
title: (
|
|
61
|
+
<text fg={active ? ink : t.textMuted} wrapMode="none">
|
|
62
|
+
{entry.description ?? ''}
|
|
63
|
+
</text>
|
|
64
|
+
),
|
|
65
|
+
trailing: (
|
|
66
|
+
<text fg={active ? ink : t.textMuted} wrapMode="none">
|
|
67
|
+
{entry.keysDisplay}
|
|
68
|
+
</text>
|
|
69
|
+
),
|
|
70
|
+
}
|
|
71
|
+
}),
|
|
72
|
+
[filtered, ink, selectedIndex, t]
|
|
68
73
|
)
|
|
69
74
|
|
|
70
75
|
const handleHover = useCallback(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useSelectionInk } from '../../../selection-ink'
|
|
1
2
|
import { useTheme } from '../../../theme'
|
|
2
3
|
import { uiTokens } from '../../../ui-tokens'
|
|
3
4
|
import { ListItem } from '../../primitives/list-item'
|
|
@@ -20,6 +21,7 @@ export function UpdateAvailableModal({
|
|
|
20
21
|
selectedIndex,
|
|
21
22
|
}: UpdateAvailableModalProps) {
|
|
22
23
|
const t = useTheme()
|
|
24
|
+
const ink = useSelectionInk()
|
|
23
25
|
return (
|
|
24
26
|
<ModalShell
|
|
25
27
|
title="Update available"
|
|
@@ -36,7 +38,7 @@ export function UpdateAvailableModal({
|
|
|
36
38
|
key={option.label}
|
|
37
39
|
active={active}
|
|
38
40
|
direction="row"
|
|
39
|
-
title={<text fg={active ?
|
|
41
|
+
title={<text fg={active ? ink : t.textMuted}>{option.label}</text>}
|
|
40
42
|
/>
|
|
41
43
|
)
|
|
42
44
|
})}
|
|
@@ -172,9 +172,9 @@ export function GitCommitModal({
|
|
|
172
172
|
|
|
173
173
|
{isConfirm || isGenerating || !isAutoCommitEnabled() ? null : (
|
|
174
174
|
<box flexDirection="row" gap={1} marginTop={1} alignItems="center">
|
|
175
|
+
{/* Filled, not framed — the same button the setup widget uses. */}
|
|
175
176
|
<box
|
|
176
|
-
|
|
177
|
-
borderColor={t.border}
|
|
177
|
+
backgroundColor={t.backgroundElement}
|
|
178
178
|
paddingLeft={1}
|
|
179
179
|
paddingRight={1}
|
|
180
180
|
flexDirection="row"
|
|
@@ -3,6 +3,7 @@ import { useMemo } from 'react'
|
|
|
3
3
|
import type { DirectoryResult } from '../../../../state/types'
|
|
4
4
|
|
|
5
5
|
import { abbreviatePath } from '../../../path-format'
|
|
6
|
+
import { useSelectionInk } from '../../../selection-ink'
|
|
6
7
|
import { getCurrentTheme, useTheme } from '../../../theme'
|
|
7
8
|
import { uiTokens } from '../../../ui-tokens'
|
|
8
9
|
import { AutoComplete, Form, type FormOptionItem, TextField } from '../shared/form'
|
|
@@ -40,6 +41,7 @@ export function CreateProjectModal({
|
|
|
40
41
|
selectedIndex,
|
|
41
42
|
}: CreateProjectModalProps) {
|
|
42
43
|
const t = useTheme()
|
|
44
|
+
const ink = useSelectionInk()
|
|
43
45
|
const dirActive = activeField === 'directory'
|
|
44
46
|
const nameActive = activeField === 'name'
|
|
45
47
|
|
|
@@ -47,12 +49,16 @@ export function CreateProjectModal({
|
|
|
47
49
|
() =>
|
|
48
50
|
results.map((result) => ({
|
|
49
51
|
key: result.path,
|
|
50
|
-
leading:
|
|
52
|
+
leading: (active) => (
|
|
53
|
+
<text fg={active ? ink : getDirectoryResultColor(result)}>
|
|
54
|
+
{getDirectoryResultIcon(result)}
|
|
55
|
+
</text>
|
|
56
|
+
),
|
|
51
57
|
title: (active) => (
|
|
52
|
-
<text fg={active ?
|
|
58
|
+
<text fg={active ? ink : t.textMuted}>{abbreviatePath(result.path)}</text>
|
|
53
59
|
),
|
|
54
60
|
})),
|
|
55
|
-
[results, t]
|
|
61
|
+
[ink, results, t]
|
|
56
62
|
)
|
|
57
63
|
|
|
58
64
|
return (
|
|
@@ -6,6 +6,7 @@ import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-
|
|
|
6
6
|
import { filterProjects } from '../../../../state/selectors'
|
|
7
7
|
import { abbreviatePath } from '../../../path-format'
|
|
8
8
|
import { orderProjectsForDisplay } from '../../../project-ordering'
|
|
9
|
+
import { useSelectionInk } from '../../../selection-ink'
|
|
9
10
|
import { useTheme } from '../../../theme'
|
|
10
11
|
import { uiTokens } from '../../../ui-tokens'
|
|
11
12
|
import { Picker, type PickerItem } from '../shared/picker'
|
|
@@ -44,6 +45,7 @@ export function ProjectPickerModal({
|
|
|
44
45
|
selectedIndex,
|
|
45
46
|
}: ProjectPickerModalProps) {
|
|
46
47
|
const t = useTheme()
|
|
48
|
+
const ink = useSelectionInk()
|
|
47
49
|
const ordered = useMemo(() => orderProjectsForDisplay(projects), [projects])
|
|
48
50
|
const filtered = useMemo(() => filterProjects(ordered, filter), [filter, ordered])
|
|
49
51
|
const hasFilter = !!(filter != null && filter !== '')
|
|
@@ -59,10 +61,10 @@ export function ProjectPickerModal({
|
|
|
59
61
|
onDelete: () => runSideEffectGlobal({ type: 'delete-selected-project' }),
|
|
60
62
|
subtitle:
|
|
61
63
|
project.projectPath != null && project.projectPath !== '' ? (
|
|
62
|
-
<text fg={t.textMuted}>{abbreviatePath(project.projectPath)}</text>
|
|
64
|
+
<text fg={active ? ink : t.textMuted}>{abbreviatePath(project.projectPath)}</text>
|
|
63
65
|
) : undefined,
|
|
64
66
|
title: (
|
|
65
|
-
<text fg={active ?
|
|
67
|
+
<text fg={active ? ink : t.textMuted}>
|
|
66
68
|
{formatProjectLine(project, currentProjectId, currentTabCount, displayIndex)}
|
|
67
69
|
</text>
|
|
68
70
|
),
|
|
@@ -72,13 +74,11 @@ export function ProjectPickerModal({
|
|
|
72
74
|
key: '__create-new__',
|
|
73
75
|
onClick: () => runSideEffectGlobal({ type: 'confirm-selected-project' }),
|
|
74
76
|
title: (
|
|
75
|
-
<text fg={selectedIndex === filtered.length ?
|
|
76
|
-
Create new project
|
|
77
|
-
</text>
|
|
77
|
+
<text fg={selectedIndex === filtered.length ? ink : t.textMuted}>Create new project</text>
|
|
78
78
|
),
|
|
79
79
|
}
|
|
80
80
|
return [...projectItems, createNewItem]
|
|
81
|
-
}, [currentProjectId, currentTabCount, filtered, ordered, selectedIndex, t])
|
|
81
|
+
}, [currentProjectId, currentTabCount, filtered, ink, ordered, selectedIndex, t])
|
|
82
82
|
|
|
83
83
|
const handleHover = useCallback(
|
|
84
84
|
(index: number) => dispatchGlobal({ index, type: 'set-modal-selection-index' }),
|
|
@@ -3,6 +3,7 @@ import { useCallback, useMemo } from 'react'
|
|
|
3
3
|
import { filterSettingRows } from '../../../../settings/search'
|
|
4
4
|
import { useAppStore } from '../../../../state/app-store'
|
|
5
5
|
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
6
|
+
import { useSelectionInk } from '../../../selection-ink'
|
|
6
7
|
import { useTheme } from '../../../theme'
|
|
7
8
|
import { uiTokens } from '../../../ui-tokens'
|
|
8
9
|
import { RowValue } from '../../settings/row-value'
|
|
@@ -28,25 +29,29 @@ export function SettingsSearchModal({
|
|
|
28
29
|
selectedIndex,
|
|
29
30
|
}: SettingsSearchModalProps) {
|
|
30
31
|
const t = useTheme()
|
|
32
|
+
const ink = useSelectionInk()
|
|
31
33
|
const projects = useAppStore((s) => s.projects)
|
|
32
34
|
const hits = useMemo(() => filterSettingRows(projects, filter), [projects, filter])
|
|
33
35
|
|
|
34
36
|
const items = useMemo<PickerItem[]>(
|
|
35
37
|
() =>
|
|
36
|
-
hits.map((hit) =>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
hits.map((hit, index) => {
|
|
39
|
+
const active = index === selectedIndex
|
|
40
|
+
return {
|
|
41
|
+
group: hit.sectionLabel,
|
|
42
|
+
key: hit.row.id,
|
|
43
|
+
// Acts on the selection, which the hover just set — the same two-step
|
|
44
|
+
// every other picker's rows use.
|
|
45
|
+
onClick: () => runSideEffectGlobal({ type: 'confirm-settings-search' }),
|
|
46
|
+
subtitle:
|
|
47
|
+
hit.row.description != null && hit.row.description !== '' ? (
|
|
48
|
+
<text fg={active ? ink : t.textMuted}>{hit.row.description}</text>
|
|
49
|
+
) : undefined,
|
|
50
|
+
title: <text fg={active ? ink : t.text}>{hit.row.label}</text>,
|
|
51
|
+
trailing: <RowValue fg={active ? ink : undefined} row={hit.row} />,
|
|
52
|
+
}
|
|
53
|
+
}),
|
|
54
|
+
[hits, ink, selectedIndex, t]
|
|
50
55
|
)
|
|
51
56
|
|
|
52
57
|
const handleHover = useCallback(
|
|
@@ -37,11 +37,21 @@ export function ModalShell({
|
|
|
37
37
|
justifyContent="center"
|
|
38
38
|
alignItems="center"
|
|
39
39
|
>
|
|
40
|
+
{/* No frame, like everything else — the panel background is what lifts the
|
|
41
|
+
modal off the terminal, and the extra column either side is what makes
|
|
42
|
+
it read as a card rather than a box drawn on the screen.
|
|
43
|
+
|
|
44
|
+
Transparent mode is the one exception that keeps the border: there is
|
|
45
|
+
no background there to lift anything, so without a rule the modal has
|
|
46
|
+
no edge at all. */}
|
|
40
47
|
<box
|
|
41
|
-
border
|
|
48
|
+
border={transparent}
|
|
42
49
|
borderColor={t.border}
|
|
43
50
|
backgroundColor={bg}
|
|
44
|
-
|
|
51
|
+
paddingTop={1}
|
|
52
|
+
paddingBottom={1}
|
|
53
|
+
paddingLeft={2}
|
|
54
|
+
paddingRight={2}
|
|
45
55
|
width={width}
|
|
46
56
|
renderAfter={transparent ? fillBorderedBoxInterior : undefined}
|
|
47
57
|
>
|
|
@@ -4,6 +4,7 @@ import type { MouseEvent as OtuiMouseEvent, ScrollBoxRenderable } from '@opentui
|
|
|
4
4
|
import { useTerminalDimensions } from '@opentui/react'
|
|
5
5
|
import { type ReactNode, useCallback, useLayoutEffect, useMemo, useRef } from 'react'
|
|
6
6
|
|
|
7
|
+
import { useSelectionInk } from '../../../selection-ink'
|
|
7
8
|
import { useTheme } from '../../../theme'
|
|
8
9
|
import { BareInput } from '../../primitives/bare-input'
|
|
9
10
|
import { ListItem } from '../../primitives/list-item'
|
|
@@ -48,7 +49,10 @@ function PickerItemCtas({
|
|
|
48
49
|
onDelete?: () => void
|
|
49
50
|
onWorkspace?: () => void
|
|
50
51
|
}) {
|
|
51
|
-
|
|
52
|
+
// These only ever render on the selected row, which is filled with `primary` —
|
|
53
|
+
// so they wear its ink, not the primary/warning/error they would carry
|
|
54
|
+
// anywhere else. `[edit]` in primary on primary is nothing at all.
|
|
55
|
+
const ink = useSelectionInk()
|
|
52
56
|
const handleEdit = useCallback(
|
|
53
57
|
(event: OtuiMouseEvent) => {
|
|
54
58
|
event.stopPropagation()
|
|
@@ -74,17 +78,17 @@ function PickerItemCtas({
|
|
|
74
78
|
<box flexDirection="row" gap={1}>
|
|
75
79
|
{onEdit ? (
|
|
76
80
|
<box onMouseDown={handleEdit}>
|
|
77
|
-
<text fg={
|
|
81
|
+
<text fg={ink}>[edit]</text>
|
|
78
82
|
</box>
|
|
79
83
|
) : null}
|
|
80
84
|
{onWorkspace ? (
|
|
81
85
|
<box onMouseDown={handleWorkspace}>
|
|
82
|
-
<text fg={
|
|
86
|
+
<text fg={ink}>[WT]</text>
|
|
83
87
|
</box>
|
|
84
88
|
) : null}
|
|
85
89
|
{onDelete ? (
|
|
86
90
|
<box onMouseDown={handleDelete}>
|
|
87
|
-
<text fg={
|
|
91
|
+
<text fg={ink}>[del]</text>
|
|
88
92
|
</box>
|
|
89
93
|
) : null}
|
|
90
94
|
</box>
|
|
@@ -5,6 +5,7 @@ import type { SnippetRecord } from '../../../../state/types'
|
|
|
5
5
|
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
6
6
|
import { filterSnippets } from '../../../../state/selectors'
|
|
7
7
|
import { isConfigSnippetId } from '../../../../state/snippet-catalog'
|
|
8
|
+
import { useSelectionInk } from '../../../selection-ink'
|
|
8
9
|
import { useTheme } from '../../../theme'
|
|
9
10
|
import { uiTokens } from '../../../ui-tokens'
|
|
10
11
|
import { Picker, type PickerItem } from '../shared/picker'
|
|
@@ -33,6 +34,7 @@ export function SnippetPickerModal({
|
|
|
33
34
|
snippets,
|
|
34
35
|
}: SnippetPickerModalProps) {
|
|
35
36
|
const t = useTheme()
|
|
37
|
+
const ink = useSelectionInk()
|
|
36
38
|
const filtered = useMemo(() => filterSnippets(snippets, filter), [filter, snippets])
|
|
37
39
|
|
|
38
40
|
const items = useMemo<PickerItem[]>(
|
|
@@ -52,21 +54,21 @@ export function SnippetPickerModal({
|
|
|
52
54
|
onEdit: fromConfig
|
|
53
55
|
? undefined
|
|
54
56
|
: () => runSideEffectGlobal({ type: 'edit-selected-snippet' }),
|
|
55
|
-
subtitle: <text fg={t.textMuted}>{truncateContent(snippet.content)}</text>,
|
|
57
|
+
subtitle: <text fg={active ? ink : t.textMuted}>{truncateContent(snippet.content)}</text>,
|
|
56
58
|
title: (
|
|
57
59
|
<box flexDirection="row">
|
|
58
|
-
<text fg={active ?
|
|
60
|
+
<text fg={active ? ink : t.textMuted}>
|
|
59
61
|
<strong>{snippet.name}</strong>
|
|
60
62
|
</text>
|
|
61
63
|
{snippet.trigger != null && snippet.trigger !== '' ? (
|
|
62
|
-
<text fg={t.textMuted}>{` :${snippet.trigger}`}</text>
|
|
64
|
+
<text fg={active ? ink : t.textMuted}>{` :${snippet.trigger}`}</text>
|
|
63
65
|
) : null}
|
|
64
|
-
{fromConfig ? <text fg={t.textMuted}>{' [config]'}</text> : null}
|
|
66
|
+
{fromConfig ? <text fg={active ? ink : t.textMuted}>{' [config]'}</text> : null}
|
|
65
67
|
</box>
|
|
66
68
|
),
|
|
67
69
|
}
|
|
68
70
|
}),
|
|
69
|
-
[filtered, selectedIndex, t]
|
|
71
|
+
[filtered, ink, selectedIndex, t]
|
|
70
72
|
)
|
|
71
73
|
|
|
72
74
|
const handleHover = useCallback(
|