claudeup 3.14.0 → 3.15.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 +1 -1
- package/src/ui/adapters/skillsAdapter.js +105 -0
- package/src/ui/adapters/skillsAdapter.ts +159 -0
- package/src/ui/components/primitives/ActionHints.js +13 -0
- package/src/ui/components/primitives/ActionHints.tsx +41 -0
- package/src/ui/components/primitives/DetailSection.js +7 -0
- package/src/ui/components/primitives/DetailSection.tsx +22 -0
- package/src/ui/components/primitives/KeyValueLine.js +8 -0
- package/src/ui/components/primitives/KeyValueLine.tsx +19 -0
- package/src/ui/components/primitives/ListCategoryRow.js +8 -0
- package/src/ui/components/primitives/ListCategoryRow.tsx +38 -0
- package/src/ui/components/primitives/MetaText.js +8 -0
- package/src/ui/components/primitives/MetaText.tsx +14 -0
- package/src/ui/components/primitives/ScopeDetail.js +32 -0
- package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
- package/src/ui/components/primitives/ScopeSquares.js +11 -0
- package/src/ui/components/primitives/ScopeSquares.tsx +33 -0
- package/src/ui/components/primitives/SelectableRow.js +5 -0
- package/src/ui/components/primitives/SelectableRow.tsx +24 -0
- package/src/ui/components/primitives/index.js +8 -0
- package/src/ui/components/primitives/index.ts +9 -0
- package/src/ui/registry.js +1 -0
- package/src/ui/registry.ts +27 -0
- package/src/ui/renderers/skillRenderers.js +82 -0
- package/src/ui/renderers/skillRenderers.tsx +209 -0
- package/src/ui/screens/SkillsScreen.js +41 -190
- package/src/ui/screens/SkillsScreen.tsx +436 -796
- package/src/ui/theme.js +47 -0
- package/src/ui/theme.ts +53 -0
package/package.json
CHANGED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the flat list of items for the SkillsScreen list panel.
|
|
3
|
+
* Extracted from SkillsScreen so it can be tested independently.
|
|
4
|
+
*/
|
|
5
|
+
export function buildSkillBrowserItems({ recommended, popular, installed, searchResults, query, isSearchLoading, }) {
|
|
6
|
+
const lowerQuery = query.toLowerCase();
|
|
7
|
+
const items = [];
|
|
8
|
+
// ── INSTALLED: always shown at top (if any) ──
|
|
9
|
+
const installedFiltered = lowerQuery
|
|
10
|
+
? installed.filter((s) => s.name.toLowerCase().includes(lowerQuery))
|
|
11
|
+
: installed;
|
|
12
|
+
if (installedFiltered.length > 0) {
|
|
13
|
+
items.push({
|
|
14
|
+
id: "cat:installed",
|
|
15
|
+
kind: "category",
|
|
16
|
+
label: `Installed (${installedFiltered.length})`,
|
|
17
|
+
title: "Installed",
|
|
18
|
+
categoryKey: "installed",
|
|
19
|
+
count: installedFiltered.length,
|
|
20
|
+
tone: "purple",
|
|
21
|
+
star: "● ",
|
|
22
|
+
});
|
|
23
|
+
for (const skill of installedFiltered) {
|
|
24
|
+
items.push({
|
|
25
|
+
id: `skill:${skill.id}`,
|
|
26
|
+
kind: "skill",
|
|
27
|
+
label: skill.name,
|
|
28
|
+
skill,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// ── RECOMMENDED: always shown, filtered when searching ──
|
|
33
|
+
const filteredRec = lowerQuery
|
|
34
|
+
? recommended.filter((s) => s.name.toLowerCase().includes(lowerQuery) ||
|
|
35
|
+
(s.description || "").toLowerCase().includes(lowerQuery))
|
|
36
|
+
: recommended;
|
|
37
|
+
items.push({
|
|
38
|
+
id: "cat:recommended",
|
|
39
|
+
kind: "category",
|
|
40
|
+
label: "Recommended",
|
|
41
|
+
title: "Recommended",
|
|
42
|
+
categoryKey: "recommended",
|
|
43
|
+
count: filteredRec.length,
|
|
44
|
+
tone: "green",
|
|
45
|
+
star: "★ ",
|
|
46
|
+
});
|
|
47
|
+
for (const skill of filteredRec) {
|
|
48
|
+
items.push({
|
|
49
|
+
id: `skill:${skill.id}`,
|
|
50
|
+
kind: "skill",
|
|
51
|
+
label: skill.name,
|
|
52
|
+
skill,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// ── SEARCH MODE ──
|
|
56
|
+
if (query.length >= 2) {
|
|
57
|
+
if (!isSearchLoading && searchResults.length > 0) {
|
|
58
|
+
const recNames = new Set(recommended.map((s) => s.name));
|
|
59
|
+
const deduped = searchResults
|
|
60
|
+
.filter((s) => !recNames.has(s.name))
|
|
61
|
+
.sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
|
|
62
|
+
if (deduped.length > 0) {
|
|
63
|
+
items.push({
|
|
64
|
+
id: "cat:search",
|
|
65
|
+
kind: "category",
|
|
66
|
+
label: `Search (${deduped.length})`,
|
|
67
|
+
title: "Search",
|
|
68
|
+
categoryKey: "popular",
|
|
69
|
+
count: deduped.length,
|
|
70
|
+
tone: "teal",
|
|
71
|
+
});
|
|
72
|
+
for (const skill of deduped) {
|
|
73
|
+
items.push({
|
|
74
|
+
id: `skill:${skill.id}`,
|
|
75
|
+
kind: "skill",
|
|
76
|
+
label: skill.name,
|
|
77
|
+
skill,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return items;
|
|
83
|
+
}
|
|
84
|
+
// ── POPULAR (default, no search query) ──
|
|
85
|
+
if (popular.length > 0) {
|
|
86
|
+
items.push({
|
|
87
|
+
id: "cat:popular",
|
|
88
|
+
kind: "category",
|
|
89
|
+
label: "Popular",
|
|
90
|
+
title: "Popular",
|
|
91
|
+
categoryKey: "popular",
|
|
92
|
+
count: popular.length,
|
|
93
|
+
tone: "teal",
|
|
94
|
+
});
|
|
95
|
+
for (const skill of popular) {
|
|
96
|
+
items.push({
|
|
97
|
+
id: `skill:${skill.id}`,
|
|
98
|
+
kind: "skill",
|
|
99
|
+
label: skill.name,
|
|
100
|
+
skill,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return items;
|
|
105
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { SkillInfo } from "../../types/index.js";
|
|
2
|
+
|
|
3
|
+
// ─── Item types ───────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface SkillCategoryItem {
|
|
6
|
+
id: string;
|
|
7
|
+
kind: "category";
|
|
8
|
+
label: string;
|
|
9
|
+
title: string;
|
|
10
|
+
categoryKey: string;
|
|
11
|
+
count?: number;
|
|
12
|
+
tone: "purple" | "green" | "teal" | "yellow" | "gray" | "red";
|
|
13
|
+
badge?: string;
|
|
14
|
+
star?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SkillSkillItem {
|
|
18
|
+
id: string;
|
|
19
|
+
kind: "skill";
|
|
20
|
+
label: string;
|
|
21
|
+
skill: SkillInfo;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type SkillBrowserItem = SkillCategoryItem | SkillSkillItem;
|
|
25
|
+
|
|
26
|
+
// ─── Adapter ─────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export interface BuildSkillBrowserItemsArgs {
|
|
29
|
+
recommended: SkillInfo[];
|
|
30
|
+
popular: SkillInfo[];
|
|
31
|
+
installed: SkillInfo[];
|
|
32
|
+
searchResults: SkillInfo[];
|
|
33
|
+
query: string;
|
|
34
|
+
isSearchLoading: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Builds the flat list of items for the SkillsScreen list panel.
|
|
39
|
+
* Extracted from SkillsScreen so it can be tested independently.
|
|
40
|
+
*/
|
|
41
|
+
export function buildSkillBrowserItems({
|
|
42
|
+
recommended,
|
|
43
|
+
popular,
|
|
44
|
+
installed,
|
|
45
|
+
searchResults,
|
|
46
|
+
query,
|
|
47
|
+
isSearchLoading,
|
|
48
|
+
}: BuildSkillBrowserItemsArgs): SkillBrowserItem[] {
|
|
49
|
+
const lowerQuery = query.toLowerCase();
|
|
50
|
+
const items: SkillBrowserItem[] = [];
|
|
51
|
+
|
|
52
|
+
// ── INSTALLED: always shown at top (if any) ──
|
|
53
|
+
const installedFiltered = lowerQuery
|
|
54
|
+
? installed.filter((s) => s.name.toLowerCase().includes(lowerQuery))
|
|
55
|
+
: installed;
|
|
56
|
+
|
|
57
|
+
if (installedFiltered.length > 0) {
|
|
58
|
+
items.push({
|
|
59
|
+
id: "cat:installed",
|
|
60
|
+
kind: "category",
|
|
61
|
+
label: `Installed (${installedFiltered.length})`,
|
|
62
|
+
title: "Installed",
|
|
63
|
+
categoryKey: "installed",
|
|
64
|
+
count: installedFiltered.length,
|
|
65
|
+
tone: "purple",
|
|
66
|
+
star: "● ",
|
|
67
|
+
});
|
|
68
|
+
for (const skill of installedFiltered) {
|
|
69
|
+
items.push({
|
|
70
|
+
id: `skill:${skill.id}`,
|
|
71
|
+
kind: "skill",
|
|
72
|
+
label: skill.name,
|
|
73
|
+
skill,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── RECOMMENDED: always shown, filtered when searching ──
|
|
79
|
+
const filteredRec = lowerQuery
|
|
80
|
+
? recommended.filter(
|
|
81
|
+
(s) =>
|
|
82
|
+
s.name.toLowerCase().includes(lowerQuery) ||
|
|
83
|
+
(s.description || "").toLowerCase().includes(lowerQuery),
|
|
84
|
+
)
|
|
85
|
+
: recommended;
|
|
86
|
+
|
|
87
|
+
items.push({
|
|
88
|
+
id: "cat:recommended",
|
|
89
|
+
kind: "category",
|
|
90
|
+
label: "Recommended",
|
|
91
|
+
title: "Recommended",
|
|
92
|
+
categoryKey: "recommended",
|
|
93
|
+
count: filteredRec.length,
|
|
94
|
+
tone: "green",
|
|
95
|
+
star: "★ ",
|
|
96
|
+
});
|
|
97
|
+
for (const skill of filteredRec) {
|
|
98
|
+
items.push({
|
|
99
|
+
id: `skill:${skill.id}`,
|
|
100
|
+
kind: "skill",
|
|
101
|
+
label: skill.name,
|
|
102
|
+
skill,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── SEARCH MODE ──
|
|
107
|
+
if (query.length >= 2) {
|
|
108
|
+
if (!isSearchLoading && searchResults.length > 0) {
|
|
109
|
+
const recNames = new Set(recommended.map((s) => s.name));
|
|
110
|
+
const deduped = searchResults
|
|
111
|
+
.filter((s) => !recNames.has(s.name))
|
|
112
|
+
.sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
|
|
113
|
+
|
|
114
|
+
if (deduped.length > 0) {
|
|
115
|
+
items.push({
|
|
116
|
+
id: "cat:search",
|
|
117
|
+
kind: "category",
|
|
118
|
+
label: `Search (${deduped.length})`,
|
|
119
|
+
title: "Search",
|
|
120
|
+
categoryKey: "popular",
|
|
121
|
+
count: deduped.length,
|
|
122
|
+
tone: "teal",
|
|
123
|
+
});
|
|
124
|
+
for (const skill of deduped) {
|
|
125
|
+
items.push({
|
|
126
|
+
id: `skill:${skill.id}`,
|
|
127
|
+
kind: "skill",
|
|
128
|
+
label: skill.name,
|
|
129
|
+
skill,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return items;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── POPULAR (default, no search query) ──
|
|
138
|
+
if (popular.length > 0) {
|
|
139
|
+
items.push({
|
|
140
|
+
id: "cat:popular",
|
|
141
|
+
kind: "category",
|
|
142
|
+
label: "Popular",
|
|
143
|
+
title: "Popular",
|
|
144
|
+
categoryKey: "popular",
|
|
145
|
+
count: popular.length,
|
|
146
|
+
tone: "teal",
|
|
147
|
+
});
|
|
148
|
+
for (const skill of popular) {
|
|
149
|
+
items.push({
|
|
150
|
+
id: `skill:${skill.id}`,
|
|
151
|
+
kind: "skill",
|
|
152
|
+
label: skill.name,
|
|
153
|
+
skill,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return items;
|
|
159
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
/**
|
|
4
|
+
* Keyboard shortcut badges for detail panels.
|
|
5
|
+
* Renders: [bg] key [/bg] label
|
|
6
|
+
*/
|
|
7
|
+
export function ActionHints({ hints }) {
|
|
8
|
+
return (_jsx("box", { flexDirection: "column", marginTop: 1, children: hints.map((hint) => (_jsxs("box", { children: [_jsxs("text", { bg: hint.tone === "danger"
|
|
9
|
+
? theme.hints.dangerBg
|
|
10
|
+
: hint.tone === "primary"
|
|
11
|
+
? theme.hints.primaryBg
|
|
12
|
+
: theme.hints.defaultBg, fg: "black", children: [" ", hint.key, " "] }), _jsxs("text", { fg: "gray", children: [" ", hint.label] })] }, `${hint.key}:${hint.label}`))) }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
|
|
4
|
+
export interface Hint {
|
|
5
|
+
key: string;
|
|
6
|
+
label: string;
|
|
7
|
+
tone?: "default" | "primary" | "danger";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ActionHintsProps {
|
|
11
|
+
hints: Hint[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Keyboard shortcut badges for detail panels.
|
|
16
|
+
* Renders: [bg] key [/bg] label
|
|
17
|
+
*/
|
|
18
|
+
export function ActionHints({ hints }: ActionHintsProps) {
|
|
19
|
+
return (
|
|
20
|
+
<box flexDirection="column" marginTop={1}>
|
|
21
|
+
{hints.map((hint) => (
|
|
22
|
+
<box key={`${hint.key}:${hint.label}`}>
|
|
23
|
+
<text
|
|
24
|
+
bg={
|
|
25
|
+
hint.tone === "danger"
|
|
26
|
+
? theme.hints.dangerBg
|
|
27
|
+
: hint.tone === "primary"
|
|
28
|
+
? theme.hints.primaryBg
|
|
29
|
+
: theme.hints.defaultBg
|
|
30
|
+
}
|
|
31
|
+
fg="black"
|
|
32
|
+
>
|
|
33
|
+
{" "}
|
|
34
|
+
{hint.key}{" "}
|
|
35
|
+
</text>
|
|
36
|
+
<text fg="gray"> {hint.label}</text>
|
|
37
|
+
</box>
|
|
38
|
+
))}
|
|
39
|
+
</box>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A labeled section in a detail panel with optional title header.
|
|
4
|
+
*/
|
|
5
|
+
export function DetailSection({ title, children }) {
|
|
6
|
+
return (_jsxs("box", { flexDirection: "column", marginTop: 1, children: [title ? (_jsx("text", { fg: "cyan", children: _jsx("strong", { children: title }) })) : null, children] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface DetailSectionProps {
|
|
4
|
+
title?: string;
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A labeled section in a detail panel with optional title header.
|
|
10
|
+
*/
|
|
11
|
+
export function DetailSection({ title, children }: DetailSectionProps) {
|
|
12
|
+
return (
|
|
13
|
+
<box flexDirection="column" marginTop={1}>
|
|
14
|
+
{title ? (
|
|
15
|
+
<text fg="cyan">
|
|
16
|
+
<strong>{title}</strong>
|
|
17
|
+
</text>
|
|
18
|
+
) : null}
|
|
19
|
+
{children}
|
|
20
|
+
</box>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Aligned label-value pair for detail panels.
|
|
4
|
+
* Label is padded to 10 chars for consistent alignment.
|
|
5
|
+
*/
|
|
6
|
+
export function KeyValueLine({ label, value }) {
|
|
7
|
+
return (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: label.padEnd(10) }), value] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface KeyValueLineProps {
|
|
4
|
+
label: string;
|
|
5
|
+
value: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Aligned label-value pair for detail panels.
|
|
10
|
+
* Label is padded to 10 chars for consistent alignment.
|
|
11
|
+
*/
|
|
12
|
+
export function KeyValueLine({ label, value }: KeyValueLineProps) {
|
|
13
|
+
return (
|
|
14
|
+
<text>
|
|
15
|
+
<span fg="gray">{label.padEnd(10)}</span>
|
|
16
|
+
{value}
|
|
17
|
+
</text>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
import { SelectableRow } from "./SelectableRow.js";
|
|
4
|
+
export function ListCategoryRow({ title, expanded = false, count, badge, tone = "gray", selected, }) {
|
|
5
|
+
const colors = theme.category[tone];
|
|
6
|
+
const label = `${expanded ? "▼" : "▶"} ${title}${count !== undefined ? ` (${count})` : ""}`;
|
|
7
|
+
return (_jsxs(SelectableRow, { selected: selected, children: [!selected && (_jsx("span", { bg: colors.bg, fg: colors.fg, children: _jsxs("strong", { children: [" ", label, " "] }) })), selected && _jsxs("strong", { children: [" ", label, " "] }), !selected && badge ? (_jsxs("span", { fg: colors.badgeFg, children: [" ", badge] })) : null] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
import { SelectableRow } from "./SelectableRow.js";
|
|
4
|
+
|
|
5
|
+
interface ListCategoryRowProps {
|
|
6
|
+
title: string;
|
|
7
|
+
expanded?: boolean;
|
|
8
|
+
count?: number;
|
|
9
|
+
badge?: string;
|
|
10
|
+
tone?: keyof typeof theme.category;
|
|
11
|
+
selected: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ListCategoryRow({
|
|
15
|
+
title,
|
|
16
|
+
expanded = false,
|
|
17
|
+
count,
|
|
18
|
+
badge,
|
|
19
|
+
tone = "gray",
|
|
20
|
+
selected,
|
|
21
|
+
}: ListCategoryRowProps) {
|
|
22
|
+
const colors = theme.category[tone];
|
|
23
|
+
const label = `${expanded ? "▼" : "▶"} ${title}${count !== undefined ? ` (${count})` : ""}`;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<SelectableRow selected={selected}>
|
|
27
|
+
{!selected && (
|
|
28
|
+
<span bg={colors.bg} fg={colors.fg}>
|
|
29
|
+
<strong> {label} </strong>
|
|
30
|
+
</span>
|
|
31
|
+
)}
|
|
32
|
+
{selected && <strong> {label} </strong>}
|
|
33
|
+
{!selected && badge ? (
|
|
34
|
+
<span fg={colors.badgeFg}> {badge}</span>
|
|
35
|
+
) : null}
|
|
36
|
+
</SelectableRow>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
/**
|
|
4
|
+
* Subdued text for versions, stars, status indicators.
|
|
5
|
+
*/
|
|
6
|
+
export function MetaText({ text, tone = "muted" }) {
|
|
7
|
+
return _jsx("span", { fg: theme.meta[tone], children: text });
|
|
8
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
|
|
4
|
+
interface MetaTextProps {
|
|
5
|
+
text: string;
|
|
6
|
+
tone?: "muted" | "warning" | "success" | "danger";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Subdued text for versions, stars, status indicators.
|
|
11
|
+
*/
|
|
12
|
+
export function MetaText({ text, tone = "muted" }: MetaTextProps) {
|
|
13
|
+
return <span fg={theme.meta[tone]}>{text}</span>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
/**
|
|
4
|
+
* The u/p/l badges used in detail panels.
|
|
5
|
+
* Renders each scope as: [bg=color] key [/bg] ● or ○ Label path
|
|
6
|
+
*/
|
|
7
|
+
export function ScopeDetail({ scopes, paths }) {
|
|
8
|
+
const items = [
|
|
9
|
+
{
|
|
10
|
+
key: "u",
|
|
11
|
+
label: "User",
|
|
12
|
+
color: theme.scopes.user,
|
|
13
|
+
active: scopes.user,
|
|
14
|
+
path: paths?.user,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
key: "p",
|
|
18
|
+
label: "Project",
|
|
19
|
+
color: theme.scopes.project,
|
|
20
|
+
active: scopes.project,
|
|
21
|
+
path: paths?.project,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
key: "l",
|
|
25
|
+
label: "Local",
|
|
26
|
+
color: theme.scopes.local,
|
|
27
|
+
active: scopes.local,
|
|
28
|
+
path: paths?.local,
|
|
29
|
+
},
|
|
30
|
+
].filter((i) => i.path !== undefined || i.active !== undefined);
|
|
31
|
+
return (_jsx("box", { flexDirection: "column", children: items.map((item) => (_jsxs("text", { children: [_jsxs("span", { bg: item.color, fg: "black", children: [" ", item.key, " "] }), _jsx("span", { fg: item.active ? item.color : "gray", children: item.active ? " ● " : " ○ " }), _jsx("span", { fg: item.color, children: item.label }), item.path && _jsxs("span", { fg: "gray", children: [" ", item.path] })] }, item.key))) }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
|
|
4
|
+
interface ScopeDetailScope {
|
|
5
|
+
user?: boolean;
|
|
6
|
+
project?: boolean;
|
|
7
|
+
local?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ScopeDetailPaths {
|
|
11
|
+
user?: string;
|
|
12
|
+
project?: string;
|
|
13
|
+
local?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ScopeDetailProps {
|
|
17
|
+
scopes: ScopeDetailScope;
|
|
18
|
+
paths?: ScopeDetailPaths;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The u/p/l badges used in detail panels.
|
|
23
|
+
* Renders each scope as: [bg=color] key [/bg] ● or ○ Label path
|
|
24
|
+
*/
|
|
25
|
+
export function ScopeDetail({ scopes, paths }: ScopeDetailProps) {
|
|
26
|
+
const items = [
|
|
27
|
+
{
|
|
28
|
+
key: "u",
|
|
29
|
+
label: "User",
|
|
30
|
+
color: theme.scopes.user,
|
|
31
|
+
active: scopes.user,
|
|
32
|
+
path: paths?.user,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: "p",
|
|
36
|
+
label: "Project",
|
|
37
|
+
color: theme.scopes.project,
|
|
38
|
+
active: scopes.project,
|
|
39
|
+
path: paths?.project,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: "l",
|
|
43
|
+
label: "Local",
|
|
44
|
+
color: theme.scopes.local,
|
|
45
|
+
active: scopes.local,
|
|
46
|
+
path: paths?.local,
|
|
47
|
+
},
|
|
48
|
+
].filter((i) => i.path !== undefined || i.active !== undefined);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<box flexDirection="column">
|
|
52
|
+
{items.map((item) => (
|
|
53
|
+
<text key={item.key}>
|
|
54
|
+
<span bg={item.color} fg="black">
|
|
55
|
+
{" "}
|
|
56
|
+
{item.key}{" "}
|
|
57
|
+
</span>
|
|
58
|
+
<span fg={item.active ? item.color : "gray"}>
|
|
59
|
+
{item.active ? " ● " : " ○ "}
|
|
60
|
+
</span>
|
|
61
|
+
<span fg={item.color}>{item.label}</span>
|
|
62
|
+
{item.path && <span fg="gray"> {item.path}</span>}
|
|
63
|
+
</text>
|
|
64
|
+
))}
|
|
65
|
+
</box>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
/**
|
|
4
|
+
* Colored filled/empty squares for scope display in list items.
|
|
5
|
+
* ■ = installed in that scope, □ = not installed.
|
|
6
|
+
*/
|
|
7
|
+
export function ScopeSquares({ user, project, local, selected = false, }) {
|
|
8
|
+
const inactive = selected ? "white" : theme.colors.dim;
|
|
9
|
+
// Returns fragments of <span> — safe to embed inside a <text> parent
|
|
10
|
+
return (_jsxs(_Fragment, { children: [_jsx("span", { fg: user ? theme.scopes.user : inactive, children: "\u25A0" }), _jsx("span", { fg: project ? theme.scopes.project : inactive, children: "\u25A0" }), local !== undefined && (_jsx("span", { fg: local ? theme.scopes.local : inactive, children: "\u25A0" }))] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
|
|
4
|
+
interface ScopeSquaresProps {
|
|
5
|
+
user: boolean;
|
|
6
|
+
project: boolean;
|
|
7
|
+
local?: boolean;
|
|
8
|
+
selected?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Colored filled/empty squares for scope display in list items.
|
|
13
|
+
* ■ = installed in that scope, □ = not installed.
|
|
14
|
+
*/
|
|
15
|
+
export function ScopeSquares({
|
|
16
|
+
user,
|
|
17
|
+
project,
|
|
18
|
+
local,
|
|
19
|
+
selected = false,
|
|
20
|
+
}: ScopeSquaresProps) {
|
|
21
|
+
const inactive = selected ? "white" : theme.colors.dim;
|
|
22
|
+
|
|
23
|
+
// Returns fragments of <span> — safe to embed inside a <text> parent
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<span fg={user ? theme.scopes.user : inactive}>■</span>
|
|
27
|
+
<span fg={project ? theme.scopes.project : inactive}>■</span>
|
|
28
|
+
{local !== undefined && (
|
|
29
|
+
<span fg={local ? theme.scopes.local : inactive}>■</span>
|
|
30
|
+
)}
|
|
31
|
+
</>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
export function SelectableRow({ selected, indent = 0, children, }) {
|
|
4
|
+
return (_jsxs("text", { bg: selected ? theme.selection.bg : undefined, fg: selected ? theme.selection.fg : undefined, children: [" ".repeat(indent), children] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
|
|
4
|
+
interface SelectableRowProps {
|
|
5
|
+
selected: boolean;
|
|
6
|
+
indent?: number;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function SelectableRow({
|
|
11
|
+
selected,
|
|
12
|
+
indent = 0,
|
|
13
|
+
children,
|
|
14
|
+
}: SelectableRowProps) {
|
|
15
|
+
return (
|
|
16
|
+
<text
|
|
17
|
+
bg={selected ? theme.selection.bg : undefined}
|
|
18
|
+
fg={selected ? theme.selection.fg : undefined}
|
|
19
|
+
>
|
|
20
|
+
{" ".repeat(indent)}
|
|
21
|
+
{children}
|
|
22
|
+
</text>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { SelectableRow } from "./SelectableRow.js";
|
|
2
|
+
export { ListCategoryRow } from "./ListCategoryRow.js";
|
|
3
|
+
export { ScopeSquares } from "./ScopeSquares.js";
|
|
4
|
+
export { ScopeDetail } from "./ScopeDetail.js";
|
|
5
|
+
export { ActionHints } from "./ActionHints.js";
|
|
6
|
+
export { MetaText } from "./MetaText.js";
|
|
7
|
+
export { KeyValueLine } from "./KeyValueLine.js";
|
|
8
|
+
export { DetailSection } from "./DetailSection.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { SelectableRow } from "./SelectableRow.js";
|
|
2
|
+
export { ListCategoryRow } from "./ListCategoryRow.js";
|
|
3
|
+
export { ScopeSquares } from "./ScopeSquares.js";
|
|
4
|
+
export { ScopeDetail } from "./ScopeDetail.js";
|
|
5
|
+
export { ActionHints } from "./ActionHints.js";
|
|
6
|
+
export type { Hint } from "./ActionHints.js";
|
|
7
|
+
export { MetaText } from "./MetaText.js";
|
|
8
|
+
export { KeyValueLine } from "./KeyValueLine.js";
|
|
9
|
+
export { DetailSection } from "./DetailSection.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|