claudeup 4.38.1 → 4.39.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 +4 -4
- package/src/__tests__/selection-scope.test.ts +100 -0
- package/src/main.tsx +26 -14
- package/src/ui/components/CategoryHeader.tsx +28 -11
- package/src/ui/components/ScrollableList.tsx +8 -0
- package/src/ui/components/layout/ScreenLayout.tsx +71 -8
- package/src/ui/screens/StylesScreen.tsx +28 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudeup",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.39.1",
|
|
4
4
|
"description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.tsx",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
"typescript": "^5.6.3"
|
|
65
65
|
},
|
|
66
66
|
"optionalDependencies": {
|
|
67
|
-
"claudeup-darwin-arm64": "4.
|
|
68
|
-
"claudeup-darwin-x64": "4.
|
|
69
|
-
"claudeup-linux-x64": "4.
|
|
67
|
+
"claudeup-darwin-arm64": "4.39.1",
|
|
68
|
+
"claudeup-darwin-x64": "4.39.1",
|
|
69
|
+
"claudeup-linux-x64": "4.39.1"
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { BoxRenderable, TextRenderable } from "@opentui/core";
|
|
3
|
+
import { createTestRenderer } from "@opentui/core/testing";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pins the OpenTUI selection behaviour claudeup relies on after enabling the
|
|
7
|
+
* mouse outside tmux (src/main.tsx): a drag that stays inside one pane selects
|
|
8
|
+
* text from that pane only, because the renderer scopes the selection to the
|
|
9
|
+
* smallest ancestor covering the dragged-over renderables. Crossing the pane
|
|
10
|
+
* divider deliberately escalates the scope to the common ancestor — that is
|
|
11
|
+
* upstream's design, not a bug, and the second test documents it so a future
|
|
12
|
+
* re-pin that changes either half is caught here rather than by a user.
|
|
13
|
+
*
|
|
14
|
+
* The geometry mirrors ScreenLayout: two column boxes side by side under one
|
|
15
|
+
* row box, text rows inside each.
|
|
16
|
+
*/
|
|
17
|
+
async function twoPaneRenderer() {
|
|
18
|
+
const { renderer, mockMouse, renderOnce } = await createTestRenderer({
|
|
19
|
+
width: 80,
|
|
20
|
+
height: 10,
|
|
21
|
+
useMouse: true,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const row = new BoxRenderable(renderer, {
|
|
25
|
+
id: "row",
|
|
26
|
+
width: 80,
|
|
27
|
+
height: 10,
|
|
28
|
+
flexDirection: "row",
|
|
29
|
+
});
|
|
30
|
+
renderer.root.add(row);
|
|
31
|
+
|
|
32
|
+
const left = new BoxRenderable(renderer, {
|
|
33
|
+
id: "left",
|
|
34
|
+
width: 40,
|
|
35
|
+
height: 10,
|
|
36
|
+
flexDirection: "column",
|
|
37
|
+
});
|
|
38
|
+
const separator = new BoxRenderable(renderer, {
|
|
39
|
+
id: "separator",
|
|
40
|
+
width: 1,
|
|
41
|
+
height: 10,
|
|
42
|
+
});
|
|
43
|
+
const right = new BoxRenderable(renderer, {
|
|
44
|
+
id: "right",
|
|
45
|
+
width: 39,
|
|
46
|
+
height: 10,
|
|
47
|
+
flexDirection: "column",
|
|
48
|
+
});
|
|
49
|
+
row.add(left);
|
|
50
|
+
row.add(separator);
|
|
51
|
+
row.add(right);
|
|
52
|
+
|
|
53
|
+
for (const line of ["alpha one", "alpha two", "alpha three"]) {
|
|
54
|
+
left.add(new TextRenderable(renderer, { content: line }));
|
|
55
|
+
}
|
|
56
|
+
for (const line of ["omega one", "omega two", "omega three"]) {
|
|
57
|
+
right.add(new TextRenderable(renderer, { content: line }));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await renderOnce();
|
|
61
|
+
return { renderer, mockMouse };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("mouse selection scope across panes", () => {
|
|
65
|
+
test("a drag inside the left pane selects left-pane text only", async () => {
|
|
66
|
+
const { renderer, mockMouse } = await twoPaneRenderer();
|
|
67
|
+
|
|
68
|
+
await mockMouse.drag(1, 0, 20, 2);
|
|
69
|
+
|
|
70
|
+
const selected = renderer.getSelection()?.getSelectedText() ?? "";
|
|
71
|
+
expect(selected).toContain("alpha");
|
|
72
|
+
expect(selected).not.toContain("omega");
|
|
73
|
+
|
|
74
|
+
renderer.destroy();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("a drag inside the right pane selects right-pane text only", async () => {
|
|
78
|
+
const { renderer, mockMouse } = await twoPaneRenderer();
|
|
79
|
+
|
|
80
|
+
await mockMouse.drag(45, 0, 70, 2);
|
|
81
|
+
|
|
82
|
+
const selected = renderer.getSelection()?.getSelectedText() ?? "";
|
|
83
|
+
expect(selected).toContain("omega");
|
|
84
|
+
expect(selected).not.toContain("alpha");
|
|
85
|
+
|
|
86
|
+
renderer.destroy();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a drag across the divider escalates to both panes — upstream's design", async () => {
|
|
90
|
+
const { renderer, mockMouse } = await twoPaneRenderer();
|
|
91
|
+
|
|
92
|
+
await mockMouse.drag(1, 0, 70, 2);
|
|
93
|
+
|
|
94
|
+
const selected = renderer.getSelection()?.getSelectedText() ?? "";
|
|
95
|
+
expect(selected).toContain("alpha");
|
|
96
|
+
expect(selected).toContain("omega");
|
|
97
|
+
|
|
98
|
+
renderer.destroy();
|
|
99
|
+
});
|
|
100
|
+
});
|
package/src/main.tsx
CHANGED
|
@@ -31,25 +31,37 @@ async function main(): Promise<void> {
|
|
|
31
31
|
// absolute fill would box the UI into whatever theme we guessed. OpenTUI's
|
|
32
32
|
// built-in default for unstyled cells is truecolor white, which is why every
|
|
33
33
|
// element now names an adaptive colour explicitly — see src/ui/theme.ts.
|
|
34
|
+
// The mouse is ON, everywhere — it carries real features now: the wheel
|
|
35
|
+
// scrolls the pane under the cursor, and a drag selects text from ONE pane
|
|
36
|
+
// at a time (terminal-native selection is a screen-wide rectangle that
|
|
37
|
+
// grabs both columns), OSC 52-copied on release below.
|
|
38
|
+
//
|
|
39
|
+
// This deliberately includes tmux. Two earlier revisions got this wrong in
|
|
40
|
+
// opposite directions: one disabled the mouse everywhere on the theory that
|
|
41
|
+
// capture breaks tmux-level pane clicking, the next kept it off only inside
|
|
42
|
+
// tmux. Both dated from when claudeup had no mouse features, so disabling
|
|
43
|
+
// cost nothing. MEASURED on a heavily tmux'd machine: tmux forwards mouse
|
|
44
|
+
// to a pane application that asks for it (htop in the same session took
|
|
45
|
+
// clicks and wheel fine while claudeup sat inert), and tmux's own `mouse`
|
|
46
|
+
// option keeps governing pane management. Terminal-native selection stays
|
|
47
|
+
// reachable via Shift+drag in most terminals.
|
|
48
|
+
//
|
|
49
|
+
// enableMouseMovement (mode-1003 "report all motion") stays off — selection
|
|
50
|
+
// needs only button-held drag events, which button reporting delivers.
|
|
34
51
|
const renderer = await createCliRenderer({
|
|
35
52
|
backgroundColor: RGBA.defaultBackground(),
|
|
36
|
-
|
|
37
|
-
// every action has a key. OpenTUI nevertheless defaults BOTH of these to
|
|
38
|
-
// true (`config.useMouse ?? true`, `config.enableMouseMovement ?? true`),
|
|
39
|
-
// which turns on button reporting and mode-1003 "report all motion".
|
|
40
|
-
//
|
|
41
|
-
// Inside tmux that is actively harmful: once a pane's application asks for
|
|
42
|
-
// mouse events, tmux hands them to that application instead of using them
|
|
43
|
-
// to select a pane or scroll history. Clicking another pane then does
|
|
44
|
-
// nothing, keyboard focus stays where it was, and what you type lands in
|
|
45
|
-
// the pane you thought you had just left.
|
|
46
|
-
//
|
|
47
|
-
// Turning it off costs nothing here and gives clicking and scrolling back
|
|
48
|
-
// to the terminal.
|
|
49
|
-
useMouse: false,
|
|
53
|
+
useMouse: true,
|
|
50
54
|
enableMouseMovement: false,
|
|
51
55
|
});
|
|
52
56
|
|
|
57
|
+
// Mouse selection → system clipboard, on release. OSC 52 survives SSH;
|
|
58
|
+
// terminals that block it simply ignore the sequence. Never fires where the
|
|
59
|
+
// mouse is off, so the tmux path pays nothing.
|
|
60
|
+
renderer.on("selection", (selection: { getSelectedText(): string }) => {
|
|
61
|
+
const text = selection?.getSelectedText() ?? "";
|
|
62
|
+
if (text.length > 0) renderer.copyToClipboardOSC52(text);
|
|
63
|
+
});
|
|
64
|
+
|
|
53
65
|
// Ask the terminal whether it is light or dark, once. Only the disabled-row
|
|
54
66
|
// tint needs this — see src/ui/theme-mode.ts for why that one case cannot be
|
|
55
67
|
// solved the way the rest of the palette is. Bounded wait: a terminal that
|
|
@@ -29,18 +29,35 @@ export function CategoryHeader({
|
|
|
29
29
|
const countBadge = count !== undefined ? ` (${count})` : "";
|
|
30
30
|
const statusText = status ? ` ${status}` : "";
|
|
31
31
|
|
|
32
|
-
//
|
|
32
|
+
// Flex row instead of one flat <text>: a flat text wraps when the pane is
|
|
33
|
+
// narrower than the row ("by MadAppGang" lost its final character and the wrap
|
|
34
|
+
// shifted every item below by one line). Here the dash filler is the only
|
|
35
|
+
// element allowed to shrink to nothing, so the badge survives at any width
|
|
36
|
+
// and the title clips last.
|
|
33
37
|
return (
|
|
34
|
-
<
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
<box flexDirection="row" width="100%" height={1} overflow="hidden">
|
|
39
|
+
<box flexShrink={1} minWidth={4} overflow="hidden" height={1}>
|
|
40
|
+
<text fg={theme.colors.text}>
|
|
41
|
+
<span fg={theme.colors.muted}>{expandIcon}</span>
|
|
42
|
+
<span fg={theme.colors.text}>
|
|
43
|
+
<strong> {title}</strong>
|
|
44
|
+
</span>
|
|
45
|
+
<span fg={theme.colors.muted}>{versionBadge}</span>
|
|
46
|
+
<span fg={theme.colors.muted}>{countBadge}</span>
|
|
47
|
+
</text>
|
|
48
|
+
</box>
|
|
49
|
+
{/* flexShrink 100: the filler must be the first thing sacrificed. With equal
|
|
50
|
+
shrink factors yoga takes width from the (larger-basis) title first, which
|
|
51
|
+
clipped a character off the title while all four dashes survived. */}
|
|
52
|
+
<box flexShrink={100} minWidth={0} overflow="hidden" height={1}>
|
|
53
|
+
<text fg={theme.colors.border}> ────</text>
|
|
54
|
+
</box>
|
|
55
|
+
{status ? (
|
|
56
|
+
<box flexShrink={0} height={1}>
|
|
57
|
+
<text fg={statusColor}>{statusText}</text>
|
|
58
|
+
</box>
|
|
59
|
+
) : null}
|
|
60
|
+
</box>
|
|
44
61
|
);
|
|
45
62
|
}
|
|
46
63
|
|
|
@@ -70,6 +70,14 @@ export function ScrollableList<T>({
|
|
|
70
70
|
<box
|
|
71
71
|
key={getKey ? getKey(item, originalIndex) : `${originalIndex}`}
|
|
72
72
|
width="100%"
|
|
73
|
+
// height=1 is load-bearing, not cosmetic. overflow="hidden" alone clips
|
|
74
|
+
// CONTENT but lets the box GROW: an over-wide <text> wraps to a second
|
|
75
|
+
// line, the row becomes two lines tall, and every item below shifts —
|
|
76
|
+
// which rendered as a phantom blank line under "Magus Marketing" and
|
|
77
|
+
// the last two plugins composited onto one row. The list's scroll math
|
|
78
|
+
// assumes one line per item; this makes that assumption true.
|
|
79
|
+
height={1}
|
|
80
|
+
flexShrink={0}
|
|
73
81
|
overflow="hidden"
|
|
74
82
|
>
|
|
75
83
|
{renderItem(item, originalIndex, originalIndex === selectedIndex)}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
2
3
|
import { useDimensions } from "../../state/DimensionsContext.js";
|
|
4
|
+
import { useKeyboard } from "../../hooks/useKeyboard.js";
|
|
5
|
+
import { useApp } from "../../state/AppContext.js";
|
|
3
6
|
import { TabBar } from "../TabBar.js";
|
|
4
7
|
import type { Screen } from "../../state/types.js";
|
|
5
8
|
import { FooterHints, type FooterHint } from "./FooterHints.js";
|
|
@@ -40,6 +43,12 @@ interface ScreenLayoutProps {
|
|
|
40
43
|
listPanel: React.ReactNode;
|
|
41
44
|
/** Right panel content (detail view) */
|
|
42
45
|
detailPanel: React.ReactNode;
|
|
46
|
+
/**
|
|
47
|
+
* Identity of the detail content, e.g. the selected row's id. When it
|
|
48
|
+
* changes, the detail pane scrolls back to the top — a reading position
|
|
49
|
+
* belongs to one document, not to the panel.
|
|
50
|
+
*/
|
|
51
|
+
detailKey?: string;
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
const HEADER_COLOR = theme.colors.accent;
|
|
@@ -54,8 +63,10 @@ export function ScreenLayout({
|
|
|
54
63
|
footerHints,
|
|
55
64
|
listPanel,
|
|
56
65
|
detailPanel,
|
|
66
|
+
detailKey,
|
|
57
67
|
}: ScreenLayoutProps) {
|
|
58
68
|
const dimensions = useDimensions();
|
|
69
|
+
const { state } = useApp();
|
|
59
70
|
|
|
60
71
|
const hasSearchBar = search && (search.isActive || search.query);
|
|
61
72
|
|
|
@@ -65,6 +76,34 @@ export function ScreenLayout({
|
|
|
65
76
|
const panelHeight = Math.max(5, dimensions.contentHeight - fixedHeight);
|
|
66
77
|
const lineWidth = Math.max(10, dimensions.terminalWidth - 4);
|
|
67
78
|
|
|
79
|
+
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
80
|
+
|
|
81
|
+
// The detail pane has no cursor, so it needs its own scroll keys: arrows
|
|
82
|
+
// move the LIST selection, and the wheel only helps when the terminal
|
|
83
|
+
// delivers mouse reports (see src/main.tsx). PgUp/PgDn page with two rows
|
|
84
|
+
// of overlap so a page turn doesn't lose the reader's place; Ctrl+U/Ctrl+D
|
|
85
|
+
// half-page in the pager tradition — and exist because a MacBook keyboard
|
|
86
|
+
// hides PgDn behind Fn+↓, where nobody finds it.
|
|
87
|
+
const detailPageStep = Math.max(1, panelHeight - 2);
|
|
88
|
+
const detailHalfStep = Math.max(1, Math.floor(panelHeight / 2));
|
|
89
|
+
useKeyboard((event) => {
|
|
90
|
+
if (state.modal) return;
|
|
91
|
+
const ctrl = event.ctrl === true;
|
|
92
|
+
if (event.name === "pageup" || (ctrl && event.name === "u")) {
|
|
93
|
+
detailScrollRef.current?.scrollBy(
|
|
94
|
+
event.name === "pageup" ? -detailPageStep : -detailHalfStep,
|
|
95
|
+
);
|
|
96
|
+
} else if (event.name === "pagedown" || (ctrl && event.name === "d")) {
|
|
97
|
+
detailScrollRef.current?.scrollBy(
|
|
98
|
+
event.name === "pagedown" ? detailPageStep : detailHalfStep,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (detailKey !== undefined) detailScrollRef.current?.scrollTo(0);
|
|
105
|
+
}, [detailKey]);
|
|
106
|
+
|
|
68
107
|
return (
|
|
69
108
|
<box flexDirection="column" height={dimensions.contentHeight}>
|
|
70
109
|
{/* Line above tabs */}
|
|
@@ -90,9 +129,14 @@ export function ScreenLayout({
|
|
|
90
129
|
flexDirection="row"
|
|
91
130
|
justifyContent="space-between"
|
|
92
131
|
>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
132
|
+
{/* The title never yields: an overlong right-hand status once
|
|
133
|
+
shrank it away and the two texts read as one sentence. Long
|
|
134
|
+
content belongs in the `notice` row, not beside the title. */}
|
|
135
|
+
<box flexShrink={0} marginRight={2}>
|
|
136
|
+
<text fg={HEADER_COLOR}>
|
|
137
|
+
<strong>{title}</strong>
|
|
138
|
+
</text>
|
|
139
|
+
</box>
|
|
96
140
|
{subtitle && <text fg={theme.colors.muted}>{subtitle}</text>}
|
|
97
141
|
{!subtitle && statusLine ? statusLine : null}
|
|
98
142
|
</box>
|
|
@@ -147,18 +191,37 @@ export function ScreenLayout({
|
|
|
147
191
|
<text fg={theme.colors.border}>{"│".repeat(panelHeight)}</text>
|
|
148
192
|
</box>
|
|
149
193
|
|
|
150
|
-
{/* Detail panel — scrollable
|
|
194
|
+
{/* Detail panel — scrollable. The thumb is themed so the reader can
|
|
195
|
+
see there is more below the fold; the track keeps the library
|
|
196
|
+
default so it stays chrome, not signal. */}
|
|
151
197
|
<box width="50%" height={panelHeight} paddingLeft={1}>
|
|
152
|
-
<scrollbox
|
|
198
|
+
<scrollbox
|
|
199
|
+
ref={detailScrollRef}
|
|
200
|
+
height={panelHeight}
|
|
201
|
+
scrollY={true}
|
|
202
|
+
scrollX={false}
|
|
203
|
+
verticalScrollbarOptions={{
|
|
204
|
+
showArrows: false,
|
|
205
|
+
trackOptions: {
|
|
206
|
+
foregroundColor: theme.colors.muted,
|
|
207
|
+
},
|
|
208
|
+
}}
|
|
209
|
+
>
|
|
153
210
|
<box flexDirection="column">{detailPanel}</box>
|
|
154
211
|
</scrollbox>
|
|
155
212
|
</box>
|
|
156
213
|
</box>
|
|
157
214
|
|
|
158
|
-
{/* Footer
|
|
215
|
+
{/* Footer — the layout appends its own scroll hint so every screen
|
|
216
|
+
advertises the detail-pane keys without each screen repeating it */}
|
|
159
217
|
<box height={1} paddingLeft={1}>
|
|
160
218
|
{Array.isArray(footerHints) ? (
|
|
161
|
-
<FooterHints
|
|
219
|
+
<FooterHints
|
|
220
|
+
hints={[
|
|
221
|
+
...(footerHints as FooterHint[]),
|
|
222
|
+
{ keys: ["^U/^D", "PgUp/Dn"], label: "scroll" },
|
|
223
|
+
]}
|
|
224
|
+
/>
|
|
162
225
|
) : typeof footerHints === "string" ? (
|
|
163
226
|
<text fg={theme.colors.muted}>{footerHints}</text>
|
|
164
227
|
) : (
|
|
@@ -866,31 +866,35 @@ export function StylesScreen() {
|
|
|
866
866
|
|
|
867
867
|
// ── Status line ───────────────────────────────────────────────────────────
|
|
868
868
|
|
|
869
|
-
//
|
|
870
|
-
//
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
//
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
869
|
+
// Transient feedback takes the full-width notice row under the header, not
|
|
870
|
+
// the header itself: a long message beside the title used to shrink the
|
|
871
|
+
// title away, and the two ran together as one garbled sentence ("claudeup
|
|
872
|
+
// Styleevidence-first ships with…"). Order matters inside the row. A
|
|
873
|
+
// message wins while it lasts, so pressing a key always acknowledges the
|
|
874
|
+
// press — otherwise the fill banner below would shadow "already reading"
|
|
875
|
+
// and the second press would look ignored again, which is the bug this
|
|
876
|
+
// path exists to fix. The banner is the fallback because `statusMsg` is
|
|
877
|
+
// local and dies when the screen unmounts, leaving it the only thing that
|
|
878
|
+
// still reports work in flight after a tab switch.
|
|
879
|
+
const noticeContent = statusMsg ? (
|
|
880
|
+
<text
|
|
881
|
+
fg={
|
|
882
|
+
statusMsg.tone === "success"
|
|
883
|
+
? theme.colors.success
|
|
884
|
+
: theme.colors.danger
|
|
885
|
+
}
|
|
886
|
+
>
|
|
887
|
+
{statusMsg.text}
|
|
886
888
|
</text>
|
|
887
889
|
) : isFilling ? (
|
|
888
|
-
<text fg={theme.colors.
|
|
889
|
-
|
|
890
|
-
Reading the codebase to fill a template… this can take a few minutes
|
|
891
|
-
</span>
|
|
890
|
+
<text fg={theme.colors.warning}>
|
|
891
|
+
Reading the codebase to fill a template… this can take a few minutes
|
|
892
892
|
</text>
|
|
893
|
-
) :
|
|
893
|
+
) : undefined;
|
|
894
|
+
|
|
895
|
+
// The header keeps only the short ambient state — it shares its row with
|
|
896
|
+
// the title and must never crowd it.
|
|
897
|
+
const statusContent = (
|
|
894
898
|
<text fg={theme.colors.text}>
|
|
895
899
|
<span fg={theme.colors.muted}>Active: </span>
|
|
896
900
|
<span
|
|
@@ -993,6 +997,7 @@ export function StylesScreen() {
|
|
|
993
997
|
title="claudeup Styles"
|
|
994
998
|
currentScreen="styles"
|
|
995
999
|
statusLine={statusContent}
|
|
1000
|
+
notice={noticeContent}
|
|
996
1001
|
search={
|
|
997
1002
|
stylesState.searchQuery || isSearchActive
|
|
998
1003
|
? {
|
|
@@ -1082,6 +1087,7 @@ export function StylesScreen() {
|
|
|
1082
1087
|
</box>
|
|
1083
1088
|
}
|
|
1084
1089
|
detailPanel={renderStyleDetail(selectedItem, detailWidth)}
|
|
1090
|
+
detailKey={selectedItem?.id ?? ""}
|
|
1085
1091
|
/>
|
|
1086
1092
|
);
|
|
1087
1093
|
}
|