@brimveyn/aimux 1.23.7 → 1.24.1
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/side-effects.ts +19 -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/state/actions.ts +3 -2
- package/src/state/reducers/modal-state.ts +24 -28
- package/src/state/text-cursor.ts +117 -0
- package/src/state/types.ts +1 -1
- package/src/state/workspace-delete-store.ts +32 -0
- package/src/ui/breaking-update-screen.tsx +7 -1
- 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 +22 -15
- 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/shared/workspace-delete-confirm.tsx +8 -7
- 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/overlays/workspace-deleting-overlay.tsx +29 -0
- 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/root.tsx +2 -0
- package/src/ui/selection-ink.ts +20 -0
|
@@ -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(
|
|
@@ -9,6 +9,7 @@ import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-
|
|
|
9
9
|
import { formatDiffStat } from '../../../../state/project-workspaces'
|
|
10
10
|
// eslint-disable-next-line no-duplicate-imports
|
|
11
11
|
import { IDLE_WORKSPACE_ACTIVITY } from '../../../../state/types'
|
|
12
|
+
import { useWorkspaceDeleteStore } from '../../../../state/workspace-delete-store'
|
|
12
13
|
import { useActivitySprite } from '../../../hooks/use-activity-sprite'
|
|
13
14
|
import { useBusySpinner } from '../../../hooks/use-busy-spinner'
|
|
14
15
|
// eslint-disable-next-line no-duplicate-imports
|
|
@@ -33,14 +34,11 @@ interface WorkspaceRowProps {
|
|
|
33
34
|
projectIndex: number
|
|
34
35
|
/** True when this row is the active cursor item. */
|
|
35
36
|
isActiveItem: boolean
|
|
36
|
-
/** True when this row's project is the current project (selection scope). */
|
|
37
|
-
inCurrentGroup: boolean
|
|
38
37
|
contentWidth: number
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
export const WorkspaceRow = memo(function WorkspaceRow({
|
|
42
41
|
contentWidth,
|
|
43
|
-
inCurrentGroup,
|
|
44
42
|
isActiveItem,
|
|
45
43
|
project,
|
|
46
44
|
projectIndex,
|
|
@@ -59,9 +57,13 @@ export const WorkspaceRow = memo(function WorkspaceRow({
|
|
|
59
57
|
// own: one shape is what keeps a cell from resolving to the wrong image.
|
|
60
58
|
const hasBranch = workspace.branch != null && workspace.branch !== ''
|
|
61
59
|
const sprite = useActivitySprite(hasBranch ? spriteStateFor(activity) : null)
|
|
60
|
+
// A delete takes seconds of git work with the row still on screen. The branch
|
|
61
|
+
// line is where it says so — the name above it is what you are looking for,
|
|
62
|
+
// and the branch under it is the one fact the delete is about to take away.
|
|
63
|
+
const isDeleting = useWorkspaceDeleteStore((s) => s.deleting[workspace.id] !== undefined)
|
|
62
64
|
// Only the working case animates, so the timer is off for every other row —
|
|
63
65
|
// and off entirely when a sprite is drawing this row instead.
|
|
64
|
-
const spinner = useBusySpinner(activity.working && sprite === null)
|
|
66
|
+
const spinner = useBusySpinner(isDeleting || (activity.working && sprite === null))
|
|
65
67
|
// A primitive, so the selector stays referentially stable across renders.
|
|
66
68
|
const hasSleepingTabs = useAppStore((s) =>
|
|
67
69
|
s.tabs.some((tab) => tab.workspaceId === workspace.id && tab.hibernated === true)
|
|
@@ -124,7 +126,6 @@ export const WorkspaceRow = memo(function WorkspaceRow({
|
|
|
124
126
|
closeTabs: true,
|
|
125
127
|
force: false,
|
|
126
128
|
projectId: project.id,
|
|
127
|
-
reason: 'Its assistant tabs will be closed and the worktree removed.',
|
|
128
129
|
type: 'open-workspace-delete-confirm',
|
|
129
130
|
workspaceId: workspace.id,
|
|
130
131
|
workspaceLabel: workspace.branch ?? workspace.name,
|
|
@@ -134,12 +135,12 @@ export const WorkspaceRow = memo(function WorkspaceRow({
|
|
|
134
135
|
return entries
|
|
135
136
|
}, [project.id, workspace.branch, workspace.id, workspace.name, workspace.source])
|
|
136
137
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
138
|
+
// No band for "this row's project is the current one": the bar is a single
|
|
139
|
+
// backgroundPanel surface now and backgroundElement is spoken for by the
|
|
140
|
+
// cursor row, which leaves no third tone that works in every theme. The
|
|
141
|
+
// cursor row — one step off the panel, plus its accent bar — is what says
|
|
142
|
+
// where you are; the group it sits in follows from that.
|
|
143
|
+
const bgColor = isActiveItem ? base.backgroundElement : undefined
|
|
143
144
|
// The cursor: a full-height accent bar down the left of the row, both lines
|
|
144
145
|
// of it. The background alone is one step of grey and gets lost among rows
|
|
145
146
|
// that carry colour of their own; a bar the height of the row is found
|
|
@@ -246,7 +247,7 @@ export const WorkspaceRow = memo(function WorkspaceRow({
|
|
|
246
247
|
</text>
|
|
247
248
|
) : null}
|
|
248
249
|
</box>
|
|
249
|
-
{branchLabel == null ? null : (
|
|
250
|
+
{branchLabel == null && !isDeleting ? null : (
|
|
250
251
|
<box flexDirection="row" alignItems="center">
|
|
251
252
|
<text fg={t.primary} selectable={false} wrapMode="none">
|
|
252
253
|
{cursorGlyph}
|
|
@@ -257,9 +258,15 @@ export const WorkspaceRow = memo(function WorkspaceRow({
|
|
|
257
258
|
<text fg={statusColor} selectable={false} wrapMode="none">
|
|
258
259
|
{sprite?.glyphs[1] ?? ' '}
|
|
259
260
|
</text>
|
|
260
|
-
|
|
261
|
-
{
|
|
262
|
-
|
|
261
|
+
{isDeleting ? (
|
|
262
|
+
<text fg={t.error} selectable={false} wrapMode="none">
|
|
263
|
+
{spinner} Deleting…
|
|
264
|
+
</text>
|
|
265
|
+
) : (
|
|
266
|
+
<text fg={t.textMuted} selectable={false} wrapMode="none">
|
|
267
|
+
{'\u{e702}'} {branchLabel}
|
|
268
|
+
</text>
|
|
269
|
+
)}
|
|
263
270
|
</box>
|
|
264
271
|
)}
|
|
265
272
|
</ContextMenuBox>
|
|
@@ -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>
|
|
@@ -6,7 +6,11 @@ import { Form } from './form'
|
|
|
6
6
|
|
|
7
7
|
interface WorkspaceDeleteConfirmProps {
|
|
8
8
|
keybindsModeId: ModeId
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* One short line naming what confirming costs beyond the delete itself.
|
|
11
|
+
* Omitted for the ordinary case, where the title already said everything.
|
|
12
|
+
*/
|
|
13
|
+
reason?: string
|
|
10
14
|
workspaceLabel: string
|
|
11
15
|
}
|
|
12
16
|
|
|
@@ -24,16 +28,13 @@ export function WorkspaceDeleteConfirm({
|
|
|
24
28
|
return (
|
|
25
29
|
<Form
|
|
26
30
|
title="Delete workspace?"
|
|
31
|
+
subtitle={workspaceLabel}
|
|
27
32
|
keybindsModeId={keybindsModeId}
|
|
28
33
|
width={uiTokens.modalWidth.md}
|
|
34
|
+
// Destructive confirmation — the one place §7 keeps an inline key hint.
|
|
29
35
|
footer={<text fg={t.textMuted}>Enter / y to delete · Esc / n to cancel</text>}
|
|
30
36
|
>
|
|
31
|
-
<
|
|
32
|
-
<text fg={t.text}>
|
|
33
|
-
Delete <strong>{workspaceLabel}</strong>?
|
|
34
|
-
</text>
|
|
35
|
-
<text fg={t.warning}>{reason}</text>
|
|
36
|
-
</box>
|
|
37
|
+
{reason == null || reason === '' ? null : <text fg={t.textMuted}>{reason}</text>}
|
|
37
38
|
</Form>
|
|
38
39
|
)
|
|
39
40
|
}
|
|
@@ -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(
|