@akiojin/gwt 3.0.0 → 3.1.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/README.ja.md +1 -1
- package/README.md +1 -1
- package/dist/cli/ui/components/App.d.ts.map +1 -1
- package/dist/cli/ui/components/App.js +19 -4
- package/dist/cli/ui/components/App.js.map +1 -1
- package/dist/cli/ui/components/parts/Header.d.ts +8 -0
- package/dist/cli/ui/components/parts/Header.d.ts.map +1 -1
- package/dist/cli/ui/components/parts/Header.js +7 -2
- package/dist/cli/ui/components/parts/Header.js.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts +3 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.js +16 -8
- package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/EnvironmentProfileScreen.d.ts +16 -0
- package/dist/cli/ui/components/screens/EnvironmentProfileScreen.d.ts.map +1 -0
- package/dist/cli/ui/components/screens/EnvironmentProfileScreen.js +576 -0
- package/dist/cli/ui/components/screens/EnvironmentProfileScreen.js.map +1 -0
- package/dist/cli/ui/hooks/useProfiles.d.ts +41 -0
- package/dist/cli/ui/hooks/useProfiles.d.ts.map +1 -0
- package/dist/cli/ui/hooks/useProfiles.js +136 -0
- package/dist/cli/ui/hooks/useProfiles.js.map +1 -0
- package/dist/cli/ui/types.d.ts +1 -1
- package/dist/cli/ui/types.d.ts.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.js +2 -2
- package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
- package/dist/config/profiles.d.ts +94 -0
- package/dist/config/profiles.d.ts.map +1 -0
- package/dist/config/profiles.js +287 -0
- package/dist/config/profiles.js.map +1 -0
- package/dist/config/tools.d.ts +10 -0
- package/dist/config/tools.d.ts.map +1 -1
- package/dist/config/tools.js +19 -2
- package/dist/config/tools.js.map +1 -1
- package/dist/types/profiles.d.ts +54 -0
- package/dist/types/profiles.d.ts.map +1 -0
- package/dist/types/profiles.js +33 -0
- package/dist/types/profiles.js.map +1 -0
- package/dist/web/client/src/components/ui/alert.d.ts +1 -1
- package/dist/web/client/src/components/ui/badge.d.ts +1 -1
- package/package.json +4 -3
- package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +40 -1
- package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +1 -1
- package/src/cli/ui/components/App.tsx +49 -36
- package/src/cli/ui/components/parts/Header.tsx +16 -1
- package/src/cli/ui/components/screens/BranchListScreen.tsx +17 -7
- package/src/cli/ui/components/screens/EnvironmentProfileScreen.tsx +924 -0
- package/src/cli/ui/hooks/useProfiles.ts +211 -0
- package/src/cli/ui/types.ts +2 -1
- package/src/cli/ui/utils/branchFormatter.ts +2 -3
- package/src/config/profiles.ts +362 -0
- package/src/config/tools.ts +20 -2
- package/src/types/profiles.ts +64 -0
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 環境変数プロファイルエディター画面
|
|
3
|
+
*
|
|
4
|
+
* プロファイルの選択・作成・削除・環境変数の編集を行います。
|
|
5
|
+
* @see specs/SPEC-dafff079/spec.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useState, useCallback, useMemo, useEffect } from "react";
|
|
9
|
+
import { Box, Text, useInput } from "ink";
|
|
10
|
+
import { Header } from "../parts/Header.js";
|
|
11
|
+
import { Footer } from "../parts/Footer.js";
|
|
12
|
+
import { Select } from "../common/Select.js";
|
|
13
|
+
import { Input } from "../common/Input.js";
|
|
14
|
+
import { Confirm } from "../common/Confirm.js";
|
|
15
|
+
import { useTerminalSize } from "../../hooks/useTerminalSize.js";
|
|
16
|
+
import { useProfiles } from "../../hooks/useProfiles.js";
|
|
17
|
+
import { isValidProfileName } from "../../../../types/profiles.js";
|
|
18
|
+
|
|
19
|
+
export interface EnvironmentProfileScreenProps {
|
|
20
|
+
onBack: () => void;
|
|
21
|
+
version?: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type ScreenMode =
|
|
25
|
+
| "list" // プロファイル一覧
|
|
26
|
+
| "view" // プロファイル詳細
|
|
27
|
+
| "create-name" // 新規作成:名前入力
|
|
28
|
+
| "create-display" // 新規作成:表示名入力
|
|
29
|
+
| "add-env-key" // 環境変数追加:キー入力
|
|
30
|
+
| "add-env-value" // 環境変数追加:値入力
|
|
31
|
+
| "edit-env-value" // 環境変数編集:値入力
|
|
32
|
+
| "confirm-delete-profile" // プロファイル削除確認
|
|
33
|
+
| "confirm-delete-env"; // 環境変数削除確認
|
|
34
|
+
|
|
35
|
+
interface ProfileItem {
|
|
36
|
+
label: string;
|
|
37
|
+
value: string;
|
|
38
|
+
profileName: string | null;
|
|
39
|
+
isActive: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface EnvVarItem {
|
|
43
|
+
label: string;
|
|
44
|
+
value: string;
|
|
45
|
+
key: string;
|
|
46
|
+
envValue: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const UI_CHROME_HEIGHT = 20; // ヘッダー/フッター/余白などの固定行数
|
|
50
|
+
const ENV_VAR_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
51
|
+
const NO_PROFILE_VALUE = "__gwt_no_profile__";
|
|
52
|
+
|
|
53
|
+
type FocusTarget = "profiles" | "env" | "osenv";
|
|
54
|
+
|
|
55
|
+
interface SelectionState {
|
|
56
|
+
profileIndex: number;
|
|
57
|
+
envIndex: number;
|
|
58
|
+
osEnvIndex: number;
|
|
59
|
+
focus: FocusTarget;
|
|
60
|
+
selectedProfileName: string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface ProfileCreationState {
|
|
64
|
+
name: string;
|
|
65
|
+
displayName: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface EnvEditState {
|
|
69
|
+
key: string;
|
|
70
|
+
value: string;
|
|
71
|
+
selectedKey: string | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type NumberUpdater = number | ((prev: number) => number);
|
|
75
|
+
type FocusUpdater = FocusTarget | ((prev: FocusTarget) => FocusTarget);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 環境変数プロファイルエディター画面
|
|
79
|
+
*/
|
|
80
|
+
export function EnvironmentProfileScreen({
|
|
81
|
+
onBack,
|
|
82
|
+
version,
|
|
83
|
+
}: EnvironmentProfileScreenProps) {
|
|
84
|
+
const { rows } = useTerminalSize();
|
|
85
|
+
const {
|
|
86
|
+
profiles,
|
|
87
|
+
loading,
|
|
88
|
+
error,
|
|
89
|
+
activeProfileName,
|
|
90
|
+
setActiveProfile,
|
|
91
|
+
createProfile,
|
|
92
|
+
deleteProfile,
|
|
93
|
+
updateEnvVar,
|
|
94
|
+
deleteEnvVar,
|
|
95
|
+
} = useProfiles();
|
|
96
|
+
|
|
97
|
+
// 画面モード
|
|
98
|
+
const [mode, setMode] = useState<ScreenMode>("list");
|
|
99
|
+
|
|
100
|
+
const [selection, setSelection] = useState<SelectionState>({
|
|
101
|
+
profileIndex: 0,
|
|
102
|
+
envIndex: 0,
|
|
103
|
+
osEnvIndex: 0,
|
|
104
|
+
focus: "profiles",
|
|
105
|
+
selectedProfileName: null,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const [profileCreation, setProfileCreation] = useState<ProfileCreationState>({
|
|
109
|
+
name: "",
|
|
110
|
+
displayName: "",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const [envEdit, setEnvEdit] = useState<EnvEditState>({
|
|
114
|
+
key: "",
|
|
115
|
+
value: "",
|
|
116
|
+
selectedKey: null,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// バリデーションエラー
|
|
120
|
+
const [validationError, setValidationError] = useState<string | null>(null);
|
|
121
|
+
|
|
122
|
+
const profileIndex = selection.profileIndex;
|
|
123
|
+
const envIndex = selection.envIndex;
|
|
124
|
+
const osEnvIndex = selection.osEnvIndex;
|
|
125
|
+
const focus = selection.focus;
|
|
126
|
+
const selectedProfileName = selection.selectedProfileName;
|
|
127
|
+
|
|
128
|
+
const newProfileName = profileCreation.name;
|
|
129
|
+
const newProfileDisplayName = profileCreation.displayName;
|
|
130
|
+
|
|
131
|
+
const newEnvKey = envEdit.key;
|
|
132
|
+
const newEnvValue = envEdit.value;
|
|
133
|
+
const selectedEnvKey = envEdit.selectedKey;
|
|
134
|
+
|
|
135
|
+
const setProfileIndex = useCallback((index: number) => {
|
|
136
|
+
setSelection((prev) => ({ ...prev, profileIndex: index }));
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
const setEnvIndex = useCallback((updater: NumberUpdater) => {
|
|
140
|
+
setSelection((prev) => ({
|
|
141
|
+
...prev,
|
|
142
|
+
envIndex:
|
|
143
|
+
typeof updater === "function" ? updater(prev.envIndex) : updater,
|
|
144
|
+
}));
|
|
145
|
+
}, []);
|
|
146
|
+
|
|
147
|
+
const setOsEnvIndex = useCallback((updater: NumberUpdater) => {
|
|
148
|
+
setSelection((prev) => ({
|
|
149
|
+
...prev,
|
|
150
|
+
osEnvIndex:
|
|
151
|
+
typeof updater === "function" ? updater(prev.osEnvIndex) : updater,
|
|
152
|
+
}));
|
|
153
|
+
}, []);
|
|
154
|
+
|
|
155
|
+
const setFocus = useCallback((updater: FocusUpdater) => {
|
|
156
|
+
setSelection((prev) => ({
|
|
157
|
+
...prev,
|
|
158
|
+
focus: typeof updater === "function" ? updater(prev.focus) : updater,
|
|
159
|
+
}));
|
|
160
|
+
}, []);
|
|
161
|
+
|
|
162
|
+
const setSelectedProfileName = useCallback((name: string | null) => {
|
|
163
|
+
setSelection((prev) => ({ ...prev, selectedProfileName: name }));
|
|
164
|
+
}, []);
|
|
165
|
+
|
|
166
|
+
const setNewProfileName = useCallback((value: string) => {
|
|
167
|
+
setProfileCreation((prev) => ({ ...prev, name: value }));
|
|
168
|
+
}, []);
|
|
169
|
+
|
|
170
|
+
const setNewProfileDisplayName = useCallback((value: string) => {
|
|
171
|
+
setProfileCreation((prev) => ({ ...prev, displayName: value }));
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
174
|
+
const setNewEnvKey = useCallback((value: string) => {
|
|
175
|
+
setEnvEdit((prev) => ({ ...prev, key: value }));
|
|
176
|
+
}, []);
|
|
177
|
+
|
|
178
|
+
const setNewEnvValue = useCallback((value: string) => {
|
|
179
|
+
setEnvEdit((prev) => ({ ...prev, value }));
|
|
180
|
+
}, []);
|
|
181
|
+
|
|
182
|
+
const setSelectedEnvKey = useCallback((key: string | null) => {
|
|
183
|
+
setEnvEdit((prev) => ({ ...prev, selectedKey: key }));
|
|
184
|
+
}, []);
|
|
185
|
+
|
|
186
|
+
// プロファイル一覧アイテム
|
|
187
|
+
const profileItems: ProfileItem[] = useMemo(() => {
|
|
188
|
+
if (!profiles) return [];
|
|
189
|
+
const items = Object.entries(profiles.profiles).map(([name, profile]) => ({
|
|
190
|
+
label: `${profile.displayName}${name === activeProfileName ? " (active)" : ""}`,
|
|
191
|
+
value: name,
|
|
192
|
+
profileName: name,
|
|
193
|
+
isActive: name === activeProfileName,
|
|
194
|
+
}));
|
|
195
|
+
return [
|
|
196
|
+
{
|
|
197
|
+
label: `(none)${activeProfileName === null ? " (active)" : ""}`,
|
|
198
|
+
value: NO_PROFILE_VALUE,
|
|
199
|
+
profileName: null,
|
|
200
|
+
isActive: activeProfileName === null,
|
|
201
|
+
},
|
|
202
|
+
...items,
|
|
203
|
+
];
|
|
204
|
+
}, [profiles, activeProfileName]);
|
|
205
|
+
|
|
206
|
+
// 現在選択中のプロファイル
|
|
207
|
+
const currentProfile =
|
|
208
|
+
selectedProfileName && profiles
|
|
209
|
+
? profiles.profiles[selectedProfileName]
|
|
210
|
+
: null;
|
|
211
|
+
|
|
212
|
+
// 環境変数一覧アイテム
|
|
213
|
+
const envItems: EnvVarItem[] = useMemo(() => {
|
|
214
|
+
if (!currentProfile) return [];
|
|
215
|
+
return Object.entries(currentProfile.env).map(([key, value]) => ({
|
|
216
|
+
label: `${key}=${value}`,
|
|
217
|
+
value: key,
|
|
218
|
+
key,
|
|
219
|
+
envValue: value,
|
|
220
|
+
}));
|
|
221
|
+
}, [currentProfile]);
|
|
222
|
+
|
|
223
|
+
// OS環境変数(プロファイルで上書きされるものをハイライト)
|
|
224
|
+
const osEnvItems = useMemo(() => {
|
|
225
|
+
const profileEnvKeys = new Set(
|
|
226
|
+
currentProfile ? Object.keys(currentProfile.env) : [],
|
|
227
|
+
);
|
|
228
|
+
return Object.entries(process.env)
|
|
229
|
+
.filter(([, value]) => value !== undefined)
|
|
230
|
+
.map(([key, value]) => ({
|
|
231
|
+
key,
|
|
232
|
+
value: value ?? "",
|
|
233
|
+
isOverwritten: profileEnvKeys.has(key),
|
|
234
|
+
}))
|
|
235
|
+
.sort((a, b) => a.key.localeCompare(b.key));
|
|
236
|
+
}, [currentProfile]);
|
|
237
|
+
|
|
238
|
+
// 配列サイズ変更に追従してインデックスをクランプ(削除後の範囲外アクセス防止)
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
if (profileItems.length === 0) {
|
|
241
|
+
if (profileIndex !== 0) {
|
|
242
|
+
setProfileIndex(0);
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (profileIndex >= profileItems.length) {
|
|
247
|
+
setProfileIndex(profileItems.length - 1);
|
|
248
|
+
}
|
|
249
|
+
}, [profileItems.length, profileIndex, setProfileIndex]);
|
|
250
|
+
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
if (envItems.length === 0) {
|
|
253
|
+
if (envIndex !== 0) {
|
|
254
|
+
setEnvIndex(0);
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (envIndex >= envItems.length) {
|
|
259
|
+
setEnvIndex(envItems.length - 1);
|
|
260
|
+
}
|
|
261
|
+
}, [envItems.length, envIndex, setEnvIndex]);
|
|
262
|
+
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
if (osEnvItems.length === 0) {
|
|
265
|
+
if (osEnvIndex !== 0) {
|
|
266
|
+
setOsEnvIndex(0);
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (osEnvIndex >= osEnvItems.length) {
|
|
271
|
+
setOsEnvIndex(osEnvItems.length - 1);
|
|
272
|
+
}
|
|
273
|
+
}, [osEnvItems.length, osEnvIndex, setOsEnvIndex]);
|
|
274
|
+
|
|
275
|
+
// プロファイルを選択してアクティブ化
|
|
276
|
+
const handleActivateProfile = useCallback(
|
|
277
|
+
async (item: ProfileItem) => {
|
|
278
|
+
try {
|
|
279
|
+
await setActiveProfile(item.profileName);
|
|
280
|
+
if (item.profileName === null) {
|
|
281
|
+
setSelectedProfileName(null);
|
|
282
|
+
setFocus("profiles");
|
|
283
|
+
setProfileIndex(0);
|
|
284
|
+
setMode("list");
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
setSelectedProfileName(item.profileName);
|
|
289
|
+
setFocus("env"); // viewモードでは環境変数にフォーカス
|
|
290
|
+
setMode("view");
|
|
291
|
+
} catch {
|
|
292
|
+
// エラー状態は useProfiles フック側で管理するため、ここでは握りつぶす
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
[setActiveProfile, setSelectedProfileName, setFocus, setProfileIndex],
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// 新規プロファイル作成開始
|
|
299
|
+
const handleStartCreateProfile = useCallback(() => {
|
|
300
|
+
setNewProfileName("");
|
|
301
|
+
setNewProfileDisplayName("");
|
|
302
|
+
setValidationError(null);
|
|
303
|
+
setMode("create-name");
|
|
304
|
+
}, []);
|
|
305
|
+
|
|
306
|
+
// 新規プロファイル名入力完了
|
|
307
|
+
const handleCreateNameSubmit = useCallback((name: string) => {
|
|
308
|
+
if (!isValidProfileName(name)) {
|
|
309
|
+
setValidationError(
|
|
310
|
+
"Invalid profile name. Use lowercase letters, numbers, and hyphens (must start and end with a letter or number).",
|
|
311
|
+
);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
setValidationError(null);
|
|
315
|
+
setNewProfileName(name);
|
|
316
|
+
setNewProfileDisplayName(name);
|
|
317
|
+
setMode("create-display");
|
|
318
|
+
}, []);
|
|
319
|
+
|
|
320
|
+
// 新規プロファイル作成完了
|
|
321
|
+
const handleCreateProfileSubmit = useCallback(
|
|
322
|
+
async (displayName: string) => {
|
|
323
|
+
try {
|
|
324
|
+
await createProfile(newProfileName, {
|
|
325
|
+
displayName,
|
|
326
|
+
env: {},
|
|
327
|
+
});
|
|
328
|
+
setSelectedProfileName(newProfileName);
|
|
329
|
+
setFocus("env"); // viewモードでは環境変数にフォーカス
|
|
330
|
+
setMode("view");
|
|
331
|
+
} catch {
|
|
332
|
+
// エラー状態は useProfiles フック側で管理するため、ここでは一覧に戻す
|
|
333
|
+
setMode("list");
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
[createProfile, newProfileName],
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// プロファイル削除確認
|
|
340
|
+
const handleConfirmDeleteProfile = useCallback(
|
|
341
|
+
async (confirmed: boolean) => {
|
|
342
|
+
if (confirmed && selectedProfileName) {
|
|
343
|
+
try {
|
|
344
|
+
await deleteProfile(selectedProfileName);
|
|
345
|
+
setSelectedProfileName(null);
|
|
346
|
+
} catch {
|
|
347
|
+
// エラー状態は useProfiles フック側で管理するため、ここでは握りつぶす
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
setMode("list");
|
|
351
|
+
},
|
|
352
|
+
[deleteProfile, selectedProfileName],
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// 環境変数追加開始
|
|
356
|
+
const handleStartAddEnv = useCallback(() => {
|
|
357
|
+
setNewEnvKey("");
|
|
358
|
+
setNewEnvValue("");
|
|
359
|
+
setValidationError(null);
|
|
360
|
+
setMode("add-env-key");
|
|
361
|
+
}, []);
|
|
362
|
+
|
|
363
|
+
// 環境変数キー入力完了
|
|
364
|
+
const handleEnvKeySubmit = useCallback((key: string) => {
|
|
365
|
+
const trimmedKey = key.trim();
|
|
366
|
+
if (!trimmedKey || !ENV_VAR_KEY_PATTERN.test(trimmedKey)) {
|
|
367
|
+
setValidationError(
|
|
368
|
+
"Invalid variable name. Use letters, numbers, and underscores (must start with a letter or underscore).",
|
|
369
|
+
);
|
|
370
|
+
setNewEnvKey(trimmedKey);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
setValidationError(null);
|
|
375
|
+
setNewEnvKey(trimmedKey);
|
|
376
|
+
setMode("add-env-value");
|
|
377
|
+
}, []);
|
|
378
|
+
|
|
379
|
+
// 環境変数追加完了
|
|
380
|
+
const handleEnvValueSubmit = useCallback(
|
|
381
|
+
async (value: string) => {
|
|
382
|
+
if (selectedProfileName) {
|
|
383
|
+
try {
|
|
384
|
+
await updateEnvVar(selectedProfileName, newEnvKey, value);
|
|
385
|
+
} catch {
|
|
386
|
+
// エラー状態は useProfiles フック側で管理するため、ここでは握りつぶす
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
setMode("view");
|
|
390
|
+
},
|
|
391
|
+
[updateEnvVar, selectedProfileName, newEnvKey],
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
// 環境変数編集開始
|
|
395
|
+
const handleStartEditEnv = useCallback(
|
|
396
|
+
(key: string, currentValue: string) => {
|
|
397
|
+
setSelectedEnvKey(key);
|
|
398
|
+
setNewEnvValue(currentValue);
|
|
399
|
+
setMode("edit-env-value");
|
|
400
|
+
},
|
|
401
|
+
[],
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
// 環境変数編集完了
|
|
405
|
+
const handleEditEnvSubmit = useCallback(
|
|
406
|
+
async (value: string) => {
|
|
407
|
+
if (selectedProfileName && selectedEnvKey) {
|
|
408
|
+
try {
|
|
409
|
+
await updateEnvVar(selectedProfileName, selectedEnvKey, value);
|
|
410
|
+
} catch {
|
|
411
|
+
// エラー状態は useProfiles フック側で管理するため、ここでは握りつぶす
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
setMode("view");
|
|
415
|
+
},
|
|
416
|
+
[updateEnvVar, selectedProfileName, selectedEnvKey],
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
// 環境変数削除確認
|
|
420
|
+
const handleConfirmDeleteEnv = useCallback(
|
|
421
|
+
async (confirmed: boolean) => {
|
|
422
|
+
if (confirmed && selectedProfileName && selectedEnvKey) {
|
|
423
|
+
try {
|
|
424
|
+
await deleteEnvVar(selectedProfileName, selectedEnvKey);
|
|
425
|
+
} catch {
|
|
426
|
+
// エラー状態は useProfiles フック側で管理するため、ここでは握りつぶす
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
setMode("view");
|
|
430
|
+
},
|
|
431
|
+
[deleteEnvVar, selectedProfileName, selectedEnvKey],
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
// キーボード入力ハンドリング
|
|
435
|
+
useInput(
|
|
436
|
+
(input, key) => {
|
|
437
|
+
// 入力モード時は他のキーハンドリングをスキップ
|
|
438
|
+
if (
|
|
439
|
+
mode === "create-name" ||
|
|
440
|
+
mode === "create-display" ||
|
|
441
|
+
mode === "add-env-key" ||
|
|
442
|
+
mode === "add-env-value" ||
|
|
443
|
+
mode === "edit-env-value"
|
|
444
|
+
) {
|
|
445
|
+
if (key.escape) {
|
|
446
|
+
setMode(mode.startsWith("create") ? "list" : "view");
|
|
447
|
+
}
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// 確認ダイアログ時は Confirm コンポーネントがハンドリング
|
|
452
|
+
if (mode === "confirm-delete-profile" || mode === "confirm-delete-env") {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Escape で戻る
|
|
457
|
+
if (key.escape) {
|
|
458
|
+
if (mode === "view") {
|
|
459
|
+
setMode("list");
|
|
460
|
+
setSelectedProfileName(null);
|
|
461
|
+
} else {
|
|
462
|
+
onBack();
|
|
463
|
+
}
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// プロファイル一覧モード
|
|
468
|
+
if (mode === "list") {
|
|
469
|
+
if (input === "n") {
|
|
470
|
+
handleStartCreateProfile();
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (input === "d" && profileItems.length > 0) {
|
|
474
|
+
const item = profileItems[profileIndex];
|
|
475
|
+
if (item?.profileName && item.profileName !== activeProfileName) {
|
|
476
|
+
setSelectedProfileName(item.profileName);
|
|
477
|
+
setMode("confirm-delete-profile");
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// プロファイル詳細モード
|
|
485
|
+
if (mode === "view") {
|
|
486
|
+
// Tab でフォーカス切り替え (env ↔ osenv)
|
|
487
|
+
if (key.tab) {
|
|
488
|
+
setFocus((prev) => (prev === "env" ? "osenv" : "env"));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// j/k でスクロール
|
|
493
|
+
if (input === "j" || key.downArrow) {
|
|
494
|
+
if (focus === "env") {
|
|
495
|
+
setEnvIndex((prev) =>
|
|
496
|
+
Math.min(prev + 1, Math.max(0, envItems.length - 1)),
|
|
497
|
+
);
|
|
498
|
+
} else if (focus === "osenv") {
|
|
499
|
+
setOsEnvIndex((prev) =>
|
|
500
|
+
Math.min(prev + 1, Math.max(0, osEnvItems.length - 1)),
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (input === "k" || key.upArrow) {
|
|
506
|
+
if (focus === "env") {
|
|
507
|
+
setEnvIndex((prev) => Math.max(prev - 1, 0));
|
|
508
|
+
} else if (focus === "osenv") {
|
|
509
|
+
setOsEnvIndex((prev) => Math.max(prev - 1, 0));
|
|
510
|
+
}
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// 環境変数操作
|
|
515
|
+
if (focus === "env") {
|
|
516
|
+
if (input === "a") {
|
|
517
|
+
handleStartAddEnv();
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (input === "e" && envItems.length > 0) {
|
|
521
|
+
const item = envItems[envIndex];
|
|
522
|
+
if (item) {
|
|
523
|
+
handleStartEditEnv(item.key, item.envValue);
|
|
524
|
+
}
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (input === "d" && envItems.length > 0) {
|
|
528
|
+
const item = envItems[envIndex];
|
|
529
|
+
if (item) {
|
|
530
|
+
setSelectedEnvKey(item.key);
|
|
531
|
+
setMode("confirm-delete-env");
|
|
532
|
+
}
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
{ isActive: true },
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
// フッターアクション
|
|
542
|
+
const getFooterActions = () => {
|
|
543
|
+
if (mode === "list") {
|
|
544
|
+
return [
|
|
545
|
+
{ key: "enter", description: "Select" },
|
|
546
|
+
{ key: "n", description: "New" },
|
|
547
|
+
{ key: "d", description: "Delete" },
|
|
548
|
+
{ key: "esc", description: "Back" },
|
|
549
|
+
];
|
|
550
|
+
}
|
|
551
|
+
if (mode === "view") {
|
|
552
|
+
return [
|
|
553
|
+
{ key: "tab", description: "Switch focus" },
|
|
554
|
+
{ key: "j/k", description: "Scroll" },
|
|
555
|
+
{ key: "a", description: "Add env" },
|
|
556
|
+
{ key: "e", description: "Edit env" },
|
|
557
|
+
{ key: "d", description: "Delete env" },
|
|
558
|
+
{ key: "esc", description: "Back" },
|
|
559
|
+
];
|
|
560
|
+
}
|
|
561
|
+
return [{ key: "esc", description: "Cancel" }];
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
// ローディング表示
|
|
565
|
+
if (loading) {
|
|
566
|
+
return (
|
|
567
|
+
<Box flexDirection="column" height={rows}>
|
|
568
|
+
<Header
|
|
569
|
+
title="Environment Profiles"
|
|
570
|
+
titleColor="magenta"
|
|
571
|
+
version={version}
|
|
572
|
+
/>
|
|
573
|
+
<Box flexGrow={1} justifyContent="center" alignItems="center">
|
|
574
|
+
<Text>Loading profiles...</Text>
|
|
575
|
+
</Box>
|
|
576
|
+
</Box>
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// エラー表示
|
|
581
|
+
if (error) {
|
|
582
|
+
return (
|
|
583
|
+
<Box flexDirection="column" height={rows}>
|
|
584
|
+
<Header
|
|
585
|
+
title="Environment Profiles"
|
|
586
|
+
titleColor="magenta"
|
|
587
|
+
version={version}
|
|
588
|
+
/>
|
|
589
|
+
<Box flexGrow={1} marginTop={1}>
|
|
590
|
+
<Text color="red">Error: {error.message}</Text>
|
|
591
|
+
</Box>
|
|
592
|
+
<Footer actions={[{ key: "esc", description: "Back" }]} />
|
|
593
|
+
</Box>
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// 新規プロファイル作成:名前入力
|
|
598
|
+
if (mode === "create-name") {
|
|
599
|
+
return (
|
|
600
|
+
<Box flexDirection="column" height={rows}>
|
|
601
|
+
<Header
|
|
602
|
+
title="Environment Profiles"
|
|
603
|
+
titleColor="magenta"
|
|
604
|
+
version={version}
|
|
605
|
+
/>
|
|
606
|
+
<Box flexDirection="column" flexGrow={1} marginTop={1}>
|
|
607
|
+
<Text>Create new profile</Text>
|
|
608
|
+
<Box marginTop={1}>
|
|
609
|
+
<Input
|
|
610
|
+
value={newProfileName}
|
|
611
|
+
onChange={setNewProfileName}
|
|
612
|
+
onSubmit={handleCreateNameSubmit}
|
|
613
|
+
label="Profile name (lowercase, a-z0-9-):"
|
|
614
|
+
placeholder="development"
|
|
615
|
+
/>
|
|
616
|
+
</Box>
|
|
617
|
+
{validationError && (
|
|
618
|
+
<Box marginTop={1}>
|
|
619
|
+
<Text color="red">{validationError}</Text>
|
|
620
|
+
</Box>
|
|
621
|
+
)}
|
|
622
|
+
</Box>
|
|
623
|
+
<Footer actions={getFooterActions()} />
|
|
624
|
+
</Box>
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// 新規プロファイル作成:表示名入力
|
|
629
|
+
if (mode === "create-display") {
|
|
630
|
+
return (
|
|
631
|
+
<Box flexDirection="column" height={rows}>
|
|
632
|
+
<Header
|
|
633
|
+
title="Environment Profiles"
|
|
634
|
+
titleColor="magenta"
|
|
635
|
+
version={version}
|
|
636
|
+
/>
|
|
637
|
+
<Box flexDirection="column" flexGrow={1} marginTop={1}>
|
|
638
|
+
<Text>Create new profile: {newProfileName}</Text>
|
|
639
|
+
<Box marginTop={1}>
|
|
640
|
+
<Input
|
|
641
|
+
value={newProfileDisplayName}
|
|
642
|
+
onChange={setNewProfileDisplayName}
|
|
643
|
+
onSubmit={handleCreateProfileSubmit}
|
|
644
|
+
label="Display name:"
|
|
645
|
+
placeholder="Development"
|
|
646
|
+
/>
|
|
647
|
+
</Box>
|
|
648
|
+
</Box>
|
|
649
|
+
<Footer actions={getFooterActions()} />
|
|
650
|
+
</Box>
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// 環境変数追加:キー入力
|
|
655
|
+
if (mode === "add-env-key") {
|
|
656
|
+
return (
|
|
657
|
+
<Box flexDirection="column" height={rows}>
|
|
658
|
+
<Header
|
|
659
|
+
title="Environment Profiles"
|
|
660
|
+
titleColor="magenta"
|
|
661
|
+
version={version}
|
|
662
|
+
/>
|
|
663
|
+
<Box flexDirection="column" flexGrow={1} marginTop={1}>
|
|
664
|
+
<Text>Add environment variable</Text>
|
|
665
|
+
<Box marginTop={1}>
|
|
666
|
+
<Input
|
|
667
|
+
value={newEnvKey}
|
|
668
|
+
onChange={setNewEnvKey}
|
|
669
|
+
onSubmit={handleEnvKeySubmit}
|
|
670
|
+
label="Variable name:"
|
|
671
|
+
placeholder="API_KEY"
|
|
672
|
+
/>
|
|
673
|
+
</Box>
|
|
674
|
+
{validationError && (
|
|
675
|
+
<Box marginTop={1}>
|
|
676
|
+
<Text color="red">{validationError}</Text>
|
|
677
|
+
</Box>
|
|
678
|
+
)}
|
|
679
|
+
</Box>
|
|
680
|
+
<Footer actions={getFooterActions()} />
|
|
681
|
+
</Box>
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// 環境変数追加:値入力
|
|
686
|
+
if (mode === "add-env-value") {
|
|
687
|
+
return (
|
|
688
|
+
<Box flexDirection="column" height={rows}>
|
|
689
|
+
<Header
|
|
690
|
+
title="Environment Profiles"
|
|
691
|
+
titleColor="magenta"
|
|
692
|
+
version={version}
|
|
693
|
+
/>
|
|
694
|
+
<Box flexDirection="column" flexGrow={1} marginTop={1}>
|
|
695
|
+
<Text>Add environment variable: {newEnvKey}</Text>
|
|
696
|
+
<Box marginTop={1}>
|
|
697
|
+
<Input
|
|
698
|
+
value={newEnvValue}
|
|
699
|
+
onChange={setNewEnvValue}
|
|
700
|
+
onSubmit={handleEnvValueSubmit}
|
|
701
|
+
label="Value:"
|
|
702
|
+
placeholder="your-value"
|
|
703
|
+
/>
|
|
704
|
+
</Box>
|
|
705
|
+
</Box>
|
|
706
|
+
<Footer actions={getFooterActions()} />
|
|
707
|
+
</Box>
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// 環境変数編集:値入力
|
|
712
|
+
if (mode === "edit-env-value") {
|
|
713
|
+
return (
|
|
714
|
+
<Box flexDirection="column" height={rows}>
|
|
715
|
+
<Header
|
|
716
|
+
title="Environment Profiles"
|
|
717
|
+
titleColor="magenta"
|
|
718
|
+
version={version}
|
|
719
|
+
/>
|
|
720
|
+
<Box flexDirection="column" flexGrow={1} marginTop={1}>
|
|
721
|
+
<Text>Edit environment variable: {selectedEnvKey}</Text>
|
|
722
|
+
<Box marginTop={1}>
|
|
723
|
+
<Input
|
|
724
|
+
value={newEnvValue}
|
|
725
|
+
onChange={setNewEnvValue}
|
|
726
|
+
onSubmit={handleEditEnvSubmit}
|
|
727
|
+
label="New value:"
|
|
728
|
+
placeholder="new-value"
|
|
729
|
+
/>
|
|
730
|
+
</Box>
|
|
731
|
+
</Box>
|
|
732
|
+
<Footer actions={getFooterActions()} />
|
|
733
|
+
</Box>
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// プロファイル削除確認
|
|
738
|
+
if (mode === "confirm-delete-profile") {
|
|
739
|
+
const profileToDelete = selectedProfileName
|
|
740
|
+
? profiles?.profiles[selectedProfileName]
|
|
741
|
+
: null;
|
|
742
|
+
return (
|
|
743
|
+
<Box flexDirection="column" height={rows}>
|
|
744
|
+
<Header
|
|
745
|
+
title="Environment Profiles"
|
|
746
|
+
titleColor="magenta"
|
|
747
|
+
version={version}
|
|
748
|
+
/>
|
|
749
|
+
<Box flexDirection="column" flexGrow={1} marginTop={1}>
|
|
750
|
+
<Confirm
|
|
751
|
+
message={`Delete profile "${profileToDelete?.displayName ?? selectedProfileName}"?`}
|
|
752
|
+
onConfirm={handleConfirmDeleteProfile}
|
|
753
|
+
/>
|
|
754
|
+
</Box>
|
|
755
|
+
</Box>
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// 環境変数削除確認
|
|
760
|
+
if (mode === "confirm-delete-env") {
|
|
761
|
+
return (
|
|
762
|
+
<Box flexDirection="column" height={rows}>
|
|
763
|
+
<Header
|
|
764
|
+
title="Environment Profiles"
|
|
765
|
+
titleColor="magenta"
|
|
766
|
+
version={version}
|
|
767
|
+
/>
|
|
768
|
+
<Box flexDirection="column" flexGrow={1} marginTop={1}>
|
|
769
|
+
<Confirm
|
|
770
|
+
message={`Delete environment variable "${selectedEnvKey}"?`}
|
|
771
|
+
onConfirm={handleConfirmDeleteEnv}
|
|
772
|
+
/>
|
|
773
|
+
</Box>
|
|
774
|
+
</Box>
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// プロファイル一覧
|
|
779
|
+
if (mode === "list") {
|
|
780
|
+
const hasProfiles = Boolean(
|
|
781
|
+
profiles && Object.keys(profiles.profiles).length > 0,
|
|
782
|
+
);
|
|
783
|
+
return (
|
|
784
|
+
<Box flexDirection="column" height={rows}>
|
|
785
|
+
<Header
|
|
786
|
+
title="Environment Profiles"
|
|
787
|
+
titleColor="magenta"
|
|
788
|
+
version={version}
|
|
789
|
+
activeProfile={activeProfileName}
|
|
790
|
+
/>
|
|
791
|
+
|
|
792
|
+
<Box flexDirection="column" flexGrow={1} marginTop={1}>
|
|
793
|
+
<Box marginBottom={1}>
|
|
794
|
+
<Text>Select a profile to activate (or choose "(none)"):</Text>
|
|
795
|
+
</Box>
|
|
796
|
+
|
|
797
|
+
<Select
|
|
798
|
+
items={profileItems}
|
|
799
|
+
onSelect={handleActivateProfile}
|
|
800
|
+
selectedIndex={profileIndex}
|
|
801
|
+
onSelectedIndexChange={setProfileIndex}
|
|
802
|
+
/>
|
|
803
|
+
{!hasProfiles && (
|
|
804
|
+
<Box marginTop={1}>
|
|
805
|
+
<Text dimColor>No profiles. Press [n] to create one.</Text>
|
|
806
|
+
</Box>
|
|
807
|
+
)}
|
|
808
|
+
</Box>
|
|
809
|
+
|
|
810
|
+
<Footer actions={getFooterActions()} />
|
|
811
|
+
</Box>
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// プロファイル詳細
|
|
816
|
+
const maxOsEnvVisible = Math.max(
|
|
817
|
+
5,
|
|
818
|
+
Math.floor((rows - UI_CHROME_HEIGHT) / 2),
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
return (
|
|
822
|
+
<Box flexDirection="column" height={rows}>
|
|
823
|
+
<Header
|
|
824
|
+
title="Environment Profiles"
|
|
825
|
+
titleColor="magenta"
|
|
826
|
+
version={version}
|
|
827
|
+
activeProfile={activeProfileName}
|
|
828
|
+
/>
|
|
829
|
+
|
|
830
|
+
<Box flexDirection="column" flexGrow={1} marginTop={1}>
|
|
831
|
+
{/* プロファイル情報 */}
|
|
832
|
+
<Box marginBottom={1}>
|
|
833
|
+
<Text bold>Profile: </Text>
|
|
834
|
+
<Text color="cyan">
|
|
835
|
+
{currentProfile?.displayName ?? selectedProfileName}
|
|
836
|
+
</Text>
|
|
837
|
+
{selectedProfileName === activeProfileName && (
|
|
838
|
+
<Text color="green"> (active)</Text>
|
|
839
|
+
)}
|
|
840
|
+
</Box>
|
|
841
|
+
|
|
842
|
+
{currentProfile?.description && (
|
|
843
|
+
<Box marginBottom={1}>
|
|
844
|
+
<Text dimColor>{currentProfile.description}</Text>
|
|
845
|
+
</Box>
|
|
846
|
+
)}
|
|
847
|
+
|
|
848
|
+
{/* プロファイル環境変数 */}
|
|
849
|
+
<Box marginBottom={1}>
|
|
850
|
+
<Text bold {...(focus === "env" ? { color: "cyan" as const } : {})}>
|
|
851
|
+
Profile Environment Variables:
|
|
852
|
+
</Text>
|
|
853
|
+
</Box>
|
|
854
|
+
|
|
855
|
+
<Box flexDirection="column" marginLeft={2} marginBottom={1}>
|
|
856
|
+
{envItems.length === 0 ? (
|
|
857
|
+
<Text dimColor>No environment variables. Press [a] to add.</Text>
|
|
858
|
+
) : (
|
|
859
|
+
envItems.map((item, idx) => {
|
|
860
|
+
const isEnvSelected = focus === "env" && idx === envIndex;
|
|
861
|
+
return (
|
|
862
|
+
<Box key={item.key}>
|
|
863
|
+
<Text
|
|
864
|
+
{...(isEnvSelected ? { color: "cyan" as const } : {})}
|
|
865
|
+
inverse={isEnvSelected}
|
|
866
|
+
>
|
|
867
|
+
{item.key}
|
|
868
|
+
</Text>
|
|
869
|
+
<Text>=</Text>
|
|
870
|
+
<Text>{item.envValue}</Text>
|
|
871
|
+
</Box>
|
|
872
|
+
);
|
|
873
|
+
})
|
|
874
|
+
)}
|
|
875
|
+
</Box>
|
|
876
|
+
|
|
877
|
+
{/* OS環境変数(上書きされるものをハイライト) */}
|
|
878
|
+
<Box marginBottom={1}>
|
|
879
|
+
<Text bold {...(focus === "osenv" ? { color: "cyan" as const } : {})}>
|
|
880
|
+
OS Environment (overwritten keys in yellow):
|
|
881
|
+
</Text>
|
|
882
|
+
</Box>
|
|
883
|
+
|
|
884
|
+
<Box flexDirection="column" marginLeft={2} overflow="hidden">
|
|
885
|
+
{osEnvItems
|
|
886
|
+
.slice(osEnvIndex, osEnvIndex + maxOsEnvVisible)
|
|
887
|
+
.map((item, idx) => {
|
|
888
|
+
// osEnvIndex は「選択中のOS環境変数のインデックス」であり、同時にスクロールの先頭位置でもある
|
|
889
|
+
// そのため、表示上は slice した先頭要素が選択状態になる
|
|
890
|
+
const actualIndex = osEnvIndex + idx;
|
|
891
|
+
const isOsEnvSelected =
|
|
892
|
+
focus === "osenv" && actualIndex === osEnvIndex;
|
|
893
|
+
return (
|
|
894
|
+
<Box key={item.key}>
|
|
895
|
+
<Text
|
|
896
|
+
{...(item.isOverwritten
|
|
897
|
+
? { color: "yellow" as const }
|
|
898
|
+
: {})}
|
|
899
|
+
inverse={isOsEnvSelected}
|
|
900
|
+
>
|
|
901
|
+
{item.key}
|
|
902
|
+
</Text>
|
|
903
|
+
<Text>=</Text>
|
|
904
|
+
<Text dimColor>
|
|
905
|
+
{item.value.slice(0, 50)}
|
|
906
|
+
{item.value.length > 50 ? "..." : ""}
|
|
907
|
+
</Text>
|
|
908
|
+
</Box>
|
|
909
|
+
);
|
|
910
|
+
})}
|
|
911
|
+
{osEnvItems.length > maxOsEnvVisible && (
|
|
912
|
+
<Text dimColor>
|
|
913
|
+
... ({osEnvIndex + 1}-
|
|
914
|
+
{Math.min(osEnvIndex + maxOsEnvVisible, osEnvItems.length)} of{" "}
|
|
915
|
+
{osEnvItems.length})
|
|
916
|
+
</Text>
|
|
917
|
+
)}
|
|
918
|
+
</Box>
|
|
919
|
+
</Box>
|
|
920
|
+
|
|
921
|
+
<Footer actions={getFooterActions()} />
|
|
922
|
+
</Box>
|
|
923
|
+
);
|
|
924
|
+
}
|